fix(transformer/async-to-generator): correct scope of inferred named FE in async-to-generator#21458
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via 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. |
b8a33d2 to
d3b1cf3
Compare
4a6a02b to
784fbfa
Compare
6501836 to
dc9b8b4
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
dc9b8b4 to
a21b1f6
Compare
Verification against Rolldown's pipelineI traced through 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 The rebuild inspects
Pre-fix, oxc's transformer set End-to-end reproductionI replicated Rolldown's exact pipeline (parse → initial SemanticBuilder → Transformer → fresh SemanticBuilder rebuild → Compressor with 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-calledRunning in Node: 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
|
|
Finally, I figured out why no output changes could resolve the issue. An incorrect |
08121a9 to
abed7d4
Compare
abed7d4 to
c5d2ef1
Compare
|
Ah, sorry, I noticed that I incorrectly minimized the reproduction in #21125 |
|
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. |
|
Similar to what the transform_conformance test does. |
I will take a look at how to achieve this |
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
c5d2ef1 to
56af2f4
Compare
### 💥 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>

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:Per the JS spec, the inferred name
foobinds 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:
create_functionsetFunctionType::FunctionDeclarationwheneverid.is_some(), even for callers using the result as a function expression. Any fresh semantic pass trustsr#typeand treats the node as a declaration — which binds the name in the enclosing wrapper scope.wrapper_scope_idrather than the function's own scope.Combined, downstream minifiers resolve a body reference like
typeNext()insideconst typeNext = async () => { typeNext(); }to the inner caller instead of the outerconst typeNext. The outer const then looks unused, gets inlined away, and the body reference fails at runtime withReferenceError: typeNext is not defined.Fix
Pass
FunctionTypeexplicitly (via newcreate_function_expressionandcreate_function_declarationhelpers), 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):
Runtime:
ReferenceError: typeNext is not defined✗Post-fix output (outer const preserved; minifier correctly drops the now-unused inner FE name):
Runtime: works correctly ✓
Closes #21125
Closes #21445