You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
generator: .return()/.throw() must run pending try/finally (and .return must honor catch) on abrupt completion #4374
Generator .return(v) and .throw(e) do not run pending try/finally (and .return ignores catch) when the generator is suspended at a yield inside a try. They complete/rethrow immediately, skipping the abrupt-completion semantics.
This is the dominant remaining cause in test262 built-ins/GeneratorPrototype (~18 of 24 failures after #4366) and a matching set in AsyncGeneratorPrototype.
Node: prints FINALLY ran, then {value:99,done:true}
Perry: returns {value:99,done:true} but skips the finally (no print)
.throw() has the same gap: it routes through catch (via CatchRoute) but never runs finally.
Root cause
In the generator state-machine transform (crates/perry-transform/src/generator/):
linearize.rs inlines a finally block into the happy-path states only (lines ~648-665) — it is not registered as a routable target. There is a CatchRoute (state-interval → catch body) for .throw, but no FinallyRoute.
lower.rs builds the .return closure trivially (done = true; return {value, done:true}) — it never touches the state machine.
build_async_throw_body dispatches to catch routes by state interval but never runs finally.
Fix shape
Add a FinallyRoute { protected_start_state, post_finally_state, body, has_yields } collected in linearize.rs alongside CatchRoute.
.return(v): for each enclosing finally covering the current state (innermost→outermost), run its body; then complete with {value: v, done: true} — unless a finally body itself performs an abrupt completion (return/throw), which supersedes (the finally-must-supersede-return cases).
.throw(e): run finally on the way to/after the catch route.
Handle nesting and the spec edges exercised by the suite: try-finally-nested-try-catch-within-{try,catch,finally}, try-finally-within-{try,finally}, try-finally-set-property-within-try, try-catch-before-try, etc.
This shares machinery with async/await and await using (which use the same linearizer + build_async_step_driver_direct), so the implementation must keep those paths byte-identical and be verified against language/statements/async-generator, built-ins/AsyncGeneratorPrototype, and the async/await suites.
A separate, smaller sub-gap in the same area: next/return/throw from the "executing" state must throw TypeError and mark the generator completed (*from-state-executing.js, ~3 tests) — needs a per-generator "executing" flag guard.
Context: surfaced after #4366 fixed the func_id-collision that was masking these as harness failures.
I’m taking a first pass on this. I’ll start by pinning the Node/Perry repro for pending try/finally during generator abrupt completion, then keep the implementation to a PR-sized transform/runtime slice.
focused Node 26/Perry repro comparison for generator .return() and .throw() through pending finally
Scope note: this PR intentionally targets non-yielding pending finalizers. Yielding-finally abrupt close and the separate executing-state guard called out in the issue still look like follow-up slices.
Activity
andrewtdiz commentedon Jun 4, 2026
I’m taking a first pass on this. I’ll start by pinning the Node/Perry repro for pending try/finally during generator abrupt completion, then keep the implementation to a PR-sized transform/runtime slice.
andrewtdiz commentedon Jun 4, 2026
Opened draft PR #4380 for this slice.
What it covers:
.return()and no-catch.throw()before completing/rethrowing.throw()through same-try finally to the next suspension pointreturnsuperseding abrupt completionValidation:
cargo check -p perry-transformcargo build --release --quiet -p perry -p perry-runtime -p perry-stdlibtests/test_issue_4374_generator_abrupt_finally.shtests/test_issue_4036_async_try_catch_after_await.shbash tests/test_issue_3583_async_semantic_parity.shcargo fmt --all -- --checkgit diff --check./scripts/check_file_size.sh.return()and.throw()through pending finallyScope note: this PR intentionally targets non-yielding pending finalizers. Yielding-finally abrupt close and the separate executing-state guard called out in the issue still look like follow-up slices.
fix(generator): run pending try/finally on .return()/.throw() abrupt …
fix(async): spec-correct async-generator yield* delegation for return…
fix(async): #5745 — async-generator yield* .throw() delegation + resu…