diff --git a/buildSrc/src/main/kotlin/NativeImageBuild.kt b/buildSrc/src/main/kotlin/NativeImageBuild.kt index da66a996..02e17d63 100644 --- a/buildSrc/src/main/kotlin/NativeImageBuild.kt +++ b/buildSrc/src/main/kotlin/NativeImageBuild.kt @@ -55,13 +55,12 @@ abstract class NativeImageBuild : DefaultTask() { @get:Inject protected abstract val execOperations: ExecOperations - private val graalVm: Provider = - arch.map { a -> - when (a) { - Architecture.AMD64 -> buildInfo.graalVmAmd64 - Architecture.AARCH64 -> buildInfo.graalVmAarch64 - } + private val graalVm: Provider = arch.map { a -> + when (a) { + Architecture.AMD64 -> buildInfo.graalVmAmd64 + Architecture.AARCH64 -> buildInfo.graalVmAarch64 } + } private val buildInfo: BuildInfo = project.extensions.getByType(BuildInfo::class.java) @@ -132,7 +131,7 @@ abstract class NativeImageBuild : DefaultTask() { add(imageName.get()) // the actual limit (currently) used by native-image is this number + 1400 (idea is to // compensate for Truffle's own nodes) - add("-H:MaxRuntimeCompileMethods=1800") + add("-H:MaxRuntimeCompileMethods=2000") add("-H:+EnforceMaxRuntimeCompileMethods") add("--enable-url-protocols=http,https") add("-H:+ReportExceptionStackTraces") @@ -151,10 +150,9 @@ abstract class NativeImageBuild : DefaultTask() { } // native-image rejects non-existing class path entries -> filter add("--class-path") - val pathInput = - classpath.filter { - it.exists() && !exclusions.any { exclude -> it.name.contains(exclude) } - } + val pathInput = classpath.filter { + it.exists() && !exclusions.any { exclude -> it.name.contains(exclude) } + } add(pathInput.asPath) // make sure dev machine stays responsive (15% slowdown on my laptop) val processors = diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java index e7c18a8b..97379229 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java @@ -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"); * 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 org.pkl.core.ast.ExpressionNode; 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; -/** 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. + * + *

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. + * + *

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 { private final ObjectMember property; private final int levelsUp; @@ -63,15 +73,17 @@ public final class ReadLocalPropertyNode extends ExpressionNode { 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."; - var objReceiver = (VmObjectLike) receiver; - var result = objReceiver.getCachedValue(property); + // Use the local property cache instead of DynamicObject storage + // to avoid shape transitions from ObjectMember keys + var objReceiver = (VmObject) receiver; + var result = objReceiver.getLocalCachedValue(property); if (result == null) { result = callNode.call(objReceiver, owner, property.getName()); - objReceiver.setCachedValue(property, result); + objReceiver.setLocalCachedValue(property, result); } return result; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java index e9cda6af..6554553b 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java @@ -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"); * 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.TruffleBoundary; 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.IndirectCallNode; import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.object.DynamicObjectLibrary; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.MemberLookupMode; @@ -61,6 +64,27 @@ public abstract class ReadPropertyNode extends ExpressionNode { 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 // efficient way. See: // 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( Object receiver, @Cached("getVmObjectSubclassOrNull(receiver)") Class cachedClass, - @Cached("create()") IndirectCallNode callNode) { + @Cached("create()") @Shared("callNode") IndirectCallNode callNode) { var object = cachedClass.cast(receiver); checkConst(object); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/PklShape.java b/pkl-core/src/main/java/org/pkl/core/runtime/PklShape.java new file mode 100644 index 00000000..a94e422f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/PklShape.java @@ -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. + * + *

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; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java index 3a80fc43..2c34ed63 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java @@ -104,7 +104,7 @@ public final class TestRunner { if (factValue == Boolean.FALSE) { if (PowerAssertions.isEnabled()) { try (var valueTracker = valueTrackerFactory.create()) { - listing.cachedValues.clear(); + listing.cleanAllCachedValues(); VmUtils.readMember(listing, idx); var failure = factFailure( diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmBytes.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmBytes.java index 3fb908c6..a2962ed9 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmBytes.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmBytes.java @@ -29,7 +29,7 @@ import org.pkl.core.util.ByteArrayUtils; import org.pkl.core.util.Nullable; @ValueType -public final class VmBytes extends VmValue implements Iterable { +public final class VmBytes implements VmValue, Iterable { private @Nullable VmList vmList; private @Nullable String base64; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java index 7d1e3880..5d026afd 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -19,6 +19,8 @@ import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Idempotent; 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 java.util.*; import java.util.function.*; @@ -43,7 +45,7 @@ import org.pkl.core.util.Nullable; // The currently implemented (and likely insufficient) solution is to // * deeply force standard library modules at initialization time. // * 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 headerSection; private final SourceSection @Nullable [] docComment; @@ -123,6 +125,16 @@ public final class VmClass extends VmValue { 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( SourceSection sourceSection, SourceSection headerSection, @@ -697,6 +709,41 @@ public final class VmClass extends VmValue { } } + /** + * Returns the Truffle Shape for instances of this class. + * + *

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. + * + *

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 * inheritance hierarchy. diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java index 1cc5fce4..d5434521 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java @@ -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"); * 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 org.organicdesign.fp.xform.Xform; -public abstract class VmCollection extends VmValue implements Iterable { +public abstract class VmCollection implements VmValue, Iterable { public interface Builder { void add(Object element); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java index a23ae322..3308235d 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java @@ -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"); * 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; @ValueType -public final class VmDataSize extends VmValue implements Comparable { +public final class VmDataSize implements VmValue, Comparable { private static final Map UNITS = Map.ofEntries( entry(Identifier.B, DataSizeUnit.BYTES), diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java index 655910fa..691d57f9 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java @@ -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"); * 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; @ValueType -public final class VmDuration extends VmValue implements Comparable { +public final class VmDuration implements VmValue, Comparable { private static final Map UNITS = Map.of( Identifier.NS, DurationUnit.NANOS, diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java index e10938d1..af813690 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java @@ -28,6 +28,8 @@ import org.pkl.core.util.EconomicMaps; public final class VmDynamic extends VmObject { private int cachedRegularMemberCount = -1; + private final int length; + private static final class EmptyHolder { private static final VmDynamic EMPTY = new VmDynamic( @@ -37,8 +39,6 @@ public final class VmDynamic extends VmObject { 0); } - private final int length; - public static VmDynamic empty() { return EmptyHolder.EMPTY; } @@ -48,7 +48,7 @@ public final class VmDynamic extends VmObject { VmObject parent, UnmodifiableEconomicMap members, int length) { - super(enclosingFrame, Objects.requireNonNull(parent), members); + super(PklShape.getRootShape(), enclosingFrame, Objects.requireNonNull(parent), members); this.length = length; } @@ -75,8 +75,7 @@ public final class VmDynamic extends VmObject { @Override @TruffleBoundary public PObject export() { - var properties = - CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + var properties = CollectionUtils.newLinkedHashMap(getCachedValueCount()); iterateMemberValues( (key, member, value) -> { @@ -109,7 +108,7 @@ public final class VmDynamic extends VmObject { other.force(false); if (getRegularMemberCount() != other.getRegularMemberCount()) return false; - var cursor = cachedValues.getEntries(); + var cursor = getCachedValueEntries(); while (cursor.advance()) { Object key = cursor.getKey(); if (isHiddenOrLocalProperty(key)) continue; @@ -130,7 +129,7 @@ public final class VmDynamic extends VmObject { force(false); var result = 0; - var cursor = cachedValues.getEntries(); + var cursor = getCachedValueEntries(); while (cursor.advance()) { var key = cursor.getKey(); @@ -150,8 +149,9 @@ public final class VmDynamic extends VmObject { if (cachedRegularMemberCount != -1) return cachedRegularMemberCount; var result = 0; - for (var key : cachedValues.getKeys()) { - if (!isHiddenOrLocalProperty(key)) result += 1; + var cursor = getCachedValueEntries(); + while (cursor.advance()) { + if (!isHiddenOrLocalProperty(cursor.getKey())) result += 1; } cachedRegularMemberCount = result; return result; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java index c3b13c0b..afffd692 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java @@ -23,9 +23,14 @@ import org.graalvm.collections.UnmodifiableEconomicMap; import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.EmptyMapCursor; +import org.pkl.core.util.MapCursor; 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 int paramCount; @@ -37,13 +42,28 @@ public final class VmFunction extends VmObjectLike { int paramCount, PklRootNode rootNode, @Nullable Object extraStorage) { - super(enclosingFrame); + this.enclosingFrame = enclosingFrame; this.thisValue = thisValue; this.paramCount = paramCount; this.rootNode = rootNode; 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() { return rootNode.getCallTarget(); } @@ -123,17 +143,27 @@ public final class VmFunction extends VmObjectLike { } @Override - public boolean iterateMemberValues(MemberValueConsumer consumer) { + public MapCursor getCachedValueEntries() { + return EmptyMapCursor.instance(); + } + + @Override + public int getCachedValueCount() { + return 0; + } + + @Override + public boolean iterateMemberValues(VmObjectLike.MemberValueConsumer consumer) { return true; } @Override - public boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer) { + public boolean forceAndIterateMemberValues(VmObjectLike.ForcedMemberValueConsumer consumer) { return true; } @Override - public boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer) { + public boolean iterateAlreadyForcedMemberValues(VmObjectLike.ForcedMemberValueConsumer consumer) { return true; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmIntSeq.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmIntSeq.java index 96ed547a..996dc619 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmIntSeq.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmIntSeq.java @@ -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"); * 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, // kotlin.internal.ProgressionUtil (Apache 2). @ValueType -public final class VmIntSeq extends VmValue implements Iterable { +public final class VmIntSeq implements VmValue, Iterable { public final long start; public final long end; public final long step; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java index 7eed3b66..05399f9d 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java @@ -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"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ public final class VmListing extends VmListingOrMapping { @Override @TruffleBoundary public List export() { - var properties = new ArrayList<>(EconomicMaps.size(cachedValues)); + var properties = new ArrayList<>(getCachedValueCount()); iterateMemberValues( (key, prop, value) -> { @@ -121,7 +121,7 @@ public final class VmListing extends VmListingOrMapping { force(false); other.force(false); - var cursor = cachedValues.getEntries(); + var cursor = getCachedValueEntries(); while (cursor.advance()) { var key = cursor.getKey(); if (key instanceof Identifier) continue; @@ -142,7 +142,7 @@ public final class VmListing extends VmListingOrMapping { force(false); var result = 0; - var cursor = cachedValues.getEntries(); + var cursor = getCachedValueEntries(); while (cursor.advance()) { var key = cursor.getKey(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java index bb3b48c4..1918fd54 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java @@ -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"); * 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.ObjectMember; import org.pkl.core.ast.type.TypeNode; -import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.Nullable; public abstract class VmListingOrMapping extends VmObject { @@ -32,24 +31,24 @@ public abstract class VmListingOrMapping extends VmObject { private final @Nullable Object typeCheckReceiver; private final @Nullable VmObjectLike typeCheckOwner; - public VmListingOrMapping( + protected VmListingOrMapping( MaterializedFrame enclosingFrame, @Nullable VmObject parent, UnmodifiableEconomicMap members) { - super(enclosingFrame, parent, members); + super(PklShape.getRootShape(), enclosingFrame, parent, members); typeCastNode = null; typeCheckReceiver = null; typeCheckOwner = null; } - public VmListingOrMapping( + protected VmListingOrMapping( MaterializedFrame enclosingFrame, @Nullable VmObject parent, UnmodifiableEconomicMap members, ListingOrMappingTypeCastNode typeCastNode, Object typeCheckReceiver, VmObjectLike typeCheckOwner) { - super(enclosingFrame, parent, members); + super(PklShape.getRootShape(), enclosingFrame, parent, members); this.typeCastNode = typeCastNode; this.typeCheckReceiver = typeCheckReceiver; this.typeCheckOwner = typeCheckOwner; @@ -88,7 +87,7 @@ public abstract class VmListingOrMapping extends VmObject { @Override @TruffleBoundary 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 (result != null || !members.isEmpty()) return result; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmMap.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmMap.java index 8a6b7ff2..56a2ddf9 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmMap.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmMap.java @@ -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.MutRrbt; -public final class VmMap extends VmValue implements Iterable> { +public final class VmMap implements VmValue, Iterable> { public static final VmMap EMPTY = new VmMap(PersistentHashMap.empty(), RrbTree.empty()); private final ImMap map; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java index fe160ddb..05a659ec 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java @@ -93,7 +93,7 @@ public final class VmMapping extends VmListingOrMapping { @Override @TruffleBoundary public Map export() { - var properties = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + var properties = CollectionUtils.newLinkedHashMap(getCachedValueCount()); iterateMemberValues( (key, prop, value) -> { @@ -128,7 +128,7 @@ public final class VmMapping extends VmListingOrMapping { other.force(false); if (getLength() != other.getLength()) return false; - var cursor = cachedValues.getEntries(); + var cursor = getCachedValueEntries(); while (cursor.advance()) { Object key = cursor.getKey(); if (key instanceof Identifier) continue; @@ -149,7 +149,7 @@ public final class VmMapping extends VmListingOrMapping { force(false); var result = 0; - var cursor = cachedValues.getEntries(); + var cursor = getCachedValueEntries(); while (cursor.advance()) { var key = cursor.getKey(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmNull.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmNull.java index 15704705..93995f90 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmNull.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmNull.java @@ -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"); * 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; @ValueType -public final class VmNull extends VmValue { +public final class VmNull implements VmValue { private static final VmNull WITHOUT_DEFAULT = new VmNull(null); // worthwhile to create this lazily? diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java index 3987d9ac..742a1c1c 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java @@ -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"); * 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.TruffleBoundary; 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.function.BiFunction; -import org.graalvm.collections.EconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.util.CollectionUtils; +import org.pkl.core.util.DynamicObjectMapCursor; import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.MapCursor; import org.pkl.core.util.Nullable; -/** Corresponds to `pkl.base#Object`. */ -public abstract class VmObject extends VmObjectLike { +/** + * Corresponds to `pkl.base#Object`. + * + *

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; protected final UnmodifiableEconomicMap members; - protected final EconomicMap cachedValues; protected int cachedHash; private boolean forced; - public VmObject( - MaterializedFrame enclosingFrame, - @Nullable VmObject parent, - UnmodifiableEconomicMap members, - EconomicMap cachedValues) { - super(enclosingFrame); - this.parent = parent; - this.members = members; - this.cachedValues = cachedValues; + /** + * Separate cache for local property values. + * + *

This is kept separate from the DynamicObject storage to avoid shape transitions. + */ + private @Nullable IdentityHashMap localPropertyCache; - assert parent != this; - } - - public VmObject( + protected VmObject( + Shape shape, MaterializedFrame enclosingFrame, @Nullable VmObject parent, UnmodifiableEconomicMap 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) { @@ -67,11 +94,13 @@ public abstract class VmObject extends VmObjectLike { } @Override + @TruffleBoundary public final boolean hasMember(Object key) { return EconomicMaps.containsKey(members, key); } @Override + @TruffleBoundary public final @Nullable ObjectMember getMember(Object key) { return EconomicMaps.get(members, key); } @@ -82,23 +111,81 @@ public abstract class VmObject extends VmObjectLike { } @Override + @TruffleBoundary public @Nullable Object getCachedValue(Object key) { - return EconomicMaps.get(cachedValues, key); - } - - @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); + return DynamicObjectLibrary.getUncached().getOrDefault(this, key, null); } @Override @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 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<>(); return iterateMembers( (key, member) -> { @@ -112,14 +199,16 @@ public abstract class VmObject extends VmObjectLike { @Override @TruffleBoundary - public final boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer) { + public final boolean forceAndIterateMemberValues( + VmObjectLike.ForcedMemberValueConsumer consumer) { force(false, false); return iterateAlreadyForcedMemberValues(consumer); } @Override @TruffleBoundary - public final boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer) { + public final boolean iterateAlreadyForcedMemberValues( + VmObjectLike.ForcedMemberValueConsumer consumer) { var visited = new HashSet<>(); return iterateMembers( (key, member) -> { @@ -158,6 +247,9 @@ public abstract class VmObject extends VmObjectLike { if (recurse) forced = true; + // use cached call node from this object's class to avoid getUncached() overhead + var callNode = getVmClass().getCachedCallNode(); + try { for (VmObjectLike owner = this; owner != null; owner = owner.getParent()) { var cursor = EconomicMaps.getEntries(owner.getMembers()); @@ -174,7 +266,7 @@ public abstract class VmObject extends VmObjectLike { var memberValue = getCachedValue(memberKey); if (memberValue == null) { try { - memberValue = VmUtils.doReadMember(this, owner, memberKey, member); + memberValue = VmUtils.doReadMember(this, owner, memberKey, member, true, callNode); } catch (VmUndefinedValueException e) { if (!allowUndefinedValues) throw e; continue; @@ -208,7 +300,7 @@ public abstract class VmObject extends VmObjectLike { */ @TruffleBoundary protected final Map exportMembers() { - var result = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + var result = CollectionUtils.newLinkedHashMap(getCachedValueCount()); iterateMemberValues( (key, member, value) -> { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java index b6f087aa..b1087fba 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java @@ -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"); * you may not use this file except in compliance with the License. @@ -15,53 +15,39 @@ */ package org.pkl.core.runtime; -import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; import java.util.function.BiFunction; import org.graalvm.collections.UnmodifiableEconomicMap; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.util.MapCursor; import org.pkl.core.util.Nullable; /** * Corresponds to `pkl.base#Object|pkl.base#Function`. The lexical scope is a chain of * `VmObjectLike` instances. */ -public abstract class VmObjectLike extends VmValue { +public interface VmObjectLike extends VmValue { /** 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) { - this.enclosingFrame = enclosingFrame; + void setExtraStorage(@Nullable Object extraStorage); + + default boolean hasExtraStorage() { + return getExtraStorage() != null; } - public final MaterializedFrame getEnclosingFrame() { - return enclosingFrame; + default @Nullable Object getEnclosingReceiver() { + return VmUtils.getReceiverOrNull(getEnclosingFrame()); } - public final @Nullable Object getEnclosingReceiver() { - return VmUtils.getReceiverOrNull(enclosingFrame); + default @Nullable VmObjectLike getEnclosingOwner() { + return VmUtils.getOwnerOrNull(getEnclosingFrame()); } - public final @Nullable VmObjectLike getEnclosingOwner() { - 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() { + default boolean isModuleObject() { 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, * 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)`. */ - @TruffleBoundary - public abstract boolean hasMember(Object key); + boolean hasMember(Object key); /** Always prefer this method over `getMembers().get(key)`. */ - @TruffleBoundary - public abstract @Nullable ObjectMember getMember(Object key); + @Nullable + ObjectMember getMember(Object key); /** Returns the declared members of this object. */ - public abstract UnmodifiableEconomicMap getMembers(); + UnmodifiableEconomicMap getMembers(); /** * 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 * receiver. */ - @TruffleBoundary - public abstract @Nullable Object getCachedValue(Object key); + @Nullable + Object getCachedValue(Object key); /** * 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 * receiver. */ - @TruffleBoundary - public abstract void setCachedValue(Object key, Object value); + void setCachedValue(Object key, Object value); /** * 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}.) */ - @TruffleBoundary - public abstract boolean hasCachedValue(Object key); + boolean hasCachedValue(Object key); + + /** Returns a cursor for iterating over all cached values in this object. */ + MapCursor 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 @@ -118,15 +108,15 @@ public abstract class VmObjectLike extends VmValue { * remaining members are not visited, and `false` is returned. Otherwise, all members are visited, * 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 * 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 @@ -135,19 +125,20 @@ public abstract class VmObjectLike extends VmValue { * members are not visited, and `false` is returned. Otherwise, all members are visited, and * `true` is returned. */ - public abstract boolean iterateMembers(BiFunction consumer); + boolean iterateMembers(BiFunction consumer); /** 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 * properties */ - public abstract Object export(); + @Override + Object export(); @FunctionalInterface - public interface MemberValueConsumer { + interface MemberValueConsumer { /** * Returns true if {@link #iterateMemberValues} should continue calling this method for the * remaining members, and false otherwise. @@ -156,7 +147,7 @@ public abstract class VmObjectLike extends VmValue { } @FunctionalInterface - public interface ForcedMemberValueConsumer { + interface ForcedMemberValueConsumer { /** * Returns true if {@link #forceAndIterateMemberValues} should continue calling this method for * the remaining members, and false otherwise. diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmPair.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmPair.java index 480300e6..33683241 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmPair.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmPair.java @@ -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"); * 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; @ValueType -public final class VmPair extends VmValue implements Iterable { +public final class VmPair implements VmValue, Iterable { private final Object first; private final Object second; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmRegex.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmRegex.java index d00ad238..df33def9 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmRegex.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmRegex.java @@ -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"); * 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; @ValueType -public final class VmRegex extends VmValue { +public final class VmRegex implements VmValue { private final Pattern pattern; public VmRegex(Pattern pattern) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java index 8ed4889c..6c26c4f4 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java @@ -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"); * 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.Nullable; -public final class VmTypeAlias extends VmValue { +public final class VmTypeAlias implements VmValue { private final SourceSection sourceSection; private final SourceSection headerSection; private final SourceSection @Nullable [] docComment; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java index a9e70938..cce46a20 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java @@ -19,6 +19,9 @@ import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; 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.UnmodifiableEconomicMap; import org.pkl.core.Composite; @@ -34,14 +37,99 @@ import org.pkl.core.util.Nullable; public final class VmTyped extends VmObject { @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( MaterializedFrame enclosingFrame, @Nullable VmTyped parent, // null -> will be initialized using lateInitVmClass() later @Nullable VmClass clazz, UnmodifiableEconomicMap members) { - super(enclosingFrame, parent, members); + this(enclosingFrame, parent, clazz, members, getShapeForClass(clazz)); + } + + /** + * Creates a new VmTyped with a custom shape. + * + *

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 members, + Shape shape) { + super(shape, enclosingFrame, parent, members); 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 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) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java index 2b0dee4d..a801c328 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java @@ -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"); * 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; -public abstract class VmValue { - public abstract VmClass getVmClass(); +public interface VmValue { + VmClass getVmClass(); - public VmTyped getPrototype() { + default VmTyped getPrototype() { return getVmClass().getPrototype(); } - public boolean isPrototype() { + default boolean isPrototype() { return false; } - public boolean isDynamic() { + default boolean isDynamic() { return this instanceof VmDynamic; } - public boolean isListing() { + default boolean isListing() { return this instanceof VmListing; } - public boolean isMapping() { + default boolean isMapping() { return this instanceof VmMapping; } - public boolean isTyped() { + default boolean isTyped() { 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 * {@link VmDynamic#hasElements() elements}. */ - public boolean isSequence() { + default boolean isSequence() { return false; } /** 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 accept(VmValueConverter converter, Iterable path); + T accept(VmValueConverter converter, Iterable path); /** 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) { 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 * be `VmNull`). */ - public static Object export(Object value) { + static Object export(Object value) { if (value instanceof VmValue vmValue) { 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. */ - public static @Nullable Object exportNullable(@Nullable Object value) { + static @Nullable Object exportNullable(@Nullable Object value) { if (value instanceof VmValue vmValue) { return vmValue.export(); } return value; } - - /** Enables calling `vmValue.equals()` when not behind a Truffle boundary. */ - @Override - public abstract boolean equals(Object obj); } diff --git a/pkl-core/src/main/java/org/pkl/core/util/DynamicObjectMapCursor.java b/pkl-core/src/main/java/org/pkl/core/util/DynamicObjectMapCursor.java new file mode 100644 index 00000000..746f7cd0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/util/DynamicObjectMapCursor.java @@ -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}. + * + *

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 { + 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); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/util/EmptyMapCursor.java b/pkl-core/src/main/java/org/pkl/core/util/EmptyMapCursor.java new file mode 100644 index 00000000..9bfa9dc6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/util/EmptyMapCursor.java @@ -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 implements MapCursor { + private static final EmptyMapCursor INSTANCE = new EmptyMapCursor<>(); + + private EmptyMapCursor() {} + + /** Returns the singleton empty cursor instance. */ + @SuppressWarnings("unchecked") + public static MapCursor instance() { + return (MapCursor) 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"); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/util/MapCursor.java b/pkl-core/src/main/java/org/pkl/core/util/MapCursor.java new file mode 100644 index 00000000..4e7ba4c5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/util/MapCursor.java @@ -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 { + /** + * 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(); +}