Repro
function f(arr: any) {
console.log('typeof:', typeof arr, 'isArray:', Array.isArray(arr));
for (const [i, v] of arr.entries()) {
console.log('iter', i, v);
}
console.log('done');
}
f([{ name: 'alice' }, { name: 'bob' }]);
Bun output (correct):
typeof: object isArray: true
iter 0 { name: "alice" }
iter 1 { name: "bob" }
done
Perry output (bug):
typeof: object isArray: true
done
The loop body never executes. Replacing arr: any with arr: any[] (or otherwise statically typing it as Array) makes the loop run correctly — same call pattern, different inferred receiver type.
Diagnostic
arr.entries() returns an iterator (perry: typeof === "object", truthy). But explicit .next() on that iterator returns { done: undefined, value: undefined } instead of { done: false, value: [0, ...] }:
const arr = [{ name: 'alice' }];
const it = arr.entries();
console.log(it.next()); // perry: { done: undefined, value: undefined }
// bun: { done: false, value: [0, { name: 'alice' }] }
So the iterator object exists but its .next() doesn't honour the iterator protocol. For-of falls through.
When arr is statically typed T[], perry has a fast-path that synthesizes [i, val] pairs directly from the array's indices — bypassing the broken iterator protocol. That's why for (const [i,v] of [1,2].entries()) works in isolation but for (const [i,v] of (someAnyTypedVal).entries()) doesn't.
Why it matters for #488
Blocks the drizzle + @perryts/postgres acceptance test, immediately after #588 + #590 unblocked the dialect-call path. drizzle's dialect.buildInsertQuery (pg-core/dialect.js:377) has:
buildInsertQuery({ table, values: valuesOrSelect, ... }) {
// ...
const values = valuesOrSelect;
for (const [valueIndex, value] of values.entries()) {
// ...
}
}
valuesOrSelect arrives via destructuring of a function param, so its static type is Any. The for-of header's iterator-protocol read fails, the loop body never runs, the SQL never gets built, and downstream sql.join(valuesSqlList) fails reading some .length on undefined.
Where I'd start digging
crates/perry-runtime/src/array.rs — Array.prototype.entries implementation. The returned iterator probably doesn't have a proper .next() method registered (or returns a non-spec-shape result).
Or alternately, in codegen: crates/perry-codegen/src/... — the for-of-with-destructure-on-entries fast path that triggers for statically-typed arrays should also fire for the dynamic-receiver path, OR the dynamic path should call into a runtime helper that knows how to read the iterator. The non-fast-path-without-runtime-helper combination is what falls through to the broken iterator.
Workaround
Manually drive the iterator (works in perry):
const len = values.length;
for (let valueIndex = 0; valueIndex < len; valueIndex++) {
const value = values[valueIndex];
// ...
}
But drizzle's source uses .entries() — patching it everywhere isn't reasonable. Cleanest fix is in the runtime / codegen.
Refs
Blocks #488. Surfaced from acceptance run on v0.5.697 after #588 + #590 landed.
Repro
Bun output (correct):
Perry output (bug):
The loop body never executes. Replacing
arr: anywitharr: any[](or otherwise statically typing it as Array) makes the loop run correctly — same call pattern, different inferred receiver type.Diagnostic
arr.entries()returns an iterator (perry:typeof === "object", truthy). But explicit.next()on that iterator returns{ done: undefined, value: undefined }instead of{ done: false, value: [0, ...] }:So the iterator object exists but its
.next()doesn't honour the iterator protocol. For-of falls through.When
arris statically typedT[], perry has a fast-path that synthesizes[i, val]pairs directly from the array's indices — bypassing the broken iterator protocol. That's whyfor (const [i,v] of [1,2].entries())works in isolation butfor (const [i,v] of (someAnyTypedVal).entries())doesn't.Why it matters for #488
Blocks the drizzle + @perryts/postgres acceptance test, immediately after #588 + #590 unblocked the dialect-call path. drizzle's
dialect.buildInsertQuery(pg-core/dialect.js:377) has:valuesOrSelectarrives via destructuring of a function param, so its static type is Any. The for-of header's iterator-protocol read fails, the loop body never runs, the SQL never gets built, and downstreamsql.join(valuesSqlList)fails reading some.lengthon undefined.Where I'd start digging
crates/perry-runtime/src/array.rs—Array.prototype.entriesimplementation. The returned iterator probably doesn't have a proper.next()method registered (or returns a non-spec-shape result).Or alternately, in codegen:
crates/perry-codegen/src/...— the for-of-with-destructure-on-entries fast path that triggers for statically-typed arrays should also fire for the dynamic-receiver path, OR the dynamic path should call into a runtime helper that knows how to read the iterator. The non-fast-path-without-runtime-helper combination is what falls through to the broken iterator.Workaround
Manually drive the iterator (works in perry):
But drizzle's source uses
.entries()— patching it everywhere isn't reasonable. Cleanest fix is in the runtime / codegen.Refs
Blocks #488. Surfaced from acceptance run on v0.5.697 after #588 + #590 landed.