Replace VmObjectLike.iterateMemberValues() with VmObjectCursor (#1879)

Motivation:
- VmObjectLike.iterateMemberValues() is an internal iterator whose loop body is a Java lambda.
  It suffers from the following limitations:
  - `continue` and `break` cannot be used.
  - `return` becomes an (awkward) replacement for `continue` and `break`.
    It cannot be used to return from the enclosing method.
  - Mutating variables requires hoisting them (MutableBoolean, etc.).
  - Node.execute() calls are very slow because they are behind a Truffle boundary.
  - Callers of iterateMemberValues() do not specify what and how they want to iterate.
    This obscures the iteration code and makes it difficult (often impossible) to optimize.

Changes:
- Add interface VmObjectCursor, an external iterator that iterates over VmObject members.
  Its design is inspired by org.graalvm.collections.MapCursor.
- Add VmObjectCursor implementations specialized for different members, VmObject subclasses, and cursor options.
- Replace VmObjectLike.iterateMemberValues() with the following methods that return VmObjectCursor:
  - properties()
  - properties(CursorOption)
  - elements()
  - elements(CursorOption)
  - entries()
  - entries(CursorOption)
  - members
  - members(CursorOption)
- Replace all usages of iterateMemberValues() with one of the above methods.
  Before: object.iterateMembersValues((key, member, value) -> { if (member.isProp()) {...} });
  After : for (var cursor = object.properties(); cursor.advance(); ) {...}

Result:
- Iteration code is much simpler and clearer.
- Iteration code that calls Node.execute() is much faster because it is no longer behind a Truffle boundary.
- Other iteration code is potentially faster and can be better optimized in the future.

Co-authored-by: translatenix <119817707+translatenix@users.noreply.github.com>
Co-authored-by: Jen Basch <jbasch94@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-16 17:47:18 +00:00
1 parent b97abe13bc
commit 9f79fe47da
63 files changed
+2739 -979

No files matched your search

@@ -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());
});
}
@@ -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);
@@ -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
@@ -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);
@@ -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();
}
@@ -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).
*
* <p>Example: {@code new foo { "pigeon"; [3] = "barn owl" } }
*/
@ImportStatic({BaseModule.class, VmUtils.class})
public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLiteralNode {
@@ -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,
@@ -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) {
@@ -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);
}
}
@@ -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");
}
@@ -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<String, Object>();
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<String> so will export to List<String>
if (!(value instanceof VmListing vmListing)) throw PklBugException.unreachableCode();
var result = new HashSet<String>(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
@@ -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<Candidate> forObject(VmObjectLike object, String key) {
var candidates = new ArrayList<Candidate>();
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;
@@ -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<TestResult>();
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<VmDynamic>(null);
var allGroupsSucceeded = new MutableBoolean(true);
var errored = new MutableBoolean(false);
VmDynamic actualExampleOutputs = null;
var allGroupsSucceeded = true;
var errored = false;
var testResults = new ArrayList<TestResult>();
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 `<fileName>-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 `<fileName>-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<TestResult>();
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(
@@ -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;
}
@@ -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<Object> {
public interface Builder<T extends VmCollection> {
@@ -126,18 +129,15 @@ public abstract class VmCollection extends VmValue implements Iterable<Object> {
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()
@@ -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.<String, Object>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<CursorOption> 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<CursorOption> 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;
@@ -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<VmDynamic> {
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<VmDynamic> {
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<VmDynamic> {
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<VmDynamic> {
private final UnmodifiableMapCursor<Object, ObjectMember>[] allMembers;
private @LateInit UnmodifiableMapCursor<Object, ObjectMember> currentMembers;
private int allMembersIndex;
private final EconomicSet<Object> 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<Object, ObjectMember> currentMember;
private final EconomicSet<Object> 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;
}
}
}
@@ -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<Object, ObjectMember, Boolean> 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<CursorOption> 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<CursorOption> 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);
@@ -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<Object> 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<CursorOption> 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<CursorOption> 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) {
@@ -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<VmListing> {
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<VmListing> {
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;
}
}
}
@@ -42,7 +42,7 @@ public abstract class VmListingOrMapping extends VmObject {
typeCheckOwner = null;
}
public VmListingOrMapping(
protected VmListingOrMapping(
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
@@ -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<CursorOption> 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<Object, Object> 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<Object, Object> 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<CursorOption> 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<CursorOption> 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;
}
}
@@ -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<VmMapping> {
private final Iterator<Object> 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<VmMapping> {
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;
}
}
}
@@ -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<Object, ObjectMember> members;
protected final EconomicMap<Object, Object> 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<Object, ObjectMember> members,
EconomicMap<Object, Object> 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<Object, ObjectMember, Boolean> 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<String, Object> exportMembers() {
Map<String, Object> 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.<String, Object>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;
}
}
@@ -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.
*
* <p>To obtain a cursor, call one of the following methods:
*
* <ul>
* <li>{@link VmObjectLike#properties}
* <li>{@link VmObjectLike#elements}
* <li>{@link VmObjectLike#entries}
* <li>{@link VmObjectLike#members}
* </ul>
*
* To customize cursor behavior, the above methods optionally accept a {@link CursorOption}.
*
* <p>A typical cursor iteration looks as follows:
*
* <pre><code>
* for (var cursor = object.properties(); cursor.advance(); ) {
* var key = cursor.key();
* var value = cursor.value();
* ...
* }
* </code></pre>
*
* <p>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.
*
* <p>Specifying this option may increase cursor performance.
*
* <p>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.
*
* <p>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.
*
* <p>The returned key has one of the following types:
*
* <ul>
* <li>{@link Identifier} for a property
* <li>{@link Long} for an element
* <li>{@link Boolean}, {@link Long}, {@link Double}, {@link String} or {@link VmValue} for an
* entry
* </ul>
*
* @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.
*
* <p>If the member's value is not already cached, the member is evaluated.
*
* <p>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.
*
* <p>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.
*
* <p>This method is supported by the following cursors:
*
* <ul>
* <li>{@link VmObjectLike#members}
* </ul>
*/
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.
*
* <p>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<T extends VmObject> 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<T extends VmObject> extends VmObjectCursor {
protected final T iteratee;
private final UnmodifiableMapCursor<Object, Object> 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;
}
}
}
@@ -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<CursorOption> options);
public abstract VmObjectCursor entries();
public abstract VmObjectCursor entries(CursorOption option);
public abstract VmObjectCursor entries(EnumSet<CursorOption> 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.
*
* <p>Unlike {@link #members()}, this will also visit hidden properties.
*/
public abstract boolean iterateMembers(BiFunction<Object, ObjectMember, Boolean> 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);
}
}
@@ -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<CursorOption> 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<CursorOption> 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) {
@@ -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<VmTyped> {
private final Iterator<Identifier> 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;
}
}
}
@@ -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<String, FileOutput> readFilesProperty(
VmObjectLike receiver, Function<VmTyped, FileOutput> fileOutputFactory) {
var filesOrNull = VmUtils.readMember(receiver, Identifier.FILES);
VmObjectLike receiver,
Function<VmTyped, FileOutput> 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<String, FileOutput>();
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();
@@ -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);
@@ -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<Object> valuePath, Object value) {
currPath = new ArrayDeque<>();
visitEntryKey(key, isFirst);
isVisitingEntryKey = true;
try {
visitEntryKey(key, isFirst);
} finally {
isVisitingEntryKey = false;
}
currPath = valuePath;
visitEntryValue(value);
}
@@ -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() {
@@ -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<Object> {
@@ -49,13 +51,16 @@ public final class PklConverter implements VmValueConverter<Object> {
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<Object> {
}
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<Object> {
return Pair.of(Identifier.get((String) prop.getFirst()), prop.getSecond());
}
private Map<VmClass, VmFunction> createTypeConverters(VmMapping converters) {
private Map<VmClass, VmFunction> createTypeConverters(
VmMapping converters, IndirectCallNode callNode) {
var result = new HashMap<VmClass, VmFunction>();
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<VmClass, VmFunction> createConvertPropertyTransformers(
VmMapping convertPropertyTransformers) {
VmMapping convertPropertyTransformers, IndirectCallNode callNode) {
var result = new HashMap<VmClass, VmFunction>();
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<Object[], VmFunction>[] createPathConverters(VmMapping converters) {
private Pair<Object[], VmFunction>[] createPathConverters(
VmMapping converters, IndirectCallNode callNode) {
var result = new ArrayList<Pair<Object[], VmFunction>>();
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]);
}
@@ -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();
}
}
@@ -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 {
@@ -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()));
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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
@@ -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);
}
@@ -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 {
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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);
@@ -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();
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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 {
@@ -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();
}
@@ -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);
}
@@ -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();
@@ -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<T> {
static final int INITIAL_LENGTH = 8;
private final ArrayFactory<T> 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 <T extends @Nullable Object> ArrayBuilder<T> of(ArrayFactory<T> factory) {
return new ArrayBuilder<>(factory);
}
private ArrayBuilder(ArrayFactory<T> 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 extends T> 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 extends T> 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> {
T[] apply(int value);
}
}
@@ -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 <K, V> @Nullable V put(Map<K, V> map, K key, V value) {
return map.put(key, value);
}
@TruffleBoundary
public static <E> boolean contains(Collection<E> collection, E value) {
return collection.contains(value);
}
}
@@ -67,4 +67,4 @@ class Ann2 extends Annotation {
class Ann3 extends Annotation {
x: String
y: Int
}
}
@@ -1,3 +1,3 @@
class Base // not open or abstract
class Derived extends Base
class Derived extends Base
@@ -1 +1 @@
class Person extends Any
class Person extends Any
@@ -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)
}
@@ -1 +1 @@
typealias Foo = List<Foo>
typealias Foo = List<Foo>
@@ -2,4 +2,4 @@ typealias Foo = List<Bar>
typealias Bar = Set<Baz>
typealias Baz = Map<String, Foo>
typealias Baz = Map<String, Foo>
@@ -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.<let expr> (file:///$snippetsDir/input/errors/traceThenIterateUndefinedMember.pkl)
@@ -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
}
@@ -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<Person?> { arrayOfNulls<Person>(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<Person>()
assertThat(array.javaClass.componentType).isEqualTo(Person::class.java)
assertThat(array).isEmpty()
val array2 = builder.toArray<Person>()
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<Person>()
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<Person>()
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<Person>()
assertThat(array.javaClass.componentType).isEqualTo(Person::class.java)
assertThat(array).hasSize(elementCount)
array.forEach { assertThat(it).isSameAs(alice) }
assertThat(builder.toArray<Person>()).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<Person>()
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<Person>()
assertThat(array2.javaClass.componentType).isEqualTo(Person::class.java)
assertThat(array2).hasSize(elementCount)
array2.forEach { assertThat(it).isSameAs(alice) }
}
}