Skip to content

fix(transformer/async-to-generator): correct scope of inferred named FE in async-to-generator#21458

Merged
graphite-app[bot] merged 1 commit intomainfrom
fix/async-to-generator-inferred-name-scope
Apr 15, 2026
Merged

fix(transformer/async-to-generator): correct scope of inferred named FE in async-to-generator#21458
graphite-app[bot] merged 1 commit intomainfrom
fix/async-to-generator-inferred-name-scope

Conversation

@Dunqing
Copy link
Copy Markdown
Member

@Dunqing Dunqing commented Apr 15, 2026

When the async-to-generator transform wraps an async arrow / anonymous async function expression whose name is inferred from its parent (e.g. const foo = async () => {}, { foo: async () => {} }), it emits a named function expression as the caller:

const foo = function () {
  var _ref = asyncToGenerator(function* () { /* body */ });
  return function foo() { return _ref.apply(this, arguments); };
}();

Per the JS spec, the inferred name foo binds only inside the named FE itself — not in the enclosing wrapper IIFE scope.

But the previous code had two bugs that together caused oxc to emit an AST that a fresh semantic analysis (e.g. Rolldown's minifier in Vite, or oxc's own minifier) would misread:

  1. create_function set FunctionType::FunctionDeclaration whenever id.is_some(), even for callers using the result as a function expression. Any fresh semantic pass trusts r#type and treats the node as a declaration — which binds the name in the enclosing wrapper scope.
  2. The inferred id's binding was registered in wrapper_scope_id rather than the function's own scope.

Combined, downstream minifiers resolve a body reference like typeNext() inside const typeNext = async () => { typeNext(); } to the inner caller instead of the outer const typeNext. The outer const then looks unused, gets inlined away, and the body reference fails at runtime with ReferenceError: typeNext is not defined.

Fix

Pass FunctionType explicitly (via new create_function_expression and create_function_declaration helpers), and place the inferred id's binding in the caller function's own scope.

Runtime verification

Ran input → oxc transform → oxc compress (same pipeline Rolldown/Vite uses):

Pre-fix output (matches the exact bug in the Vite issue — outer const inlined away):

(function() {
  var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
  return function typeNext() { return _ref.apply(this, arguments); };
})()();

Runtime: ReferenceError: typeNext is not defined

Post-fix output (outer const preserved; minifier correctly drops the now-unused inner FE name):

const typeNext = function() {
  var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
  return function() { return _ref.apply(this, arguments); };
}();
typeNext();

Runtime: works correctly ✓

Closes #21125
Closes #21445

Copy link
Copy Markdown
Member Author

Dunqing commented Apr 15, 2026


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added A-transformer Area - Transformer / Transpiler C-bug Category - Bug labels Apr 15, 2026
@Dunqing Dunqing force-pushed the fix/async-to-generator-inferred-name-scope branch 2 times, most recently from b8a33d2 to d3b1cf3 Compare April 15, 2026 06:47
@Dunqing Dunqing changed the title fix(transformer): bind inferred async-to-generator names in FE's own scope fix(transformer): rename outer bindings shadowed by async-to-generator inferred names Apr 15, 2026
@Dunqing Dunqing force-pushed the fix/async-to-generator-inferred-name-scope branch 2 times, most recently from 4a6a02b to 784fbfa Compare April 15, 2026 07:05
@Dunqing Dunqing changed the title fix(transformer): rename outer bindings shadowed by async-to-generator inferred names fix(transformer): correct scope of inferred named FE in async-to-generator Apr 15, 2026
@Dunqing Dunqing force-pushed the fix/async-to-generator-inferred-name-scope branch 2 times, most recently from 6501836 to dc9b8b4 Compare April 15, 2026 07:42
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 15, 2026

Merging this PR will not alter performance

✅ 44 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing fix/async-to-generator-inferred-name-scope (c5d2ef1) with main (b3ed467)2

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (7c98d52) during the generation of this report, so b3ed467 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Dunqing Dunqing force-pushed the fix/async-to-generator-inferred-name-scope branch from dc9b8b4 to a21b1f6 Compare April 15, 2026 07:55
@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Apr 15, 2026

Verification against Rolldown's pipeline

I traced through rolldown/crates/rolldown/src/utils/pre_process_ecma_ast.rs (where Rolldown runs oxc's transformer) and reproduced the exact pipeline to confirm this fix propagates correctly downstream.

How Rolldown uses oxc

// Step 3: Transform (consumes initial scoping, drops the returned scoping)
let scoping = self.recreate_scoping(&mut scoping, program);
let _ret = Transformer::new(...).build_with_scoping(scoping, program);

// Step 5: Tree-shake (rebuilds scoping from AST before DCE)
let scoping = self.recreate_scoping(&mut scoping, program);  // ← SemanticBuilder::new().build(program)
Compressor::new(allocator).dead_code_elimination_with_scoping(program, scoping, options);

Between Step 3 and Step 5, Rolldown discards the transformer-returned scoping and calls SemanticBuilder::new().build(program) — rebuilding scoping from scratch by walking the post-transform AST.

The rebuild inspects func.r#type (via func.is_declaration() / func.is_expression() in oxc_semantic::builder::visit_function):

  • is_declaration() → binds name in parent scope (before enter_scope).
  • is_expression() → binds name in own scope (after enter_scope).

Pre-fix, oxc's transformer set r#type = FunctionDeclaration on the inner caller function even though it's wrapped in Expression::FunctionExpression(...). Rolldown's rebuild honors r#type, binding the name in the wrapper IIFE scope, causing DCE to (correctly per its view) inline the "unused" outer const.

End-to-end reproduction

I replicated Rolldown's exact pipeline (parse → initial SemanticBuilder → Transformer → fresh SemanticBuilder rebuild → Compressor with TreeShakeOptions) and ran the issue's input through both pre-fix and post-fix:

Pre-fix pipeline output:

import _asyncToGenerator from "@oxc-project/runtime/helpers/asyncToGenerator";
(function() {
    var _ref = _asyncToGenerator(function* () {
        yield new Promise((resolve) => setTimeout(resolve, 1e3));
        console.log("typeNext");
        typeNext();            // ReferenceError at runtime
    });
    return function typeNext() {
        return _ref.apply(this, arguments);
    };
})()();                         // outer const DCE'd away, IIFE double-called

Running in Node: ReferenceError: typeNext is not defined ← matches the Vite issue exactly.

Post-fix pipeline output:

import _asyncToGenerator from "@oxc-project/runtime/helpers/asyncToGenerator";
const typeNext = function() {  // outer const preserved
    var _ref = _asyncToGenerator(function* () {
        yield new Promise((resolve) => setTimeout(resolve, 1e3));
        console.log("typeNext");
        typeNext();             // correctly resolves to outer
    });
    return function typeNext() {
        return _ref.apply(this, arguments);
    };
}();
typeNext();

Running in Node: works correctly.

Summary

  • No changes needed in Rolldown. The fix is purely in oxc's transformer emitting correct r#type and scope placement.
  • Every tool that runs SemanticBuilder on oxc's post-transform AST benefits — including oxc's own minifier.
  • The PR's transform_checker "Bindings mismatch" errors (pre-fix, now gone post-fix) are the exact symptom of what Rolldown's rebuild saw. Those tests are a direct proxy for Rolldown's behavior.

@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Apr 15, 2026

Finally, I figured out why no output changes could resolve the issue. An incorrect func.r#type causes semantic rebuilding in a different way, which leads to this bug.

@Dunqing Dunqing marked this pull request as ready for review April 15, 2026 08:29
@Dunqing Dunqing requested a review from overlookmotel as a code owner April 15, 2026 08:29
@Dunqing Dunqing force-pushed the fix/async-to-generator-inferred-name-scope branch 2 times, most recently from 08121a9 to abed7d4 Compare April 15, 2026 08:37
@Dunqing Dunqing requested a review from sapphi-red April 15, 2026 08:40
@Dunqing Dunqing force-pushed the fix/async-to-generator-inferred-name-scope branch from abed7d4 to c5d2ef1 Compare April 15, 2026 08:41
@sapphi-red
Copy link
Copy Markdown
Member

Ah, sorry, I noticed that I incorrectly minimized the reproduction in #21125

@sapphi-red
Copy link
Copy Markdown
Member

I wonder if it makes sense to add debug mode in Rolldown that verifies whether generating semantic data from the intermediate code outputs the same semantic data.

@sapphi-red
Copy link
Copy Markdown
Member

Similar to what the transform_conformance test does.

@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Apr 15, 2026

I wonder if it makes sense to add debug mode in Rolldown that verifies whether generating semantic data from the intermediate code outputs the same semantic data.

I will take a look at how to achieve this

@Dunqing Dunqing changed the title fix(transformer): correct scope of inferred named FE in async-to-generator fix(transformer/async-to-generator): correct scope of inferred named FE in async-to-generator Apr 15, 2026
@Dunqing Dunqing added the 0-merge Merge with Graphite Merge Queue label Apr 15, 2026
Copy link
Copy Markdown
Member Author

Dunqing commented Apr 15, 2026

Merge activity

…FE in async-to-generator (#21458)

When the async-to-generator transform wraps an async arrow / anonymous async function expression whose name is inferred from its parent (e.g. `const foo = async () => {}`, `{ foo: async () => {} }`), it emits a named function expression as the caller:

```js
const foo = function () {
  var _ref = asyncToGenerator(function* () { /* body */ });
  return function foo() { return _ref.apply(this, arguments); };
}();
```

Per the JS spec, the inferred name `foo` binds only inside the named FE itself — not in the enclosing wrapper IIFE scope.

But the previous code had two bugs that together caused oxc to emit an AST that a fresh semantic analysis (e.g. Rolldown's minifier in Vite, or oxc's own minifier) would misread:

1. `create_function` set `FunctionType::FunctionDeclaration` whenever `id.is_some()`, even for callers using the result as a function expression. Any fresh semantic pass trusts `r#type` and treats the node as a declaration — which binds the name in the enclosing wrapper scope.
2. The inferred id's binding was registered in `wrapper_scope_id` rather than the function's own scope.

Combined, downstream minifiers resolve a body reference like `typeNext()` inside `const typeNext = async () => { typeNext(); }` to the inner caller instead of the outer `const typeNext`. The outer const then looks unused, gets inlined away, and the body reference fails at runtime with `ReferenceError: typeNext is not defined`.

## Fix

Pass `FunctionType` explicitly (via new `create_function_expression` and `create_function_declaration` helpers), and place the inferred id's binding in the caller function's own scope.

## Runtime verification

Ran input → oxc transform → oxc compress (same pipeline Rolldown/Vite uses):

**Pre-fix** output (matches the exact bug in the Vite issue — outer const inlined away):
```js
(function() {
  var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
  return function typeNext() { return _ref.apply(this, arguments); };
})()();
```
Runtime: `ReferenceError: typeNext is not defined` ✗

**Post-fix** output (outer const preserved; minifier correctly drops the now-unused inner FE name):
```js
const typeNext = function() {
  var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
  return function() { return _ref.apply(this, arguments); };
}();
typeNext();
```
Runtime: works correctly ✓

Closes #21125
Closes #21445
@graphite-app graphite-app Bot force-pushed the fix/async-to-generator-inferred-name-scope branch from c5d2ef1 to 56af2f4 Compare April 15, 2026 14:06
@graphite-app graphite-app Bot merged commit 56af2f4 into main Apr 15, 2026
26 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Apr 15, 2026
@graphite-app graphite-app Bot deleted the fix/async-to-generator-inferred-name-scope branch April 15, 2026 14:11
Dunqing added a commit that referenced this pull request Apr 16, 2026
### 💥 BREAKING CHANGES

- 24fb7eb allocator: [**BREAKING**] Rename `Box` and `Vec` methods
(#21395) (overlookmotel)

### 🚀 Features

- ce5072d parser: Support `turbopack` magic comments (#20803) (Kane
Wang)
- f5deb55 napi/transform: Expose `optimizeConstEnums` and
`optimizeEnums` options (#21388) (Dunqing)
- 24b03de data_structures: Introduce `NonNullConst` and `NonNullMut`
pointer types (#21425) (overlookmotel)

### 🐛 Bug Fixes

- d7a359a ecmascript: Treat update expressions as unconditionally
side-effectful (#21456) (Dunqing)
- 56af2f4 transformer/async-to-generator: Correct scope of inferred
named FE in async-to-generator (#21458) (Dunqing)
- b3ed467 minifier: Avoid illegal `var;` when folding unused arguments
copy loop (#21421) (fazba)
- b0e8f13 minifier: Preserve `var` inside `catch` with same-named
parameter (#21366) (Dunqing)
- 4fb73a7 transformer/typescript: Preserve execution order for accessor
with `useDefineForClassFields: false` (#21369) (Dunqing)

### ⚡ Performance

- da3cc16 parser: Refactor out `LexerContext` (#21275) (Ulrich Stark)

### 📚 Documentation

- c5b19bb allocator: Reformat comments in `Arena` (#21448)
(overlookmotel)
- 091e88e lexer: Update doc comment about perf benefit of reading
through references (#21423) (overlookmotel)
- 922cbee allocator: Remove references to "bump" from comments (#21397)
(overlookmotel)

Co-authored-by: Dunqing <29533304+Dunqing@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-transformer Area - Transformer / Transpiler C-bug Category - Bug

Projects

None yet

2 participants