Skip to content

generator: .return()/.throw() must run pending try/finally (and .return must honor catch) on abrupt completion #4374

Closed

Description

@proggeramlug

Summary

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.

Repro

function* g(){ try { yield 1; yield 2; } finally { console.log("FINALLY ran"); } }
const it = g();
it.next();        // {value:1, done:false} — suspended inside try
it.return(99);    // Node: prints "FINALLY ran", returns {value:99, done:true}
  • 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

  1. Add a FinallyRoute { protected_start_state, post_finally_state, body, has_yields } collected in linearize.rs alongside CatchRoute.
  2. .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).
  3. .throw(e): run finally on the way to/after the catch route.
  4. 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.

Activity

  1. andrewtdiz commented on Jun 4, 2026

    @andrewtdiz
    Contributor

    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.

  2. andrewtdiz commented on Jun 4, 2026

    @andrewtdiz
    Contributor

    Opened draft PR #4380 for this slice.

    What it covers:

    • adds a routable non-yielding finally state for generator try/finally regions
    • runs pending finalizers during .return() and no-catch .throw() before completing/rethrowing
    • resumes caught .throw() through same-try finally to the next suspension point
    • covers nested finally order and finalizer return superseding abrupt completion

    Validation:

    • cargo check -p perry-transform
    • cargo build --release --quiet -p perry -p perry-runtime -p perry-stdlib
    • tests/test_issue_4374_generator_abrupt_finally.sh
    • tests/test_issue_4036_async_try_catch_after_await.sh
    • bash tests/test_issue_3583_async_semantic_parity.sh
    • cargo fmt --all -- --check
    • git diff --check
    • ./scripts/check_file_size.sh
    • 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.

  3. added 2 commits that reference this issue on Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions