Skip to content

Replace VmObjectLike.iterateMemberValues() with VmObjectCursor - #1879

Merged
bioball merged 12 commits into
apple:mainfrom
bioball:object-cursor
Oct 16, 2026
Merged

bioball merged 12 commits into
apple:mainfrom
bioball:object-cursor

Conversation

@bioball

@bioball bioball commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Special thanks to @odenix for doing most of the work here!

This changes how object evaluation occurs.
Namely, this uses a VmObjectCursor API for iteration, while actual object evaluation is lifted out of truffle boundaries and made available for partial evaluation.

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(); ) {...}
  • Use LoopConditionProfile in various nodes to track iterations and help truffle JIT; we can do this now because this code is now within PE-friendly regions.

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.

[native-pkl-cli-linux-amd64]

@bioball
bioball force-pushed the object-cursor branch 2 times, most recently from af44871 to 54d475d Compare September 25, 2026 20:10
Comment thread pkl-core/src/main/java/org/pkl/core/runtime/VmObjectCursor.java
Comment thread pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java Outdated
Comment thread pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java
Comment thread pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java
Comment thread pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java Outdated

@HT154 HT154 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Comment thread pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java
Comment thread pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java Outdated
^^^^
at undefinedProperty7#Person.name (file:///$snippetsDir/input/errors/undefinedProperty7.pkl)

The above error occurred when rendering path `name` of module `file:///$snippetsDir/input/errors/undefinedProperty7.pkl`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here?
The test input explicitly says:

// we don't expect a path to be shown in the error message, because the renderer's traverser cannot
// tell if the thrown error came from reading 

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point; this is definitely wrong.

Comment on lines +143 to +150
var oldFlags = flags;
if (recurse) {
if (isDeepForced()) return;
flags |= (DEEP_FORCE_FLAG | SHALLOW_FORCE_FLAG);
} else {
if (isShallowForced()) return;
flags |= SHALLOW_FORCE_FLAG;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some regressions on how objects are read:

local m1 = new Mapping { ["a"] = 1; ["b"] = 2 }

m2 = (m1) {
  // false here, true on main
  ["any"] = m2.any((_, v) -> v == 2)
  // false here, true on main
  ["containsValue"] = m2.containsValue(2)
}

m3 = (m1) {
  // true here, false on main
  ["isEmpty"] = m3.isEmpty
}

local l1 = new Listing { 1; 2 }

l2 = (l1) {
  // false here, true on main
  l2.contains(2)
  // false here, true on main
  l2.any((it) -> it == 1)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, great catch. The issue here was in the tracking of the flags on VmObject; m2.any is calls during a shallow force of m2, and returns a cached object cursor.

Comment thread pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java Outdated

@stackoverflow stackoverflow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. But the native build is failing:

=== Found 297 compilation blocklist violations ===

Probably because some @TruffleBoundary was removed in places where it's needed.

odenix and others added 12 commits October 16, 2026 09:55
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: Jen Basch <jbasch94@gmail.com>
@bioball
bioball enabled auto-merge (squash) October 16, 2026 17:33
@bioball
bioball merged commit 9f79fe4 into apple:main Oct 16, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants