diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java index 0b3c20637..980b22161 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java @@ -15,6 +15,7 @@ */ package org.pkl.core; +import com.oracle.truffle.api.nodes.IndirectCallNode; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -195,7 +196,9 @@ public final class EvaluatorImpl implements Evaluator { (module) -> { var output = VmUtils.readModuleOutput(module); return VmUtils.readFilesProperty( - output, (fileOutput) -> new FileOutputImpl(this, fileOutput)); + output, + (fileOutput) -> new FileOutputImpl(this, fileOutput), + IndirectCallNode.getUncached()); }); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java index c2c7987d8..99ef287c0 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java @@ -19,6 +19,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.NodeChild; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; import org.jspecify.annotations.Nullable; @@ -34,6 +35,9 @@ public abstract class EqualNode extends ExpressionNode { super(sourceSection); } + /** When using this method, pass {@code null} in for {@code leftNode} and {@code rightNode}. */ + public abstract boolean executeWith(VirtualFrame frame, Object left, Object right); + @Specialization protected boolean eval(String left, String right) { return left.equals(right); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java index 07ea357c9..9a685be61 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java @@ -23,22 +23,31 @@ import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.profiles.LoopConditionProfile; import com.oracle.truffle.api.source.SourceSection; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.internal.ReadCursorValueNode; +import org.pkl.core.ast.internal.ReadCursorValueNodeGen; import org.pkl.core.ast.type.TypeNode; import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.Iterators.TruffleIterator; import org.pkl.core.util.ArrayUtils; public abstract class GeneratorForNode extends GeneratorMemberNode { private final FrameDescriptor generatorDescriptor; + private final LoopConditionProfile loopConditionProfile = LoopConditionProfile.create(); @Child private ExpressionNode iterableNode; @Child private @Nullable UnresolvedTypeNode unresolvedKeyTypeNode; @Child private @Nullable UnresolvedTypeNode unresolvedValueTypeNode; @Children private final GeneratorMemberNode[] childNodes; @Child private @Nullable TypeNode keyTypeNode; @Child private @Nullable TypeNode valueTypeNode; + + @Child + private ReadCursorValueNode readCursorValueNode = ReadCursorValueNodeGen.create(sourceSection); + private final int keySlot; private final int valueSlot; private final int[] slotsToCopy; @@ -101,23 +110,29 @@ public abstract class GeneratorForNode extends GeneratorMemberNode { @Specialization protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmList iterable) { - long idx = 0; - for (var element : iterable) { - executeIteration(frame, parent, data, idx++, element); + loopConditionProfile.profileCounted(iterable.getLength()); + for (var idx = 0L; loopConditionProfile.inject(idx < iterable.getLength()); idx++) { + executeIteration(frame, parent, data, idx, iterable.get(idx)); } } @Specialization protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmMap iterable) { - for (var entry : iterable) { + loopConditionProfile.profileCounted(iterable.getLength()); + var iterator = new TruffleIterator<>(iterable); + while (loopConditionProfile.inject(iterator.hasNext())) { + var entry = iterator.next(); executeIteration(frame, parent, data, VmUtils.getKey(entry), VmUtils.getValue(entry)); } } @Specialization protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmSet iterable) { - long idx = 0; - for (var element : iterable) { + var idx = 0L; + loopConditionProfile.profileCounted(iterable.getLength()); + var iterator = new TruffleIterator<>(iterable); + while (loopConditionProfile.inject(iterator.hasNext())) { + var element = iterator.next(); executeIteration(frame, parent, data, idx++, element); } } @@ -125,16 +140,20 @@ public abstract class GeneratorForNode extends GeneratorMemberNode { @Specialization protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmIntSeq iterable) { var length = iterable.getLength(); - for (long key = 0, value = iterable.start; key < length; key++, value += iterable.step) { + loopConditionProfile.profileCounted(iterable.getLength()); + for (long key = 0, value = iterable.start; + loopConditionProfile.inject(key < length); + key++, value += iterable.step) { executeIteration(frame, parent, data, key, value); } } @Specialization protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmBytes iterable) { - long idx = 0; - for (var byt : iterable.getBytes()) { - executeIteration(frame, parent, data, idx++, (long) byt); + var bytes = iterable.getBytes(); + loopConditionProfile.profileCounted(bytes.length); + for (var idx = 0; loopConditionProfile.inject(idx < bytes.length); idx++) { + executeIteration(frame, parent, data, (long) idx, (long) bytes[idx]); } } @@ -150,16 +169,32 @@ public abstract class GeneratorForNode extends GeneratorMemberNode { } private void doEvalObject(VirtualFrame frame, VmObject iterable, Object parent, ObjectData data) { - var materializedFrame = frame.materialize(); - iterable.forceAndIterateMemberValues( - (key, member, value) -> { - var convertedKey = member.isProp() ? key.toString() : key; - // TODO: Executing iteration behind a Truffle boundary is bad for performance. - // This and similar cases will be fixed in an upcoming PR that replaces method - // `(forceAnd)iterateMemberValues` with cursor-based external iterators. - executeIteration(materializedFrame, parent, data, convertedKey, value); - return true; - }); + // TODO: skip types for module objects? + var cursor = iterable.members(); + if (cursor.getLength() == -1) { + doEvalObjectProfiled(frame, parent, data, cursor); + } else { + doEvalObjectCounted(frame, parent, data, cursor); + } + } + + private void doEvalObjectCounted( + VirtualFrame frame, Object parent, ObjectData data, VmObjectCursor cursor) { + loopConditionProfile.profileCounted(cursor.getLength()); + while (loopConditionProfile.inject(cursor.advance())) { + var key = cursor.isProperty() ? cursor.keyToString() : cursor.key(); + var value = readCursorValueNode.execute(frame, cursor); + executeIteration(frame, parent, data, key, value); + } + } + + private void doEvalObjectProfiled( + VirtualFrame frame, Object parent, ObjectData data, VmObjectCursor cursor) { + while (loopConditionProfile.profile(cursor.advance())) { + var key = cursor.isProperty() ? cursor.keyToString() : cursor.key(); + var value = readCursorValueNode.execute(frame, cursor); + executeIteration(frame, parent, data, key, value); + } } @ExplodeLoop diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java index d4b798110..2ad2799c5 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java @@ -23,8 +23,11 @@ import com.oracle.truffle.api.dsl.Fallback; import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.profiles.LoopConditionProfile; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.internal.ReadCursorValueNode; +import org.pkl.core.ast.internal.ReadCursorValueNodeGen; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.runtime.BaseModule; import org.pkl.core.runtime.Identifier; @@ -41,12 +44,18 @@ import org.pkl.core.runtime.VmMap; import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmNull; import org.pkl.core.runtime.VmObject; +import org.pkl.core.runtime.VmObjectCursor; import org.pkl.core.runtime.VmTyped; import org.pkl.core.runtime.VmUtils; @ImportStatic(BaseModule.class) public abstract class GeneratorSpreadNode extends GeneratorMemberNode { + private final LoopConditionProfile loopConditionProfile = LoopConditionProfile.create(); @Child private ExpressionNode iterableNode; + + @Child + private ReadCursorValueNode readCursorValueNode = ReadCursorValueNodeGen.create(sourceSection); + private final boolean nullable; public GeneratorSpreadNode( @@ -189,53 +198,73 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode { } protected void doEvalDynamic(VirtualFrame frame, ObjectData data, VmObject iterable) { - var materializedFrame = frame.materialize(); - iterable.forceAndIterateMemberValues( - (key, member, value) -> { - if (member.isElement()) { - data.addElement(materializedFrame, createMember(member, value), this); - } else { - data.addMember(materializedFrame, key, createMember(member, value), this); - } - return true; - }); + var cursor = iterable.members(); + var counted = isCounted(cursor); + while (advance(cursor, counted)) { + var member = cursor.member(); + var value = readCursorValueNode.execute(frame, cursor); + if (member.isElement()) { + data.addElement(frame, createMember(member, value), this); + } else { + data.addMember(frame, cursor.key(), createMember(member, value), this); + } + } } private void doEvalMapping(VirtualFrame frame, ObjectData data, VmObject iterable) { - var materializedFrame = frame.materialize(); - iterable.forceAndIterateMemberValues( - (key, member, value) -> { - if (member.isElement() || member.isProp()) { - cannotHaveMember(BaseModule.getMappingClass(), member); - } - data.addMember(materializedFrame, key, createMember(member, value), this); - return true; - }); + var cursor = iterable.members(); + var counted = isCounted(cursor); + while (advance(cursor, counted)) { + var member = cursor.member(); + if (member.isElement() || member.isProp()) { + cannotHaveMember(BaseModule.getMappingClass(), member); + } + data.addMember( + frame, + cursor.key(), + createMember(member, readCursorValueNode.execute(frame, cursor)), + this); + } } private void doEvalListing(VirtualFrame frame, ObjectData data, VmObject iterable) { - var materializedFrame = frame.materialize(); - iterable.forceAndIterateMemberValues( - (key, member, value) -> { - if (member.isEntry() || member.isProp()) { - cannotHaveMember(getListingClass(), member); - } - data.addElement(materializedFrame, createMember(member, value), this); - return true; - }); + var cursor = iterable.members(); + var counted = isCounted(cursor); + while (advance(cursor, counted)) { + var member = cursor.member(); + if (member.isEntry() || member.isProp()) { + cannotHaveMember(getListingClass(), member); + } + data.addElement( + frame, createMember(member, readCursorValueNode.execute(frame, cursor)), this); + } } private void doEvalTyped(VirtualFrame frame, VmClass clazz, ObjectData data, VmObject iterable) { - var materializedFrame = frame.materialize(); - iterable.forceAndIterateMemberValues( - (key, member, value) -> { - if (member.isElement() || member.isEntry()) { - cannotHaveMember(clazz, member); - } - checkIsValidTypedProperty(clazz, member); - data.addProperty(materializedFrame, createMember(member, value), this); - return true; - }); + var cursor = iterable.members(); + var counted = isCounted(cursor); + while (advance(cursor, counted)) { + var member = cursor.member(); + if (member.isElement() || member.isEntry()) { + cannotHaveMember(clazz, member); + } + checkIsValidTypedProperty(clazz, member); + data.addProperty( + frame, createMember(member, readCursorValueNode.execute(frame, cursor)), this); + } + } + + private boolean isCounted(VmObjectCursor cursor) { + var length = cursor.getLength(); + if (length == -1) return false; + loopConditionProfile.profileCounted(length); + return true; + } + + private boolean advance(VmObjectCursor cursor, boolean counted) { + return counted + ? loopConditionProfile.inject(cursor.advance()) + : loopConditionProfile.profile(cursor.advance()); } // handles both `List` and `Set` @@ -250,7 +279,7 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode { .withProgramValue("Value", iterable) .build(); } - spreadIterable(frame, data, iterable); + spreadIterable(frame, data, iterable, iterable.getLength()); } private void doEvalMap(VirtualFrame frame, VmClass parent, ObjectData data, VmMap iterable) { @@ -278,7 +307,7 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode { .withProgramValue("Value", iterable) .build(); } - spreadIterable(frame, data, iterable); + spreadIterable(frame, data, iterable, iterable.getLength()); } private void doEvalBytes(VirtualFrame frame, VmClass parent, ObjectData data, VmBytes iterable) { @@ -290,7 +319,7 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode { .withProgramValue("Value", iterable) .build(); } - spreadIterable(frame, data, iterable); + spreadIterable(frame, data, iterable, iterable.getLength()); } private void cannotHaveMember(VmClass clazz, ObjectMember member) { @@ -352,9 +381,11 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode { return result; } - private void spreadIterable(VirtualFrame frame, ObjectData data, Iterable iterable) { + private void spreadIterable( + VirtualFrame frame, ObjectData data, Iterable iterable, long length) { + loopConditionProfile.profileCounted(length); var iterator = new TruffleIterator<>(iterable); - while (iterator.hasNext()) { + while (loopConditionProfile.inject(iterator.hasNext())) { var elem = iterator.next(); var member = VmUtils.createSyntheticObjectElement(String.valueOf(data.length()), elem); data.addElement(frame, member, this); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java index f6441af73..bd5e92d2e 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java @@ -148,7 +148,7 @@ public abstract class ConstantEntriesLiteralNode extends SpecializedObjectLitera "checkIsValidListingAmendment()", "checkMaxListingMemberIndex(0)" }) - protected void evalListingClass(@SuppressWarnings("unused") VmClass parent) { + protected Object evalListingClass(@SuppressWarnings("unused") VmClass parent) { CompilerDirectives.transferToInterpreter(); throw exceptionBuilder().unreachableCode().build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java index 32f33d4c1..829ece78e 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java @@ -34,8 +34,9 @@ import org.pkl.core.runtime.*; import org.pkl.core.util.EconomicMaps; /** - * Object literal that contains both elements and entries (and possibly properties). Example: `new - * foo { "pigeon", [3] = "barn owl" }` + * Object literal that contains both elements and entries (and possibly properties). + * + *

Example: {@code new foo { "pigeon"; [3] = "barn owl" } } */ @ImportStatic({BaseModule.class, VmUtils.class}) public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLiteralNode { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java index c627bc2c6..9f27a97c1 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java @@ -42,6 +42,7 @@ public abstract class ObjectLiteralNode extends ExpressionNode { protected final boolean isCustomThisScope; protected final @Nullable FrameDescriptor parametersDescriptor; @Children protected final UnresolvedTypeNode[] parameterTypes; + @Child protected @Nullable AmendFunctionNode amendFunctionNode; public ObjectLiteralNode( SourceSection sourceSection, diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java index db630a056..4e852e424 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java @@ -16,9 +16,11 @@ package org.pkl.core.ast.expression.member; import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.NodeChild; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.runtime.*; @@ -36,13 +38,14 @@ public abstract class InferParentWithinPropertyNode extends ExpressionNode { } @Specialization(guards = "!owner.isPrototype()") - protected Object evalTypedObject(VirtualFrame frame, VmTyped owner) { + protected Object evalTypedObject( + VirtualFrame frame, VmTyped owner, @Cached("create()") IndirectCallNode callNode) { if (isLocalProperty) { return getLocalPropertyDefaultValue(frame, owner); } try { - var result = VmUtils.readMemberOrNull(owner.getPrototype(), ownPropertyName, false); + var result = VmUtils.readMemberOrNull(owner.getPrototype(), ownPropertyName, false, callNode); assert result != null : "every property has a default"; return result; } catch (VmUndefinedValueException e) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/ReadCursorValueNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/ReadCursorValueNode.java new file mode 100644 index 000000000..7711e0afc --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/ReadCursorValueNode.java @@ -0,0 +1,54 @@ +/* + * 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.ast.internal; + +import com.oracle.truffle.api.dsl.Bind; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.jspecify.annotations.Nullable; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.VmObjectCursor; + +public abstract class ReadCursorValueNode extends PklNode { + protected ReadCursorValueNode() {} + + protected ReadCursorValueNode(SourceSection sourceSection) { + super(sourceSection); + } + + public abstract Object execute(VirtualFrame frame, VmObjectCursor cursor); + + @Specialization(guards = "cachedValue != null") + protected Object evalCached( + @SuppressWarnings("unused") VmObjectCursor cursor, + @Bind("cursor.cachedValueOrNull()") Object cachedValue) { + return cachedValue; + } + + @Specialization(replaces = "evalCached") + protected Object eval( + VmObjectCursor cursor, + @Bind("cursor.cachedValueOrNull()") @SuppressWarnings("unused") @Nullable Object cachedValue, + @Cached("create()") IndirectCallNode callNode) { + if (cachedValue != null) { + return cachedValue; + } + return cursor.value(callNode); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java index 3188ee753..a8951a53d 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java @@ -36,6 +36,10 @@ public final class BaseModule extends StdLibModule { return AnyClass.instance; } + public static VmClass getObjectClass() { + return ObjectClass.instance; + } + public static VmClass getTypedClass() { return TypedClass.instance; } @@ -256,6 +260,10 @@ public final class BaseModule extends StdLibModule { static final VmClass instance = loadClass("Any"); } + private static final class ObjectClass { + static final VmClass instance = loadClass("Object"); + } + private static final class TypedClass { static final VmClass instance = loadClass("Typed"); } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java index a1a4d1a1b..41cf95ce3 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java @@ -19,6 +19,7 @@ import static org.pkl.core.runtime.VmUtils.REPL_TEXT; import static org.pkl.core.runtime.VmUtils.REPL_TEXT_URI; import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.nodes.IndirectCallNode; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; @@ -67,6 +68,7 @@ import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.externalreader.ExternalReaderProcessException; import org.pkl.core.module.ModuleKeys; import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.GlobResolver; import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; @@ -174,30 +176,27 @@ public final class CommandSpecParser { var subcommandNames = new HashMap(); var subcommandsProperty = (VmObject) VmUtils.readMember(commandInfo, Identifier.SUBCOMMANDS); subcommandsProperty.force(false, false); - subcommandsProperty.iterateAlreadyForcedMemberValues( - (key, member, value) -> { - // track the subcommand index at first - // replaced by subcommand name in doParse if reading name succeeds - path.push("subcommands[" + key + "]"); - var spec = doParse((VmTyped) value, path); - path.pop(); - var existingNameIndex = subcommandNames.get(spec.name()); - if (existingNameIndex != null) { - throw exceptionBuilder() - .withSourceSection(member.getSourceSection()) - .evalError( - "commandSubcommandConflict", - VmUtils.readMember(commandInfo, Identifier.NAME), - spec.name(), - existingNameIndex, - key) - .build(); - } - subcommandNames.put(spec.name(), key); - subcommands.add(spec); - return true; - }); - + var callNode = IndirectCallNode.getUncached(); + for (var cursor = subcommandsProperty.members(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = (VmTyped) cursor.value(callNode); + path.push("subcommands[" + cursor.keyToString() + "]"); + var spec = doParse(value, path); + path.pop(); + var existingNameIndex = subcommandNames.get(spec.name()); + if (existingNameIndex != null) { + throw exceptionBuilder() + .withSourceSection(cursor.member().getSourceSection()) + .evalError( + "commandSubcommandConflict", + VmUtils.readMember(commandInfo, Identifier.NAME), + spec.name(), + existingNameIndex, + cursor.keyToString()) + .build(); + } + subcommandNames.put(spec.name(), cursor.key()); + subcommands.add(spec); + } return subcommands; } @@ -1087,7 +1086,10 @@ public final class CommandSpecParser { // otherwise value is Listing so will export to List if (!(value instanceof VmListing vmListing)) throw PklBugException.unreachableCode(); var result = new HashSet(vmListing.getLength()); - vmListing.forceAndIterateMemberValues((key, member, val) -> result.add((String) val)); + var callNode = IndirectCallNode.getUncached(); + for (var cursor = vmListing.members(); cursor.advance(); ) { + result.add((String) cursor.value(callNode)); + } return new Fixed(result); } @@ -1259,7 +1261,7 @@ public final class CommandSpecParser { var output = VmUtils.readModuleOutput(evaluated); return new CommandSpec.Result( VmUtils.readBytesProperty(output).export(), - VmUtils.readFilesProperty(output, makeFileOutput)); + VmUtils.readFilesProperty(output, makeFileOutput, IndirectCallNode.getUncached())); } // endregion diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java b/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java index a0615ab25..28eb33bae 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java @@ -20,6 +20,7 @@ import java.util.Comparator; import java.util.List; import org.jspecify.annotations.Nullable; import org.pkl.core.ValueFormatter; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.util.StringSimilarity; public final class KeyLookupSuggestions { @@ -49,15 +50,13 @@ public final class KeyLookupSuggestions { public static List forObject(VmObjectLike object, String key) { var candidates = new ArrayList(); - object.iterateMemberValues( - (memberKey, member, value) -> { - if (!(memberKey instanceof String stringKey)) return true; - var similarity = STRING_SIMILARITY.similarity(stringKey, key); - if (similarity >= SIMILARITY_THRESHOLD) { - candidates.add(new Candidate(stringKey, similarity)); - } - return true; - }); + for (var cursor = object.entries(CursorOption.ANY_ORDER); cursor.advance(); ) { + if (!(cursor.key() instanceof String stringKey)) continue; + var similarity = STRING_SIMILARITY.similarity(stringKey, key); + if (similarity >= SIMILARITY_THRESHOLD) { + candidates.add(new Candidate(stringKey, similarity)); + } + } candidates.sort(Comparator.naturalOrder()); return candidates; 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 bee4b5532..68259ca84 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 @@ -15,6 +15,7 @@ */ package org.pkl.core.runtime; +import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.io.IOException; @@ -40,8 +41,6 @@ import org.pkl.core.stdlib.base.PcfRenderer; import org.pkl.core.util.AnsiStringBuilder; import org.pkl.core.util.AnsiTheme; import org.pkl.core.util.EconomicMaps; -import org.pkl.core.util.MutableBoolean; -import org.pkl.core.util.MutableReference; /** Runs test results examples and facts. */ public final class TestRunner { @@ -84,54 +83,52 @@ public final class TestRunner { } private TestSectionResults runFacts(VmTyped testModule) { - var facts = VmUtils.readMember(testModule, Identifier.FACTS); + var callNode = IndirectCallNode.getUncached(); + var facts = VmUtils.readMember(testModule, Identifier.FACTS, callNode); if (facts instanceof VmNull) return new TestSectionResults(TestSectionName.FACTS, List.of()); var testResults = new ArrayList(); var factsMapping = (VmMapping) facts; - factsMapping.forceAndIterateMemberValues( - (groupKey, groupMember, groupValue) -> { - var listing = (VmListing) groupValue; - var name = String.valueOf(groupKey); - var resultBuilder = new TestResult.Builder(name); - listing.iterateMembers( - (idx, member) -> { - if (member.isLocalOrExternalOrHidden()) { - return true; - } - try { - var factValue = VmUtils.readMember(listing, idx); - if (factValue == Boolean.FALSE) { - if (PowerAssertions.isEnabled() && member.getSourceSection().isAvailable()) { - try (var valueTracker = valueTrackerFactory.create()) { - listing.cachedValues.clear(); - VmUtils.readMember(listing, idx); - var failure = - factFailure( - member.getSourceSection(), - getDisplayUri(member), - valueTracker.values()); - resultBuilder.addFailure(failure); - } - } else { - var failure = - factFailure(member.getSourceSection(), getDisplayUri(member), null); - resultBuilder.addFailure(failure); - } - } else { - resultBuilder.addSuccess(); - } - } catch (VmException err) { - var error = - new TestResults.Error( - err.getMessage(), err.toPklException(stackFrameTransformer, useColor)); - resultBuilder.addError(error); - } - return true; - }); - testResults.add(resultBuilder.build()); - return true; - }); + for (var cursor = factsMapping.entries(); cursor.advance(); ) { + var groupKey = cursor.key(); + var listing = (VmListing) cursor.value(callNode); + var name = String.valueOf(groupKey); + var resultBuilder = new TestResult.Builder(name); + for (var lCursor = listing.elements(); lCursor.advance(); ) { + var idx = lCursor.key(); + var member = lCursor.member(); + if (member.isLocalOrExternalOrHidden()) { + continue; + } + try { + var factValue = lCursor.value(callNode); + if (factValue == Boolean.FALSE) { + if (PowerAssertions.isEnabled() && member.getSourceSection().isAvailable()) { + try (var valueTracker = valueTrackerFactory.create()) { + listing.cachedValues.clear(); + // Must use `readMember` here to force a re-eval of this fact. + VmUtils.readMember(listing, idx, callNode); + var failure = + factFailure( + member.getSourceSection(), getDisplayUri(member), valueTracker.values()); + resultBuilder.addFailure(failure); + } + } else { + var failure = factFailure(member.getSourceSection(), getDisplayUri(member), null); + resultBuilder.addFailure(failure); + } + } else { + resultBuilder.addSuccess(); + } + } catch (VmException err) { + var error = + new TestResults.Error( + err.getMessage(), err.toPklException(stackFrameTransformer, useColor)); + resultBuilder.addError(error); + } + } + testResults.add(resultBuilder.build()); + } return new TestSectionResults(TestSectionName.FACTS, Collections.unmodifiableList(testResults)); } @@ -181,120 +178,111 @@ public final class TestRunner { private TestSectionResults doRunAndValidateExamples( VmMapping examples, Path expectedOutputFile, Path actualOutputFile) { var expectedExampleOutputs = loadExampleOutputs(expectedOutputFile); - var actualExampleOutputs = new MutableReference(null); - var allGroupsSucceeded = new MutableBoolean(true); - var errored = new MutableBoolean(false); + VmDynamic actualExampleOutputs = null; + var allGroupsSucceeded = true; + var errored = false; var testResults = new ArrayList(); - examples.forceAndIterateMemberValues( - (groupKey, groupMember, groupValue) -> { - var testName = String.valueOf(groupKey); - var group = (VmListing) groupValue; - var expectedGroup = - (VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey); - var testResultBuilder = new TestResult.Builder(testName); + var callNode = IndirectCallNode.getUncached(); - if (expectedGroup == null) { - testResultBuilder.addFailure( - examplePropertyMismatchFailure(getDisplayUri(groupMember), testName, true)); - testResults.add(testResultBuilder.build()); - return true; + for (var groupCursor = examples.entries(); groupCursor.advance(); ) { + var groupKey = groupCursor.key(); + var groupMember = groupCursor.member(); + var testName = String.valueOf(groupKey); + var group = (VmListing) groupCursor.value(callNode); + var expectedGroup = (VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey); + var testResultBuilder = new TestResult.Builder(testName); + + if (expectedGroup == null) { + testResultBuilder.addFailure( + examplePropertyMismatchFailure(getDisplayUri(groupMember), testName, true)); + testResults.add(testResultBuilder.build()); + continue; + } + + if (group.getLength() != expectedGroup.getLength()) { + testResultBuilder.addFailure( + exampleLengthMismatchFailure( + getDisplayUri(groupMember), + testName, + expectedGroup.getLength(), + group.getLength())); + testResults.add(testResultBuilder.build()); + continue; + } + + for (var exampleCursor = group.elements(); exampleCursor.advance(); ) { + var exampleIndex = exampleCursor.key(); + + Object exampleValue; + try { + exampleValue = exampleCursor.value(callNode); + } catch (VmException err) { + errored = true; + testResultBuilder.addError( + new TestResults.Error( + err.getMessage(), err.toPklException(stackFrameTransformer, useColor))); + continue; + } + var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex, callNode); + + var exampleValuePcf = renderAsPcf(exampleValue); + var expectedValuePcf = renderAsPcf(expectedValue); + + if (!(exampleValuePcf.equals(expectedValuePcf))) { + if (actualExampleOutputs == null) { + // immediately write and load `-actual.pcf` + // so that we can generate deep link with correct line number for each + // mismatch + writeExampleOutputs(actualOutputFile, examples); + actualExampleOutputs = loadExampleOutputs(actualOutputFile); } - if (group.getLength() != expectedGroup.getLength()) { - testResultBuilder.addFailure( - exampleLengthMismatchFailure( - getDisplayUri(groupMember), - testName, - expectedGroup.getLength(), - group.getLength())); - testResults.add(testResultBuilder.build()); - return true; + var expectedMember = VmUtils.findMember(expectedGroup, exampleIndex); + assert expectedMember != null; + + var actualGroup = (VmObjectLike) VmUtils.readMemberOrNull(actualExampleOutputs, groupKey); + var actualMember = + actualGroup == null ? null : VmUtils.findMember(actualGroup, exampleIndex); + if (actualMember == null) { + // file was written earlier in this method; + // must have been tampered with by another process + throw new VmExceptionBuilder() + .evalError("invalidOutputFileStructure", actualOutputFile) + .build(); } - group.iterateMembers( - ((exampleIndex, exampleMember) -> { - if (exampleMember.isLocalOrExternalOrHidden()) { - return true; - } + testResultBuilder.addFailure( + exampleFailure( + getDisplayUri(exampleCursor.member()), + getDisplayUri(expectedMember), + expectedValuePcf, + getDisplayUri(actualMember), + exampleValuePcf, + testResultBuilder.getCount())); + } else { + testResultBuilder.addSuccess(); + } + } - Object exampleValue; - try { - exampleValue = VmUtils.readMember(group, exampleIndex); - } catch (VmException err) { - errored.set(true); - testResultBuilder.addError( - new TestResults.Error( - err.getMessage(), err.toPklException(stackFrameTransformer, useColor))); - return true; - } - var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex); + testResults.add(testResultBuilder.build()); + } - var exampleValuePcf = renderAsPcf(exampleValue); - var expectedValuePcf = renderAsPcf(expectedValue); + for (var groupCursor = expectedExampleOutputs.entries(); groupCursor.advance(); ) { + var groupKey = groupCursor.key(); + if (examples.getCachedValue(groupKey) == null) { + var testName = String.valueOf(groupKey); + allGroupsSucceeded = false; + var result = + new TestResult.Builder(testName) + .addFailure( + examplePropertyMismatchFailure( + getDisplayUri(groupCursor.member()), testName, false)) + .build(); + testResults.add(result); + } + } - if (!(exampleValuePcf.equals(expectedValuePcf))) { - if (actualExampleOutputs.isNull()) { - // immediately write and load `-actual.pcf` - // so that we can generate deep link with correct line number for each - // mismatch - writeExampleOutputs(actualOutputFile, examples); - actualExampleOutputs.set(loadExampleOutputs(actualOutputFile)); - } - - var expectedMember = VmUtils.findMember(expectedGroup, exampleIndex); - assert expectedMember != null; - - var actualGroup = - (VmObjectLike) VmUtils.readMemberOrNull(actualExampleOutputs.get(), groupKey); - var actualMember = - actualGroup == null ? null : VmUtils.findMember(actualGroup, exampleIndex); - if (actualMember == null) { - // file was written earlier in this method; - // must have been tampered with by another process - throw new VmExceptionBuilder() - .evalError("invalidOutputFileStructure", actualOutputFile) - .build(); - } - - testResultBuilder.addFailure( - exampleFailure( - getDisplayUri(exampleMember), - getDisplayUri(expectedMember), - expectedValuePcf, - getDisplayUri(actualMember), - exampleValuePcf, - testResultBuilder.getCount())); - } else { - testResultBuilder.addSuccess(); - } - - return true; - })); - - testResults.add(testResultBuilder.build()); - - return true; - }); - - expectedExampleOutputs.iterateMembers( - (groupKey, groupMember) -> { - if (groupMember.isLocalOrExternalOrHidden()) { - return true; - } - if (examples.getCachedValue(groupKey) == null) { - var testName = String.valueOf(groupKey); - allGroupsSucceeded.set(false); - var result = - new TestResult.Builder(testName) - .addFailure( - examplePropertyMismatchFailure(getDisplayUri(groupMember), testName, false)) - .build(); - testResults.add(result); - } - return true; - }); - - if (!allGroupsSucceeded.get() && actualExampleOutputs.isNull() && !errored.get()) { + if (!allGroupsSucceeded && actualExampleOutputs == null && !errored) { writeExampleOutputs(actualOutputFile, examples); } return new TestSectionResults( @@ -303,40 +291,33 @@ public final class TestRunner { private TestSectionResults doRunAndWriteExamples(VmMapping examples, Path outputFile) { var testResults = new ArrayList(); - var allSucceeded = new MutableBoolean(true); - examples.forceAndIterateMemberValues( - (groupKey, groupMember, groupValue) -> { - var testName = String.valueOf(groupKey); - var listing = (VmListing) groupValue; - var testResultBuilder = new TestResult.Builder(testName); - var success = new MutableBoolean(true); - listing.iterateMembers( - (idx, member) -> { - if (member.isLocalOrExternalOrHidden()) { - return true; - } - try { - VmUtils.readMember(listing, idx); - return true; - } catch (VmException err) { - testResultBuilder.addError( - new TestResults.Error( - err.getMessage(), err.toPklException(stackFrameTransformer, useColor))); - allSucceeded.set(false); - success.set(false); - return true; - } - }); - if (success.get()) { - // treat writing an example as a message - testResultBuilder.setExampleWritten(true); - testResultBuilder.addFailure( - writtenExampleOutputFailure(testName, getDisplayUri(groupMember))); - } - testResults.add(testResultBuilder.build()); - return true; - }); - if (allSucceeded.get()) { + var allSucceeded = true; + var callNode = IndirectCallNode.getUncached(); + for (var cursor = examples.entries(); cursor.advance(); ) { + var testName = cursor.keyToString(); + var groupMember = cursor.member(); + var listing = (VmListing) cursor.value(callNode); + var testResultBuilder = new TestResult.Builder(testName); + var success = true; + for (var listingCursor = listing.elements(); listingCursor.advance(); ) { + try { + listingCursor.value(callNode); + } catch (VmException err) { + testResultBuilder.addError( + new TestResults.Error( + err.getMessage(), err.toPklException(stackFrameTransformer, useColor))); + allSucceeded = false; + success = false; + } + } + if (success) { + testResultBuilder.setExampleWritten(true); + testResultBuilder.addFailure( + writtenExampleOutputFailure(testName, getDisplayUri(groupMember))); + } + testResults.add(testResultBuilder.build()); + } + if (allSucceeded) { writeExampleOutputs(outputFile, examples); } return new TestSectionResults( 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 08ade02f0..6620a8eec 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 @@ -864,21 +864,14 @@ public final class VmClass extends VmValue { var size = superclass == null ? 0 : superclass.getAllRegularPropertyNames().size(); var result = EconomicSet.create(size); + if (superclass != null) { + result.addAll(superclass.getAllRegularPropertyNames()); + } for (var property : EconomicMaps.getValues(declaredProperties)) { if (!(property.isLocal() || isHiddenProperty(property.getName()) || property.isExternal())) { result.add(property.getName()); } } - - if (superclass == null) { - return result; - } - - if (result.isEmpty()) { - return superclass.getAllRegularPropertyNames(); - } - - result.addAll(superclass.getAllRegularPropertyNames()); return result; } 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 4d4bfbb59..0709e3870 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 @@ -17,8 +17,11 @@ package org.pkl.core.runtime; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.VirtualFrame; import java.util.Iterator; import org.organicdesign.fp.xform.Xform; +import org.pkl.core.ast.internal.ReadCursorValueNode; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; public abstract class VmCollection extends VmValue implements Iterable { public interface Builder { @@ -126,18 +129,15 @@ public abstract class VmCollection extends VmValue implements Iterable { return VmList.create(result); } - @TruffleBoundary - public final VmCollection flatten() { + public final VmCollection flatten(VirtualFrame frame, ReadCursorValueNode readCursorValueNode) { var builder = builder(); for (var elem : this) { if (elem instanceof Iterable iterable) { builder.addAll(iterable); } else if (elem instanceof VmListing listing) { - listing.forceAndIterateMemberValues( - (key, member, value) -> { - builder.add(value); - return true; - }); + for (var cursor = listing.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + builder.add(readCursorValueNode.execute(frame, cursor)); + } } else { CompilerDirectives.transferToInterpreter(); throw new VmExceptionBuilder() 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 d38641d30..3f1c88a7f 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 @@ -17,12 +17,23 @@ package org.pkl.core.runtime; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; +import java.util.EnumSet; import java.util.Objects; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; import org.pkl.core.PClassInfo; import org.pkl.core.PObject; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmDynamicCursors.CachedElementCursor; +import org.pkl.core.runtime.VmDynamicCursors.CachedEntryCursor; +import org.pkl.core.runtime.VmDynamicCursors.ElementCursor; +import org.pkl.core.runtime.VmDynamicCursors.EntryCursor; +import org.pkl.core.runtime.VmDynamicCursors.MemberCursor; +import org.pkl.core.runtime.VmDynamicCursors.PropertyCursor; +import org.pkl.core.runtime.VmDynamicCursors.UnorderedEntryCursor; +import org.pkl.core.runtime.VmDynamicCursors.UnorderedMemberCursor; +import org.pkl.core.runtime.VmDynamicCursors.UnorderedPropertyCursor; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.util.CollectionUtils; import org.pkl.core.util.EconomicMaps; @@ -74,19 +85,16 @@ public final class VmDynamic extends VmObject { } @Override - @TruffleBoundary public PObject export() { - assert forced : "Value was not forced prior to export"; + assert isDeepForced() : "Value was not forced prior to export"; var properties = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); - - iterateAlreadyForcedMemberValues( - (key, member, value) -> { - properties.put(VmUtils.toPklString(key), VmValue.export(value)); - return true; - }); - + // export all members, not just properties + for (var cursor = members(); cursor.advance(); ) { + CollectionUtils.put( + properties, cursor.key().toString(), VmValue.export(cursor.cachedValue())); + } return new PObject(PClassInfo.Dynamic, properties); } @@ -100,6 +108,77 @@ public final class VmDynamic extends VmObject { return converter.convertDynamic(this, path); } + @Override + public VmObjectCursor properties() { + return new PropertyCursor(this); + } + + @Override + public VmObjectCursor properties(CursorOption option) { + // never shallow-force because it's impossible to only force properties + var anyOrder = option == CursorOption.ANY_ORDER; + return anyOrder ? new UnorderedPropertyCursor(this) : new PropertyCursor(this); + } + + @Override + public VmObjectCursor elements() { + return new ElementCursor(this); + } + + @Override + public VmObjectCursor elements(CursorOption option) { + // never shallow-force because it's impossible to only force elements + var anyOrder = option == CursorOption.ANY_ORDER; + return anyOrder && isShallowForced() ? new CachedElementCursor(this) : new ElementCursor(this); + } + + @Override + public VmObjectCursor elements(EnumSet options) { + // never shallow-force because it's impossible to only force elements + var anyOrder = CollectionUtils.contains(options, CursorOption.ANY_ORDER); + return anyOrder && isShallowForced() ? new CachedElementCursor(this) : new ElementCursor(this); + } + + @Override + public VmObjectCursor entries() { + return new EntryCursor(this); + } + + @Override + public VmObjectCursor entries(CursorOption option) { + // never shallow-force because it's impossible to only force entries + var anyOrder = option == CursorOption.ANY_ORDER; + if (anyOrder) { + return isShallowForced() ? new CachedEntryCursor(this) : new UnorderedEntryCursor(this); + } + return new EntryCursor(this); + } + + @Override + public VmObjectCursor entries(EnumSet options) { + // never shallow-force because it's impossible to only force entries + var anyOrder = CollectionUtils.contains(options, CursorOption.ANY_ORDER); + if (anyOrder) { + return isShallowForced() ? new CachedEntryCursor(this) : new UnorderedEntryCursor(this); + } + return new EntryCursor(this); + } + + @Override + public VmObjectCursor members() { + return new MemberCursor(this); + } + + @Override + public VmObjectCursor members(CursorOption option) { + if (option == CursorOption.ALL_VALUES) { + force(false, false); + } + return option == CursorOption.ANY_ORDER + ? new UnorderedMemberCursor(this) + : new MemberCursor(this); + } + @Override @TruffleBoundary public boolean equals(@Nullable Object obj) { @@ -149,10 +228,9 @@ public final class VmDynamic extends VmObject { return result; } - // assumes object has been forced public int getRegularMemberCount() { + assert isShallowForced(); if (cachedRegularMemberCount != -1) return cachedRegularMemberCount; - var result = 0; for (var key : cachedValues.getKeys()) { if (!isHiddenOrLocalProperty(key)) result += 1; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamicCursors.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamicCursors.java new file mode 100644 index 000000000..5e1c42856 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamicCursors.java @@ -0,0 +1,326 @@ +/* + * 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. + * 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.nodes.IndirectCallNode; +import org.graalvm.collections.EconomicSet; +import org.graalvm.collections.UnmodifiableMapCursor; +import org.jspecify.annotations.Nullable; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmObjectCursor.AbstractCachedMemberCursor; +import org.pkl.core.runtime.VmObjectCursor.AbstractOrderedCursor; +import org.pkl.core.util.ArrayBuilder; +import org.pkl.core.util.EconomicSets; +import org.pkl.core.util.LateInit; + +final class VmDynamicCursors { + private VmDynamicCursors() {} + + static final class PropertyCursor extends AbstractMemberCursor { + public PropertyCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean shouldVisit(ObjectMember member) { + return member.isProp() + && member.getNameOrNull() != Identifier.DEFAULT + && !member.isLocalOrExternalOrHidden(); + } + + @Override + public boolean isProperty() { + return true; + } + } + + static final class UnorderedPropertyCursor extends AbstractUnorderedMemberCursor { + public UnorderedPropertyCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean shouldVisit(ObjectMember member) { + return member.isProp() + && member.getNameOrNull() != Identifier.DEFAULT + && !member.isLocalOrExternalOrHidden(); + } + + @Override + public boolean isProperty() { + return true; + } + } + + static final class ElementCursor extends AbstractOrderedCursor { + private int currentIndex = -1; + + public ElementCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean advance() { + currentIndex += 1; + return currentIndex < iteratee.getLength(); + } + + @Override + public Object key() { + assert currentIndex >= 0 && currentIndex < iteratee.getLength() : "illegal state"; + return (long) currentIndex; + } + + @Override + public int getLength() { + return iteratee.getLength(); + } + + @Override + public boolean isElement() { + return true; + } + } + + static final class CachedElementCursor extends AbstractCachedMemberCursor { + public CachedElementCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + protected boolean shouldVisit(Object key) { + return key instanceof Long l && l < iteratee.getLength(); + } + + @Override + public boolean isElement() { + return true; + } + } + + static final class EntryCursor extends AbstractMemberCursor { + public EntryCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean shouldVisit(ObjectMember member) { + if (member.isProp()) { + return false; + } + return !(key() instanceof Long l) || l >= iteratee.getLength(); + } + + @Override + public boolean isEntry() { + return true; + } + } + + static final class UnorderedEntryCursor extends AbstractUnorderedMemberCursor { + public UnorderedEntryCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean shouldVisit(ObjectMember member) { + if (member.isProp()) { + return false; + } + return !(key() instanceof Long l) || l >= iteratee.getLength(); + } + + @Override + public boolean isEntry() { + return true; + } + } + + static final class CachedEntryCursor extends AbstractCachedMemberCursor { + public CachedEntryCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + protected boolean shouldVisit(Object key) { + if (key instanceof Identifier) return false; + if (key instanceof Long l) return l >= iteratee.getLength(); + return true; + } + + @Override + public boolean isEntry() { + return true; + } + } + + static final class MemberCursor extends AbstractMemberCursor { + public MemberCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean shouldVisit(ObjectMember member) { + return !member.isLocalOrExternalOrHidden() && member.getNameOrNull() != Identifier.DEFAULT; + } + + @Override + public boolean isProperty() { + return key() instanceof Identifier; + } + + @Override + public boolean isElement() { + return key() instanceof Long l && l < iteratee.getLength(); + } + + @Override + public boolean isEntry() { + return !isProperty() && !isElement(); + } + } + + static final class UnorderedMemberCursor extends AbstractUnorderedMemberCursor { + public UnorderedMemberCursor(VmDynamic iteratee) { + super(iteratee); + } + + @Override + public boolean shouldVisit(ObjectMember member) { + return !member.isLocalOrExternalOrHidden() && member.getNameOrNull() != Identifier.DEFAULT; + } + + @Override + public boolean isProperty() { + return key() instanceof Identifier; + } + + @Override + public boolean isElement() { + return key() instanceof Long l && l < iteratee.getLength(); + } + + @Override + public boolean isEntry() { + return !isProperty() && !isElement(); + } + } + + abstract static sealed class AbstractMemberCursor extends AbstractOrderedCursor { + private final UnmodifiableMapCursor[] allMembers; + private @LateInit UnmodifiableMapCursor currentMembers; + private int allMembersIndex; + private final EconomicSet seenKeys = EconomicSets.create(); + + public AbstractMemberCursor(VmDynamic iteratee) { + super(iteratee); + var builder = ArrayBuilder.of(UnmodifiableMapCursor[]::new); + for (VmObject object = iteratee; + object != null && object.parent != objectPrototype; + object = object.parent) { + builder.add(object.members.getEntries()); + } + allMembers = builder.toOversizedArray(); + allMembersIndex = builder.lastIndex(); + currentMembers = allMembers[allMembersIndex]; + } + + public abstract boolean shouldVisit(ObjectMember member); + + @Override + public boolean advance() { + while (true) { + while (currentMembers.advance()) { + if (EconomicSets.add(seenKeys, currentMembers.getKey()) + && shouldVisit(currentMembers.getValue())) return true; + } + allMembersIndex -= 1; + if (allMembersIndex < 0) return false; + currentMembers = allMembers[allMembersIndex]; + } + } + + @Override + public final int getLength() { + return -1; + } + + @Override + public Object key() { + return currentMembers.getKey(); + } + } + + abstract static sealed class AbstractUnorderedMemberCursor extends VmObjectCursor { + private static final VmTyped dynamicPrototype = BaseModule.getDynamicClass().getPrototype(); + + protected final VmDynamic iteratee; + private VmObject currentObject; + private UnmodifiableMapCursor currentMember; + private final EconomicSet seenKeys = EconomicSets.create(); + + public AbstractUnorderedMemberCursor(VmDynamic iteratee) { + this.iteratee = iteratee; + currentObject = iteratee; + currentMember = currentObject.members.getEntries(); + } + + @Override + protected VmObject iteratee() { + return iteratee; + } + + protected abstract boolean shouldVisit(ObjectMember member); + + @Override + public final boolean advance() { + while (true) { + while (currentMember.advance()) { + if (EconomicSets.add(seenKeys, currentMember.getKey()) + && shouldVisit(currentMember.getValue())) return true; + } + var parent = currentObject.parent; + if (parent == dynamicPrototype) return false; + assert parent != null; + currentObject = parent; + currentMember = currentObject.members.getEntries(); + } + } + + @Override + public final Object key() { + return currentMember.getKey(); + } + + @Override + public final Object value(IndirectCallNode callNode) { + var key = key(); + var value = iteratee.getCachedValue(key); + if (value != null) return value; + return VmUtils.doReadMember(iteratee, currentObject, key, member(), true, callNode); + } + + @Override + public final @Nullable Object cachedValueOrNull() { + return iteratee.getCachedValue(key()); + } + + @Override + public final int getLength() { + return -1; + } + } +} 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 0a5c988f5..1f6fe66cb 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 @@ -18,11 +18,14 @@ package org.pkl.core.runtime; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.frame.MaterializedFrame; +import java.util.EnumSet; import java.util.function.BiFunction; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; +import org.pkl.core.runtime.VmObjectCursor.EmptyCursor; import org.pkl.core.util.EconomicMaps; public final class VmFunction extends VmObjectLike { @@ -139,26 +142,61 @@ public final class VmFunction extends VmObjectLike { .build(); } - @Override - public boolean iterateMemberValues(MemberValueConsumer consumer) { - return true; - } - - @Override - public boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer) { - return true; - } - - @Override - public boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer) { - return true; - } - @Override public boolean iterateMembers(BiFunction consumer) { return true; } + @Override + public VmObjectCursor properties() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor properties(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements(EnumSet options) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries(EnumSet options) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor members() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor members(CursorOption option) { + return EmptyCursor.INSTANCE; + } + @Override public VmClass getVmClass() { return BaseModule.getFunctionNClass(paramCount); 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 500f53388..aa6a39a36 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 @@ -18,11 +18,17 @@ package org.pkl.core.runtime; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmListingCursors.CachedElementCursor; +import org.pkl.core.runtime.VmListingCursors.ElementCursor; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; +import org.pkl.core.runtime.VmObjectCursor.EmptyCursor; +import org.pkl.core.util.CollectionUtils; import org.pkl.core.util.EconomicMaps; public final class VmListing extends VmListingOrMapping { @@ -62,10 +68,6 @@ public final class VmListing extends VmListingOrMapping { this.length = length; } - public static boolean isDefaultProperty(Object propertyKey) { - return propertyKey == Identifier.DEFAULT; - } - public int getLength() { return length; } @@ -87,19 +89,13 @@ public final class VmListing extends VmListingOrMapping { @Override @TruffleBoundary public List export() { - assert forced : "Value was not forced prior to export"; + assert isDeepForced() : "Value was not forced prior to export"; - var properties = new ArrayList<>(EconomicMaps.size(cachedValues)); - - iterateAlreadyForcedMemberValues( - (key, prop, value) -> { - if (isDefaultProperty(key)) return true; - - properties.add(VmValue.export(value)); - return true; - }); - - return properties; + var elements = new ArrayList<>(EconomicMaps.size(cachedValues)); + for (var cursor = elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + elements.add(VmValue.export(cursor.cachedValue())); + } + return elements; } @Override @@ -112,6 +108,77 @@ public final class VmListing extends VmListingOrMapping { return converter.convertListing(this, path); } + @Override + @TruffleBoundary + public VmObjectCursor elements() { + return new ElementCursor(this); + } + + @Override + @TruffleBoundary + public VmObjectCursor elements(CursorOption option) { + if (option == CursorOption.ANY_ORDER) { + return isShallowForced() ? new CachedElementCursor(this) : new ElementCursor(this); + } + if (option == CursorOption.ALL_VALUES) { + force(false, false); + return new ElementCursor(this); + } + return new ElementCursor(this); + } + + @Override + public VmObjectCursor elements(EnumSet options) { + var anyOrder = CollectionUtils.contains(options, CursorOption.ANY_ORDER); + var allValues = CollectionUtils.contains(options, CursorOption.ALL_VALUES); + var lazyRequired = CollectionUtils.contains(options, CursorOption.LAZY_REQUIRED); + if (anyOrder) { + if (isShallowForced()) { + return new CachedElementCursor(this); + } + if (allValues && !lazyRequired) { + force(false, false); + return new CachedElementCursor(this); + } + } + return new ElementCursor(this); + } + + @Override + public VmObjectCursor properties() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor properties(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries(EnumSet options) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor members() { + return elements(); + } + + @Override + public VmObjectCursor members(CursorOption option) { + return elements(option); + } + @Override @TruffleBoundary public boolean equals(@Nullable Object obj) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmListingCursors.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmListingCursors.java new file mode 100644 index 000000000..6d120dcbf --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmListingCursors.java @@ -0,0 +1,69 @@ +/* + * 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. + * 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 org.pkl.core.runtime.VmObjectCursor.AbstractCachedMemberCursor; +import org.pkl.core.runtime.VmObjectCursor.AbstractOrderedCursor; + +final class VmListingCursors { + private VmListingCursors() {} + + static final class ElementCursor extends AbstractOrderedCursor { + private int currentIndex = -1; + + public ElementCursor(VmListing iteratee) { + super(iteratee); + } + + @Override + public boolean advance() { + currentIndex += 1; + return currentIndex < iteratee.getLength(); + } + + @Override + public Object key() { + assert currentIndex >= 0 && currentIndex < iteratee.getLength() : "illegal state"; + return (long) currentIndex; + } + + @Override + public boolean isElement() { + return true; + } + + @Override + public int getLength() { + return iteratee.getLength(); + } + } + + static final class CachedElementCursor extends AbstractCachedMemberCursor { + public CachedElementCursor(VmListing iteratee) { + super(iteratee); + } + + @Override + protected boolean shouldVisit(Object key) { + return key instanceof Long; // skip local and hidden properties + } + + @Override + public boolean isElement() { + return true; + } + } +} 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 29a152095..b6652bce3 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 @@ -42,7 +42,7 @@ public abstract class VmListingOrMapping extends VmObject { typeCheckOwner = null; } - public VmListingOrMapping( + protected VmListingOrMapping( MaterializedFrame enclosingFrame, @Nullable VmObject parent, UnmodifiableEconomicMap members, 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 8656c129d..19e138dcd 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 @@ -16,23 +16,27 @@ package org.pkl.core.runtime; import com.google.errorprone.annotations.concurrent.GuardedBy; +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.frame.MaterializedFrame; -import java.util.HashSet; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import java.util.EnumSet; import java.util.Map; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmMappingCursors.CachedEntryCursor; +import org.pkl.core.runtime.VmMappingCursors.EntryCursor; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; +import org.pkl.core.runtime.VmObjectCursor.EmptyCursor; import org.pkl.core.util.CollectionUtils; import org.pkl.core.util.EconomicMaps; -import org.pkl.core.util.MutableLong; public final class VmMapping extends VmListingOrMapping { - private long cachedLength = -1; - - @GuardedBy("this") - private @Nullable VmSet __allKeys; + private static final EnumSet isEmptyComputationCursor = + EnumSet.of(CursorOption.ANY_ORDER, CursorOption.LAZY_REQUIRED); private static final class EmptyHolder { private static final VmMapping EMPTY = @@ -42,6 +46,13 @@ public final class VmMapping extends VmListingOrMapping { EconomicMaps.create()); } + @CompilationFinal private long cachedLength = -1; + @CompilationFinal private boolean isEmptyComputed; + @CompilationFinal private boolean isEmpty; + + @GuardedBy("this") + private @Nullable VmSet __allKeys; + public static VmMapping empty() { return EmptyHolder.EMPTY; } @@ -63,10 +74,6 @@ public final class VmMapping extends VmListingOrMapping { super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner); } - public static boolean isDefaultProperty(Object propertyKey) { - return propertyKey == Identifier.DEFAULT; - } - @Override public VmClass getVmClass() { return BaseModule.getMappingClass(); @@ -91,21 +98,22 @@ public final class VmMapping extends VmListingOrMapping { } @Override - @TruffleBoundary public Map export() { - assert forced : "Value was not forced prior to export"; + assert isDeepForced() : "Value was not forced prior to export"; - var properties = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + var entries = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + for (var cursor = entries(); cursor.advance(); ) { + entries.put(VmValue.export(cursor.key()), VmValue.export(cursor.cachedValue())); + } + return entries; + } - iterateAlreadyForcedMemberValues( - (key, prop, value) -> { - if (isDefaultProperty(key)) return true; - - properties.put(VmValue.export(key), VmValue.export(value)); - return true; - }); - - return properties; + public Map toMap(IndirectCallNode callNode) { + var entries = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + for (var cursor = entries(); cursor.advance(); ) { + entries.put(cursor.key(), cursor.value(callNode)); + } + return entries; } @Override @@ -118,6 +126,73 @@ public final class VmMapping extends VmListingOrMapping { return converter.convertMapping(this, path); } + @Override + public VmObjectCursor properties() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor properties(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements(EnumSet options) { + return EmptyCursor.INSTANCE; + } + + public VmObjectCursor entries() { + return new EntryCursor(this); + } + + public VmObjectCursor entries(CursorOption option) { + if (option == CursorOption.ANY_ORDER) { + return isShallowForced() ? new CachedEntryCursor(this) : new EntryCursor(this); + } + if (option == CursorOption.ALL_VALUES) { + force(false, false); + return new EntryCursor(this); + } + return new EntryCursor(this); + } + + @Override + public VmObjectCursor entries(EnumSet options) { + var anyOrder = CollectionUtils.contains(options, CursorOption.ANY_ORDER); + var allValues = CollectionUtils.contains(options, CursorOption.ALL_VALUES); + var lazyRequired = CollectionUtils.contains(options, CursorOption.LAZY_REQUIRED); + if (anyOrder) { + if (isShallowForced()) { + return new CachedEntryCursor(this); + } + if (allValues && !lazyRequired) { + force(false, false); + return new CachedEntryCursor(this); + } + } + return new EntryCursor(this); + } + + @Override + public VmObjectCursor members() { + return entries(); + } + + @Override + public VmObjectCursor members(CursorOption option) { + return entries(option); + } + @Override @TruffleBoundary public boolean equals(@Nullable Object obj) { @@ -167,25 +242,23 @@ public final class VmMapping extends VmListingOrMapping { return result; } - @TruffleBoundary public long getLength() { if (cachedLength != -1) return cachedLength; - var count = new MutableLong(0); - var visited = new HashSet<>(); - iterateMembers( - (key, member) -> { - var alreadyVisited = !visited.add(key); - // important to record hidden member as visited before skipping it - // because any overriding member won't carry a `hidden` identifier - if (alreadyVisited || member.isLocalOrExternalOrHidden()) return true; - count.getAndIncrement(); - return true; - }); - cachedLength = count.get(); + CompilerDirectives.transferToInterpreterAndInvalidate(); + var count = 0; + for (var cursor = entries(CursorOption.LAZY_REQUIRED); cursor.advance(); ) { + count++; + } + cachedLength = count; return cachedLength; } public boolean isEmpty() { - return getLength() == 0; + if (!isEmptyComputed) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + isEmptyComputed = true; + isEmpty = !entries(isEmptyComputationCursor).advance(); + } + return isEmpty; } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmMappingCursors.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmMappingCursors.java new file mode 100644 index 000000000..760159c2f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmMappingCursors.java @@ -0,0 +1,77 @@ +/* + * 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. + * 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 java.util.Iterator; +import org.jspecify.annotations.Nullable; +import org.pkl.core.runtime.VmObjectCursor.AbstractCachedMemberCursor; +import org.pkl.core.runtime.VmObjectCursor.AbstractOrderedCursor; + +final class VmMappingCursors { + private VmMappingCursors() {} + + static final class EntryCursor extends AbstractOrderedCursor { + private final Iterator keys; + private @Nullable Object currentKey; + + public EntryCursor(VmMapping iteratee) { + super(iteratee); + keys = iteratee.getAllKeys().iterator(); + } + + @Override + public boolean advance() { + if (keys.hasNext()) { + currentKey = keys.next(); + return true; + } + currentKey = null; + return false; + } + + @Override + public Object key() { + assert currentKey != null : "illegal state"; + return currentKey; + } + + @Override + public boolean isEntry() { + return true; + } + + @Override + public int getLength() { + return -1; + } + } + + static final class CachedEntryCursor extends AbstractCachedMemberCursor { + public CachedEntryCursor(VmMapping iteratee) { + super(iteratee); + } + + @Override + protected boolean shouldVisit(Object key) { + return !(key instanceof Identifier); // skip local and hidden properties + } + + @Override + public boolean isEntry() { + return true; + } + } +} 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 feb4b3e1a..0215f2ab8 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 @@ -15,6 +15,7 @@ */ package org.pkl.core.runtime; +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.frame.MaterializedFrame; @@ -29,12 +30,18 @@ import org.pkl.core.util.EconomicMaps; /** Corresponds to `pkl.base#Object`. */ public abstract class VmObject extends VmObjectLike { + private static final byte START_SHALLOW_FORCE_FLAG = 0x1; + private static final byte START_DEEP_FORCE_FLAG = 0x2; + private static final byte SHALLOW_FORCE_FLAG = 0x4; + private static final byte DEEP_FORCE_FLAG = 0x8; + @CompilationFinal protected @Nullable VmObject parent; protected final UnmodifiableEconomicMap members; protected final EconomicMap cachedValues; protected int cachedHash; - protected boolean forced; + private byte flags; + private final int depth; public VmObject( MaterializedFrame enclosingFrame, @@ -42,6 +49,7 @@ public abstract class VmObject extends VmObjectLike { UnmodifiableEconomicMap members, EconomicMap cachedValues) { super(enclosingFrame); + depth = parent == null ? 0 : parent.depth + 1; this.parent = parent; this.members = members; this.cachedValues = cachedValues; @@ -71,6 +79,16 @@ public abstract class VmObject extends VmObjectLike { return EconomicMaps.containsKey(members, key); } + public final @Nullable ObjectMember getRootFirstMember(Object key) { + if (parent != null) { + var parentMember = parent.getRootFirstMember(key); + if (parentMember != null) { + return parentMember; + } + } + return getMember(key); + } + @Override public final @Nullable ObjectMember getMember(Object key) { return EconomicMaps.get(members, key); @@ -96,43 +114,6 @@ public abstract class VmObject extends VmObjectLike { return EconomicMaps.containsKey(cachedValues, key); } - @Override - @TruffleBoundary - public final boolean iterateMemberValues(MemberValueConsumer consumer) { - var visited = new HashSet<>(); - return iterateMembers( - (key, member) -> { - var alreadyVisited = !visited.add(key); - // important to record hidden member as visited before skipping it - // because any overriding member won't carry a `hidden` identifier - if (alreadyVisited || member.isLocalOrExternalOrHidden()) return true; - return consumer.accept(key, member, getCachedValue(key)); - }); - } - - @Override - @TruffleBoundary - public final boolean forceAndIterateMemberValues(ForcedMemberValueConsumer consumer) { - force(false, false); - return iterateAlreadyForcedMemberValues(consumer); - } - - @Override - @TruffleBoundary - public final boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer) { - var visited = new HashSet<>(); - return iterateMembers( - (key, member) -> { - var alreadyVisited = !visited.add(key); - // important to record hidden member as visited before skipping it - // because any overriding member won't carry a `hidden` identifier - if (alreadyVisited || member.isLocalOrExternalOrHidden()) return true; - Object cachedValue = getCachedValue(key); - assert cachedValue != null; // forced - return consumer.accept(key, member, cachedValue); - }); - } - @Override @TruffleBoundary public final boolean iterateMembers(BiFunction consumer) { @@ -150,14 +131,33 @@ public abstract class VmObject extends VmObjectLike { return true; } + protected boolean isShallowForced() { + return (flags & (SHALLOW_FORCE_FLAG | DEEP_FORCE_FLAG)) != 0; + } + + protected boolean isDeepForced() { + return (flags & DEEP_FORCE_FLAG) != 0; + } + + protected boolean isStartDeepForce() { + return (flags & START_DEEP_FORCE_FLAG) != 0; + } + + protected boolean isStartShallowForce() { + return (flags & (START_SHALLOW_FORCE_FLAG | START_DEEP_FORCE_FLAG)) != 0; + } + /** Evaluates this object's members. Skips local, hidden, and external members. */ @Override - @TruffleBoundary public final void force(boolean allowUndefinedValues, boolean recurse) { - if (forced) return; - - // set eagerly to break reference cycles - if (recurse) forced = true; + var oldFlags = flags; + if (recurse) { + if (isStartDeepForce()) return; + flags |= (START_DEEP_FORCE_FLAG | START_SHALLOW_FORCE_FLAG); + } else { + if (isStartShallowForce()) return; + flags |= START_SHALLOW_FORCE_FLAG; + } var fullyForced = true; @@ -191,12 +191,17 @@ public abstract class VmObject extends VmObjectLike { } } } catch (Throwable t) { - forced = false; + CompilerDirectives.transferToInterpreter(); + flags = oldFlags; throw t; } // make sure uncached values are not marked as forced - if (recurse && !fullyForced) forced = false; + if (recurse && !fullyForced) { + flags = 0; + } else { + flags |= recurse ? DEEP_FORCE_FLAG : SHALLOW_FORCE_FLAG; + } } @Override @@ -218,17 +223,14 @@ public abstract class VmObject extends VmObjectLike { */ @TruffleBoundary protected final Map exportMembers() { - Map result = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); - assert forced : "Value was not forced prior to export"; - - iterateAlreadyForcedMemberValues( - (key, member, value) -> { - if (member.isClass() || member.isTypeAlias()) return true; - - result.put(key.toString(), VmValue.export(value)); - return true; - }); + assert isDeepForced() : "Value was not forced prior to export"; + var result = CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + for (var cursor = members(); cursor.advance(); ) { + var member = cursor.member(); + if (member.isType()) continue; + CollectionUtils.put(result, cursor.key().toString(), VmValue.export(cursor.cachedValue())); + } return result; } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectCursor.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectCursor.java new file mode 100644 index 000000000..2e90231e7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectCursor.java @@ -0,0 +1,331 @@ +/* + * 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. + * 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.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.graalvm.collections.UnmodifiableMapCursor; +import org.jspecify.annotations.Nullable; +import org.pkl.core.ast.member.ObjectMember; + +/** + * A cursor for iterating over the keys and values of {@link VmObject} members. + * + *

To obtain a cursor, call one of the following methods: + * + *

    + *
  • {@link VmObjectLike#properties} + *
  • {@link VmObjectLike#elements} + *
  • {@link VmObjectLike#entries} + *
  • {@link VmObjectLike#members} + *
+ * + * To customize cursor behavior, the above methods optionally accept a {@link CursorOption}. + * + *

A typical cursor iteration looks as follows: + * + *


+ *   for (var cursor = object.properties(); cursor.advance(); ) {
+ *     var key = cursor.key();
+ *     var value = cursor.value();
+ *     ...
+ *   }
+ * 
+ * + *

Cursors do not visit {@code local}, {@code external}, and {@code hidden} properties. Only + * {@link VmTyped#members} visits {@linkplain ObjectMember#isClass() classes} and {@linkplain + * ObjectMember#isTypeAlias() type aliases} of {@linkplain VmObjectLike#isModuleObject() module + * objects}. + */ +public abstract class VmObjectCursor { + protected static final VmTyped objectPrototype = BaseModule.getObjectClass().getPrototype(); + + public enum CursorOption { + /** + * Indicates that a cursor may choose any iteration order. + * + *

Specifying this option may increase cursor performance. + * + *

If this option is not specified, members are visited in declaration order. For elements, + * declaration order is equivalent to ascending order. + */ + ANY_ORDER, + /** + * Indicates that the caller will request every value of this cursor unless it encounters an + * unexpected error. + * + *

Specifying this option may increase cursor performance. + */ + ALL_VALUES, + /** + * Indicates that a cursor must not evaluate any member before its value is requested. This + * option should only be specified if a lazyness guarantee is required for correctness. + */ + LAZY_REQUIRED + } + + /** + * Advances this cursor to the next object member. + * + * @return {@code true} if a next member exists, {@code false} otherwise + */ + public abstract boolean advance(); + + /** + * Returns the key of the current object member. + * + *

The returned key has one of the following types: + * + *

    + *
  • {@link Identifier} for a property + *
  • {@link Long} for an element + *
  • {@link Boolean}, {@link Long}, {@link Double}, {@link String} or {@link VmValue} for an + * entry + *
+ * + * @throws RuntimeException if this method is called before the first call to {@link #advance()} + * or after {@link #advance()} returned {@code false} + */ + public abstract Object key(); + + /** + * Returns the value of the current object member. + * + *

If the member's value is not already cached, the member is evaluated. + * + *

If calling from a node, use {@link org.pkl.core.ast.internal.ReadCursorValueNode} instead of + * calling this directly. + * + * @throws RuntimeException if this method is called before the first call to {@link #advance()} + * or after {@link #advance()} returned {@code false} + */ + public abstract Object value(IndirectCallNode callNode); + + /** Returns the cached value for this member, or {@code null} if it does not exist. */ + public abstract @Nullable Object cachedValueOrNull(); + + /** Returns the cached value for this member, asserting that it exists. */ + public final Object cachedValue() { + var value = cachedValueOrNull(); + assert value != null; + return value; + } + + public boolean isProperty() { + return false; + } + + public boolean isElement() { + return false; + } + + public boolean isEntry() { + return false; + } + + protected abstract VmObject iteratee(); + + /** + * Returns the current member. + * + *

If a member is overridden in the prototype chain, this method returns the original + * definition, whereas {@link #value(IndirectCallNode)} returns the value corresponding to the + * overriding definition. As a consequence, {@code member().isElement()} will correctly identify + * elements overridden with entry syntax. + * + *

This method is supported by the following cursors: + * + *

    + *
  • {@link VmObjectLike#members} + *
+ */ + public final ObjectMember member() { + var key = key(); + var iteratee = iteratee(); + var ret = iteratee.getRootFirstMember(key); + assert ret != null; + return ret; + } + + @TruffleBoundary + public final String keyToString() { + return key().toString(); + } + + /** + * The length of the members to be iterated. + * + *

Returns {@code -1} if the length is not statically known (requires evaluation to compute). + */ + public abstract int getLength(); + + static final class EmptyCursor extends VmObjectCursor { + public static final EmptyCursor INSTANCE = new EmptyCursor(); + + private EmptyCursor() {} + + @Override + public boolean advance() { + return false; + } + + @Override + public Object key() { + throw new IllegalStateException("empty cursor"); + } + + @Override + public Object value(IndirectCallNode ignored) { + throw new IllegalStateException("empty cursor"); + } + + @Override + public @Nullable Object cachedValueOrNull() { + throw new IllegalStateException("empty cursor"); + } + + @Override + public boolean isProperty() { + throw new IllegalStateException("empty cursor"); + } + + @Override + public boolean isElement() { + throw new IllegalStateException("empty cursor"); + } + + @Override + public boolean isEntry() { + throw new IllegalStateException("empty cursor"); + } + + @Override + protected VmObject iteratee() { + throw new IllegalStateException("empty cursor"); + } + + @Override + public int getLength() { + return 0; + } + } + + abstract static class AbstractOrderedCursor extends VmObjectCursor { + protected final T iteratee; + + public AbstractOrderedCursor(T iteratee) { + this.iteratee = iteratee; + } + + @Override + public final Object value(IndirectCallNode callNode) { + var key = key(); + var value = iteratee.getCachedValue(key); + if (value != null) return value; + // find and evaluate bottom-most member for key (simplified version of VmUtils.readMember()) + for (VmObject object = iteratee; + object != null && object.parent != objectPrototype; + object = object.parent) { + var member = object.getMember(key); + if (member != null) { + return VmUtils.doReadMember(iteratee, object, key, member, true, callNode); + } + } + throw unreachableCode(); + } + + @Override + public final @Nullable Object cachedValueOrNull() { + return iteratee.getCachedValue(key()); + } + + @TruffleBoundary + private VmException unreachableCode() { + return new VmExceptionBuilder().unreachableCode().build(); + } + + @Override + protected VmObject iteratee() { + return iteratee; + } + } + + abstract static class AbstractCachedMemberCursor extends VmObjectCursor { + protected final T iteratee; + private final UnmodifiableMapCursor cachedValues; + @CompilationFinal private int length = -1; + + @TruffleBoundary + AbstractCachedMemberCursor(T iteratee) { + this.iteratee = iteratee; + cachedValues = iteratee.cachedValues.getEntries(); + } + + protected abstract boolean shouldVisit(Object key); + + @Override + @TruffleBoundary + public boolean advance() { + while (true) { + if (!cachedValues.advance()) return false; + if (shouldVisit(cachedValues.getKey())) return true; + } + } + + @Override + @TruffleBoundary + public Object key() { + return cachedValues.getKey(); + } + + @Override + @TruffleBoundary + public final Object value(IndirectCallNode ignored) { + return cachedValues.getValue(); + } + + @Override + public final Object cachedValueOrNull() { + return cachedValues.getValue(); + } + + @Override + protected VmObject iteratee() { + return iteratee; + } + + public final int getLength() { + if (length == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + length = computeLength(); + } + return length; + } + + private int computeLength() { + var ret = 0; + var cursor = iteratee.cachedValues.getEntries(); + while (cursor.advance()) { + if (shouldVisit(cursor.getKey())) { + ret++; + } + } + return ret; + } + } +} 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 36b506a14..0d155fe10 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 @@ -17,10 +17,12 @@ package org.pkl.core.runtime; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; +import java.util.EnumSet; import java.util.function.BiFunction; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; /** * Corresponds to `pkl.base#Object|pkl.base#Function`. The lexical scope is a chain of @@ -140,35 +142,34 @@ public abstract class VmObjectLike extends VmValue { @TruffleBoundary public abstract boolean hasCachedValue(Object key); - /** - * Iterates over member definitions and their values in order of their definition, from the top of - * the prototype chain downwards. If a member value has not yet been evaluated, a `null` `value` - * is passed to `consumer`. If a member is defined in multiple objects in the prototype chain, - * i.e., is overridden along the way, it is visited only once, with the initial (i.e., upmost) - * `member` and the final (i.e., downmost) `value`. (This peculiar behavior serves two purposes in - * the current implementation: it guarantees that a `hidden` property is still recognized as such - * when overridden, and that an element is still recognized as such when overridden with entry - * syntax. It also means that members are visited in order of (first) definition.) Local, hidden, - * and external properties are not visited. If an invocation of `consumer` returns `false`, the - * remaining members are not visited, and `false` is returned. Otherwise, all members are visited, - * and `true` is returned. - */ - public abstract boolean iterateMemberValues(MemberValueConsumer consumer); + public abstract VmObjectCursor properties(); - /** - * 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); + public abstract VmObjectCursor properties(CursorOption option); - public abstract boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consumer); + public abstract VmObjectCursor elements(); + + public abstract VmObjectCursor elements(CursorOption option); + + public abstract VmObjectCursor elements(EnumSet options); + + public abstract VmObjectCursor entries(); + + public abstract VmObjectCursor entries(CursorOption option); + + public abstract VmObjectCursor entries(EnumSet options); + + public abstract VmObjectCursor members(); + + public abstract VmObjectCursor members(CursorOption option); /** * Iterates over member definitions in order of their definition, from the top of the prototype * chain downwards. If a member is defined multiple times, each occurrence is visited. Local - * properties are not visited. If an invocation of `consumer` returns `false`, the remaining - * members are not visited, and `false` is returned. Otherwise, all members are visited, and - * `true` is returned. + * properties are not visited. If an invocation of {@code consumer} returns {@code false}, the + * remaining members are not visited, and {@code false} is returned. Otherwise, all members are + * visited, and {@code true} is returned. + * + *

Unlike {@link #members()}, this will also visit hidden properties. */ public abstract boolean iterateMembers(BiFunction consumer); @@ -180,22 +181,4 @@ public abstract class VmObjectLike extends VmValue { * properties */ public abstract Object export(); - - @FunctionalInterface - public interface MemberValueConsumer { - /** - * Returns true if {@link #iterateMemberValues} should continue calling this method for the - * remaining members, and false otherwise. - */ - boolean accept(Object key, ObjectMember member, @Nullable Object value); - } - - @FunctionalInterface - public interface ForcedMemberValueConsumer { - /** - * Returns true if {@link #forceAndIterateMemberValues} should continue calling this method for - * the remaining members, and false otherwise. - */ - boolean accept(Object key, ObjectMember member, Object value); - } } 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 1e3fb1284..eb843b44e 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,7 @@ 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 java.util.EnumSet; import org.graalvm.collections.EconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; @@ -28,6 +29,9 @@ import org.pkl.core.PObject; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.expression.unary.ImportNode; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; +import org.pkl.core.runtime.VmObjectCursor.EmptyCursor; +import org.pkl.core.runtime.VmTypedCursors.PropertyCursor; import org.pkl.core.util.EconomicMaps; public final class VmTyped extends VmObject { @@ -165,7 +169,7 @@ public final class VmTyped extends VmObject { @Override @TruffleBoundary public Composite export() { - assert forced : "Value was not forced prior to export"; + assert isDeepForced() : "Value was not forced prior to export"; assert clazz != null; if (!isModuleObject()) { return new PObject(clazz.getPClassInfo(), exportMembers()); @@ -189,6 +193,63 @@ public final class VmTyped extends VmObject { return converter.convertTyped(this, path); } + @Override + public VmObjectCursor properties() { + return new PropertyCursor(this, true); + } + + @Override + public VmObjectCursor properties(CursorOption option) { + // don't force module objects to avoid forcing types (too conservative?) + if (option == CursorOption.ALL_VALUES && !isModuleObject()) { + force(false, false); + } + return new PropertyCursor(this, true); + } + + @Override + public VmObjectCursor elements() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor elements(EnumSet options) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries() { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries(CursorOption option) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor entries(EnumSet options) { + return EmptyCursor.INSTANCE; + } + + @Override + public VmObjectCursor members() { + return new PropertyCursor(this, false); + } + + @Override + public VmObjectCursor members(CursorOption option) { + if (option == CursorOption.ANY_ORDER) { + force(false, false); + } + return new PropertyCursor(this, false); + } + @Override @TruffleBoundary public boolean equals(@Nullable Object obj) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypedCursors.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypedCursors.java new file mode 100644 index 000000000..524e273b2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypedCursors.java @@ -0,0 +1,99 @@ +/* + * 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. + * 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.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.Iterator; +import org.jspecify.annotations.Nullable; +import org.pkl.core.runtime.VmObjectCursor.AbstractOrderedCursor; + +// Doesn't offer CachedPropertyCursor because filtering out local, hidden, and type properties +// while iterating over VmObject.cachedValues likely won't be any better than using PropertyCursor +// in the first place. +final class VmTypedCursors { + private VmTypedCursors() {} + + static final class PropertyCursor extends AbstractOrderedCursor { + private final Iterator identifiers; + private final boolean skipTypes; + private @Nullable Object currentKey; + @CompilationFinal int length = -1; + + @SuppressWarnings({"unchecked", "rawtypes"}) + public PropertyCursor(VmTyped iteratee, boolean skipTypes) { + super(iteratee); + identifiers = (Iterator) iteratee.getVmClass().getAllRegularPropertyNames().iterator(); + this.skipTypes = skipTypes && iteratee.isModuleObject(); + } + + @Override + @TruffleBoundary + public boolean advance() { + if (identifiers.hasNext()) { + currentKey = identifiers.next(); + if (skipTypes) { + while (member().isType()) { + if (identifiers.hasNext()) { + currentKey = identifiers.next(); + } else { + currentKey = null; + return false; + } + } + } + return true; + } + currentKey = null; + return false; + } + + @Override + public Object key() { + assert currentKey != null : "illegal state"; + return currentKey; + } + + @Override + public boolean isProperty() { + return true; + } + + @Override + public int getLength() { + if (length == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + length = computeLength(); + } + return length; + } + + private int computeLength() { + var length = 0; + var clazz = iteratee.getVmClass(); + for (var identifier : iteratee.getVmClass().getAllRegularPropertyNames()) { + var member = clazz.getProperty((Identifier) identifier); + assert member != null; + if (skipTypes && member.isType()) { + continue; + } + length++; + } + return length; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java index bace0c398..2293e786f 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java @@ -61,6 +61,7 @@ import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.module.ModuleKey; import org.pkl.core.module.ModuleKeys; import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.Pair; import org.pkl.parser.Parser; @@ -293,43 +294,43 @@ public final class VmUtils { } public static Map readFilesProperty( - VmObjectLike receiver, Function fileOutputFactory) { - var filesOrNull = VmUtils.readMember(receiver, Identifier.FILES); + VmObjectLike receiver, + Function fileOutputFactory, + IndirectCallNode callNode) { + var filesOrNull = VmUtils.readMember(receiver, Identifier.FILES, callNode); if (filesOrNull instanceof VmNull) { return Map.of(); } var files = (VmMapping) filesOrNull; var result = new LinkedHashMap(); - files.forceAndIterateMemberValues( - (key, member, value) -> { - assert member.isEntry(); - result.put((String) key, fileOutputFactory.apply((VmTyped) value)); - return true; - }); + for (var cursor = files.members(CursorOption.ALL_VALUES); cursor.advance(); ) { + var key = cursor.key(); + var member = cursor.member(); + assert member.isEntry(); + var value = (VmTyped) cursor.value(callNode); + result.put((String) key, fileOutputFactory.apply(value)); + } return result; } - @TruffleBoundary public static Object readMember(VmObjectLike receiver, Object memberKey) { var result = readMemberOrNull(receiver, memberKey); if (result != null) return result; + CompilerDirectives.transferToInterpreter(); throw new VmExceptionBuilder().cannotFindMember(receiver, memberKey).build(); } - @TruffleBoundary public static @Nullable Object readMemberOrNull( VmObjectLike receiver, Object memberKey, boolean checkType) { return readMemberOrNull(receiver, memberKey, checkType, IndirectCallNode.getUncached()); } - @TruffleBoundary public static @Nullable Object readMemberOrNull( VmObjectLike receiver, Object memberKey, IndirectCallNode callNode) { return readMemberOrNull(receiver, memberKey, true, callNode); } - @TruffleBoundary public static @Nullable Object readMemberOrNull(VmObjectLike receiver, Object memberKey) { var cachedValue = receiver.getCachedValue(memberKey); if (cachedValue != null) { @@ -342,17 +343,16 @@ public final class VmUtils { * Before calling this method, always try `VmObject.getCachedValue()`. (This method writes to the * cache, but doesn't read from it.) */ - @TruffleBoundary public static Object doReadMember( VmObjectLike receiver, VmObjectLike owner, Object memberKey, ObjectMember member) { return doReadMember(receiver, owner, memberKey, member, true, IndirectCallNode.getUncached()); } - @TruffleBoundary public static Object readMember( VmObjectLike receiver, Object memberKey, IndirectCallNode callNode) { var result = readMemberOrNull(receiver, memberKey, true, callNode); if (result != null) return result; + CompilerDirectives.transferToInterpreter(); throw new VmExceptionBuilder() .cannotFindMember(receiver, memberKey) @@ -360,13 +360,12 @@ public final class VmUtils { .build(); } - @TruffleBoundary public static @Nullable Object readMemberOrNull( VmObjectLike receiver, Object memberKey, boolean checkType, IndirectCallNode callNode) { assert (!(memberKey instanceof Identifier identifier) || !identifier.isLocalProp()) : "Must use ReadLocalPropertyNode for local properties."; - final var cachedValue = receiver.getCachedValue(memberKey); + var cachedValue = receiver.getCachedValue(memberKey); if (cachedValue != null) return cachedValue; for (var owner = receiver; owner != null; owner = owner.getParent()) { @@ -382,7 +381,6 @@ public final class VmUtils { * Before calling this method, always try `VmObject.getCachedValue()`. (This method writes to the * cache, but doesn't read from it.) */ - @TruffleBoundary public static Object doReadMember( VmObjectLike receiver, VmObjectLike owner, @@ -604,6 +602,11 @@ public final class VmUtils { builder.append(string); } + @TruffleBoundary + public static void appendToBuilder(StringBuilder builder, Object object) { + builder.append(object); + } + @TruffleBoundary public static String builderToString(StringBuilder builder) { return builder.toString(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java index 7babe75ea..366b4ca3b 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java @@ -18,7 +18,7 @@ package org.pkl.core.runtime; import java.util.ArrayDeque; import java.util.Deque; import org.pkl.core.ValueFormatter; -import org.pkl.core.util.MutableBoolean; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.parser.Lexer; /** @@ -332,56 +332,57 @@ public final class VmValueRenderer { currIndent += indent; - var isEmpty = new MutableBoolean(true); - object.iterateMemberValues( - (key, member, value) -> { - // don't render type definitions - if (member.isClass() || member.isTypeAlias()) return true; + var isEmpty = true; + for (var cursor = object.members(CursorOption.LAZY_REQUIRED); cursor.advance(); ) { + var key = cursor.key(); + var value = cursor.cachedValueOrNull(); + var member = cursor.member(); + // don't render type definitions + if (member.isType()) continue; - if (isEmpty.get()) { - append(leadingOrTrailingNewline); - isEmpty.set(false); - } + if (isEmpty) { + append(leadingOrTrailingNewline); + isEmpty = false; + } - append(currIndent); + append(currIndent); - if (member.isProp()) { - contexts.push(Context.IMPLICIT); - writeIdentifier(key.toString()); - if (value instanceof VmObjectLike) { - append(' '); - } else { - append(" = "); - } - } else if (member.isElement()) { - contexts.push(Context.EXPLICIT); - } else { - assert member.isEntry(); - contexts.push(Context.EXPLICIT); - append('['); - visit(key); - append(']'); - contexts.pop(); - contexts.push(Context.IMPLICIT); - if (value instanceof VmObjectLike) { - append(' '); - } else { - append(" = "); - } - } + if (member.isProp()) { + contexts.push(Context.IMPLICIT); + writeIdentifier(key.toString()); + if (value instanceof VmObjectLike) { + append(' '); + } else { + append(" = "); + } + } else if (member.isElement()) { + contexts.push(Context.EXPLICIT); + } else { + assert member.isEntry(); + contexts.push(Context.EXPLICIT); + append('['); + visit(key); + append(']'); + contexts.pop(); + contexts.push(Context.IMPLICIT); + if (value instanceof VmObjectLike) { + append(' '); + } else { + append(" = "); + } + } - if (value == null) { // not forced - append('?'); - } else { - visit(value); - } + if (value == null) { // not forced + append('?'); + } else { + visit(value); + } - contexts.pop(); - append(interiorNewline); - return true; - }); + contexts.pop(); + append(interiorNewline); + } - if (!isEmpty.get()) { + if (!isEmpty) { // replace last interiorNewline with leadingOrTrailingNewline builder.delete(builder.length() - interiorNewline.length(), builder.length()); append(leadingOrTrailingNewline); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java b/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java index 17c24416f..87c886303 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java @@ -34,6 +34,7 @@ import org.pkl.core.runtime.VmListing; import org.pkl.core.runtime.VmMap; import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.runtime.VmReference; import org.pkl.core.runtime.VmSet; import org.pkl.core.runtime.VmTypeAlias; @@ -69,6 +70,9 @@ public abstract class AbstractRenderer implements VmValueVisitor { /** The (closest) {@link SourceSection} of the value being visited, for better error messages. */ protected @Nullable SourceSection currSourceSection = null; + /** Whether we are visiting an entry key or not. */ + private boolean isVisitingEntryKey; + protected AbstractRenderer( String name, PklConverter converter, boolean skipNullProperties, boolean skipNullEntries) { this.name = name; @@ -118,9 +122,9 @@ public abstract class AbstractRenderer implements VmValueVisitor { try { VmValueVisitor.super.visit(value); } catch (VmUndefinedValueException e) { - // Only fill in the path if rendering the leaf member that threw the error. We know we are - // at a leaf if the error's receiver is the same as `value`. - if (e.getReceiver() == value) { + // Only fill in a hint if we are not visiting an entry key; when visiting an entry key, + // `currPath` is not tracked from the actual root value (see `visitEntryKeyValue`). + if (e.getReceiver() == value && !isVisitingEntryKey) { throw e.fillInHint(currPath, topLevelValue); } throw e; @@ -193,18 +197,17 @@ public abstract class AbstractRenderer implements VmValueVisitor { enclosingValue = value; var isFirst = new MutableBoolean(true); - value.iterateAlreadyForcedMemberValues( - (memberKey, member, memberValue) -> { - if (member.isClass() || member.isTypeAlias()) return true; - assert member.isProp(); - doVisitProperty( - (Identifier) memberKey, - memberValue, - value.getVmClass().getProperty((Identifier) memberKey), - member.getSourceSection(), - isFirst); - return true; - }); + for (var cursor = value.properties(CursorOption.ALL_VALUES); cursor.advance(); ) { + var member = cursor.member(); + if (member.isClass() || member.isTypeAlias()) continue; + assert member.isProp(); + doVisitProperty( + (Identifier) cursor.key(), + cursor.cachedValue(), + value.getVmClass().getProperty((Identifier) cursor.key()), + cursor.member().getSourceSection(), + isFirst); + } enclosingValue = prevEnclosingValue; endTyped(value, isFirst.get()); @@ -220,21 +223,20 @@ public abstract class AbstractRenderer implements VmValueVisitor { var isFirst = new MutableBoolean(true); var canRenderPropertyOrEntry = canRenderPropertyOrEntryOf(value); - value.iterateAlreadyForcedMemberValues( - (memberKey, member, memberValue) -> { - var sourceSection = member.getSourceSection(); - if (member.isProp()) { - if (!canRenderPropertyOrEntry) cannotRenderObjectWithElementsAndOtherMembers(value); - doVisitProperty((Identifier) memberKey, memberValue, null, sourceSection, isFirst); - } else if (member.isEntry()) { - if (!canRenderPropertyOrEntry) cannotRenderObjectWithElementsAndOtherMembers(value); - doVisitEntry(memberKey, memberValue, sourceSection, isFirst); - } else { - doVisitElement((long) memberKey, memberValue, sourceSection, isFirst.getAndSetFalse()); - } - return true; - }); - + for (var cursor = value.members(CursorOption.ALL_VALUES); cursor.advance(); ) { + var memberKey = cursor.key(); + var memberValue = cursor.cachedValue(); + var sourceSection = cursor.member().getSourceSection(); + if (cursor.isProperty()) { + if (!canRenderPropertyOrEntry) cannotRenderObjectWithElementsAndOtherMembers(value); + doVisitProperty((Identifier) memberKey, memberValue, null, sourceSection, isFirst); + } else if (cursor.isEntry()) { + if (!canRenderPropertyOrEntry) cannotRenderObjectWithElementsAndOtherMembers(value); + doVisitEntry(memberKey, memberValue, sourceSection, isFirst); + } else { + doVisitElement((long) memberKey, memberValue, sourceSection, isFirst.getAndSetFalse()); + } + } enclosingValue = prevEnclosingValue; endDynamic(value, isFirst.get()); } @@ -259,13 +261,13 @@ public abstract class AbstractRenderer implements VmValueVisitor { enclosingValue = value; var isFirst = new MutableBoolean(true); - value.iterateAlreadyForcedMemberValues( - (memberKey, member, memberValue) -> { - assert member.isElement(); - doVisitElement( - (long) memberKey, memberValue, member.getSourceSection(), isFirst.getAndSetFalse()); - return true; - }); + for (var cursor = value.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + doVisitElement( + (long) cursor.key(), + cursor.cachedValue(), + cursor.member().getSourceSection(), + isFirst.getAndSetFalse()); + } enclosingValue = prevEnclosingValue; endListing(value, isFirst.get()); @@ -280,12 +282,10 @@ public abstract class AbstractRenderer implements VmValueVisitor { enclosingValue = value; var isFirst = new MutableBoolean(true); - value.iterateAlreadyForcedMemberValues( - (memberKey, member, memberValue) -> { - assert member.isEntry(); - doVisitEntry(memberKey, memberValue, member.getSourceSection(), isFirst); - return true; - }); + for (var cursor = value.entries(CursorOption.ALL_VALUES); cursor.advance(); ) { + assert cursor.member().isEntry(); + doVisitEntry(cursor.key(), cursor.cachedValue(), cursor.member().getSourceSection(), isFirst); + } enclosingValue = prevEnclosingValue; endMapping(value, isFirst.get()); @@ -380,7 +380,12 @@ public abstract class AbstractRenderer implements VmValueVisitor { protected void visitEntryKeyValue( Object key, boolean isFirst, Deque valuePath, Object value) { currPath = new ArrayDeque<>(); - visitEntryKey(key, isFirst); + isVisitingEntryKey = true; + try { + visitEntryKey(key, isFirst); + } finally { + isVisitingEntryKey = false; + } currPath = valuePath; visitEntryValue(value); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalMemberNode.java b/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalMemberNode.java index ea08f9e83..19bb33733 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalMemberNode.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalMemberNode.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. @@ -28,7 +28,7 @@ public abstract class ExternalMemberNode extends ExpressionNode { super(); } - protected abstract ExpressionNode getReceiverNode(); + public abstract ExpressionNode getReceiverNode(); @TruffleBoundary protected final String getQualifiedMemberName() { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java b/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java index 9bf04ed44..847f25664 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java @@ -15,10 +15,12 @@ */ package org.pkl.core.stdlib; +import com.oracle.truffle.api.nodes.IndirectCallNode; import java.util.*; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.member.ClassProperty; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.util.Pair; public final class PklConverter implements VmValueConverter { @@ -49,13 +51,16 @@ public final class PklConverter implements VmValueConverter { private final @Nullable VmFunction referenceConverter; private PklConverter( - VmMapping converters, VmMapping convertPropertyTransformers, Object rendererOrParser) { + VmMapping converters, + VmMapping convertPropertyTransformers, + Object rendererOrParser, + IndirectCallNode callNode) { converters.force(false, false); convertPropertyTransformers.force(false, false); - typeConverters = createTypeConverters(converters); + typeConverters = createTypeConverters(converters, callNode); this.convertPropertyTransformers = - createConvertPropertyTransformers(convertPropertyTransformers); - pathConverters = createPathConverters(converters); + createConvertPropertyTransformers(convertPropertyTransformers, callNode); + pathConverters = createPathConverters(converters, callNode); this.rendererOrParser = rendererOrParser; stringConverter = typeConverters.get(BaseModule.getStringClass()); @@ -81,19 +86,23 @@ public final class PklConverter implements VmValueConverter { } public static final PklConverter NOOP = - new PklConverter(VmMapping.empty(), VmMapping.empty(), VmNull.withoutDefault()); + new PklConverter( + VmMapping.empty(), + VmMapping.empty(), + VmNull.withoutDefault(), + IndirectCallNode.getUncached()); - public static PklConverter fromRenderer(VmTyped renderer) { - var converters = (VmMapping) VmUtils.readMember(renderer, Identifier.CONVERTERS); + public static PklConverter fromRenderer(VmTyped renderer, IndirectCallNode callNode) { + var converters = (VmMapping) VmUtils.readMember(renderer, Identifier.CONVERTERS, callNode); var convertPropertyTransformers = (VmMapping) VmUtils.readMember(renderer, Identifier.CONVERT_PROPERTY_TRANSFORMERS); - return new PklConverter(converters, convertPropertyTransformers, renderer); + return new PklConverter(converters, convertPropertyTransformers, renderer, callNode); } - public static PklConverter fromParser(VmTyped parser) { - var converters = (VmMapping) VmUtils.readMember(parser, Identifier.CONVERTERS); + public static PklConverter fromParser(VmTyped parser, IndirectCallNode callNode) { + var converters = (VmMapping) VmUtils.readMember(parser, Identifier.CONVERTERS, callNode); return new PklConverter( - converters, VmMapping.empty(), parser); // no annotation converters in parsers + converters, VmMapping.empty(), parser, callNode); // no annotation converters in parsers } @Override @@ -234,44 +243,38 @@ public final class PklConverter implements VmValueConverter { return Pair.of(Identifier.get((String) prop.getFirst()), prop.getSecond()); } - private Map createTypeConverters(VmMapping converters) { + private Map createTypeConverters( + VmMapping converters, IndirectCallNode callNode) { var result = new HashMap(); - converters.iterateMemberValues( - (key, member, value) -> { - assert value != null; // forced in ctor - if (key instanceof VmClass vmClass) { - result.put(vmClass, (VmFunction) value); - } - return true; - }); + for (var cursor = converters.entries(CursorOption.ANY_ORDER); cursor.advance(); ) { + if (cursor.key() instanceof VmClass vmClass) { + result.put(vmClass, (VmFunction) cursor.value(callNode)); + } + } return result; } private Map createConvertPropertyTransformers( - VmMapping convertPropertyTransformers) { + VmMapping convertPropertyTransformers, IndirectCallNode callNode) { var result = new HashMap(); - convertPropertyTransformers.iterateMemberValues( - (key, member, value) -> { - assert value != null; // forced in ctor - result.put((VmClass) key, ((VmFunction) value)); - return true; - }); - + for (var cursor = convertPropertyTransformers.members(CursorOption.ALL_VALUES); + cursor.advance(); ) { + var value = cursor.value(callNode); + result.put((VmClass) cursor.key(), (VmFunction) value); + } return result; } @SuppressWarnings("unchecked") - private Pair[] createPathConverters(VmMapping converters) { + private Pair[] createPathConverters( + VmMapping converters, IndirectCallNode callNode) { var result = new ArrayList>(); var parser = new PathSpecParser(); - converters.iterateMemberValues( - (key, member, value) -> { - assert value != null; // forced in ctor - if (key instanceof String string) { - result.add(Pair.of(parser.parse(string), (VmFunction) value)); - } - return true; - }); + for (var cursor = converters.entries(); cursor.advance(); ) { + if (cursor.key() instanceof String string) { + result.add(Pair.of(parser.parse(string), (VmFunction) cursor.value(callNode))); + } + } return result.toArray(new Pair[0]); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/DynamicNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/DynamicNodes.java index 06ee12eea..17affbb8e 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/DynamicNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/DynamicNodes.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. @@ -16,9 +16,13 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.stdlib.ExternalMethod0Node; import org.pkl.core.stdlib.ExternalMethod1Node; @@ -65,30 +69,33 @@ public final class DynamicNodes { public abstract static class toMap extends ExternalMethod0Node { @Specialization - protected VmMap eval(VmDynamic self) { + protected VmMap eval( + VirtualFrame frame, + VmDynamic self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { var builder = VmMap.builder(); - self.forceAndIterateMemberValues( // could be smarter and skip forcing elements - (key, member, value) -> { - if (!member.isElement()) { - builder.add(key instanceof Identifier ? key.toString() : key, value); - } - return true; - }); + for (var cursor = self.members(); cursor.advance(); ) { + if (!cursor.isElement()) { + var key = cursor.key(); + builder.add( + key instanceof Identifier ? key.toString() : key, + readCursorValueNode.execute(frame, cursor)); + } + } return builder.build(); } } public abstract static class toList extends ExternalMethod0Node { @Specialization - protected VmList eval(VmDynamic self) { + protected VmList eval( + VirtualFrame frame, + VmDynamic self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { var builder = VmList.EMPTY.builder(); - self.forceAndIterateMemberValues( // could be smarter and only force elements - (key, member, value) -> { - if (member.isElement()) { - builder.add(value); - } - return true; - }); + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + builder.add(readCursorValueNode.execute(frame, cursor)); + } return builder.build(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java index 4f4ac7284..9c8627083 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.pkl.core.runtime.*; import org.pkl.core.stdlib.AbstractStringRenderer; import org.pkl.core.stdlib.ExternalMethod1Node; @@ -29,12 +31,13 @@ public final class JsonRendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { // JSON document can have any top-level value // https://stackoverflow.com/a/3833312 // http://www.ietf.org/rfc/rfc7159.txt var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -42,17 +45,20 @@ public final class JsonRendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } - private static JsonRenderer createRenderer(VmTyped self, StringBuilder builder) { + private static JsonRenderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); - return new JsonRenderer(builder, indent, PklConverter.fromRenderer(self), omitNullProperties); + return new JsonRenderer( + builder, indent, PklConverter.fromRenderer(self, callNode), omitNullProperties); } private static final class JsonRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java index 5ff1dd824..c42b19a1a 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java @@ -17,6 +17,7 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.LoopNode; @@ -25,6 +26,7 @@ import org.jspecify.annotations.Nullable; import org.pkl.core.ast.expression.binary.*; import org.pkl.core.ast.internal.IsInstanceOfNode; import org.pkl.core.ast.internal.IsInstanceOfNodeGen; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.ast.internal.ToStringNode; import org.pkl.core.ast.internal.ToStringNodeGen; import org.pkl.core.ast.lambda.*; @@ -651,8 +653,11 @@ public final class ListNodes { public abstract static class flatten extends ExternalMethod0Node { @Specialization - protected VmList eval(VmList self) { - return (VmList) self.flatten(); + protected VmList eval( + VirtualFrame frame, + VmList self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + return (VmList) self.flatten(frame, readCursorValueNode); } } @@ -1285,8 +1290,8 @@ public final class ListNodes { var iter = self.iterator(); var builder = VmUtils.createBuilder(); VmUtils.appendToBuilder(builder, toStringNode.executeWith(frame, iter.next())); - - while (loopConditionProfile.profile(iter.hasNext())) { + loopConditionProfile.profileCounted(self.getLength()); + while (loopConditionProfile.inject(iter.hasNext())) { VmUtils.appendToBuilder(builder, separator); VmUtils.appendToBuilder(builder, toStringNode.executeWith(frame, iter.next())); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java index ad214eb05..2c6a0d6d4 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java @@ -17,15 +17,18 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.nodes.LoopNode; import org.pkl.core.ast.PklNode; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.ast.internal.ToStringNode; import org.pkl.core.ast.internal.ToStringNodeGen; import org.pkl.core.ast.lambda.*; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.stdlib.ExternalMethod0Node; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.ExternalMethod2Node; @@ -65,11 +68,12 @@ public final class ListingNodes { public abstract static class getOrNull extends ExternalMethod1Node { @Specialization - protected Object eval(VmListing self, long index) { + protected Object eval( + VmListing self, long index, @Cached("create()") IndirectCallNode callNode) { if (index < 0 || index >= self.getLength()) { return VmNull.withoutDefault(); } - return VmUtils.readMember(self, index); + return VmUtils.readMember(self, index, callNode); } } @@ -83,16 +87,24 @@ public final class ListingNodes { var defaultFunction = (VmFunction) VmUtils.readMember(self, Identifier.DEFAULT, callNode); return applyNode.execute(defaultFunction, index); } - return VmUtils.readMember(self, index); + return VmUtils.readMember(self, index, callNode); } } public abstract static class isDistinct extends ExternalPropertyNode { @Specialization - @TruffleBoundary - protected boolean eval(VmListing self) { - var visitedValues = CollectionUtils.newHashSet(); - return self.forceAndIterateMemberValues((key, member, value) -> visitedValues.add(value)); + protected boolean eval( + VirtualFrame frame, + VmListing self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var seenValues = EconomicSets.create(); + for (var cursor = self.elements(CursorOption.ANY_ORDER); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + if (!EconomicSets.add(seenValues, value)) { + return false; + } + } + return true; } } @@ -100,80 +112,90 @@ public final class ListingNodes { @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); @Specialization - protected boolean eval(VmListing self, VmFunction selector) { - var visitedValues = CollectionUtils.newHashSet(); - return self.forceAndIterateMemberValues( - (key, member, value) -> visitedValues.add(applyNode.execute(selector, value))); + protected boolean eval( + VirtualFrame frame, + VmListing self, + VmFunction selector, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var seenValues = EconomicSets.create(); + for (var cursor = self.elements(CursorOption.ANY_ORDER); cursor.advance(); ) { + var cursorValue = readCursorValueNode.execute(frame, cursor); + var value = applyNode.execute(selector, cursorValue); + if (!EconomicSets.add(seenValues, value)) return false; + } + return true; } } public abstract static class distinct extends ExternalPropertyNode { @Specialization - protected VmListing eval(VmListing self) { - var visitedValues = CollectionUtils.newHashSet(); + protected VmListing eval( + VirtualFrame frame, + VmListing self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var seenValues = EconomicSets.create(); var builder = new VmObjectBuilder(); - - self.forceAndIterateMemberValues( - (key, member, value) -> { - if (visitedValues.add(value)) { - builder.addElement(value); - } - return true; - }); - + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + if (EconomicSets.add(seenValues, value)) { + builder.addElement(value); + } + } return builder.toListing(); } } public abstract static class first extends ExternalPropertyNode { @Specialization - protected Object eval(VmListing self) { + protected Object eval(VmListing self, @Cached("create()") IndirectCallNode callNode) { checkNonEmpty(self, this); - return VmUtils.readMember(self, 0L); + return VmUtils.readMember(self, 0L, callNode); } } public abstract static class firstOrNull extends ExternalPropertyNode { @Specialization - protected Object eval(VmListing self) { + protected Object eval(VmListing self, @Cached("create()") IndirectCallNode callNode) { if (self.isEmpty()) { return VmNull.withoutDefault(); } - return VmUtils.readMember(self, 0L); + return VmUtils.readMember(self, 0L, callNode); } } public abstract static class last extends ExternalPropertyNode { @Specialization - protected Object eval(VmListing self) { + protected Object eval(VmListing self, @Cached("create()") IndirectCallNode callNode) { checkNonEmpty(self, this); - return VmUtils.readMember(self, self.getLength() - 1L); + return VmUtils.readMember(self, self.getLength() - 1L, callNode); } } public abstract static class lastOrNull extends ExternalPropertyNode { @Specialization - protected Object eval(VmListing self) { + protected Object eval(VmListing self, @Cached("create()") IndirectCallNode callNode) { var length = self.getLength(); - return length == 0 ? VmNull.withoutDefault() : VmUtils.readMember(self, length - 1L); + return length == 0 + ? VmNull.withoutDefault() + : VmUtils.readMember(self, length - 1L, callNode); } } public abstract static class single extends ExternalPropertyNode { @Specialization - protected Object eval(VmListing self) { + protected Object eval(VmListing self, @Cached("create()") IndirectCallNode callNode) { checkSingleton(self, this); - return VmUtils.readMember(self, 0L); + return VmUtils.readMember(self, 0L, callNode); } } public abstract static class singleOrNull extends ExternalPropertyNode { @Specialization - protected Object eval(VmListing self) { + protected Object eval(VmListing self, @Cached("create()") IndirectCallNode callNode) { if (self.getLength() != 1) { return VmNull.withoutDefault(); } - return VmUtils.readMember(self, 0L); + return VmUtils.readMember(self, 0L, callNode); } } @@ -181,18 +203,20 @@ public final class ListingNodes { @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); @Specialization - protected VmListing eval(VmListing self, VmFunction selector) { - var visitedValues = CollectionUtils.newHashSet(); + protected VmListing eval( + VirtualFrame frame, + VmListing self, + VmFunction selector, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var seenValues = EconomicSets.create(); var builder = new VmObjectBuilder(); - - self.forceAndIterateMemberValues( - (key, member, value) -> { - if (visitedValues.add(applyNode.execute(selector, value))) { - builder.addElement(value); - } - return true; - }); - + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + var selectorValue = applyNode.execute(selector, value); + if (EconomicSets.add(seenValues, selectorValue)) { + builder.addElement(value); + } + } return builder.toListing(); } } @@ -201,17 +225,16 @@ public final class ListingNodes { @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); @Specialization - protected boolean eval(VmListing self, VmFunction predicate) { - var result = new MutableBoolean(true); - self.iterateMemberValues( - (key, member, value) -> { - if (value == null) { - value = VmUtils.readMember(self, key); - } - result.set(applyNode.executeBoolean(predicate, value)); - return result.get(); - }); - return result.get(); + protected boolean eval( + VirtualFrame frame, + VmListing self, + VmFunction predicate, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + for (var cursor = self.elements(CursorOption.ANY_ORDER); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + if (!applyNode.executeBoolean(predicate, value)) return false; + } + return true; } } @@ -219,34 +242,39 @@ public final class ListingNodes { @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); @Specialization - protected boolean eval(VmListing self, VmFunction predicate) { - var result = new MutableBoolean(false); - self.iterateMemberValues( - (key, member, value) -> { - if (value == null) { - value = VmUtils.readMember(self, key); - } - result.set(applyNode.executeBoolean(predicate, value)); - return !result.get(); - }); - return result.get(); + protected boolean eval( + VirtualFrame frame, + VmListing self, + VmFunction predicate, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + for (var cursor = self.elements(CursorOption.ANY_ORDER); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + if (applyNode.executeBoolean(predicate, value)) return true; + } + return false; } } public abstract static class contains extends ExternalMethod1Node { @Specialization - protected boolean eval(VmListing self, Object element) { - var result = new MutableBoolean(false); - self.iterateMemberValues( - (key, member, value) -> { - if (value == null) { - value = VmUtils.readMember(self, key); - } - result.set(element.equals(value)); - return !result.get(); - }); - LoopNode.reportLoopCount(this, self.getLength()); - return result.get(); + protected boolean eval( + VirtualFrame frame, + VmListing self, + Object element, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + for (var cursor = self.elements(CursorOption.ANY_ORDER); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + // TODO replace with EqualNode + if (equals(value, element)) { + return true; + } + } + return false; + } + + @TruffleBoundary + private boolean equals(Object a, Object b) { + return a.equals(b); } } @@ -254,15 +282,19 @@ public final class ListingNodes { @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); @Specialization - protected Object eval(VmListing self, Object initial, VmFunction function) { - var result = new MutableReference<>(initial); - self.forceAndIterateMemberValues( - (key, member, value) -> { - result.set(applyLambdaNode.execute(function, result.get(), value)); - return true; - }); + protected Object eval( + VirtualFrame frame, + VmListing self, + Object initial, + VmFunction function, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var result = initial; + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + result = applyLambdaNode.execute(function, result, value); + } LoopNode.reportLoopCount(this, self.getLength()); - return result.get(); + return result; } } @@ -270,17 +302,19 @@ public final class ListingNodes { @Child private ApplyVmFunction3Node applyLambdaNode = ApplyVmFunction3NodeGen.create(); @Specialization - protected Object eval(VmListing self, Object initial, VmFunction function) { - var index = new MutableLong(0); - var result = new MutableReference<>(initial); - self.forceAndIterateMemberValues( - (key, member, value) -> { - result.set( - applyLambdaNode.execute(function, index.getAndIncrement(), result.get(), value)); - return true; - }); + protected Object eval( + VirtualFrame frame, + VmListing self, + Object initial, + VmFunction function, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var result = initial; + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + result = applyLambdaNode.execute(function, cursor.key(), result, value); + } LoopNode.reportLoopCount(this, self.getLength()); - return result.get(); + return result; } } @@ -288,21 +322,25 @@ public final class ListingNodes { @Child ToStringNode toStringNode = ToStringNodeGen.create(sourceSection, null); @Specialization - protected Object eval(VirtualFrame frame, VmListing self, String separator) { + protected Object eval( + VirtualFrame frame, + VmListing self, + String separator, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { if (self.isEmpty()) return ""; - var builder = new StringBuilder(); - var materializedFrame = frame.materialize(); - self.forceAndIterateMemberValues( - (key, member, value) -> { - if (!key.equals(0L)) { - builder.append(separator); - } - builder.append(toStringNode.executeWith(materializedFrame, value)); - return true; - }); + var isFirst = true; + for (var cursor = self.elements(); cursor.advance(); ) { + if (isFirst) { + isFirst = false; + } else { + VmUtils.appendToBuilder(builder, separator); + } + var value = toStringNode.executeWith(frame, readCursorValueNode.execute(frame, cursor)); + VmUtils.appendToBuilder(builder, value); + } LoopNode.reportLoopCount(this, self.getLength()); - return builder.toString(); + return VmUtils.builderToString(builder); } @Override @@ -313,26 +351,30 @@ public final class ListingNodes { public abstract static class toList extends ExternalMethod0Node { @Specialization - protected VmList eval(VmListing self) { + protected VmList eval( + VirtualFrame frame, + VmListing self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { var builder = VmList.EMPTY.builder(); - self.forceAndIterateMemberValues( - (key, member, value) -> { - builder.add(value); - return true; - }); + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + builder.add(value); + } return builder.build(); } } public abstract static class toSet extends ExternalMethod0Node { @Specialization - protected VmSet eval(VmListing self) { + protected VmSet eval( + VirtualFrame frame, + VmListing self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { var builder = VmSet.EMPTY.builder(); - self.forceAndIterateMemberValues( - (key, member, value) -> { - builder.add(value); - return true; - }); + for (var cursor = self.elements(CursorOption.ALL_VALUES); cursor.advance(); ) { + var value = readCursorValueNode.execute(frame, cursor); + builder.add(value); + } return builder.build(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java index 8b14049be..8ddc67f81 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java @@ -15,20 +15,23 @@ */ package org.pkl.core.stdlib.base; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.ast.lambda.ApplyVmFunction1Node; import org.pkl.core.ast.lambda.ApplyVmFunction2Node; import org.pkl.core.ast.lambda.ApplyVmFunction2NodeGen; import org.pkl.core.ast.lambda.ApplyVmFunction3Node; import org.pkl.core.ast.lambda.ApplyVmFunction3NodeGen; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.stdlib.ExternalMethod0Node; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.ExternalMethod2Node; import org.pkl.core.stdlib.ExternalPropertyNode; -import org.pkl.core.util.MutableBoolean; -import org.pkl.core.util.MutableReference; public final class MappingNodes { private MappingNodes() {} @@ -66,7 +69,7 @@ public final class MappingNodes { protected boolean eval(VmMapping self, Object key) { if (self.hasCachedValue(key)) return true; - for (VmObjectLike curr = self; curr != null; curr = curr.getParent()) { + for (VmObject curr = self; curr != null; curr = curr.getParent()) { if (curr.hasMember(key)) return true; } @@ -76,17 +79,29 @@ public final class MappingNodes { public abstract static class containsValue extends ExternalMethod1Node { @Specialization - protected boolean eval(VmMapping self, Object value) { - var foundValue = new MutableBoolean(false); - self.iterateMemberValues( - (key, member, memberValue) -> { - if (memberValue == null) { - memberValue = VmUtils.readMember(self, key); - } - foundValue.set(value.equals(memberValue)); - return !foundValue.get(); - }); - return foundValue.get(); + protected boolean eval( + VirtualFrame frame, + VmMapping self, + Object value, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + for (var cursor = self.entries(CursorOption.ANY_ORDER); cursor.advance(); ) { + var cursorValue = readCursorValueNode.execute(frame, cursor); + // TODO replace with EqualNode + if (equals(value, cursorValue)) { + return true; + } + } + return false; + } + + @TruffleBoundary + private boolean equals(Object a, Object b) { + return a.equals(b); + } + + @Override + public boolean isInstrumentable() { + return false; } } @@ -119,14 +134,19 @@ public final class MappingNodes { @Child private ApplyVmFunction3Node applyLambdaNode = ApplyVmFunction3NodeGen.create(); @Specialization - protected Object eval(VmMapping self, Object initial, VmFunction function) { - var result = new MutableReference<>(initial); - self.forceAndIterateMemberValues( - (key, def, value) -> { - result.set(applyLambdaNode.execute(function, result.get(), key, value)); - return true; - }); - return result.get(); + protected Object eval( + VirtualFrame frame, + VmMapping self, + Object initial, + VmFunction function, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + var result = initial; + for (var cursor = self.entries(); cursor.advance(); ) { + result = + applyLambdaNode.execute( + function, result, cursor.key(), readCursorValueNode.execute(frame, cursor)); + } + return result; } } @@ -134,17 +154,16 @@ public final class MappingNodes { @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); @Specialization - protected boolean eval(VmMapping self, VmFunction function) { - var result = new MutableBoolean(true); - self.iterateMemberValues( - (key, member, value) -> { - if (value == null) { - value = VmUtils.readMember(self, key); - } - result.set(applyLambdaNode.executeBoolean(function, key, value)); - return result.get(); - }); - return result.get(); + protected boolean eval( + VirtualFrame frame, + VmMapping self, + VmFunction function, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + for (var cursor = self.entries(CursorOption.ANY_ORDER); cursor.advance(); ) { + if (!applyLambdaNode.executeBoolean( + function, cursor.key(), readCursorValueNode.execute(frame, cursor))) return false; + } + return true; } } @@ -152,29 +171,31 @@ public final class MappingNodes { @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); @Specialization - protected boolean eval(VmMapping self, VmFunction function) { - var result = new MutableBoolean(false); - self.iterateMemberValues( - (key, member, value) -> { - if (value == null) { - value = VmUtils.readMember(self, key); - } - result.set(applyLambdaNode.executeBoolean(function, key, value)); - return !result.get(); - }); - return result.get(); + protected boolean eval( + VirtualFrame frame, + VmMapping self, + VmFunction function, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + for (var cursor = self.entries(CursorOption.ANY_ORDER); cursor.advance(); ) { + if (applyLambdaNode.executeBoolean( + function, cursor.key(), readCursorValueNode.execute(frame, cursor))) { + return true; + } + } + return false; } } public abstract static class toMap extends ExternalMethod0Node { @Specialization - protected VmMap eval(VmMapping self) { + protected VmMap eval( + VirtualFrame frame, + VmMapping self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { var builder = VmMap.builder(); - self.forceAndIterateMemberValues( - (key, def, value) -> { - builder.add(key, value); - return true; - }); + for (var cursor = self.entries(); cursor.advance(); ) { + builder.add(cursor.key(), readCursorValueNode.execute(frame, cursor)); + } return builder.build(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java index ef64dc082..43171a300 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.pkl.core.runtime.*; import org.pkl.core.stdlib.AbstractStringRenderer; import org.pkl.core.stdlib.ExternalMethod1Node; @@ -29,9 +31,10 @@ public final class PListRendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -39,16 +42,18 @@ public final class PListRendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } - private static PListRenderer createRenderer(VmTyped self, StringBuilder builder) { + private static PListRenderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); - return new PListRenderer(builder, indent, PklConverter.fromRenderer(self)); + return new PListRenderer(builder, indent, PklConverter.fromRenderer(self, callNode)); } // keep in sync with org.pkl.core.PListRenderer diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java index 607c1ecb9..cd1660adc 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.pkl.core.runtime.*; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.PklConverter; @@ -27,9 +29,10 @@ public final class PcfRendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -37,22 +40,25 @@ public final class PcfRendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } - private static PcfRenderer createRenderer(VmTyped self, StringBuilder builder) { - var indent = (String) VmUtils.readMember(self, Identifier.INDENT); - var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); + private static PcfRenderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { + var indent = (String) VmUtils.readMember(self, Identifier.INDENT, callNode); + var omitNullProperties = + (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES, callNode); var useCustomStringDelimiters = - (boolean) VmUtils.readMember(self, Identifier.USE_CUSTOM_STRING_DELIMITERS); + (boolean) VmUtils.readMember(self, Identifier.USE_CUSTOM_STRING_DELIMITERS, callNode); return new PcfRenderer( builder, indent, - PklConverter.fromRenderer(self), + PklConverter.fromRenderer(self, callNode), omitNullProperties, useCustomStringDelimiters); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java index c85e90e06..78f76bd36 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmBytes; import org.pkl.core.runtime.VmDataSize; @@ -49,9 +51,10 @@ public final class PropertiesRendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -59,18 +62,21 @@ public final class PropertiesRendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } - private static PropertiesRenderer createRenderer(VmTyped self, StringBuilder builder) { - var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); - var restrictCharset = (boolean) VmUtils.readMember(self, Identifier.RESTRICT_CHARSET); + private static PropertiesRenderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { + var omitNullProperties = + (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES, callNode); + var restrictCharset = (boolean) VmUtils.readMember(self, Identifier.RESTRICT_CHARSET, callNode); return new PropertiesRenderer( - builder, omitNullProperties, restrictCharset, PklConverter.fromRenderer(self)); + builder, omitNullProperties, restrictCharset, PklConverter.fromRenderer(self, callNode)); } private static final class PropertiesRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java index ba5b1b98b..332698374 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java @@ -17,6 +17,7 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.LoopNode; @@ -27,6 +28,7 @@ import org.pkl.core.ast.expression.binary.LessThanNode; import org.pkl.core.ast.expression.binary.LessThanNodeGen; import org.pkl.core.ast.internal.IsInstanceOfNode; import org.pkl.core.ast.internal.IsInstanceOfNodeGen; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.ast.internal.ToStringNode; import org.pkl.core.ast.internal.ToStringNodeGen; import org.pkl.core.ast.lambda.*; @@ -428,8 +430,11 @@ public final class SetNodes { public abstract static class flatten extends ExternalMethod0Node { @Specialization - protected VmSet eval(VmSet self) { - return (VmSet) self.flatten(); + protected VmSet eval( + VirtualFrame frame, + VmSet self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { + return (VmSet) self.flatten(frame, readCursorValueNode); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/TypedNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/TypedNodes.java index 99dd5edd3..07540f44e 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/TypedNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/TypedNodes.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. @@ -15,9 +15,13 @@ */ package org.pkl.core.stdlib.base; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.stdlib.ExternalMethod0Node; import org.pkl.core.stdlib.ExternalMethod1Node; @@ -51,15 +55,14 @@ public final class TypedNodes { public abstract static class toMap extends ExternalMethod0Node { @Specialization - protected VmMap eval(VmTyped self) { + protected VmMap eval( + VirtualFrame frame, + VmTyped self, + @Cached("create()") ReadCursorValueNode readCursorValueNode) { var builder = VmMap.builder(); - self.forceAndIterateMemberValues( - (memberKey, memberDef, memberValue) -> { - // exclude type definitions - if (memberDef.isClass() || memberDef.isTypeAlias()) return true; - builder.add(memberKey.toString(), memberValue); - return true; - }); + for (var cursor = self.properties(CursorOption.ALL_VALUES); cursor.advance(); ) { + builder.add(cursor.keyToString(), readCursorValueNode.execute(frame, cursor)); + } return builder.build(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java index 2259c27ff..c5ffa46d1 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmBytes; import org.pkl.core.runtime.VmCollection; @@ -30,6 +32,7 @@ import org.pkl.core.runtime.VmListing; import org.pkl.core.runtime.VmMap; import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.runtime.VmPair; import org.pkl.core.runtime.VmRegex; import org.pkl.core.runtime.VmSet; @@ -38,7 +41,6 @@ import org.pkl.core.runtime.VmUtils; import org.pkl.core.stdlib.AbstractStringRenderer; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.PklConverter; -import org.pkl.core.util.MutableBoolean; import org.pkl.core.util.yaml.YamlEmitter; public final class YamlRendererNodes { @@ -47,9 +49,10 @@ public final class YamlRendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -57,29 +60,35 @@ public final class YamlRendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } - private static YamlRenderer createRenderer(VmTyped self, StringBuilder builder) { - var mode = ((String) VmUtils.readMember(self, Identifier.MODE)); - var indentWidth = ((Long) VmUtils.readMember(self, Identifier.INDENT_WIDTH)).intValue(); - var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); - var isStream = (boolean) VmUtils.readMember(self, Identifier.IS_STREAM); + private static YamlRenderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { + var mode = ((String) VmUtils.readMember(self, Identifier.MODE, callNode)); + var indentWidth = + ((Long) VmUtils.readMember(self, Identifier.INDENT_WIDTH, callNode)).intValue(); + var omitNullProperties = + (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES, callNode); + var isStream = (boolean) VmUtils.readMember(self, Identifier.IS_STREAM, callNode); return new YamlRenderer( builder, " ".repeat(indentWidth), - PklConverter.fromRenderer(self), + PklConverter.fromRenderer(self, callNode), omitNullProperties, mode, - isStream); + isStream, + callNode); } private static final class YamlRenderer extends AbstractStringRenderer { private final boolean isStream; + private final IndirectCallNode callNode; private final YamlEmitter emitter; private final String elementIndent; @@ -89,10 +98,12 @@ public final class YamlRendererNodes { PklConverter converter, boolean omitNullProperties, String mode, - boolean isStream) { + boolean isStream, + IndirectCallNode callNode) { super("YAML", builder, indent, converter, omitNullProperties, omitNullProperties); this.isStream = isStream; + this.callNode = callNode; this.emitter = YamlEmitter.create(builder, mode, indent); elementIndent = indent.substring(1); } @@ -109,16 +120,14 @@ public final class YamlRendererNodes { private void visitStream(Object value) { if (value instanceof VmListing listing) { - var isFirst = new MutableBoolean(true); - listing.forceAndIterateMemberValues( - ((key, member, element) -> { - if (!isFirst.getAndSetFalse()) { - startNewLine(); - builder.append("---"); - } - visit(element); - return true; - })); + var cursor = listing.elements(CursorOption.ALL_VALUES); + if (!cursor.advance()) return; + visit(cursor.value(callNode)); + while (cursor.advance()) { + startNewLine(); + builder.append("---"); + visit(cursor.value(callNode)); + } return; } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java index d7fbfb6a0..b03fb6fa8 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java @@ -17,6 +17,7 @@ package org.pkl.core.stdlib.json; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.IndirectCallNode; import java.util.*; @@ -39,20 +40,21 @@ public final class ParserNodes { public abstract static class parse extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected Object eval(VmTyped self, String text) { - return doParse(self, text); + protected Object eval( + VmTyped self, String text, @Shared @Cached("create()") IndirectCallNode callNode) { + return doParse(self, text, callNode); } @Specialization @TruffleBoundary protected Object eval( - VmTyped self, VmTyped resource, @Cached("create()") IndirectCallNode callNode) { + VmTyped self, VmTyped resource, @Shared @Cached("create()") IndirectCallNode callNode) { var text = (String) VmUtils.readMember(resource, Identifier.TEXT, callNode); - return doParse(self, text); + return doParse(self, text, callNode); } - private Object doParse(VmTyped self, String text) { - var converter = PklConverter.fromParser(self); + private Object doParse(VmTyped self, String text, IndirectCallNode callNode) { + var converter = PklConverter.fromParser(self, callNode); var useMapping = (boolean) VmUtils.readMember(self, Identifier.USE_MAPPING); var handler = new Handler(converter, useMapping); var parser = new JsonParser(handler); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java index 725b56abb..49c976a7b 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.jsonnet; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import java.util.Set; import java.util.regex.Pattern; import org.pkl.core.runtime.Identifier; @@ -43,19 +45,22 @@ import org.pkl.core.util.ArrayCharEscaper; import org.pkl.core.util.IoUtils; public final class RendererNodes { - private static Renderer createRenderer(VmTyped self, StringBuilder builder) { + private static Renderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { var indent = (String) VmNull.unwrap(VmUtils.readMember(self, Identifier.INDENT)); if (indent == null) indent = ""; var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); - return new Renderer(builder, indent, omitNullProperties, PklConverter.fromRenderer(self)); + return new Renderer( + builder, indent, omitNullProperties, PklConverter.fromRenderer(self, callNode)); } public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -63,9 +68,10 @@ public final class RendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/net/NetNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/net/NetNodes.java index dac03b1f4..66872b931 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/net/NetNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/net/NetNodes.java @@ -16,10 +16,16 @@ package org.pkl.core.stdlib.net; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.LoopNode; +import com.oracle.truffle.api.profiles.LoopConditionProfile; +import org.pkl.core.ast.internal.ReadCursorValueNode; import org.pkl.core.runtime.VmListing; import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.util.url.PercentEncoder; import org.pkl.core.util.url.UrlFactory; @@ -55,25 +61,37 @@ public final class NetNodes { public abstract static class buildQuery extends ExternalMethod1Node { @Specialization - @TruffleBoundary - protected String eval(@SuppressWarnings("unused") VmTyped self, VmMapping parameters) { - var out = new StringBuilder(); - parameters.forceAndIterateMemberValues( - (name, def, values) -> { - ((VmListing) values) - .forceAndIterateMemberValues( - (index, valueDef, value) -> { - if (!out.isEmpty()) { - out.append('&'); - } - PercentEncoder.encodeForm(out, (String) name); - out.append('='); - PercentEncoder.encodeForm(out, (String) value); - return true; - }); - return true; - }); - return out.toString(); + protected String eval( + VirtualFrame frame, + @SuppressWarnings("unused") VmTyped self, + VmMapping parameters, + @Cached("create()") ReadCursorValueNode readCursorValueNode, + @Cached("create()") LoopConditionProfile outerLoopProfile, + @Cached("create()") LoopConditionProfile innerLoopProfile) { + var out = VmUtils.createBuilder(); + var count = 0; + var isFirst = true; + // don't use `inject()` for outer loop because mapping length can possibly transfer to + // interpreter. + for (var cursor = parameters.members(); outerLoopProfile.profile(cursor.advance()); ) { + var values = (VmListing) readCursorValueNode.execute(frame, cursor); + innerLoopProfile.profileCounted(values.getLength()); + for (var listingCursor = values.members(); + innerLoopProfile.inject(listingCursor.advance()); ) { + count++; + if (isFirst) { + isFirst = false; + } else { + VmUtils.appendToBuilder(out, '&'); + } + PercentEncoder.encodeForm(out, cursor.keyToString()); + VmUtils.appendToBuilder(out, '='); + var value = (String) readCursorValueNode.execute(frame, listingCursor); + PercentEncoder.encodeForm(out, value); + } + } + LoopNode.reportLoopCount(this, count); + return VmUtils.builderToString(out); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java index 9400035e9..f93d45e24 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.pklbinary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.msgpack.core.MessageBufferPacker; import org.msgpack.core.MessagePack; import org.pkl.core.runtime.VmBytes; @@ -30,9 +32,10 @@ public final class RendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected VmBytes eval(VmTyped self, Object value) { + protected VmBytes eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var packer = MessagePack.newDefaultBufferPacker(); - createRenderer(self, packer).renderDocument(value); + createRenderer(self, packer, callNode).renderDocument(value); return new VmBytes(packer.toByteArray()); } } @@ -40,14 +43,16 @@ public final class RendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected VmBytes eval(VmTyped self, Object value) { + protected VmBytes eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var packer = MessagePack.newDefaultBufferPacker(); - createRenderer(self, packer).renderValue(value); + createRenderer(self, packer, callNode).renderValue(value); return new VmBytes(packer.toByteArray()); } } - private static VmPklBinaryEncoder createRenderer(VmTyped self, MessageBufferPacker packer) { - return new VmPklBinaryEncoder(packer, PklConverter.fromRenderer(self)); + private static VmPklBinaryEncoder createRenderer( + VmTyped self, MessageBufferPacker packer, IndirectCallNode callNode) { + return new VmPklBinaryEncoder(packer, PklConverter.fromRenderer(self, callNode)); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java index 53607898e..8c1e4ce34 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java @@ -16,7 +16,9 @@ package org.pkl.core.stdlib.protobuf; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -90,9 +92,10 @@ public final class RendererNodes { public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -100,9 +103,10 @@ public final class RendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } @@ -138,10 +142,10 @@ public final class RendererNodes { } @TruffleBoundary - private static ProtobufRenderer createRenderer(VmTyped self, StringBuilder builder) { - var indent = (String) VmUtils.readMember(self, Identifier.INDENT); - - return new ProtobufRenderer(builder, indent, PklConverter.fromRenderer(self)); + private static ProtobufRenderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { + var indent = (String) VmUtils.readMember(self, Identifier.INDENT, callNode); + return new ProtobufRenderer(builder, indent, PklConverter.fromRenderer(self, callNode)); } private static final class ProtobufRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReporter.java b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReporter.java index c755f2dd0..985177b00 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReporter.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReporter.java @@ -15,6 +15,7 @@ */ package org.pkl.core.stdlib.test.report; +import com.oracle.truffle.api.nodes.IndirectCallNode; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; @@ -208,7 +209,15 @@ public final class JUnitReporter implements TestReporter { private static String renderXML(String indent, String version, VmDynamic value) { var builder = new StringBuilder(); - var renderer = new Renderer(builder, indent, version, "", VmMapping.empty(), PklConverter.NOOP); + var renderer = + new Renderer( + builder, + indent, + version, + "", + VmMapping.empty(), + PklConverter.NOOP, + IndirectCallNode.getUncached()); renderer.renderDocument(value); return builder.toString(); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java index ae1f9b4c8..66ef48dd9 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java @@ -16,9 +16,12 @@ package org.pkl.core.stdlib.xml; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; import org.jspecify.annotations.Nullable; import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmObjectCursor.CursorOption; import org.pkl.core.stdlib.AbstractStringRenderer; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.PklConverter; @@ -28,7 +31,8 @@ import org.pkl.core.util.xml.XmlValidator; public final class RendererNodes { private RendererNodes() {} - private static Renderer createRenderer(VmTyped self, StringBuilder builder) { + private static Renderer createRenderer( + VmTyped self, StringBuilder builder, IndirectCallNode callNode) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); var xmlVersion = (String) VmUtils.readMember(self, Identifier.XML_VERSION); var rootElementName = (String) VmUtils.readMember(self, Identifier.ROOT_ELEMENT_NAME); @@ -40,15 +44,17 @@ public final class RendererNodes { xmlVersion, rootElementName, rootElementAttributes, - PklConverter.fromRenderer(self)); + PklConverter.fromRenderer(self, callNode), + callNode); } public abstract static class renderDocument extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderDocument(value); + createRenderer(self, builder, callNode).renderDocument(value); return builder.toString(); } } @@ -56,9 +62,10 @@ public final class RendererNodes { public abstract static class renderValue extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected String eval(VmTyped self, Object value) { + protected String eval( + VmTyped self, Object value, @Cached("create()") IndirectCallNode callNode) { var builder = new StringBuilder(); - createRenderer(self, builder).renderValue(value); + createRenderer(self, builder, callNode).renderValue(value); return builder.toString(); } } @@ -79,6 +86,7 @@ public final class RendererNodes { private final String rootElementName; private final VmMapping rootElementAttributes; private final XmlValidator validator; + private final IndirectCallNode callNode; private int lineNumber = 0; private @Nullable Object deferredKey; @@ -89,12 +97,14 @@ public final class RendererNodes { String version, String rootElementName, VmMapping rootElementAttributes, - PklConverter converter) { + PklConverter converter, + IndirectCallNode callNode) { super("XML", builder, indent, converter, true, true); this.version = version; this.rootElementName = rootElementName; this.rootElementAttributes = rootElementAttributes; validator = XmlValidator.create(version); + this.callNode = callNode; } @Override @@ -414,27 +424,24 @@ public final class RendererNodes { } builder.append("<").append(name); if (attributes != null) { - attributes.forceAndIterateMemberValues( - (key, member, value) -> { - builder.append(' '); - // this check will be unnecessary once we have an XmlElement Pkl class - if (!(key instanceof String string)) { - throw new VmExceptionBuilder() - .typeMismatch(name, BaseModule.getStringClass()) - .build(); - } - validateName(string, "attribute"); - builder.append(string).append("=\""); - // this check will be unnecessary once we have an XmlElement Pkl class - if (!isScalar(value)) { - throw new VmExceptionBuilder() - // can only report two expected types for now - .typeMismatch(name, BaseModule.getStringClass(), BaseModule.getBooleanClass()) - .build(); - } - builder.append(stringEscaper.escape(value.toString())).append("\""); - return true; - }); + for (var cursor = attributes.entries(); cursor.advance(); ) { + builder.append(' '); + // this check will be unnecessary once we have an XmlElement Pkl class + if (!(cursor.key() instanceof String string)) { + throw new VmExceptionBuilder().typeMismatch(name, BaseModule.getStringClass()).build(); + } + validateName(string, "attribute"); + builder.append(string).append("=\""); + var value = cursor.value(callNode); + // this check will be unnecessary once we have an XmlElement Pkl class + if (!isScalar(value)) { + throw new VmExceptionBuilder() + // can only report two expected types for now + .typeMismatch(name, BaseModule.getStringClass(), BaseModule.getBooleanClass()) + .build(); + } + builder.append(stringEscaper.escape(value.toString())).append("\""); + } } builder.append(">"); @@ -444,12 +451,10 @@ public final class RendererNodes { if (isXmlElement(content)) { // this special casing is necessary because renderXmlElement() is implemented in terms of // this method - ((VmDynamic) content) - .forceAndIterateMemberValues( - (key, member, value) -> { - if (member.isElement()) doVisitElement(value); - return true; - }); + for (var cursor = ((VmDynamic) content).elements(CursorOption.ALL_VALUES); + cursor.advance(); ) { + doVisitElement(cursor.value(callNode)); + } } else { visit(content); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java index e6543a931..1bddcdf20 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java @@ -17,6 +17,7 @@ package org.pkl.core.stdlib.yaml; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.source.Source; @@ -50,22 +51,23 @@ public final class ParserNodes { public abstract static class parse extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected Object eval(VmTyped self, String text) { + protected Object eval( + VmTyped self, String text, @Shared @Cached("create()") IndirectCallNode callNode) { var uri = "input_string"; - return doParse(self, text, uri); + return doParse(self, text, uri, callNode); } @Specialization @TruffleBoundary protected Object eval( - VmTyped self, VmTyped resource, @Cached("create()") IndirectCallNode callNode) { + VmTyped self, VmTyped resource, @Shared @Cached("create()") IndirectCallNode callNode) { var text = (String) VmUtils.readMember(resource, Identifier.TEXT, callNode); var uri = (String) VmUtils.readMember(resource, Identifier.URI, callNode); - return doParse(self, text, uri); + return doParse(self, text, uri, callNode); } - private Object doParse(VmTyped self, String text, String uri) { - var converter = PklConverter.fromParser(self); + private Object doParse(VmTyped self, String text, String uri, IndirectCallNode callNode) { + var converter = PklConverter.fromParser(self, callNode); var load = createLoad(self, text, uri, converter); try { @@ -86,22 +88,23 @@ public final class ParserNodes { public abstract static class parseAll extends ExternalMethod1Node { @Specialization @TruffleBoundary - protected VmList eval(VmTyped self, String text) { + protected VmList eval( + VmTyped self, String text, @Shared @Cached("create()") IndirectCallNode callNode) { var uri = "input_string"; - return doParseAll(self, text, uri); + return doParseAll(self, text, uri, callNode); } @Specialization @TruffleBoundary protected Object eval( - VmTyped self, VmTyped resource, @Cached("create()") IndirectCallNode callNode) { + VmTyped self, VmTyped resource, @Shared @Cached("create()") IndirectCallNode callNode) { var text = (String) VmUtils.readMember(resource, Identifier.TEXT, callNode); var uri = (String) VmUtils.readMember(resource, Identifier.URI, callNode); - return doParseAll(self, text, uri); + return doParseAll(self, text, uri, callNode); } - private VmList doParseAll(VmTyped self, String text, String uri) { - var converter = PklConverter.fromParser(self); + private VmList doParseAll(VmTyped self, String text, String uri, IndirectCallNode callNode) { + var converter = PklConverter.fromParser(self, callNode); var load = createLoad(self, text, uri, converter); var builder = VmList.EMPTY.builder(); diff --git a/pkl-core/src/main/java/org/pkl/core/util/ArrayBuilder.java b/pkl-core/src/main/java/org/pkl/core/util/ArrayBuilder.java new file mode 100644 index 000000000..a28054162 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/util/ArrayBuilder.java @@ -0,0 +1,103 @@ +/* + * 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. + * 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 org.jspecify.annotations.Nullable; + +/** + * A builder for typed arrays that is convenient, efficient, and safe for Truffle partial + * evaluation. + */ +public final class ArrayBuilder { + static final int INITIAL_LENGTH = 8; + + private final ArrayFactory factory; + private T @Nullable [] array; + private int nextIndex; + + /** + * Returns a new builder that creates arrays with the specified factory. The first time an element + * is added, an array of length 8 is created. Whenever the array becomes too small, it is grown to + * twice its previous length. + */ + public static ArrayBuilder of(ArrayFactory factory) { + return new ArrayBuilder<>(factory); + } + + private ArrayBuilder(ArrayFactory factory) { + this.factory = factory; + } + + /** Adds {@code value} to the array. */ + public void add(T value) { + if (array == null) { + array = factory.apply(INITIAL_LENGTH); + } else if (nextIndex >= array.length) { + var newArray = factory.apply(Math.multiplyExact(array.length, 2)); + System.arraycopy(array, 0, newArray, 0, array.length); + array = newArray; + } + array[nextIndex] = value; + nextIndex += 1; + } + + /** Returns the current array, which may be larger than the number of elements added. */ + @SuppressWarnings("unchecked") + public R[] toOversizedArray() { + return array == null ? (R[]) factory.apply(0) : (R[]) array; + } + + /** + * Returns an array that exactly fits the number of elements added, potentially requiring a copy. + */ + @SuppressWarnings("unchecked") + public R[] toArray() { + if (array == null) { + return (R[]) factory.apply(0); + } + if (array.length == nextIndex) { + return (R[]) array; + } + var newArray = factory.apply(nextIndex); + System.arraycopy(array, 0, newArray, 0, nextIndex); + return (R[]) newArray; + } + + /** Tells whether no elements have been added to the array. */ + public boolean isEmpty() { + return nextIndex == 0; + } + + /** The number of elements added to the array. */ + public int length() { + return nextIndex; + } + + /** The index of the last element added to the array. */ + public int lastIndex() { + return nextIndex - 1; + } + + /** + * Use own type instead of {@link java.util.function.IntFunction} because SnakeYAML uses it too. + * Otherwise, native-iamge's analyzer will think that SnakeYAML's code is also reachable for + * partial evaluation. + */ + @FunctionalInterface + public interface ArrayFactory { + T[] apply(int value); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/util/CollectionUtils.java b/pkl-core/src/main/java/org/pkl/core/util/CollectionUtils.java index a793f4599..7c2361a5b 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/CollectionUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/CollectionUtils.java @@ -60,4 +60,14 @@ public final class CollectionUtils { // this is exactly how java.util.HashMap does it return (int) Math.ceil(expectedSize / (double) LOAD_FACTOR); } + + @TruffleBoundary + public static @Nullable V put(Map map, K key, V value) { + return map.put(key, value); + } + + @TruffleBoundary + public static boolean contains(Collection collection, E value) { + return collection.contains(value); + } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation1.pkl index 1dc1afa76..d2686b30f 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation1.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation1.pkl @@ -67,4 +67,4 @@ class Ann2 extends Annotation { class Ann3 extends Annotation { x: String y: Int -} \ No newline at end of file +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/classes/inheritanceError1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/classes/inheritanceError1.pkl index 197ae20f3..118e0ead3 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/classes/inheritanceError1.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/classes/inheritanceError1.pkl @@ -1,3 +1,3 @@ class Base // not open or abstract -class Derived extends Base \ No newline at end of file +class Derived extends Base diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/extendExternalClass.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/extendExternalClass.pkl index 700d22d87..3dc6b11ff 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/extendExternalClass.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/extendExternalClass.pkl @@ -1 +1 @@ -class Person extends Any \ No newline at end of file +class Person extends Any diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/objects/lateBinding5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/objects/lateBinding5.pkl new file mode 100644 index 000000000..8eb1e0213 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/objects/lateBinding5.pkl @@ -0,0 +1,20 @@ +local m1 = new Mapping { + ["a"] = 1 + ["b"] = 2 +} + +m2 = (m1) { + ["any"] = m2.any((_, v) -> v == 2) + ["containsValue"] = m2.containsValue(2) +} + +m3 = (m1) { + ["isEmpty"] = m3.isEmpty +} + +local l1 = new Listing { 1; 2 } + +l2 = (l1) { + l2.contains(2) + l2.any((it) -> it == 1) +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias1.pkl index c7987d817..a07149b55 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias1.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias1.pkl @@ -1 +1 @@ -typealias Foo = List \ No newline at end of file +typealias Foo = List diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias2.pkl index 3aa602f4f..ea1e2dcf2 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias2.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/cyclicTypeAlias2.pkl @@ -2,4 +2,4 @@ typealias Foo = List typealias Bar = Set -typealias Baz = Map \ No newline at end of file +typealias Baz = Map diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/traceThenIterateUndefinedMember.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/traceThenIterateUndefinedMember.err index 964db68c9..043bd51ec 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/traceThenIterateUndefinedMember.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/traceThenIterateUndefinedMember.err @@ -10,6 +10,10 @@ x | undefinedValue ^^^^^^^^^^^^^^ at traceThenIterateUndefinedMember#items[#1] (file:///$snippetsDir/input/errors/traceThenIterateUndefinedMember.pkl) +xx | for (item in items) { + ^^^^^^^^^^^^^^^^^^^^^ +at traceThenIterateUndefinedMember#result (file:///$snippetsDir/input/errors/traceThenIterateUndefinedMember.pkl) + xx | let (_ = trace(items)) ^^^^^^^^^^^^^^^^^^^^^^ at traceThenIterateUndefinedMember#result. (file:///$snippetsDir/input/errors/traceThenIterateUndefinedMember.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/objects/lateBinding5.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/objects/lateBinding5.pcf new file mode 100644 index 000000000..52bb2c950 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/objects/lateBinding5.pcf @@ -0,0 +1,17 @@ +m2 { + ["a"] = 1 + ["b"] = 2 + ["any"] = true + ["containsValue"] = true +} +m3 { + ["a"] = 1 + ["b"] = 2 + ["isEmpty"] = false +} +l2 { + 1 + 2 + true + true +} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/util/ArrayBuilderTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/util/ArrayBuilderTest.kt new file mode 100644 index 000000000..e146be034 --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/util/ArrayBuilderTest.kt @@ -0,0 +1,108 @@ +/* + * 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. + * 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 org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +// ArrayBuilder is designed for Java; it is less nice to use from Kotlin +class ArrayBuilderTest { + private data class Person(val name: String) + + private val builder = ArrayBuilder.of { arrayOfNulls(it) } + private val alice = Person("Alice") + + @Test + fun `add zero elements`() { + val elementCount = 0 + assertThat(builder.isEmpty).isTrue() + assertThat(builder.length()).isEqualTo(elementCount) + assertThat(builder.lastIndex()).isEqualTo(elementCount - 1) + + val array = builder.toOversizedArray() + assertThat(array.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array).isEmpty() + + val array2 = builder.toArray() + assertThat(array2.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array2).isEmpty() + } + + @Test + fun `add fewer elements than initial array length`() { + val elementCount = ArrayBuilder.INITIAL_LENGTH - 1 + repeat(elementCount) { builder.add(alice) } + + assertThat(builder.isEmpty).isFalse() + assertThat(builder.length()).isEqualTo(elementCount) + assertThat(builder.lastIndex()).isEqualTo(elementCount - 1) + + val array = builder.toOversizedArray() + assertThat(array.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array).hasSize(ArrayBuilder.INITIAL_LENGTH) + for (i in 0 until elementCount) { + assertThat(array[i]).isSameAs(alice) + } + for (i in elementCount..array.lastIndex) { + assertThat(array[i]).isNull() + } + + val array2 = builder.toArray() + assertThat(array2.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array2).hasSize(elementCount) + } + + @Test + fun `add same number of elements as initial array length`() { + val elementCount = ArrayBuilder.INITIAL_LENGTH + repeat(elementCount) { builder.add(alice) } + + assertThat(builder.isEmpty).isFalse() + assertThat(builder.length()).isEqualTo(elementCount) + assertThat(builder.lastIndex()).isEqualTo(elementCount - 1) + + val array = builder.toOversizedArray() + assertThat(array.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array).hasSize(elementCount) + array.forEach { assertThat(it).isSameAs(alice) } + assertThat(builder.toArray()).isSameAs(array) + } + + @Test + fun `add more elements than initial array length`() { + val elementCount = ArrayBuilder.INITIAL_LENGTH + 1 + repeat(elementCount) { builder.add(alice) } + + assertThat(builder.isEmpty).isFalse() + assertThat(builder.length()).isEqualTo(elementCount) + assertThat(builder.lastIndex()).isEqualTo(elementCount - 1) + + val array = builder.toOversizedArray() + assertThat(array.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array).hasSize(ArrayBuilder.INITIAL_LENGTH * 2) + for (i in 0 until elementCount) { + assertThat(array[i]).isSameAs(alice) + } + for (i in elementCount..array.lastIndex) { + assertThat(array[i]).isNull() + } + + val array2 = builder.toArray() + assertThat(array2.javaClass.componentType).isEqualTo(Person::class.java) + assertThat(array2).hasSize(elementCount) + array2.forEach { assertThat(it).isSameAs(alice) } + } +}