Skip to content

Commit 9f79fe4

Browse files
bioballodenixHT154
authored
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>
1 parent b97abe1 commit 9f79fe4

63 files changed

Lines changed: 2740 additions & 980 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.pkl.core;
1717

18+
import com.oracle.truffle.api.nodes.IndirectCallNode;
1819
import java.io.IOException;
1920
import java.net.URI;
2021
import java.net.URISyntaxException;
@@ -195,7 +196,9 @@ public Map<String, FileOutput> evaluateOutputFiles(ModuleSource moduleSource) {
195196
(module) -> {
196197
var output = VmUtils.readModuleOutput(module);
197198
return VmUtils.readFilesProperty(
198-
output, (fileOutput) -> new FileOutputImpl(this, fileOutput));
199+
output,
200+
(fileOutput) -> new FileOutputImpl(this, fileOutput),
201+
IndirectCallNode.getUncached());
199202
});
200203
}
201204

‎pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.oracle.truffle.api.dsl.Cached;
2020
import com.oracle.truffle.api.dsl.NodeChild;
2121
import com.oracle.truffle.api.dsl.Specialization;
22+
import com.oracle.truffle.api.frame.VirtualFrame;
2223
import com.oracle.truffle.api.nodes.NodeInfo;
2324
import com.oracle.truffle.api.source.SourceSection;
2425
import org.jspecify.annotations.Nullable;
@@ -34,6 +35,9 @@ protected EqualNode(SourceSection sourceSection) {
3435
super(sourceSection);
3536
}
3637

38+
/** When using this method, pass {@code null} in for {@code leftNode} and {@code rightNode}. */
39+
public abstract boolean executeWith(VirtualFrame frame, Object left, Object right);
40+
3741
@Specialization
3842
protected boolean eval(String left, String right) {
3943
return left.equals(right);

‎pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java‎

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,31 @@
2323
import com.oracle.truffle.api.frame.FrameDescriptor;
2424
import com.oracle.truffle.api.frame.VirtualFrame;
2525
import com.oracle.truffle.api.nodes.ExplodeLoop;
26+
import com.oracle.truffle.api.profiles.LoopConditionProfile;
2627
import com.oracle.truffle.api.source.SourceSection;
2728
import org.jspecify.annotations.Nullable;
2829
import org.pkl.core.ast.ExpressionNode;
30+
import org.pkl.core.ast.internal.ReadCursorValueNode;
31+
import org.pkl.core.ast.internal.ReadCursorValueNodeGen;
2932
import org.pkl.core.ast.type.TypeNode;
3033
import org.pkl.core.ast.type.UnresolvedTypeNode;
3134
import org.pkl.core.runtime.*;
35+
import org.pkl.core.runtime.Iterators.TruffleIterator;
3236
import org.pkl.core.util.ArrayUtils;
3337

3438
public abstract class GeneratorForNode extends GeneratorMemberNode {
3539
private final FrameDescriptor generatorDescriptor;
40+
private final LoopConditionProfile loopConditionProfile = LoopConditionProfile.create();
3641
@Child private ExpressionNode iterableNode;
3742
@Child private @Nullable UnresolvedTypeNode unresolvedKeyTypeNode;
3843
@Child private @Nullable UnresolvedTypeNode unresolvedValueTypeNode;
3944
@Children private final GeneratorMemberNode[] childNodes;
4045
@Child private @Nullable TypeNode keyTypeNode;
4146
@Child private @Nullable TypeNode valueTypeNode;
47+
48+
@Child
49+
private ReadCursorValueNode readCursorValueNode = ReadCursorValueNodeGen.create(sourceSection);
50+
4251
private final int keySlot;
4352
private final int valueSlot;
4453
private final int[] slotsToCopy;
@@ -101,40 +110,50 @@ protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmDynami
101110

102111
@Specialization
103112
protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmList iterable) {
104-
long idx = 0;
105-
for (var element : iterable) {
106-
executeIteration(frame, parent, data, idx++, element);
113+
loopConditionProfile.profileCounted(iterable.getLength());
114+
for (var idx = 0L; loopConditionProfile.inject(idx < iterable.getLength()); idx++) {
115+
executeIteration(frame, parent, data, idx, iterable.get(idx));
107116
}
108117
}
109118

110119
@Specialization
111120
protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmMap iterable) {
112-
for (var entry : iterable) {
121+
loopConditionProfile.profileCounted(iterable.getLength());
122+
var iterator = new TruffleIterator<>(iterable);
123+
while (loopConditionProfile.inject(iterator.hasNext())) {
124+
var entry = iterator.next();
113125
executeIteration(frame, parent, data, VmUtils.getKey(entry), VmUtils.getValue(entry));
114126
}
115127
}
116128

117129
@Specialization
118130
protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmSet iterable) {
119-
long idx = 0;
120-
for (var element : iterable) {
131+
var idx = 0L;
132+
loopConditionProfile.profileCounted(iterable.getLength());
133+
var iterator = new TruffleIterator<>(iterable);
134+
while (loopConditionProfile.inject(iterator.hasNext())) {
135+
var element = iterator.next();
121136
executeIteration(frame, parent, data, idx++, element);
122137
}
123138
}
124139

125140
@Specialization
126141
protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmIntSeq iterable) {
127142
var length = iterable.getLength();
128-
for (long key = 0, value = iterable.start; key < length; key++, value += iterable.step) {
143+
loopConditionProfile.profileCounted(iterable.getLength());
144+
for (long key = 0, value = iterable.start;
145+
loopConditionProfile.inject(key < length);
146+
key++, value += iterable.step) {
129147
executeIteration(frame, parent, data, key, value);
130148
}
131149
}
132150

133151
@Specialization
134152
protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmBytes iterable) {
135-
long idx = 0;
136-
for (var byt : iterable.getBytes()) {
137-
executeIteration(frame, parent, data, idx++, (long) byt);
153+
var bytes = iterable.getBytes();
154+
loopConditionProfile.profileCounted(bytes.length);
155+
for (var idx = 0; loopConditionProfile.inject(idx < bytes.length); idx++) {
156+
executeIteration(frame, parent, data, (long) idx, (long) bytes[idx]);
138157
}
139158
}
140159

@@ -150,16 +169,32 @@ protected void fallback(VirtualFrame frame, Object parent, ObjectData data, Obje
150169
}
151170

152171
private void doEvalObject(VirtualFrame frame, VmObject iterable, Object parent, ObjectData data) {
153-
var materializedFrame = frame.materialize();
154-
iterable.forceAndIterateMemberValues(
155-
(key, member, value) -> {
156-
var convertedKey = member.isProp() ? key.toString() : key;
157-
// TODO: Executing iteration behind a Truffle boundary is bad for performance.
158-
// This and similar cases will be fixed in an upcoming PR that replaces method
159-
// `(forceAnd)iterateMemberValues` with cursor-based external iterators.
160-
executeIteration(materializedFrame, parent, data, convertedKey, value);
161-
return true;
162-
});
172+
// TODO: skip types for module objects?
173+
var cursor = iterable.members();
174+
if (cursor.getLength() == -1) {
175+
doEvalObjectProfiled(frame, parent, data, cursor);
176+
} else {
177+
doEvalObjectCounted(frame, parent, data, cursor);
178+
}
179+
}
180+
181+
private void doEvalObjectCounted(
182+
VirtualFrame frame, Object parent, ObjectData data, VmObjectCursor cursor) {
183+
loopConditionProfile.profileCounted(cursor.getLength());
184+
while (loopConditionProfile.inject(cursor.advance())) {
185+
var key = cursor.isProperty() ? cursor.keyToString() : cursor.key();
186+
var value = readCursorValueNode.execute(frame, cursor);
187+
executeIteration(frame, parent, data, key, value);
188+
}
189+
}
190+
191+
private void doEvalObjectProfiled(
192+
VirtualFrame frame, Object parent, ObjectData data, VmObjectCursor cursor) {
193+
while (loopConditionProfile.profile(cursor.advance())) {
194+
var key = cursor.isProperty() ? cursor.keyToString() : cursor.key();
195+
var value = readCursorValueNode.execute(frame, cursor);
196+
executeIteration(frame, parent, data, key, value);
197+
}
163198
}
164199

165200
@ExplodeLoop

‎pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java‎

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import com.oracle.truffle.api.dsl.ImportStatic;
2424
import com.oracle.truffle.api.dsl.Specialization;
2525
import com.oracle.truffle.api.frame.VirtualFrame;
26+
import com.oracle.truffle.api.profiles.LoopConditionProfile;
2627
import com.oracle.truffle.api.source.SourceSection;
2728
import org.pkl.core.ast.ExpressionNode;
29+
import org.pkl.core.ast.internal.ReadCursorValueNode;
30+
import org.pkl.core.ast.internal.ReadCursorValueNodeGen;
2831
import org.pkl.core.ast.member.ObjectMember;
2932
import org.pkl.core.runtime.BaseModule;
3033
import org.pkl.core.runtime.Identifier;
@@ -41,12 +44,18 @@
4144
import org.pkl.core.runtime.VmMapping;
4245
import org.pkl.core.runtime.VmNull;
4346
import org.pkl.core.runtime.VmObject;
47+
import org.pkl.core.runtime.VmObjectCursor;
4448
import org.pkl.core.runtime.VmTyped;
4549
import org.pkl.core.runtime.VmUtils;
4650

4751
@ImportStatic(BaseModule.class)
4852
public abstract class GeneratorSpreadNode extends GeneratorMemberNode {
53+
private final LoopConditionProfile loopConditionProfile = LoopConditionProfile.create();
4954
@Child private ExpressionNode iterableNode;
55+
56+
@Child
57+
private ReadCursorValueNode readCursorValueNode = ReadCursorValueNodeGen.create(sourceSection);
58+
5059
private final boolean nullable;
5160

5261
public GeneratorSpreadNode(
@@ -189,53 +198,73 @@ protected void fallback(VirtualFrame frame, Object parent, ObjectData data, Obje
189198
}
190199

191200
protected void doEvalDynamic(VirtualFrame frame, ObjectData data, VmObject iterable) {
192-
var materializedFrame = frame.materialize();
193-
iterable.forceAndIterateMemberValues(
194-
(key, member, value) -> {
195-
if (member.isElement()) {
196-
data.addElement(materializedFrame, createMember(member, value), this);
197-
} else {
198-
data.addMember(materializedFrame, key, createMember(member, value), this);
199-
}
200-
return true;
201-
});
201+
var cursor = iterable.members();
202+
var counted = isCounted(cursor);
203+
while (advance(cursor, counted)) {
204+
var member = cursor.member();
205+
var value = readCursorValueNode.execute(frame, cursor);
206+
if (member.isElement()) {
207+
data.addElement(frame, createMember(member, value), this);
208+
} else {
209+
data.addMember(frame, cursor.key(), createMember(member, value), this);
210+
}
211+
}
202212
}
203213

204214
private void doEvalMapping(VirtualFrame frame, ObjectData data, VmObject iterable) {
205-
var materializedFrame = frame.materialize();
206-
iterable.forceAndIterateMemberValues(
207-
(key, member, value) -> {
208-
if (member.isElement() || member.isProp()) {
209-
cannotHaveMember(BaseModule.getMappingClass(), member);
210-
}
211-
data.addMember(materializedFrame, key, createMember(member, value), this);
212-
return true;
213-
});
215+
var cursor = iterable.members();
216+
var counted = isCounted(cursor);
217+
while (advance(cursor, counted)) {
218+
var member = cursor.member();
219+
if (member.isElement() || member.isProp()) {
220+
cannotHaveMember(BaseModule.getMappingClass(), member);
221+
}
222+
data.addMember(
223+
frame,
224+
cursor.key(),
225+
createMember(member, readCursorValueNode.execute(frame, cursor)),
226+
this);
227+
}
214228
}
215229

216230
private void doEvalListing(VirtualFrame frame, ObjectData data, VmObject iterable) {
217-
var materializedFrame = frame.materialize();
218-
iterable.forceAndIterateMemberValues(
219-
(key, member, value) -> {
220-
if (member.isEntry() || member.isProp()) {
221-
cannotHaveMember(getListingClass(), member);
222-
}
223-
data.addElement(materializedFrame, createMember(member, value), this);
224-
return true;
225-
});
231+
var cursor = iterable.members();
232+
var counted = isCounted(cursor);
233+
while (advance(cursor, counted)) {
234+
var member = cursor.member();
235+
if (member.isEntry() || member.isProp()) {
236+
cannotHaveMember(getListingClass(), member);
237+
}
238+
data.addElement(
239+
frame, createMember(member, readCursorValueNode.execute(frame, cursor)), this);
240+
}
226241
}
227242

228243
private void doEvalTyped(VirtualFrame frame, VmClass clazz, ObjectData data, VmObject iterable) {
229-
var materializedFrame = frame.materialize();
230-
iterable.forceAndIterateMemberValues(
231-
(key, member, value) -> {
232-
if (member.isElement() || member.isEntry()) {
233-
cannotHaveMember(clazz, member);
234-
}
235-
checkIsValidTypedProperty(clazz, member);
236-
data.addProperty(materializedFrame, createMember(member, value), this);
237-
return true;
238-
});
244+
var cursor = iterable.members();
245+
var counted = isCounted(cursor);
246+
while (advance(cursor, counted)) {
247+
var member = cursor.member();
248+
if (member.isElement() || member.isEntry()) {
249+
cannotHaveMember(clazz, member);
250+
}
251+
checkIsValidTypedProperty(clazz, member);
252+
data.addProperty(
253+
frame, createMember(member, readCursorValueNode.execute(frame, cursor)), this);
254+
}
255+
}
256+
257+
private boolean isCounted(VmObjectCursor cursor) {
258+
var length = cursor.getLength();
259+
if (length == -1) return false;
260+
loopConditionProfile.profileCounted(length);
261+
return true;
262+
}
263+
264+
private boolean advance(VmObjectCursor cursor, boolean counted) {
265+
return counted
266+
? loopConditionProfile.inject(cursor.advance())
267+
: loopConditionProfile.profile(cursor.advance());
239268
}
240269

241270
// handles both `List` and `Set`
@@ -250,7 +279,7 @@ private void doEvalCollection(
250279
.withProgramValue("Value", iterable)
251280
.build();
252281
}
253-
spreadIterable(frame, data, iterable);
282+
spreadIterable(frame, data, iterable, iterable.getLength());
254283
}
255284

256285
private void doEvalMap(VirtualFrame frame, VmClass parent, ObjectData data, VmMap iterable) {
@@ -278,7 +307,7 @@ private void doEvalIntSeq(
278307
.withProgramValue("Value", iterable)
279308
.build();
280309
}
281-
spreadIterable(frame, data, iterable);
310+
spreadIterable(frame, data, iterable, iterable.getLength());
282311
}
283312

284313
private void doEvalBytes(VirtualFrame frame, VmClass parent, ObjectData data, VmBytes iterable) {
@@ -290,7 +319,7 @@ private void doEvalBytes(VirtualFrame frame, VmClass parent, ObjectData data, Vm
290319
.withProgramValue("Value", iterable)
291320
.build();
292321
}
293-
spreadIterable(frame, data, iterable);
322+
spreadIterable(frame, data, iterable, iterable.getLength());
294323
}
295324

296325
private void cannotHaveMember(VmClass clazz, ObjectMember member) {
@@ -352,9 +381,11 @@ private ObjectMember createMember(ObjectMember prototype, Object value) {
352381
return result;
353382
}
354383

355-
private void spreadIterable(VirtualFrame frame, ObjectData data, Iterable<?> iterable) {
384+
private void spreadIterable(
385+
VirtualFrame frame, ObjectData data, Iterable<?> iterable, long length) {
386+
loopConditionProfile.profileCounted(length);
356387
var iterator = new TruffleIterator<>(iterable);
357-
while (iterator.hasNext()) {
388+
while (loopConditionProfile.inject(iterator.hasNext())) {
358389
var elem = iterator.next();
359390
var member = VmUtils.createSyntheticObjectElement(String.valueOf(data.length()), elem);
360391
data.addElement(frame, member, this);

‎pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ protected VmDynamic evalDynamicClass(
148148
"checkIsValidListingAmendment()",
149149
"checkMaxListingMemberIndex(0)"
150150
})
151-
protected void evalListingClass(@SuppressWarnings("unused") VmClass parent) {
151+
protected Object evalListingClass(@SuppressWarnings("unused") VmClass parent) {
152152
CompilerDirectives.transferToInterpreter();
153153
throw exceptionBuilder().unreachableCode().build();
154154
}

‎pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
import org.pkl.core.util.EconomicMaps;
3535

3636
/**
37-
* Object literal that contains both elements and entries (and possibly properties). Example: `new
38-
* foo { "pigeon", [3] = "barn owl" }`
37+
* Object literal that contains both elements and entries (and possibly properties).
38+
*
39+
* <p>Example: {@code new foo { "pigeon"; [3] = "barn owl" } }
3940
*/
4041
@ImportStatic({BaseModule.class, VmUtils.class})
4142
public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLiteralNode {

‎pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public abstract class ObjectLiteralNode extends ExpressionNode {
4242
protected final boolean isCustomThisScope;
4343
protected final @Nullable FrameDescriptor parametersDescriptor;
4444
@Children protected final UnresolvedTypeNode[] parameterTypes;
45+
@Child protected @Nullable AmendFunctionNode amendFunctionNode;
4546

4647
public ObjectLiteralNode(
4748
SourceSection sourceSection,

0 commit comments

Comments
 (0)