Compare commits

...

1 Commits

Author SHA1 Message Date
Islon Scherer 750e983366 Move to truffle object model 2026-04-15 17:23:32 +02:00
28 changed files with 638 additions and 174 deletions
+3 -5
View File
@@ -55,8 +55,7 @@ abstract class NativeImageBuild : DefaultTask() {
@get:Inject protected abstract val execOperations: ExecOperations @get:Inject protected abstract val execOperations: ExecOperations
private val graalVm: Provider<BuildInfo.GraalVm> = private val graalVm: Provider<BuildInfo.GraalVm> = arch.map { a ->
arch.map { a ->
when (a) { when (a) {
Architecture.AMD64 -> buildInfo.graalVmAmd64 Architecture.AMD64 -> buildInfo.graalVmAmd64
Architecture.AARCH64 -> buildInfo.graalVmAarch64 Architecture.AARCH64 -> buildInfo.graalVmAarch64
@@ -132,7 +131,7 @@ abstract class NativeImageBuild : DefaultTask() {
add(imageName.get()) add(imageName.get())
// the actual limit (currently) used by native-image is this number + 1400 (idea is to // the actual limit (currently) used by native-image is this number + 1400 (idea is to
// compensate for Truffle's own nodes) // compensate for Truffle's own nodes)
add("-H:MaxRuntimeCompileMethods=1800") add("-H:MaxRuntimeCompileMethods=2000")
add("-H:+EnforceMaxRuntimeCompileMethods") add("-H:+EnforceMaxRuntimeCompileMethods")
add("--enable-url-protocols=http,https") add("--enable-url-protocols=http,https")
add("-H:+ReportExceptionStackTraces") add("-H:+ReportExceptionStackTraces")
@@ -151,8 +150,7 @@ abstract class NativeImageBuild : DefaultTask() {
} }
// native-image rejects non-existing class path entries -> filter // native-image rejects non-existing class path entries -> filter
add("--class-path") add("--class-path")
val pathInput = val pathInput = classpath.filter {
classpath.filter {
it.exists() && !exclusions.any { exclude -> it.name.contains(exclude) } it.exists() && !exclusions.any { exclude -> it.name.contains(exclude) }
} }
add(pathInput.asPath) add(pathInput.asPath)
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -22,10 +22,20 @@ import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmObject;
import org.pkl.core.runtime.VmUtils; import org.pkl.core.runtime.VmUtils;
/** Reads a local non-constant property that is known to exist in the lexical scope of this node. */ /**
* Reads a local non-constant property that is known to exist in the lexical scope of this node.
*
* <p>Local property values are cached using the ObjectMember as the key (identity-based) rather
* than the property name. This is necessary because the same property name can exist at different
* declaration sites in an amends chain, and we need to distinguish between them for correct
* late-binding semantics.
*
* <p>The cache is stored in a separate IdentityHashMap in VmObject (not in the DynamicObject
* storage) to avoid shape transitions that would destroy cache locality.
*/
public final class ReadLocalPropertyNode extends ExpressionNode { public final class ReadLocalPropertyNode extends ExpressionNode {
private final ObjectMember property; private final ObjectMember property;
private final int levelsUp; private final int levelsUp;
@@ -63,15 +73,17 @@ public final class ReadLocalPropertyNode extends ExpressionNode {
owner = owner.getEnclosingOwner(); owner = owner.getEnclosingOwner();
} }
assert receiver instanceof VmObjectLike assert receiver instanceof VmObject
: "Assumption: This node isn't used in Truffle ASTs of `external` pkl.base classes whose values aren't VmObject's."; : "Assumption: This node isn't used in Truffle ASTs of `external` pkl.base classes whose values aren't VmObject's.";
var objReceiver = (VmObjectLike) receiver; // Use the local property cache instead of DynamicObject storage
var result = objReceiver.getCachedValue(property); // to avoid shape transitions from ObjectMember keys
var objReceiver = (VmObject) receiver;
var result = objReceiver.getLocalCachedValue(property);
if (result == null) { if (result == null) {
result = callNode.call(objReceiver, owner, property.getName()); result = callNode.call(objReceiver, owner, property.getName());
objReceiver.setCachedValue(property, result); objReceiver.setLocalCachedValue(property, result);
} }
return result; return result;
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -19,9 +19,12 @@ import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.*; import com.oracle.truffle.api.dsl.*;
import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.nodes.IndirectCallNode;
import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.MemberLookupMode; import org.pkl.core.ast.MemberLookupMode;
@@ -61,6 +64,27 @@ public abstract class ReadPropertyNode extends ExpressionNode {
this(sourceSection, propertyName, MemberLookupMode.EXPLICIT_RECEIVER, false); this(sourceSection, propertyName, MemberLookupMode.EXPLICIT_RECEIVER, false);
} }
// Optimized specialization for VmTyped using DynamicObjectLibrary
@Specialization
protected Object evalTyped(
VmTyped receiver,
@CachedLibrary(limit = "3") DynamicObjectLibrary objectLibrary,
@Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
checkConst(receiver);
// fast path: check cache using optimized library access
var result = receiver.getCachedValue(propertyName, objectLibrary);
if (result != null) return result;
// slow path: look up member in prototype chain and compute value
result = VmUtils.readMemberOrNull(receiver, propertyName, true, callNode);
if (result != null) return result;
CompilerDirectives.transferToInterpreter();
throw cannotFindProperty(receiver);
}
// This method effectively covers `VmObject receiver` but is implemented in a more // This method effectively covers `VmObject receiver` but is implemented in a more
// efficient way. See: // efficient way. See:
// https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces // https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces
@@ -68,7 +92,7 @@ public abstract class ReadPropertyNode extends ExpressionNode {
protected Object evalObject( protected Object evalObject(
Object receiver, Object receiver,
@Cached("getVmObjectSubclassOrNull(receiver)") Class<? extends VmObjectLike> cachedClass, @Cached("getVmObjectSubclassOrNull(receiver)") Class<? extends VmObjectLike> cachedClass,
@Cached("create()") IndirectCallNode callNode) { @Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
var object = cachedClass.cast(receiver); var object = cachedClass.cast(receiver);
checkConst(object); checkConst(object);
@@ -0,0 +1,38 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.object.Shape;
/** Factory for Truffle {@link Shape} instances used by Pkl objects. */
public final class PklShape {
/** The root shape for all Pkl object instances. */
private static final Shape ROOT_SHAPE = Shape.newBuilder().build();
private PklShape() {}
/**
* Returns the root shape for Pkl objects.
*
* <p>This is the base shape from which all instance shapes derive. Properties are added
* dynamically as values are cached via {@link
* com.oracle.truffle.api.object.DynamicObjectLibrary#put}.
*/
public static Shape getRootShape() {
return ROOT_SHAPE;
}
}
@@ -104,7 +104,7 @@ public final class TestRunner {
if (factValue == Boolean.FALSE) { if (factValue == Boolean.FALSE) {
if (PowerAssertions.isEnabled()) { if (PowerAssertions.isEnabled()) {
try (var valueTracker = valueTrackerFactory.create()) { try (var valueTracker = valueTrackerFactory.create()) {
listing.cachedValues.clear(); listing.cleanAllCachedValues();
VmUtils.readMember(listing, idx); VmUtils.readMember(listing, idx);
var failure = var failure =
factFailure( factFailure(
@@ -29,7 +29,7 @@ import org.pkl.core.util.ByteArrayUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@ValueType @ValueType
public final class VmBytes extends VmValue implements Iterable<Long> { public final class VmBytes implements VmValue, Iterable<Long> {
private @Nullable VmList vmList; private @Nullable VmList vmList;
private @Nullable String base64; private @Nullable String base64;
@@ -19,6 +19,8 @@ import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Idempotent; import com.oracle.truffle.api.dsl.Idempotent;
import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import com.oracle.truffle.api.object.Shape;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import java.util.*; import java.util.*;
import java.util.function.*; import java.util.function.*;
@@ -43,7 +45,7 @@ import org.pkl.core.util.Nullable;
// The currently implemented (and likely insufficient) solution is to // The currently implemented (and likely insufficient) solution is to
// * deeply force standard library modules at initialization time. // * deeply force standard library modules at initialization time.
// * ensure that any further mutation (e.g., lazy initialization in VmClass) is thread-safe. // * ensure that any further mutation (e.g., lazy initialization in VmClass) is thread-safe.
public final class VmClass extends VmValue { public final class VmClass implements VmValue {
private final SourceSection sourceSection; private final SourceSection sourceSection;
private final SourceSection headerSection; private final SourceSection headerSection;
private final SourceSection @Nullable [] docComment; private final SourceSection @Nullable [] docComment;
@@ -123,6 +125,16 @@ public final class VmClass extends VmValue {
private final Object mapToTypedMembersLock = new Object(); private final Object mapToTypedMembersLock = new Object();
// Shape for instances of this class - used for Truffle's Dynamic Object Model
@LateInit
@GuardedBy("instanceShapeLock")
private Shape __instanceShape;
private final Object instanceShapeLock = new Object();
/** Cached IndirectCallNode for force() operations. */
private final IndirectCallNode cachedCallNode = IndirectCallNode.create();
public VmClass( public VmClass(
SourceSection sourceSection, SourceSection sourceSection,
SourceSection headerSection, SourceSection headerSection,
@@ -697,6 +709,41 @@ public final class VmClass extends VmValue {
} }
} }
/**
* Returns the Truffle Shape for instances of this class.
*
* <p>The shape is lazily initialized from the root shape. Instance shapes are used by the Dynamic
* Object Model to provide optimized property storage and inline caching for cached property
* values.
*/
public Shape getInstanceShape() {
synchronized (instanceShapeLock) {
if (__instanceShape == null) {
__instanceShape = buildInstanceShape();
}
return __instanceShape;
}
}
@TruffleBoundary
private Shape buildInstanceShape() {
// start with the superclass shape if available, otherwise use root shape
if (superclass != null) {
return superclass.getInstanceShape();
}
return PklShape.getRootShape();
}
/**
* Returns the cached IndirectCallNode for force() operations.
*
* <p>This node is shared by all instances of this class and avoids the overhead of {@code
* IndirectCallNode.getUncached()} which performs a lookup on every call.
*/
public IndirectCallNode getCachedCallNode() {
return cachedCallNode;
}
/** /**
* Tells if the given property defines a member of this class. Requires a fully initialized * Tells if the given property defines a member of this class. Requires a fully initialized
* inheritance hierarchy. * inheritance hierarchy.
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.util.Iterator; import java.util.Iterator;
import org.organicdesign.fp.xform.Xform; import org.organicdesign.fp.xform.Xform;
public abstract class VmCollection extends VmValue implements Iterable<Object> { public abstract class VmCollection implements VmValue, Iterable<Object> {
public interface Builder<T extends VmCollection> { public interface Builder<T extends VmCollection> {
void add(Object element); void add(Object element);
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import org.pkl.core.util.MathUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@ValueType @ValueType
public final class VmDataSize extends VmValue implements Comparable<VmDataSize> { public final class VmDataSize implements VmValue, Comparable<VmDataSize> {
private static final Map<Identifier, DataSizeUnit> UNITS = private static final Map<Identifier, DataSizeUnit> UNITS =
Map.ofEntries( Map.ofEntries(
entry(Identifier.B, DataSizeUnit.BYTES), entry(Identifier.B, DataSizeUnit.BYTES),
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ import org.pkl.core.util.DurationUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@ValueType @ValueType
public final class VmDuration extends VmValue implements Comparable<VmDuration> { public final class VmDuration implements VmValue, Comparable<VmDuration> {
private static final Map<Identifier, DurationUnit> UNITS = private static final Map<Identifier, DurationUnit> UNITS =
Map.of( Map.of(
Identifier.NS, DurationUnit.NANOS, Identifier.NS, DurationUnit.NANOS,
@@ -28,6 +28,8 @@ import org.pkl.core.util.EconomicMaps;
public final class VmDynamic extends VmObject { public final class VmDynamic extends VmObject {
private int cachedRegularMemberCount = -1; private int cachedRegularMemberCount = -1;
private final int length;
private static final class EmptyHolder { private static final class EmptyHolder {
private static final VmDynamic EMPTY = private static final VmDynamic EMPTY =
new VmDynamic( new VmDynamic(
@@ -37,8 +39,6 @@ public final class VmDynamic extends VmObject {
0); 0);
} }
private final int length;
public static VmDynamic empty() { public static VmDynamic empty() {
return EmptyHolder.EMPTY; return EmptyHolder.EMPTY;
} }
@@ -48,7 +48,7 @@ public final class VmDynamic extends VmObject {
VmObject parent, VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
int length) { int length) {
super(enclosingFrame, Objects.requireNonNull(parent), members); super(PklShape.getRootShape(), enclosingFrame, Objects.requireNonNull(parent), members);
this.length = length; this.length = length;
} }
@@ -75,8 +75,7 @@ public final class VmDynamic extends VmObject {
@Override @Override
@TruffleBoundary @TruffleBoundary
public PObject export() { public PObject export() {
var properties = var properties = CollectionUtils.<String, Object>newLinkedHashMap(getCachedValueCount());
CollectionUtils.<String, Object>newLinkedHashMap(EconomicMaps.size(cachedValues));
iterateMemberValues( iterateMemberValues(
(key, member, value) -> { (key, member, value) -> {
@@ -109,7 +108,7 @@ public final class VmDynamic extends VmObject {
other.force(false); other.force(false);
if (getRegularMemberCount() != other.getRegularMemberCount()) return false; if (getRegularMemberCount() != other.getRegularMemberCount()) return false;
var cursor = cachedValues.getEntries(); var cursor = getCachedValueEntries();
while (cursor.advance()) { while (cursor.advance()) {
Object key = cursor.getKey(); Object key = cursor.getKey();
if (isHiddenOrLocalProperty(key)) continue; if (isHiddenOrLocalProperty(key)) continue;
@@ -130,7 +129,7 @@ public final class VmDynamic extends VmObject {
force(false); force(false);
var result = 0; var result = 0;
var cursor = cachedValues.getEntries(); var cursor = getCachedValueEntries();
while (cursor.advance()) { while (cursor.advance()) {
var key = cursor.getKey(); var key = cursor.getKey();
@@ -150,8 +149,9 @@ public final class VmDynamic extends VmObject {
if (cachedRegularMemberCount != -1) return cachedRegularMemberCount; if (cachedRegularMemberCount != -1) return cachedRegularMemberCount;
var result = 0; var result = 0;
for (var key : cachedValues.getKeys()) { var cursor = getCachedValueEntries();
if (!isHiddenOrLocalProperty(key)) result += 1; while (cursor.advance()) {
if (!isHiddenOrLocalProperty(cursor.getKey())) result += 1;
} }
cachedRegularMemberCount = result; cachedRegularMemberCount = result;
return result; return result;
@@ -23,9 +23,14 @@ import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.EmptyMapCursor;
import org.pkl.core.util.MapCursor;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public final class VmFunction extends VmObjectLike { public final class VmFunction implements VmObjectLike {
// Own fields (VmFunction does not extend DynamicObject, so it has its own storage)
private final MaterializedFrame enclosingFrame;
private @Nullable Object extraStorage;
private final Object thisValue; private final Object thisValue;
private final int paramCount; private final int paramCount;
@@ -37,13 +42,28 @@ public final class VmFunction extends VmObjectLike {
int paramCount, int paramCount,
PklRootNode rootNode, PklRootNode rootNode,
@Nullable Object extraStorage) { @Nullable Object extraStorage) {
super(enclosingFrame); this.enclosingFrame = enclosingFrame;
this.thisValue = thisValue; this.thisValue = thisValue;
this.paramCount = paramCount; this.paramCount = paramCount;
this.rootNode = rootNode; this.rootNode = rootNode;
this.extraStorage = extraStorage; this.extraStorage = extraStorage;
} }
@Override
public MaterializedFrame getEnclosingFrame() {
return enclosingFrame;
}
@Override
public @Nullable Object getExtraStorage() {
return extraStorage;
}
@Override
public void setExtraStorage(@Nullable Object extraStorage) {
this.extraStorage = extraStorage;
}
public RootCallTarget getCallTarget() { public RootCallTarget getCallTarget() {
return rootNode.getCallTarget(); return rootNode.getCallTarget();
} }
@@ -123,17 +143,27 @@ public final class VmFunction extends VmObjectLike {
} }
@Override @Override
public boolean iterateMemberValues(MemberValueConsumer consumer) { public MapCursor<Object, Object> getCachedValueEntries() {
return EmptyMapCursor.instance();
}
@Override
public int getCachedValueCount() {
return 0;
}
@Override
public boolean iterateMemberValues(VmObjectLike.MemberValueConsumer consumer) {
return true; return true;
} }
@Override @Override
public boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer) { public boolean forceAndIterateMemberValues(VmObjectLike.ForcedMemberValueConsumer consumer) {
return true; return true;
} }
@Override @Override
public boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer) { public boolean iterateAlreadyForcedMemberValues(VmObjectLike.ForcedMemberValueConsumer consumer) {
return true; return true;
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ import org.pkl.core.util.Nullable;
// Some code copied from kotlin.ranges.Progressions, kotlin.ranges.ProgressionIterators, // Some code copied from kotlin.ranges.Progressions, kotlin.ranges.ProgressionIterators,
// kotlin.internal.ProgressionUtil (Apache 2). // kotlin.internal.ProgressionUtil (Apache 2).
@ValueType @ValueType
public final class VmIntSeq extends VmValue implements Iterable<Long> { public final class VmIntSeq implements VmValue, Iterable<Long> {
public final long start; public final long start;
public final long end; public final long end;
public final long step; public final long step;
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -87,7 +87,7 @@ public final class VmListing extends VmListingOrMapping {
@Override @Override
@TruffleBoundary @TruffleBoundary
public List<Object> export() { public List<Object> export() {
var properties = new ArrayList<>(EconomicMaps.size(cachedValues)); var properties = new ArrayList<>(getCachedValueCount());
iterateMemberValues( iterateMemberValues(
(key, prop, value) -> { (key, prop, value) -> {
@@ -121,7 +121,7 @@ public final class VmListing extends VmListingOrMapping {
force(false); force(false);
other.force(false); other.force(false);
var cursor = cachedValues.getEntries(); var cursor = getCachedValueEntries();
while (cursor.advance()) { while (cursor.advance()) {
var key = cursor.getKey(); var key = cursor.getKey();
if (key instanceof Identifier) continue; if (key instanceof Identifier) continue;
@@ -142,7 +142,7 @@ public final class VmListing extends VmListingOrMapping {
force(false); force(false);
var result = 0; var result = 0;
var cursor = cachedValues.getEntries(); var cursor = getCachedValueEntries();
while (cursor.advance()) { while (cursor.advance()) {
var key = cursor.getKey(); var key = cursor.getKey();
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.ast.type.TypeNode; import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public abstract class VmListingOrMapping extends VmObject { public abstract class VmListingOrMapping extends VmObject {
@@ -32,24 +31,24 @@ public abstract class VmListingOrMapping extends VmObject {
private final @Nullable Object typeCheckReceiver; private final @Nullable Object typeCheckReceiver;
private final @Nullable VmObjectLike typeCheckOwner; private final @Nullable VmObjectLike typeCheckOwner;
public VmListingOrMapping( protected VmListingOrMapping(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
@Nullable VmObject parent, @Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) { UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, parent, members); super(PklShape.getRootShape(), enclosingFrame, parent, members);
typeCastNode = null; typeCastNode = null;
typeCheckReceiver = null; typeCheckReceiver = null;
typeCheckOwner = null; typeCheckOwner = null;
} }
public VmListingOrMapping( protected VmListingOrMapping(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
@Nullable VmObject parent, @Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
ListingOrMappingTypeCastNode typeCastNode, ListingOrMappingTypeCastNode typeCastNode,
Object typeCheckReceiver, Object typeCheckReceiver,
VmObjectLike typeCheckOwner) { VmObjectLike typeCheckOwner) {
super(enclosingFrame, parent, members); super(PklShape.getRootShape(), enclosingFrame, parent, members);
this.typeCastNode = typeCastNode; this.typeCastNode = typeCastNode;
this.typeCheckReceiver = typeCheckReceiver; this.typeCheckReceiver = typeCheckReceiver;
this.typeCheckOwner = typeCheckOwner; this.typeCheckOwner = typeCheckOwner;
@@ -88,7 +87,7 @@ public abstract class VmListingOrMapping extends VmObject {
@Override @Override
@TruffleBoundary @TruffleBoundary
public final @Nullable Object getCachedValue(Object key) { public final @Nullable Object getCachedValue(Object key) {
var result = EconomicMaps.get(cachedValues, key); var result = super.getCachedValue(key);
// if this object has members, `this[key]` may differ from `parent[key]`, so stop the search // if this object has members, `this[key]` may differ from `parent[key]`, so stop the search
if (result != null || !members.isEmpty()) return result; if (result != null || !members.isEmpty()) return result;
@@ -30,7 +30,7 @@ import org.pkl.core.util.paguro.RrbTree;
import org.pkl.core.util.paguro.RrbTree.ImRrbt; import org.pkl.core.util.paguro.RrbTree.ImRrbt;
import org.pkl.core.util.paguro.RrbTree.MutRrbt; import org.pkl.core.util.paguro.RrbTree.MutRrbt;
public final class VmMap extends VmValue implements Iterable<Map.Entry<Object, Object>> { public final class VmMap implements VmValue, Iterable<Map.Entry<Object, Object>> {
public static final VmMap EMPTY = new VmMap(PersistentHashMap.empty(), RrbTree.empty()); public static final VmMap EMPTY = new VmMap(PersistentHashMap.empty(), RrbTree.empty());
private final ImMap<Object, Object> map; private final ImMap<Object, Object> map;
@@ -93,7 +93,7 @@ public final class VmMapping extends VmListingOrMapping {
@Override @Override
@TruffleBoundary @TruffleBoundary
public Map<Object, Object> export() { public Map<Object, Object> export() {
var properties = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); var properties = CollectionUtils.newLinkedHashMap(getCachedValueCount());
iterateMemberValues( iterateMemberValues(
(key, prop, value) -> { (key, prop, value) -> {
@@ -128,7 +128,7 @@ public final class VmMapping extends VmListingOrMapping {
other.force(false); other.force(false);
if (getLength() != other.getLength()) return false; if (getLength() != other.getLength()) return false;
var cursor = cachedValues.getEntries(); var cursor = getCachedValueEntries();
while (cursor.advance()) { while (cursor.advance()) {
Object key = cursor.getKey(); Object key = cursor.getKey();
if (key instanceof Identifier) continue; if (key instanceof Identifier) continue;
@@ -149,7 +149,7 @@ public final class VmMapping extends VmListingOrMapping {
force(false); force(false);
var result = 0; var result = 0;
var cursor = cachedValues.getEntries(); var cursor = getCachedValueEntries();
while (cursor.advance()) { while (cursor.advance()) {
var key = cursor.getKey(); var key = cursor.getKey();
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import org.pkl.core.PNull;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@ValueType @ValueType
public final class VmNull extends VmValue { public final class VmNull implements VmValue {
private static final VmNull WITHOUT_DEFAULT = new VmNull(null); private static final VmNull WITHOUT_DEFAULT = new VmNull(null);
// worthwhile to create this lazily? // worthwhile to create this lazily?
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -18,42 +18,69 @@ package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.object.Shape;
import java.util.*; import java.util.*;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.UnmodifiableEconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.CollectionUtils; import org.pkl.core.util.CollectionUtils;
import org.pkl.core.util.DynamicObjectMapCursor;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.MapCursor;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
/** Corresponds to `pkl.base#Object`. */ /**
public abstract class VmObject extends VmObjectLike { * Corresponds to `pkl.base#Object`.
*
* <p>Extends {@link DynamicObject} to leverage Truffle's object storage and inline caching
* capabilities. Cached property values are stored directly in this object using the Dynamic Object
* Model.
*/
public abstract class VmObject extends DynamicObject implements VmObjectLike {
// moved from VmObjectLike
protected final MaterializedFrame enclosingFrame;
protected @Nullable Object extraStorage;
@CompilationFinal protected @Nullable VmObject parent; @CompilationFinal protected @Nullable VmObject parent;
protected final UnmodifiableEconomicMap<Object, ObjectMember> members; protected final UnmodifiableEconomicMap<Object, ObjectMember> members;
protected final EconomicMap<Object, Object> cachedValues;
protected int cachedHash; protected int cachedHash;
private boolean forced; private boolean forced;
public VmObject( /**
MaterializedFrame enclosingFrame, * Separate cache for local property values.
@Nullable VmObject parent, *
UnmodifiableEconomicMap<Object, ObjectMember> members, * <p>This is kept separate from the DynamicObject storage to avoid shape transitions.
EconomicMap<Object, Object> cachedValues) { */
super(enclosingFrame); private @Nullable IdentityHashMap<ObjectMember, Object> localPropertyCache;
this.parent = parent;
this.members = members;
this.cachedValues = cachedValues;
assert parent != this; protected VmObject(
} Shape shape,
public VmObject(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
@Nullable VmObject parent, @Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) { UnmodifiableEconomicMap<Object, ObjectMember> members) {
this(enclosingFrame, parent, members, EconomicMaps.create()); super(shape);
this.enclosingFrame = enclosingFrame;
this.parent = parent;
this.members = members;
assert parent != this;
}
@Override
public final MaterializedFrame getEnclosingFrame() {
return enclosingFrame;
}
@Override
public final @Nullable Object getExtraStorage() {
return extraStorage;
}
@Override
public final void setExtraStorage(@Nullable Object extraStorage) {
this.extraStorage = extraStorage;
} }
public final void lateInitParent(VmObject parent) { public final void lateInitParent(VmObject parent) {
@@ -67,11 +94,13 @@ public abstract class VmObject extends VmObjectLike {
} }
@Override @Override
@TruffleBoundary
public final boolean hasMember(Object key) { public final boolean hasMember(Object key) {
return EconomicMaps.containsKey(members, key); return EconomicMaps.containsKey(members, key);
} }
@Override @Override
@TruffleBoundary
public final @Nullable ObjectMember getMember(Object key) { public final @Nullable ObjectMember getMember(Object key) {
return EconomicMaps.get(members, key); return EconomicMaps.get(members, key);
} }
@@ -82,23 +111,81 @@ public abstract class VmObject extends VmObjectLike {
} }
@Override @Override
@TruffleBoundary
public @Nullable Object getCachedValue(Object key) { public @Nullable Object getCachedValue(Object key) {
return EconomicMaps.get(cachedValues, key); return DynamicObjectLibrary.getUncached().getOrDefault(this, key, null);
}
@Override
public final void setCachedValue(Object key, Object value) {
EconomicMaps.put(cachedValues, key, value);
}
@Override
public final boolean hasCachedValue(Object key) {
return EconomicMaps.containsKey(cachedValues, key);
} }
@Override @Override
@TruffleBoundary @TruffleBoundary
public final boolean iterateMemberValues(MemberValueConsumer consumer) { public void setCachedValue(Object key, Object value) {
DynamicObjectLibrary.getUncached().put(this, key, value);
}
@Override
@TruffleBoundary
public boolean hasCachedValue(Object key) {
return DynamicObjectLibrary.getUncached().containsKey(this, key);
}
@Override
public MapCursor<Object, Object> getCachedValueEntries() {
return new DynamicObjectMapCursor(this);
}
@Override
@TruffleBoundary
public int getCachedValueCount() {
return DynamicObjectLibrary.getUncached().getKeyArray(this).length;
}
/**
* Clean all cached values. Local or otherwise. Resets cached values to null without removing the
* keys, preserving the object's shape for pre-allocated slots.
*/
@TruffleBoundary
public void cleanAllCachedValues() {
if (localPropertyCache != null) {
localPropertyCache.clear();
}
var lib = DynamicObjectLibrary.getUncached();
Object[] keys = lib.getKeyArray(this);
for (Object key : keys) {
lib.put(this, key, null);
}
forced = false;
}
/**
* Gets a cached local property value.
*
* @param property the ObjectMember representing the local property declaration
* @return the cached value, or null if not cached
*/
@TruffleBoundary
public @Nullable Object getLocalCachedValue(ObjectMember property) {
return localPropertyCache == null ? null : localPropertyCache.get(property);
}
/**
* Sets a cached local property value.
*
* @param property the ObjectMember representing the local property declaration
* @param value the value to cache
*/
@TruffleBoundary
public void setLocalCachedValue(ObjectMember property, Object value) {
if (localPropertyCache == null) {
localPropertyCache = new IdentityHashMap<>(4);
}
localPropertyCache.put(property, value);
}
@Override
@TruffleBoundary
public final boolean iterateMemberValues(VmObjectLike.MemberValueConsumer consumer) {
var visited = new HashSet<>(); var visited = new HashSet<>();
return iterateMembers( return iterateMembers(
(key, member) -> { (key, member) -> {
@@ -112,14 +199,16 @@ public abstract class VmObject extends VmObjectLike {
@Override @Override
@TruffleBoundary @TruffleBoundary
public final boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer) { public final boolean forceAndIterateMemberValues(
VmObjectLike.ForcedMemberValueConsumer consumer) {
force(false, false); force(false, false);
return iterateAlreadyForcedMemberValues(consumer); return iterateAlreadyForcedMemberValues(consumer);
} }
@Override @Override
@TruffleBoundary @TruffleBoundary
public final boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer) { public final boolean iterateAlreadyForcedMemberValues(
VmObjectLike.ForcedMemberValueConsumer consumer) {
var visited = new HashSet<>(); var visited = new HashSet<>();
return iterateMembers( return iterateMembers(
(key, member) -> { (key, member) -> {
@@ -158,6 +247,9 @@ public abstract class VmObject extends VmObjectLike {
if (recurse) forced = true; if (recurse) forced = true;
// use cached call node from this object's class to avoid getUncached() overhead
var callNode = getVmClass().getCachedCallNode();
try { try {
for (VmObjectLike owner = this; owner != null; owner = owner.getParent()) { for (VmObjectLike owner = this; owner != null; owner = owner.getParent()) {
var cursor = EconomicMaps.getEntries(owner.getMembers()); var cursor = EconomicMaps.getEntries(owner.getMembers());
@@ -174,7 +266,7 @@ public abstract class VmObject extends VmObjectLike {
var memberValue = getCachedValue(memberKey); var memberValue = getCachedValue(memberKey);
if (memberValue == null) { if (memberValue == null) {
try { try {
memberValue = VmUtils.doReadMember(this, owner, memberKey, member); memberValue = VmUtils.doReadMember(this, owner, memberKey, member, true, callNode);
} catch (VmUndefinedValueException e) { } catch (VmUndefinedValueException e) {
if (!allowUndefinedValues) throw e; if (!allowUndefinedValues) throw e;
continue; continue;
@@ -208,7 +300,7 @@ public abstract class VmObject extends VmObjectLike {
*/ */
@TruffleBoundary @TruffleBoundary
protected final Map<String, Object> exportMembers() { protected final Map<String, Object> exportMembers() {
var result = CollectionUtils.<String, Object>newLinkedHashMap(EconomicMaps.size(cachedValues)); var result = CollectionUtils.<String, Object>newLinkedHashMap(getCachedValueCount());
iterateMemberValues( iterateMemberValues(
(key, member, value) -> { (key, member, value) -> {
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,53 +15,39 @@
*/ */
package org.pkl.core.runtime; package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.MaterializedFrame;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import org.graalvm.collections.UnmodifiableEconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.MapCursor;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
/** /**
* Corresponds to `pkl.base#Object|pkl.base#Function`. The lexical scope is a chain of * Corresponds to `pkl.base#Object|pkl.base#Function`. The lexical scope is a chain of
* `VmObjectLike` instances. * `VmObjectLike` instances.
*/ */
public abstract class VmObjectLike extends VmValue { public interface VmObjectLike extends VmValue {
/** The frame that was active when this object was instantiated. * */ /** The frame that was active when this object was instantiated. * */
protected final MaterializedFrame enclosingFrame; MaterializedFrame getEnclosingFrame();
protected @Nullable Object extraStorage; @Nullable
Object getExtraStorage();
protected VmObjectLike(MaterializedFrame enclosingFrame) { void setExtraStorage(@Nullable Object extraStorage);
this.enclosingFrame = enclosingFrame;
default boolean hasExtraStorage() {
return getExtraStorage() != null;
} }
public final MaterializedFrame getEnclosingFrame() { default @Nullable Object getEnclosingReceiver() {
return enclosingFrame; return VmUtils.getReceiverOrNull(getEnclosingFrame());
} }
public final @Nullable Object getEnclosingReceiver() { default @Nullable VmObjectLike getEnclosingOwner() {
return VmUtils.getReceiverOrNull(enclosingFrame); return VmUtils.getOwnerOrNull(getEnclosingFrame());
} }
public final @Nullable VmObjectLike getEnclosingOwner() { default boolean isModuleObject() {
return VmUtils.getOwnerOrNull(enclosingFrame);
}
public final boolean hasExtraStorage() {
return extraStorage != null;
}
public Object getExtraStorage() {
assert extraStorage != null;
return extraStorage;
}
public final void setExtraStorage(@Nullable Object extraStorage) {
this.extraStorage = extraStorage;
}
public boolean isModuleObject() {
return false; return false;
} }
@@ -69,41 +55,45 @@ public abstract class VmObjectLike extends VmValue {
* Returns the parent object in the prototype chain. For each concrete subclass X of VmObjectLike, * Returns the parent object in the prototype chain. For each concrete subclass X of VmObjectLike,
* the exact return type of this method is `X|VmTyped`. * the exact return type of this method is `X|VmTyped`.
*/ */
public abstract @Nullable VmObjectLike getParent(); @Nullable
VmObjectLike getParent();
/** Always prefer this method over `getMembers().containsKey(key)`. */ /** Always prefer this method over `getMembers().containsKey(key)`. */
@TruffleBoundary boolean hasMember(Object key);
public abstract boolean hasMember(Object key);
/** Always prefer this method over `getMembers().get(key)`. */ /** Always prefer this method over `getMembers().get(key)`. */
@TruffleBoundary @Nullable
public abstract @Nullable ObjectMember getMember(Object key); ObjectMember getMember(Object key);
/** Returns the declared members of this object. */ /** Returns the declared members of this object. */
public abstract UnmodifiableEconomicMap<Object, ObjectMember> getMembers(); UnmodifiableEconomicMap<Object, ObjectMember> getMembers();
/** /**
* Reads from the properties cache for this object. The cache contains the values of all members * Reads from the properties cache for this object. The cache contains the values of all members
* defined in this object or an ancestor thereof which have been requested with this object as the * defined in this object or an ancestor thereof which have been requested with this object as the
* receiver. * receiver.
*/ */
@TruffleBoundary @Nullable
public abstract @Nullable Object getCachedValue(Object key); Object getCachedValue(Object key);
/** /**
* Writes to the properties cache for this object. The cache contains the values of all members * Writes to the properties cache for this object. The cache contains the values of all members
* defined in this object or an ancestor thereof which have been requested with this object as the * defined in this object or an ancestor thereof which have been requested with this object as the
* receiver. * receiver.
*/ */
@TruffleBoundary void setCachedValue(Object key, Object value);
public abstract void setCachedValue(Object key, Object value);
/** /**
* Prefer this method over {@link #getCachedValue} if the value is not required. (There is no * Prefer this method over {@link #getCachedValue} if the value is not required. (There is no
* point in calling this method to determine whether to call {@link #getCachedValue}.) * point in calling this method to determine whether to call {@link #getCachedValue}.)
*/ */
@TruffleBoundary boolean hasCachedValue(Object key);
public abstract boolean hasCachedValue(Object key);
/** Returns a cursor for iterating over all cached values in this object. */
MapCursor<Object, Object> getCachedValueEntries();
/** Returns the number of cached values in this object. */
int getCachedValueCount();
/** /**
* Iterates over member definitions and their values in order of their definition, from the top of * Iterates over member definitions and their values in order of their definition, from the top of
@@ -118,15 +108,15 @@ public abstract class VmObjectLike extends VmValue {
* remaining members are not visited, and `false` is returned. Otherwise, all members are visited, * remaining members are not visited, and `false` is returned. Otherwise, all members are visited,
* and `true` is returned. * and `true` is returned.
*/ */
public abstract boolean iterateMemberValues(MemberValueConsumer consumer); boolean iterateMemberValues(MemberValueConsumer consumer);
/** /**
* Same as {@link #iterateMemberValues} except that it first performs a shallow {@link #force}. As * Same as {@link #iterateMemberValues} except that it first performs a shallow {@link #force}. As
* a consequence, values passed to {@code consumer} are guaranteed to be non-null. * a consequence, values passed to {@code consumer} are guaranteed to be non-null.
*/ */
public abstract boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer); boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer);
public abstract boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer); boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer);
/** /**
* Iterates over member definitions in order of their definition, from the top of the prototype * Iterates over member definitions in order of their definition, from the top of the prototype
@@ -135,19 +125,20 @@ public abstract class VmObjectLike extends VmValue {
* members are not visited, and `false` is returned. Otherwise, all members are visited, and * members are not visited, and `false` is returned. Otherwise, all members are visited, and
* `true` is returned. * `true` is returned.
*/ */
public abstract boolean iterateMembers(BiFunction<Object, ObjectMember, Boolean> consumer); boolean iterateMembers(BiFunction<Object, ObjectMember, Boolean> consumer);
/** Forces shallow or recursive (deep) evaluation of this object. */ /** Forces shallow or recursive (deep) evaluation of this object. */
public abstract void force(boolean allowUndefinedValues, boolean recurse); void force(boolean allowUndefinedValues, boolean recurse);
/** /**
* Exports this object to an external representation. Does not export local, hidden, or external * Exports this object to an external representation. Does not export local, hidden, or external
* properties * properties
*/ */
public abstract Object export(); @Override
Object export();
@FunctionalInterface @FunctionalInterface
public interface MemberValueConsumer { interface MemberValueConsumer {
/** /**
* Returns true if {@link #iterateMemberValues} should continue calling this method for the * Returns true if {@link #iterateMemberValues} should continue calling this method for the
* remaining members, and false otherwise. * remaining members, and false otherwise.
@@ -156,7 +147,7 @@ public abstract class VmObjectLike extends VmValue {
} }
@FunctionalInterface @FunctionalInterface
public interface ForcedMemberValueConsumer { interface ForcedMemberValueConsumer {
/** /**
* Returns true if {@link #forceAndIterateMemberValues} should continue calling this method for * Returns true if {@link #forceAndIterateMemberValues} should continue calling this method for
* the remaining members, and false otherwise. * the remaining members, and false otherwise.
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ import org.pkl.core.Pair;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@ValueType @ValueType
public final class VmPair extends VmValue implements Iterable<Object> { public final class VmPair implements VmValue, Iterable<Object> {
private final Object first; private final Object first;
private final Object second; private final Object second;
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ import org.pkl.core.ValueFormatter;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@ValueType @ValueType
public final class VmRegex extends VmValue { public final class VmRegex implements VmValue {
private final Pattern pattern; private final Pattern pattern;
public VmRegex(Pattern pattern) { public VmRegex(Pattern pattern) {
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -36,7 +36,7 @@ import org.pkl.core.ast.type.TypeNode.UnknownTypeNode;
import org.pkl.core.util.LateInit; import org.pkl.core.util.LateInit;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public final class VmTypeAlias extends VmValue { public final class VmTypeAlias implements VmValue {
private final SourceSection sourceSection; private final SourceSection sourceSection;
private final SourceSection headerSection; private final SourceSection headerSection;
private final SourceSection @Nullable [] docComment; private final SourceSection @Nullable [] docComment;
@@ -19,6 +19,9 @@ import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.instrumentation.InstrumentableNode.WrapperNode; import com.oracle.truffle.api.instrumentation.InstrumentableNode.WrapperNode;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.api.object.Shape;
import org.graalvm.collections.EconomicMap; import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.UnmodifiableEconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.Composite; import org.pkl.core.Composite;
@@ -34,14 +37,99 @@ import org.pkl.core.util.Nullable;
public final class VmTyped extends VmObject { public final class VmTyped extends VmObject {
@CompilationFinal @LateInit private VmClass clazz; @CompilationFinal @LateInit private VmClass clazz;
/**
* Creates a new VmTyped with the class's instance shape.
*
* @param enclosingFrame the frame that was active when this object was instantiated
* @param parent the parent in the prototype chain, or null
* @param clazz the class of this object, or null if it will be late-initialized
* @param members the declared members of this object
*/
public VmTyped( public VmTyped(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
@Nullable VmTyped parent, @Nullable VmTyped parent,
// null -> will be initialized using lateInitVmClass() later // null -> will be initialized using lateInitVmClass() later
@Nullable VmClass clazz, @Nullable VmClass clazz,
UnmodifiableEconomicMap<Object, ObjectMember> members) { UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, parent, members); this(enclosingFrame, parent, clazz, members, getShapeForClass(clazz));
}
/**
* Creates a new VmTyped with a custom shape.
*
* <p>This constructor is used when a specific shape is needed, such as when amending an object
* where the shape may differ from the class's base instance shape.
*
* @param enclosingFrame the frame that was active when this object was instantiated
* @param parent the parent in the prototype chain, or null
* @param clazz the class of this object, or null if it will be late-initialized
* @param members the declared members of this object
* @param shape the Truffle shape for this object's cached value storage
*/
public VmTyped(
MaterializedFrame enclosingFrame,
@Nullable VmTyped parent,
@Nullable VmClass clazz,
UnmodifiableEconomicMap<Object, ObjectMember> members,
Shape shape) {
super(shape, enclosingFrame, parent, members);
this.clazz = clazz; this.clazz = clazz;
// pre-allocate cache slots for all members to stabilize the shape
preallocateCacheSlots(members);
}
/**
* Pre-allocates cache slots for all members by putting null values. This creates shape
* transitions upfront so all instances share the same final shape.
*/
private void preallocateCacheSlots(UnmodifiableEconomicMap<Object, ObjectMember> members) {
var library = DynamicObjectLibrary.getUncached();
var cursor = members.getEntries();
while (cursor.advance()) {
library.put(this, cursor.getKey(), null);
}
}
private static Shape getShapeForClass(@Nullable VmClass clazz) {
return clazz != null ? clazz.getInstanceShape() : PklShape.getRootShape();
}
/** Returns this object for cached value storage. */
public DynamicObject getCachedValuesStorage() {
return this;
}
/**
* Gets a cached value using the provided library for PE-optimized access.
*
* @param key the property key
* @param library the DynamicObjectLibrary to use (should be cached via @CachedLibrary)
* @return the cached value, or null if not present
*/
public @Nullable Object getCachedValue(Object key, DynamicObjectLibrary library) {
return library.getOrDefault(this, key, null);
}
/**
* Sets a cached value using the provided library for PE-optimized access.
*
* @param key the property key
* @param value the value to cache
* @param library the DynamicObjectLibrary to use (should be cached via @CachedLibrary)
*/
public void setCachedValue(Object key, Object value, DynamicObjectLibrary library) {
library.put(this, key, value);
}
/**
* Checks if a cached value exists using the provided library for PE-optimized access.
*
* @param key the property key
* @param library the DynamicObjectLibrary to use (should be cached via @CachedLibrary)
* @return true if a value is cached for this key
*/
public boolean hasCachedValue(Object key, DynamicObjectLibrary library) {
return library.containsKey(this, key);
} }
public void lateInitVmClass(VmClass clazz) { public void lateInitVmClass(VmClass clazz) {
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -17,30 +17,30 @@ package org.pkl.core.runtime;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public abstract class VmValue { public interface VmValue {
public abstract VmClass getVmClass(); VmClass getVmClass();
public VmTyped getPrototype() { default VmTyped getPrototype() {
return getVmClass().getPrototype(); return getVmClass().getPrototype();
} }
public boolean isPrototype() { default boolean isPrototype() {
return false; return false;
} }
public boolean isDynamic() { default boolean isDynamic() {
return this instanceof VmDynamic; return this instanceof VmDynamic;
} }
public boolean isListing() { default boolean isListing() {
return this instanceof VmListing; return this instanceof VmListing;
} }
public boolean isMapping() { default boolean isMapping() {
return this instanceof VmMapping; return this instanceof VmMapping;
} }
public boolean isTyped() { default boolean isTyped() {
return this instanceof VmTyped; return this instanceof VmTyped;
} }
@@ -48,21 +48,21 @@ public abstract class VmValue {
* Tells if this value is a {@link VmCollection}, {@link VmListing}, or {@link VmDynamic} with * Tells if this value is a {@link VmCollection}, {@link VmListing}, or {@link VmDynamic} with
* {@link VmDynamic#hasElements() elements}. * {@link VmDynamic#hasElements() elements}.
*/ */
public boolean isSequence() { default boolean isSequence() {
return false; return false;
} }
/** Forces recursive (deep) evaluation of this value. */ /** Forces recursive (deep) evaluation of this value. */
public abstract void force(boolean allowUndefinedValues); void force(boolean allowUndefinedValues);
public abstract Object export(); Object export();
public abstract void accept(VmValueVisitor visitor); void accept(VmValueVisitor visitor);
public abstract <T> T accept(VmValueConverter<T> converter, Iterable<Object> path); <T> T accept(VmValueConverter<T> converter, Iterable<Object> path);
/** Forces recursive (deep) evaluation of the given value. */ /** Forces recursive (deep) evaluation of the given value. */
public static void force(Object value, boolean allowUndefinedValues) { static void force(Object value, boolean allowUndefinedValues) {
if (value instanceof VmValue vmValue) { if (value instanceof VmValue vmValue) {
vmValue.force(allowUndefinedValues); vmValue.force(allowUndefinedValues);
} }
@@ -72,7 +72,7 @@ public abstract class VmValue {
* Used to export values other than object member values. Such values aren't `@Nullable` (but can * Used to export values other than object member values. Such values aren't `@Nullable` (but can
* be `VmNull`). * be `VmNull`).
*/ */
public static Object export(Object value) { static Object export(Object value) {
if (value instanceof VmValue vmValue) { if (value instanceof VmValue vmValue) {
return vmValue.export(); return vmValue.export();
} }
@@ -80,14 +80,10 @@ public abstract class VmValue {
} }
/** Used to export object member values. Such values are `null` if they haven't been forced. */ /** Used to export object member values. Such values are `null` if they haven't been forced. */
public static @Nullable Object exportNullable(@Nullable Object value) { static @Nullable Object exportNullable(@Nullable Object value) {
if (value instanceof VmValue vmValue) { if (value instanceof VmValue vmValue) {
return vmValue.export(); return vmValue.export();
} }
return value; return value;
} }
/** Enables calling `vmValue.equals()` when not behind a Truffle boundary. */
@Override
public abstract boolean equals(Object obj);
} }
@@ -0,0 +1,65 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
/**
* A {@link MapCursor} implementation that iterates over the properties of a {@link DynamicObject}.
*
* <p>This cursor provides allocation-free iteration over DynamicObject properties by caching the
* key array at construction time and accessing values on demand.
*/
public final class DynamicObjectMapCursor implements MapCursor<Object, Object> {
private final DynamicObject object;
private final DynamicObjectLibrary library;
private final Object[] keys;
private int index = -1;
/**
* Creates a cursor for iterating over the given DynamicObject's properties.
*
* @param object the DynamicObject to iterate over
*/
public DynamicObjectMapCursor(DynamicObject object) {
this.object = object;
this.library = DynamicObjectLibrary.getUncached();
this.keys = library.getKeyArray(object);
}
@Override
public boolean advance() {
index++;
return index < keys.length;
}
@Override
public Object getKey() {
if (index < 0 || index >= keys.length) {
throw new IllegalStateException("Cursor not positioned on a valid entry");
}
return keys[index];
}
@Override
public Object getValue() {
if (index < 0 || index >= keys.length) {
throw new IllegalStateException("Cursor not positioned on a valid entry");
}
return library.getOrDefault(object, keys[index], null);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util;
/** A cursor that iterates over zero entries. */
public final class EmptyMapCursor<K, V> implements MapCursor<K, V> {
private static final EmptyMapCursor<Object, Object> INSTANCE = new EmptyMapCursor<>();
private EmptyMapCursor() {}
/** Returns the singleton empty cursor instance. */
@SuppressWarnings("unchecked")
public static <K, V> MapCursor<K, V> instance() {
return (MapCursor<K, V>) INSTANCE;
}
@Override
public boolean advance() {
return false;
}
@Override
public K getKey() {
throw new IllegalStateException("Cannot get key from empty cursor");
}
@Override
public V getValue() {
throw new IllegalStateException("Cannot get value from empty cursor");
}
}
@@ -0,0 +1,40 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util;
/** A cursor for iterating over map entries without allocating Entry objects. */
public interface MapCursor<K, V> {
/**
* Advances the cursor to the next entry.
*
* @return true if there is a next entry, false if iteration is complete
*/
boolean advance();
/**
* Returns the key at the current cursor position.
*
* @throws IllegalStateException if called before {@link #advance()} or after it returns false
*/
K getKey();
/**
* Returns the value at the current cursor position.
*
* @throws IllegalStateException if called before {@link #advance()} or after it returns false
*/
V getValue();
}