Repository navigation
Sloppy script block function used before its declaration in a loop is undefined #10079
Description
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
./fixtureExpected 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
92e2161c9and
all nine coherent Wasm/provider archives, hashes verified before/after.
--enable-wasm-runtimewas additionally used for that locally frozen graph. - That compiler is based on main
603b074acplus 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 frozen81b47b47cgenerator-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
hoistedvaridentity, 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
proggeramlug commentedon Sep 14, 2026
Fixed by merge train 185r (#10242) at 9fda98d.