Move to truffle object model

This commit is contained in:
Islon Scherer
2026-04-15 17:21:29 +02:00
parent 2e0b4a3a97
commit 750e983366
28 changed files with 638 additions and 174 deletions
@@ -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.
*
* <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 {
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;
@@ -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<? extends VmObjectLike> cachedClass,
@Cached("create()") IndirectCallNode callNode) {
@Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
var object = cachedClass.cast(receiver);
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 (PowerAssertions.isEnabled()) {
try (var valueTracker = valueTrackerFactory.create()) {
listing.cachedValues.clear();
listing.cleanAllCachedValues();
VmUtils.readMember(listing, idx);
var failure =
factFailure(
@@ -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<Long> {
public final class VmBytes implements VmValue, Iterable<Long> {
private @Nullable VmList vmList;
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.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.
*
* <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
* 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");
* 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<Object> {
public abstract class VmCollection implements VmValue, Iterable<Object> {
public interface Builder<T extends VmCollection> {
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");
* 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<VmDataSize> {
public final class VmDataSize implements VmValue, Comparable<VmDataSize> {
private static final Map<Identifier, DataSizeUnit> UNITS =
Map.ofEntries(
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");
* 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<VmDuration> {
public final class VmDuration implements VmValue, Comparable<VmDuration> {
private static final Map<Identifier, DurationUnit> UNITS =
Map.of(
Identifier.NS, DurationUnit.NANOS,
@@ -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<Object, ObjectMember> 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.<String, Object>newLinkedHashMap(EconomicMaps.size(cachedValues));
var properties = CollectionUtils.<String, Object>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;
@@ -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<Object, Object> 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;
}
@@ -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<Long> {
public final class VmIntSeq implements VmValue, Iterable<Long> {
public final long start;
public final long end;
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");
* 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<Object> 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();
@@ -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<Object, ObjectMember> 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<Object, ObjectMember> 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;
@@ -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<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());
private final ImMap<Object, Object> map;
@@ -93,7 +93,7 @@ public final class VmMapping extends VmListingOrMapping {
@Override
@TruffleBoundary
public Map<Object, Object> 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();
@@ -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?
@@ -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`.
*
* <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;
protected final UnmodifiableEconomicMap<Object, ObjectMember> members;
protected final EconomicMap<Object, Object> cachedValues;
protected int cachedHash;
private boolean forced;
public VmObject(
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
EconomicMap<Object, Object> cachedValues) {
super(enclosingFrame);
this.parent = parent;
this.members = members;
this.cachedValues = cachedValues;
/**
* Separate cache for local property values.
*
* <p>This is kept separate from the DynamicObject storage to avoid shape transitions.
*/
private @Nullable IdentityHashMap<ObjectMember, Object> localPropertyCache;
assert parent != this;
}
public VmObject(
protected VmObject(
Shape shape,
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
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) {
@@ -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<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<>();
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<String, Object> exportMembers() {
var result = CollectionUtils.<String, Object>newLinkedHashMap(EconomicMaps.size(cachedValues));
var result = CollectionUtils.<String, Object>newLinkedHashMap(getCachedValueCount());
iterateMemberValues(
(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");
* 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<Object, ObjectMember> getMembers();
UnmodifiableEconomicMap<Object, ObjectMember> 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<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
@@ -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<Object, ObjectMember, Boolean> consumer);
boolean iterateMembers(BiFunction<Object, ObjectMember, Boolean> 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.
@@ -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<Object> {
public final class VmPair implements VmValue, Iterable<Object> {
private final Object first;
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");
* 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) {
@@ -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;
@@ -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<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;
// 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) {
@@ -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> 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. */
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);
}
@@ -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();
}