Skip to content

Sloppy script block function used before its declaration in a loop is undefined #10079

Description

@proggeramlug

Problem

A block function used before its textual declaration inside a for body is
not initialized before that use in sloppy/script mode. The same program passes
when compiled as an ES module. This is independent of async/generator lowering
and does not require Claude Code or any application files.

function hoistedVar() {
  const callbacks: Array<() => number> = [];
  for (let index = 0; index < 3; index++) {
    callbacks.push(read);
    var value = index;
    function read() { return value; }
  }
  return callbacks.map(callback => callback()).join(',');
}
console.log(hoistedVar());

Put fixture.ts in a directory outside an ESM package. Compile with:

PERRY_LL_OPT_LEVEL=0 perry compile fixture.ts -o fixture \
  --no-cache --no-auto-optimize --no-codegen --platform bun
./fixture

Expected and Node26.5.1 output: 2,2,2, exit0. Perry instead exits1 with
TypeError: value is not a function. Adding only a sibling package.json
containing {"type":"module"} makes the identical source pass. Both the
original long test filename and fixture.ts give the same context-sensitive
result; this is not filename-dependent.

Evidence / precise scope

  • Reproduced at O0/default native roots with frozen compiler 92e2161c9 and
    all nine coherent Wasm/provider archives, hashes verified before/after.
    --enable-wasm-runtime was additionally used for that locally frozen graph.
  • That compiler is based on main 603b074ac plus fix(codegen): let minsize choose ordinary shadow-root inlining #10076's minsize policy;
    it is not represented as an exact-main binary. The policy is inactive at
    O0, and its HIR/transform source is byte-identical to main603b. fix(codegen): initialize missing cells in reused boxed declarations #10049's
    generator fix is absent from this toolchain.
  • The larger original control also fails in script context and passes in ESM
    context on the older frozen 81b47b47c generator-fix toolchain. All generator,
    ordinary yield/await, retained callback and TDZ/recursion assertions complete
    before its isolated hoisted-var failure.
  • Retained IR explains the failure without a memory-safety hypothesis: script
    lowering initializes the function binding to undefined, reads it into the
    array, and only later allocates the block function. ESM IR allocates the
    function before the corresponding array push.

Related prior implementation: #5297. This is distinct from #10051's repeated
lexical-TDZ reset issue. The module-context loss surfaced when #10047 copied a
fixture from the repository's ESM package into a plain temporary directory.

Acceptance

  • The standalone case must match Node in an explicitly non-module directory,
    and retain the ESM behavior. Do not make the script case pass by adding an
    ESM marker or strict directive to it.
  • Cover use before textual block declaration, repeated loop entry, shared
    hoisted var identity, fresh lexical captures, and outside-block Annex B
    binding updates without collapsing the lexical and outer var bindings.
  • Verify O0/Os/Oz, with fixtures that pin their package/module context instead
    of inheriting the harness directory's package.json.

Local evidence: hoisted-var-script-92e2161c9-20260911.log (2 failures) and
hoisted-var-esm-92e2161c9-20260911.log (2 passes), with complete source, retained
IR, exact outputs and artifact hashes in perry-generator-control-provenance-hulqzY
and perry-generator-control-provenance-8GEKYt under the macOS temp directory.

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