Skip to content

fix(hir): continue in for-await drivers skipped the iterator advance (spin)#6196

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/for-await-continue-spin
Jul 9, 2026
Merged

fix(hir): continue in for-await drivers skipped the iterator advance (spin)#6196
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/for-await-continue-spin

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Every iterator-driver desugar that modeled for await (x of src) as

let __result = await __iter.next();
while (!__result.done) {
    <bind x>; <user body>;
    __result = await __iter.next();   // advance at the body TAIL
}

broke continue: it jumps to the while condition, skips the tail advance, and re-processes the same result forever — an infinite spin observed as a hang. Six copies of the shape existed (lower/stmt_loops.rs ×3: generic for-await, recognized-generator-call, Web-ReadableStream reader; lower_decl/body_stmt.rs ×2; lower_decl/body_stmt/for_await.rs ×1).

The array/lazy sync path (lazy_iter_for_stmt) already documents this exact footgun"a while with the advance at the body tail would skip it on continue and spin" — and dodges it with Stmt::For's update clause, which the await-capable drivers can't use (no await in a for update).

Ten-line repro (hangs pre-fix, node prints both values + done):

async function* events() { yield "a"; yield "ping"; yield "b"; }
async function* filtered() {
  for await (const w of events()) {
    if (w === "ping") continue;   // ← spins forever on the same result
    yield w;
  }
}
for await (const x of filtered()) console.log(x);

Canonical real-world failure: an SSE consumer's for await (const ev of stream) { if (ev.event === "ping") continue; … } — a large esbuild-bundled CLI app hung on the first real server ping event. Local mocks that never sent pings masked it completely; ping is the only SSE event the client skips via continue.

Fix

All six drivers now advance at the TOP:

let __result = undefined;
while (true) {
    __result = await __iter.next();
    if (__result.done) break;
    <bind x>; <user body>;
}

continue falls to the while (true) condition and re-runs the advance. break/return/throw behavior is unchanged — the synthetic if done break is appended after the abrupt-close rewrite over the user body, so normal completion never runs a spurious IteratorClose. The three stmt_loops.rs sites share a new iter_driver_while_stmt helper; the body_stmt copies mirror it.

Tests

New crates/perry/tests/for_await_continue.rs (5 tests, node-oracle-verified, 30s hang guard since pre-fix they spin forever):

  • continue inside for-await in an async generator (the SSE shape)
  • in a plain async fn
  • over a custom [Symbol.asyncIterator] iterable (runtime GetAsyncIterator driver)
  • over a Web ReadableStream (getReader()/read() driver)
  • break + nested-loop-continue behavior guard

(The pre-existing c262_parity::logical_property_assignment_short_circuits_the_store_4586 failure reproduces identically on pristine main — unrelated.)

Summary by CodeRabbit

  • Bug Fixes

    • Improved for await and iterator-based loop handling so continue now advances to the next item correctly instead of reusing the same completed result.
    • Fixed async loops over generators, custom async iterators, and readable streams to avoid hangs and infinite spinning.
    • Preserved correct behavior for break and nested-loop continue cases.
  • Tests

    • Added regression coverage for for await loops to verify correct output and prevent future loop-control regressions.

…e (spin)

Every iterator-driver desugar that modeled `for await (x of src)` as

    let __result = await __iter.next();
    while (!__result.done) {
        <bind x>; <user body>;
        __result = await __iter.next();   // advance at the body TAIL
    }

broke `continue`: it jumps to the `while` condition, skips the tail
advance, and re-processes the SAME result forever — an infinite spin
observed as a hang. Six copies of the shape existed:

- lower/stmt_loops.rs: the generic runtime for-await driver, the
  recognized-generator-call driver, and the Web-ReadableStream
  getReader()/read() driver;
- lower_decl/body_stmt.rs: the latter two again (function-body copies);
- lower_decl/body_stmt/for_await.rs: the function-body generic driver.

The array/lazy sync path (`lazy_iter_for_stmt`) already documented this
exact footgun and dodged it with `Stmt::For`'s update clause — which the
await-capable drivers can't use (no `await` in a `for` update). All six
now advance at the TOP:

    let __result = undefined;
    while (true) {
        __result = await __iter.next();
        if (__result.done) break;
        <bind x>; <user body>;
    }

`continue` falls to the `while (true)` condition and re-runs the
advance; `break`/`return`/`throw` behavior is unchanged (the synthetic
`if done break` is appended AFTER the abrupt-close rewrite over the user
body, so normal completion never runs a spurious IteratorClose).

Canonical real-world failure: an SSE consumer's
`for await (const ev of stream) { if (ev.event === "ping") continue; …}`
— a large esbuild-bundled CLI app hung on the FIRST real server ping
event (local mocks that never sent pings masked it; the ping event is
the only one that SDK skips via `continue`).

Tests: crates/perry/tests/for_await_continue.rs — continue inside
for-await in an async generator (the SSE shape), in a plain async fn,
over a custom `[Symbol.asyncIterator]` iterable (runtime GetAsyncIterator
driver), and over a Web ReadableStream (reader driver); plus a
break/nested-continue behavior guard. All node-oracle-verified, with a
30s hang guard (pre-fix these spin forever). perry-hir unit suites pass
(the one c262_parity failure — `o.x ??= 2` store lowering — fails
identically on pristine main; unrelated).
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes an infinite-spin bug in for await / for...of loop lowering by restructuring iterator-driven loops (async generators, custom async iterators, and ReadableStream) to advance the iterator (next()/read()) at the top of a while (true) loop rather than at the tail, ensuring continue correctly re-drives iteration. Adds regression tests.

Changes

Advance-at-top Iterator Driver Fix

Layer / File(s) Summary
New iter_driver_while_stmt helper and call sites
crates/perry-hir/src/lower/stmt_loops.rs
Adds iter_driver_while_stmt that sets result from next_call at loop top, breaks on done, then runs body; applies it in for-await iterator, for-of iterator-protocol, and ReadableStream lowering, changing result-local init to undefined.
body_stmt.rs advance-at-top rewrite
crates/perry-hir/src/lower_decl/body_stmt.rs
Rewrites ReadableStream and generic iterator-protocol loops in lower_body_stmt to while (true) with result assignment and done/break check inside the body instead of the loop condition.
for_await.rs synthesized loop update
crates/perry-hir/src/lower_decl/body_stmt/for_await.rs
Changes __result init to undefined and restructures the synthesized driver loop to advance next() first, break on done, then bind/execute user body.
Regression tests
crates/perry/tests/for_await_continue.rs
Adds compile-and-run test helper with a 30s hang guard, and tests for continue/break across async generators, custom async iterators, and ReadableStream for-await loops, including nested-loop continue semantics.

Estimated code review effort: 4 (Complex) | ~50 minutes

Possibly related PRs

  • PerryTS/perry#5810: Both PRs change how for await/async-iterator consumption drives next() and handles done completion.

Suggested labels: run-extended-tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main fix: preventing skipped advances and spins in for-await drivers.
Description check ✅ Passed The description covers the problem, fix, and tests, but it does not follow the template headings and omits Related issue and Checklist sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry-hir/src/lower/stmt_loops.rs (1)

361-393: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider exposing iter_driver_while_stmt (or a shared equivalent) so the other two files reuse it.

The advance-at-top shape here is re-inlined verbatim in lower_decl/body_stmt.rs (ReadableStream + iterator-protocol) and lower_decl/body_stmt/for_await.rs. Those copies even reference this helper in their comments. Since it was exactly this shape diverging across the "six copies" that produced the hang, promoting this to a pub(crate) helper and calling it from all sites would prevent future drift. The implementation itself is correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/stmt_loops.rs` around lines 361 - 393, The
advance-at-top iterator-driver loop is duplicated in other lowering paths, which
risks the copies drifting again; expose iter_driver_while_stmt as a shared
pub(crate) helper (or equivalent common helper) and update
lower_decl/body_stmt.rs and lower_decl/body_stmt/for_await.rs to call it instead
of inlining the shape. Keep the implementation in stmt_loops.rs as the single
source of truth so the iterator-driver behavior stays consistent across all
sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-hir/src/lower/stmt_loops.rs`:
- Around line 361-393: The advance-at-top iterator-driver loop is duplicated in
other lowering paths, which risks the copies drifting again; expose
iter_driver_while_stmt as a shared pub(crate) helper (or equivalent common
helper) and update lower_decl/body_stmt.rs and lower_decl/body_stmt/for_await.rs
to call it instead of inlining the shape. Keep the implementation in
stmt_loops.rs as the single source of truth so the iterator-driver behavior
stays consistent across all sites.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bd80e267-7432-4997-aad7-fc91be287be5

📥 Commits

Reviewing files that changed from the base of the PR and between 95059b8 and 4bf4e9f.

📒 Files selected for processing (4)
  • crates/perry-hir/src/lower/stmt_loops.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-hir/src/lower_decl/body_stmt/for_await.rs
  • crates/perry/tests/for_await_continue.rs

@proggeramlug
proggeramlug merged commit d7cb72f into PerryTS:main Jul 9, 2026
25 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