Parenthesize `async` when it starts a for-of loop initializer (#31326)
### What does this PR do?
Fixes a printer bug found by fuzzing (invariant: printed output must
reparse):
```js
new Bun.Transpiler({ loader: "tsx", target: "bun" }).transformSync("for (\\u0061sync of [7]);")
// before: "for (async of [7])\n ;\n" ← not valid JS, Bun (and JSC) cannot reparse it
// after: "for ((async) of [7])\n ;\n" ← reparses and runs
```
`\u0061sync` is a legal identifier spelling of `async` and is valid as a
for-of loop variable, but the for-of grammar has the restriction
`[lookahead ∉ { let, async of }]`, so the *token sequence* `async of`
may not start the initializer. The printer emitted the identifier as a
bare `async`, producing output that neither Bun's parser nor JSC
accepts.
### Cause
`src/js_printer/lib.rs` only parenthesized `let` at the start of a
for-of initializer (`for ((let) of x)`), not `async`.
### Fix
Add an `IsFollowedByOf` expr flag that `SForOf` passes into
`print_for_loop_init`. When an identifier named `async` is printed at
the very start of a for-of initializer and is directly followed by `of`,
it is wrapped in parentheses — mirroring the existing `let` handling and
esbuild's behavior.
- `for (async.x of y)`, `for (async[0] of y)`, `for (x[async] of y)`,
`for (async in x)` are unchanged (no parens needed).
- `for await (\u0061sync of x)` is also wrapped: Bun's parser (and JSC)
reject the bare spelling there as well.
### Verification
- New test in `test/bundler/transpiler/transpiler.test.js` (`for-of loop
variable named async`): fails on bun without this change, passes with
it.
- Full `test/bundler/transpiler/transpiler.test.js` passes with the
debug build (147 pass / 0 fail).
- Runtime check: `let \u0061sync; for (\u0061sync of [7, 8])
log.push(async * 2)` executes correctly with the debug build. R
robobun committed
80df5b17deb040a89256886dcf1cf816be61a176
Parent: 08652f2
Committed by GitHub <noreply@github.com>
on 5/24/2026, 8:49:57 PM