Skip to content

Fix #435, #669, #625, #641: arrow/closure capture in generator & async-arrow state machines#699

Merged
nickna merged 1 commit into
mainfrom
wrk/peaceful-wescoff-a9683e
Jun 16, 2026
Merged

Fix #435, #669, #625, #641: arrow/closure capture in generator & async-arrow state machines#699
nickna merged 1 commit into
mainfrom
wrk/peaceful-wescoff-a9683e

Conversation

@nickna

@nickna nickna commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Compiled-mode arrows that capture variables inside generator and async-arrow state machines were miscompiled. Each issue's primary case is now fixed and passes --verify; deeper sub-cases fail-fast with a clear compile error (never a silent miscompile) and are tracked by follow-up issues.

What & why

#435 / #669 — arrows inside sync generator bodies

  • Collection gap: CollectArrowsFromExpr had no Expr.Yield case, so an arrow inside a yielded expression (yield arr.map(x => …)) was never registered → the callback compiled to ldnull"Array.prototype.map callback is not callable". Added the case.
  • Capturing arrows: GeneratorMoveNextEmitter is the only emitter that uses the base ExpressionEmitterBase.EmitArrowFunction, so I taught the base to build the display class for capturing arrows via the existing GetThisField / GetHoistedVariableField hooks. This fixes read / loop-var / this captures without adding a 4th per-emitter duplicate (it actually reduces the triple-sync surface).
  • this capture: GeneratorStateAnalyzer.UsesThis didn't descend into arrows, so this used only inside an arrow left the receiver field (<>4__this) undefined → NRE. Fixed with a focused sub-visitor.
  • Write-captures (forEach(n => s += n)) need a function display class the generator SM lacks → clear compile error, tracked by Compiled: arrow inside a generator body that writes a captured variable needs a function-level display class (mutation lost) #674.

#625 — async arrow writing a captured variable emitted unverifiable IL (StackUnexpected)

A boxed value-type state machine cannot be verifiably mutated in placeunbox yields a readonly (controlled-mutability) managed pointer, so stfld through it fails ILVerify and can drop the write in complex state machines. Fix promotes direct-child written captures into the reference-type function display class; the arrow reads/writes outer.functionDC.field (a class reference → verifiable). Async-method and top-level-arrow variants → #682.

#641 — capturing async arrow nested in a top-level async arrow was unsupported

Threads the single capture through the standalone stub's $TSFunction "target" prepend slot, loaded from the enclosing arrow's frame. Standalone captures are resolved in the emitter at low priority (below module globals) so a top-level variable a standalone arrow closes over still reads live from its static field rather than the by-value snapshot. Write-back and multi-capture → #684.

Verification

Check Result
Full xUnit suite 12,713 / 12,713
TypeScriptConformance 31 / 31 (no type-checker code changed)
New regression tests 29 (generator arrow bodies, async captured writes, nested async captures)
--verify on every repro passes
Test262 corpus could not run here — pre-existing 0x80131506 test-host crash (crashes before executing any test; independent of these compiled-only changes)

A mid-work regression (6 *_CapturedTopLevel_AsyncArrow tests) was caught and fixed: the standalone-capture read had to move to low priority in the emitter, since a top-level var is also registered as a standalone capture but must read live from its static field.

ILVerifier now surfaces the failing IL offset / found / expected in its error messages (it previously reported only "Unexpected type on the stack" with no location) — this is what made #625 diagnosable.

Reviewer notes

…c-arrow state machines

Compiled-mode arrows that capture variables inside generator and async-arrow
state machines were miscompiled. Each issue's primary case is now fixed and
passes --verify; deeper sub-cases fail-fast with a clear compile error and are
tracked by follow-up issues rather than silently miscompiling.

#435/#669 - arrows inside sync generator bodies:
- Add the missing Expr.Yield case to CollectArrowsFromExpr; arrows inside a
  yielded expression were never collected, so the callback compiled to ldnull
  ("Array.prototype.map callback is not callable").
- Teach the base EmitArrowFunction to build the display class for capturing
  arrows via the existing GetThisField/GetHoistedVariableField hooks.
  GeneratorMoveNextEmitter is the only emitter using the base default, so this
  fixes read/loop-var/this captures without adding a 4th per-emitter duplicate.
- Fix GeneratorStateAnalyzer.UsesThis to see `this` used only inside an arrow,
  so an instance generator method materializes its receiver (else the captured
  `this` snapshot was null -> NRE).
- Reject an arrow that WRITES a captured generator variable (the generator SM
  has no function display class to share storage) -> tracked by #674.

#625 - async arrow writing a captured variable emitted unverifiable IL:
- A boxed value-type state machine cannot be mutated in place by verifiable IL
  (unbox yields a readonly managed pointer; stfld through it fails ILVerify and
  can drop the write). Promote direct-child written captures into the
  reference-type function display class and have the arrow read/write
  outer.functionDC.field. Async-method and top-level-arrow variants -> #682.

#641 - capturing async arrow nested in a top-level async arrow was unsupported:
- Thread the single capture through the standalone stub's $TSFunction prepend
  slot, loaded from the enclosing arrow's frame. Resolve standalone captures in
  the emitter at LOW priority (below module globals) so a top-level variable a
  standalone arrow closes over still reads live from its static field rather
  than the by-value snapshot. Write-back and multi-capture -> #684.

Shared CapturedWriteAnalysis.CollectImmediateWrites backs the write-capture
guards. ILVerifier now surfaces the failing IL offset/found/expected.

Verification: full xUnit suite 12,713/12,713; TypeScriptConformance 31/31; 29 new
regression tests (generator arrow bodies, async captured writes, nested async
captures). Test262 corpus could not run here due to the pre-existing 0x80131506
test-host crash (independent of these compiled-only changes).
@nickna nickna merged commit 5e031e0 into main Jun 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant