Skip to content

fix(transform): #1047 — async early return followed by an unreached await#1062

Merged
proggeramlug merged 1 commit into
mainfrom
fix/1047-async-early-return-yield-state
May 19, 2026
Merged

fix(transform): #1047 — async early return followed by an unreached await#1062
proggeramlug merged 1 commit into
mainfrom
fix/1047-async-early-return-yield-state

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Fixes #1047. The generator transform only rewrote user-level return X statements when the state's exit was StateExit::Done. States ending in StateExit::Yield (i.e. states followed by an await) left those returns as raw Stmt::Return(X), so when an early return was taken next() returned the bare expression value; the AsyncStepChain caller treated the missing .done as false and re-entered the same state — infinite loop.

The fix runs the same body_contains_returnprepend_done_before_returns + rewrite_returns_as_done sequence on Yield-state bodies that StateExit::Done already does.

Reproducer

async function getKid(): Promise<string> {
    const existing = await findExisting();      // state 0 → 1 (Yield)
    if (existing) return existing.kid;           // <-- raw Return inside state 1
    const kid = `dek-${Date.now().toString(36)}`;
    await insertNew(kid);                        // state 1 → 2 (Yield, unreached)
    return kid;
}

Before: infinite loop, [inner] existing: the-kid printed millions of times.
After: k: the-kid typeof: string — matches Node byte-for-byte.

Test plan

  • test-files/test_issue_1047_async_early_return_unreached_await.ts matches node --experimental-strip-types
  • Focused build green
  • No regressions in existing async tests (cargo build --release -p perry-transform -p perry)

…wait

In the generator transform, user-level `return X` statements inside a
state body were only rewritten (to set `__gen_done = true` and wrap the
value in an iter-result with `done = true`) when the state's exit was
`StateExit::Done`. States ending in `StateExit::Yield` left those
returns as raw `Stmt::Return(X)`.

When a `Yield` state had an early return in front of the synthesized
`state = next; return iter_result(value, false)` tail — i.e. the
canonical `if (existing) return existing.kid; ... await ...; return kid;`
shape — next() returned the bare expression value. The AsyncStepChain
caller treated the missing `.done` as `false` and re-entered the same
state with `state_id` unchanged (the synthesized advance never ran).
Result: infinite loop, re-evaluating state 1's body.

The fix is one block: run the same `body_contains_return` →
`prepend_done_before_returns` + `rewrite_returns_as_done` sequence on
Yield-state bodies that StateExit::Done already does. Done-flag set
and iter-result wrapping make every early-return path terminal so the
async-step driver short-circuits via `js_iter_result_get_done()`.

Verified with `test-files/test_issue_1047_async_early_return_unreached_await.ts`
byte-for-byte against `node --experimental-strip-types`.
@proggeramlug proggeramlug merged commit 3856caa into main May 19, 2026
9 checks passed
@proggeramlug proggeramlug deleted the fix/1047-async-early-return-yield-state branch May 19, 2026 03:53
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.

async fn early-return followed by an unreached await infinite-loops

1 participant