Skip to content

for (const [i,v] of arr.entries()) silently iterates 0 times when arr is statically typed any #597

Description

@proggeramlug

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.rsArray.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions