Standalone reproduction
async function* delegate(value: number) { yield value; }
async function* outer() {
let base = 40;
for (let index = 0; index < 2; index++) {
yield* delegate(index);
let getter = () => base + index;
let wrapper = () => getter();
yield wrapper();
}
}
async function main() {
const values: number[] = [];
for await (const value of outer()) values.push(value);
if (values.join(',') !== '0,40,1,41') throw new Error(values.join(','));
console.log('PASS: delegated loop captures local callable');
}
main().catch(error => { console.error(error); process.exit(1); });
Node 26.5.1 passes. Perry exits 1 with TypeError: value is not a function.
No application, network, child process, credentials, or npm dependency is needed.
Verified on macOS arm64 with a coherently built/frozen compiler and all nine
runtime/provider archives at f3e8d5d1a66753cb00d15da552ad73abb8740520 (Perry
0.5.1532), an explicit diagnostic integration of #10042/#10046 onto fetched main
603b074ac. Native compile uses --platform bun --enable-wasm-runtime --no-auto-optimize, LLVM Oz, and compact shadow-GC settings. This is an exact
tested integration identity, not a claim that those PRs have merged.
Control cases with a local callback after a simple yield, await, or yield*
outside a loop pass. The delegated loop fails. A larger source exposed the same
shape, and LLDB showed the nested closure's getter capture already contains
TAG_UNDEFINED at function entry, before its attempted invocation.
Located mechanism
Retained LLVM has two emitted copies of the loop continuation. The getter's
box-pointer alloca is entry-initialized to undefined. The first copy calls
js_box_alloc_bits and stores the new cell into that slot. The second copy
creates the getter closure and writes/captures through the same slot, but emits
no corresponding box allocation. On that resumed path the slot is still
undefined. This is deterministic initialization/control-flow failure, not
evidence of a moved pointer.
Follow-up native verification narrows this to the general existing-local reuse
guard in codegen/stmt/let_stmt.rs (the #1803 hoisted-var redeclaration path),
before the boxed Let allocation arm. The reproduced getter is an ordinary
boxed Let, not a PreallocateBoxes directive. A PreallocateBoxes-only candidate
in draft #10049 passed its IR tests but still failed both native GC modes; it
was not accepted as a fix. Preserve real hoisted-var/parameter binding identity
while ensuring a reused boxed declaration has a cell before its initializer
or a nested capturing callback executes. Per-iteration and TDZ behavior need
native regression coverage too.
This appears distinct from #10023's nested destructuring/liveness case: this
reproducer has no destructuring, and the declaration is present in the resumed
path while its storage initialization is missing.
Acceptance should include Node/native O0/Os/Oz parity with default/compact GC,
retained per-iteration closures, empty/nonempty delegates, ordinary yield/await
controls, and preallocated/TDZ/hoisted-binding regressions. Keep the fix and
tests independent of any application extraction or bundle.
Standalone reproduction
Node 26.5.1 passes. Perry exits 1 with
TypeError: value is not a function.No application, network, child process, credentials, or npm dependency is needed.
Verified on macOS arm64 with a coherently built/frozen compiler and all nine
runtime/provider archives at
f3e8d5d1a66753cb00d15da552ad73abb8740520(Perry0.5.1532), an explicit diagnostic integration of #10042/#10046 onto fetched main
603b074ac. Native compile uses--platform bun --enable-wasm-runtime --no-auto-optimize, LLVM Oz, and compact shadow-GC settings. This is an exacttested integration identity, not a claim that those PRs have merged.
Control cases with a local callback after a simple yield, await, or yield*
outside a loop pass. The delegated loop fails. A larger source exposed the same
shape, and LLDB showed the nested closure's getter capture already contains
TAG_UNDEFINEDat function entry, before its attempted invocation.Located mechanism
Retained LLVM has two emitted copies of the loop continuation. The getter's
box-pointer alloca is entry-initialized to undefined. The first copy calls
js_box_alloc_bitsand stores the new cell into that slot. The second copycreates the getter closure and writes/captures through the same slot, but emits
no corresponding box allocation. On that resumed path the slot is still
undefined. This is deterministic initialization/control-flow failure, not
evidence of a moved pointer.
Follow-up native verification narrows this to the general existing-local reuse
guard in
codegen/stmt/let_stmt.rs(the #1803 hoisted-var redeclaration path),before the boxed Let allocation arm. The reproduced getter is an ordinary
boxed Let, not a PreallocateBoxes directive. A PreallocateBoxes-only candidate
in draft #10049 passed its IR tests but still failed both native GC modes; it
was not accepted as a fix. Preserve real hoisted-var/parameter binding identity
while ensuring a reused boxed declaration has a cell before its initializer
or a nested capturing callback executes. Per-iteration and TDZ behavior need
native regression coverage too.
This appears distinct from #10023's nested destructuring/liveness case: this
reproducer has no destructuring, and the declaration is present in the resumed
path while its storage initialization is missing.
Acceptance should include Node/native O0/Os/Oz parity with default/compact GC,
retained per-iteration closures, empty/nonempty delegates, ordinary yield/await
controls, and preallocated/TDZ/hoisted-binding regressions. Keep the fix and
tests independent of any application extraction or bundle.