Skip to content

codegen: captured let loses its TDZ on the second loop iteration #10051

Description

@proggeramlug

Independent reproducer

This is a synchronous language-semantics bug, not an application/Bun API dependency.
The source below runs without a bundle, credentials, network, or the generator fix.

function test() {
  const results: string[] = [];
  for (let index = 0; index < 2; index++) {
    let read = () => value;
    try { read(); results.push('missing-tdz'); }
    catch (error) { results.push(error instanceof ReferenceError ? 'tdz' : 'wrong-error'); }
    let value: number;
    results.push(String(read()));
    value = index;
    results.push(String(read()));
    let recurse = (n: number): number => n === 0 ? value : recurse(n - 1);
    results.push(String(recurse(2)));
  }
  return results.join(',');
}
console.log(test());

Pinned Node 26.5.1 prints:
tdz,undefined,0,0,tdz,undefined,1,1

Perry prints:
tdz,undefined,0,0,missing-tdz,undefined,1,1

The second iteration's read before the let declaration must throw ReferenceError.
The no-initializer declaration must then end that iteration's TDZ with undefined.

Before/after evidence

macOS arm64, Wasm host enabled, compact GC (RS4GC=0, shadow stack=1,
inline shadow slot=0, full outline IC=1), auto-optimization disabled.
Every case uses a frozen compiler and all nine coherent provider archives from
its exact source commit, not mixed libraries or source-stamp overrides.

Identical failure at both O0 and Oz on all three trees:

Thus this is not introduced by #10049. Its initial broad control fixture exposed
the existing bug. The separate generator callback reproduction now passes.

Example native compile (with a matched build's runtime directory configured):
PERRY_LL_OPT_LEVEL=0 perry compile tdz-loop.ts -o tdz-loop --platform bun --enable-wasm-runtime --no-auto-optimize
Use fresh output/cache paths and run under a 15-second timeout.

Implementation scope / acceptance

  • Preserve per-iteration lexical binding identity and reestablish TDZ on every
    block entry; do not reset shared var bindings or reuse a previous iteration's cell.
  • Audit TDZ box preallocation and loop capture/freshening paths. The precise
    responsible lowering path has not yet been isolated; this issue is behavioral evidence.
  • Add a byte-for-byte Node/native gap regression, then cover retained callbacks,
    initialized and uninitialized let, const, recursion, and shared var controls.
  • Verify O0/Os/Oz under default and compact GC. No application rebuild is required.

Local evidence: tdz-loop-before-after-20260911.log and the six-case result
record /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/perry-tdz-loop-ab-szGh6Q/result.json.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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