Repository navigation
codegen: lower try/catch to LLVM invoke/landingpad, delete the setjmp/longjmp path #7302
Copy link
Copy link
Closed
Description
Perry lowers try/catch to setjmp/longjmp (perry-codegen/src/stmt/try_stmt.rs; zero uses of invoke today). That single choice causes three separate problems:
- Precise moving-GC roots are unsound in
tryfunctions — alongjmpcan jump past agc.statepoint'sgc.relocate, so the relocated pointer's write-back never runs. This is why the statepoint experiment (gc: RewriteStatepointsForGC over managed-pointer SSA — the measured-only road to native-root file-size parity #7174) excludeshas_tryfunctions and routes them to a plain-stack-map fallback that is itself unsound (root slots recorded as caller-saved registers; 3/60 locations on one probe). Under RS4GC,mem2regcannot promote the volatile allocas setjmp requires, so try-region roots never enter SSA and never join agc-livebundle. - ~570 lines of machinery exist only to fight the register allocator:
volatile_setjmp.rs(376) +setjmp_abi.rs(193) implement C99 7.13.2.1p3. - Every
tryfunction is pessimized:returns_twice+#1(noinline) on the whole function.
Moving to invoke/landingpad collapses all three and is the prerequisite for #7174 deleting its unsound has_try fallback (statepoints can then cover every function).
Plan
- Phase 0 (done) — standalone spike: invoke + landingpad + catch-all against a
_Unwind_RaiseExceptionthrow path. All green on macOS arm64, including bit-exact NaN-box payload round-trip, nested rethrow, finally re-raise, uncaught. Decisions: Perry-ownedperry_eh_personality(Itanium LSDA walk; no libc++abi, no dependence onrust_eh_personality); thrown value stays in the GC-rooted TLS slot (landing pad ignores{ptr, i32});js_throwkeeps its savepoint restores and raises;js_call_catchingkeeps its Rust-boundary longjmp trap (Rust cannot catch foreign exceptions; a raise never crosses an openjs_call_catchingframe). - Phase 2 (measured early) — cross-Rust-frame unwinding matrix:
panic=unwindaborts on any helper with an interior Rust call site (RFC 2945 abort guards);panic=abortwith default flags strands the exception (no unwind tables);panic=abort+-C force-unwind-tables=yesis exactly longjmp-equivalent (caught, Drops skipped) and keeps the at-throw savepoint restores correct unchanged. The mass-extern "C-unwind"alternative was measured and rejected (abort-on-missed-annotation; incompatible with panic=abort). - Phase 1 (in progress) — full codegen lowering behind a temporary
PERRY_EHflag: every potentially-throwing call inside an active handler scope becomes aninvoke; rethrows in catch/finally wire to the enclosing pad (spike-confirmed rule); async rejection boundary converted the same way. - Phase 3 (merge gate) — flip the default, delete
volatile_setjmp.rs,setjmp_abi.rs, thereturns_twice/#1handling and the flag itself. A PR leaving both paths in place must not merge. - Windows (
x86_64-pc-windows-msvc, CI-exercised) — no Itanium unwinder there; SEH funclet form planned (RaiseException+catchswitch/catchpad, personality__C_specific_handler, fixed filter matching a Perry-owned exception code). Shared invoke-conversion infrastructure; per-triple dispatch/landing shape (assetjmp_abiselects per-triple today).
Acceptance
- Gap suite byte-for-byte vs the pinned Node oracle; exception-heavy corpus extended with every Phase-1 edge (normal/caught/rethrown, finally on both edges, return-inside-try, nesting).
- New GC probe: allocate inside
try, throw across a collection point, verify caught value + surviving locals (none exists today — which is why the unsound fallback went unnoticed). - Perf on try/catch-heavy microbenchmarks, both directions.
- Line delta strongly negative.
- "Subject was live" assert: emitted IR grepped for
invoke/landingpad; runtime carries a self-check for unwind-table presence (a strayRUSTFLAGSdroppingforce-unwind-tablesmust fail loudly, not strand throws).
Design doc: docs/invoke-eh-experiment.md on branch exp/invoke-eh.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat(eh): invoke/landingpad try lowering behind PERRY_EH=invoke (#7302)
test(eh): #7302 exception corpus — structural paths, cross-helper thr…
fix(eh): rewrite phi predecessor labels past inline invoke splits (#7302
feat(eh): SEH funclet variant for windows-msvc (#7302)
feat(eh): unwind-table self-check on first js_eh_try_push (#7302)
fix(eh): _Unwind_Backtrace trace fn must return _URC_NO_REASON (#7302)
docs(eh): gap-suite attribution + perf matrix with V8 calibration (#7302
feat(eh)!: flip to invoke/landingpad, delete the setjmp lowering (#7302)
Merge origin/main: typed-LlInst layer (#7301) × invoke-EH (#7302)
docs(eh): changelog fragment + final status (#7302, PR #7305)
docs(eh): final sweep results — 95.4%, crash set collapsed to the one…
Exception lowering: setjmp/longjmp → LLVM invoke/landingpad (#7302) (#…
Merge origin/main: invoke-EH lands, so statepoints cover every function
ci(eh): gate the owned transport on liveness and agreement (#7302)
test(eh): cover throws on perry/thread workers + CI arm (#7302)
fix(build)!: the SHIPPED runtime was built panic=unwind (#7302) (#7309)
perf(eh): owned single-phase unwinder — 14.3x on deep unwinds, 5.6x o…
fix(native-backend): construct invoke/landingpad/personality in the i…
fix(native-backend): decline SEH funclets, refresh stale corpora, dro…
gc: address CodeRabbit review — two hangs/holes, one real format gap
Native-frame GC roots via LLVM statepoints, opt-in (#7173, #7174) (#7314
gc: a stack-map record must belong to the function the ip is in (#7314 …
fix(gc): route RS4GC through the in-process LLVM backend (#7339)
chore(gc): delete the GcCallEffect::NeverReturns variant that is neve…
bench(public): regenerate the Node/Bun baseline at v0.5.1335 and rest…