mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(react_compiler): replace all compiled functions in a single AST walk (#24403)
## What `OxcReplaceFnVisitor` walked the entire program from the root **once per compiled function** — O(replacements × AST size). This collects all non-gated replacements into an `FxHashMap<ScopeId, &CodegenFunction>` and applies them in a **single traversal**, with a `remaining` counter that stops the walk early once every target has been applied. ## Why it's equivalent - Non-gated replacement is a pure **in-place field edit** of a function node — it never inserts or removes `program.body` statements — so running the whole batch before the outlined-declaration insertion and gating loop (which *do* restructure `program.body`) cannot affect their results. - The **gated** path is untouched: it stays in the loop because it replaces a function with a `gating() ? … : …` conditional and clones the *original* body. Each replacement is gated or non-gated, never both. - A top-down walk reaches an outer function before any nested one and does not recurse into a replaced node, preserving the "outer wins, inner clobbered" behavior the N separate walks already produced — independent of list order or nesting. ## Verification - `cargo test -p oxc_react_compiler`: the 1806-fixture snapshot corpus passes **unchanged** (byte-identical output), plus all transform integration tests. Net: N full-program traversals → 1, scaling with the number of compiled functions per file.
This commit is contained in:
1 file changed
+81
-50
@@ -2257,13 +2257,61 @@ fn ox_build_compiled_expression<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Visitor that replaces a compiled function in the oxc AST by matching the
|
||||
/// original function's scope. Mirrors the Babel `ReplaceFnVisitor`.
|
||||
/// Visitor that replaces every compiled function in the oxc AST in a single
|
||||
/// traversal, each matched by its original function's scope. Mirrors the Babel
|
||||
/// `ReplaceFnVisitor`, batched so the program is walked once regardless of how
|
||||
/// many functions were compiled.
|
||||
struct OxcReplaceFnVisitor<'a, 'b> {
|
||||
ast: &'b AstBuilder<'a>,
|
||||
scope_id: ScopeId,
|
||||
codegen: &'b CodegenFunction<'a>,
|
||||
done: bool,
|
||||
/// Compiled replacement for each original function, keyed by the function's
|
||||
/// `scope_id` cell. Codegen-built nodes carry no cells, so they are never
|
||||
/// matched.
|
||||
replacements: FxHashMap<ScopeId, &'b CodegenFunction<'a>>,
|
||||
/// Replacements not yet applied; the walk stops once it reaches zero.
|
||||
remaining: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b> OxcReplaceFnVisitor<'a, 'b> {
|
||||
fn replace_function(&self, func: &mut oxc::Function<'a>, codegen: &CodegenFunction<'a>) {
|
||||
// When the compiled function does not initialize a memo cache, the body is
|
||||
// left essentially intact, so the original TS signature (type parameters,
|
||||
// `this` parameter, return type, and per-parameter type annotations) is
|
||||
// preserved. Functions that memoize drop these types, mirroring Babel.
|
||||
let keep_types = codegen.memo_slots_used == 0;
|
||||
let mut params = codegen.params.clone_in(self.ast.allocator());
|
||||
if keep_types {
|
||||
copy_param_ts_metadata(self.ast.allocator(), &mut params, &func.params);
|
||||
} else {
|
||||
func.type_parameters = None;
|
||||
func.return_type = None;
|
||||
func.this_param = None;
|
||||
}
|
||||
func.id = codegen.id.clone_in(self.ast.allocator());
|
||||
func.params = params;
|
||||
func.body = Some(codegen.body.clone_in(self.ast.allocator()));
|
||||
func.generator = codegen.generator;
|
||||
func.r#async = codegen.is_async;
|
||||
func.declare = false;
|
||||
}
|
||||
|
||||
fn replace_arrow(
|
||||
&self,
|
||||
arrow: &mut oxc::ArrowFunctionExpression<'a>,
|
||||
codegen: &CodegenFunction<'a>,
|
||||
) {
|
||||
let keep_types = codegen.memo_slots_used == 0;
|
||||
let mut params = codegen.params.clone_in(self.ast.allocator());
|
||||
if keep_types {
|
||||
copy_param_ts_metadata(self.ast.allocator(), &mut params, &arrow.params);
|
||||
} else {
|
||||
arrow.type_parameters = None;
|
||||
arrow.return_type = None;
|
||||
}
|
||||
arrow.params = params;
|
||||
arrow.body = codegen.body.clone_in(self.ast.allocator());
|
||||
arrow.r#async = codegen.is_async;
|
||||
arrow.expression = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> oxc_ast_visit::VisitMut<'a> for OxcReplaceFnVisitor<'a, 'b> {
|
||||
@@ -2272,54 +2320,29 @@ impl<'a, 'b> oxc_ast_visit::VisitMut<'a> for OxcReplaceFnVisitor<'a, 'b> {
|
||||
func: &mut oxc::Function<'a>,
|
||||
flags: oxc_syntax::scope::ScopeFlags,
|
||||
) {
|
||||
if self.done {
|
||||
if self.remaining == 0 {
|
||||
return;
|
||||
}
|
||||
if func.scope_id.get() == Some(self.scope_id) {
|
||||
// When the compiled function does not initialize a memo cache, the body is
|
||||
// left essentially intact, so the original TS signature (type parameters,
|
||||
// `this` parameter, return type, and per-parameter type annotations) is
|
||||
// preserved. Functions that memoize drop these types, mirroring Babel.
|
||||
let keep_types = self.codegen.memo_slots_used == 0;
|
||||
let mut params = self.codegen.params.clone_in(self.ast.allocator());
|
||||
if keep_types {
|
||||
copy_param_ts_metadata(self.ast.allocator(), &mut params, &func.params);
|
||||
} else {
|
||||
func.type_parameters = None;
|
||||
func.return_type = None;
|
||||
func.this_param = None;
|
||||
if let Some(scope_id) = func.scope_id.get() {
|
||||
if let Some(&codegen) = self.replacements.get(&scope_id) {
|
||||
self.replace_function(func, codegen);
|
||||
self.remaining -= 1;
|
||||
return;
|
||||
}
|
||||
func.id = self.codegen.id.clone_in(self.ast.allocator());
|
||||
func.params = params;
|
||||
func.body = Some(self.codegen.body.clone_in(self.ast.allocator()));
|
||||
func.generator = self.codegen.generator;
|
||||
func.r#async = self.codegen.is_async;
|
||||
func.declare = false;
|
||||
self.done = true;
|
||||
return;
|
||||
}
|
||||
oxc_ast_visit::walk_mut::walk_function(self, func, flags);
|
||||
}
|
||||
|
||||
fn visit_arrow_function_expression(&mut self, arrow: &mut oxc::ArrowFunctionExpression<'a>) {
|
||||
if self.done {
|
||||
if self.remaining == 0 {
|
||||
return;
|
||||
}
|
||||
if arrow.scope_id.get() == Some(self.scope_id) {
|
||||
let keep_types = self.codegen.memo_slots_used == 0;
|
||||
let mut params = self.codegen.params.clone_in(self.ast.allocator());
|
||||
if keep_types {
|
||||
copy_param_ts_metadata(self.ast.allocator(), &mut params, &arrow.params);
|
||||
} else {
|
||||
arrow.type_parameters = None;
|
||||
arrow.return_type = None;
|
||||
if let Some(scope_id) = arrow.scope_id.get() {
|
||||
if let Some(&codegen) = self.replacements.get(&scope_id) {
|
||||
self.replace_arrow(arrow, codegen);
|
||||
self.remaining -= 1;
|
||||
return;
|
||||
}
|
||||
arrow.params = params;
|
||||
arrow.body = self.codegen.body.clone_in(self.ast.allocator());
|
||||
arrow.r#async = self.codegen.is_async;
|
||||
arrow.expression = false;
|
||||
self.done = true;
|
||||
return;
|
||||
}
|
||||
oxc_ast_visit::walk_mut::walk_arrow_function_expression(self, arrow);
|
||||
}
|
||||
@@ -2625,6 +2648,22 @@ fn ox_transform_program<'a>(
|
||||
// would corrupt the parent expression.
|
||||
let mut appended_outlined_decls: Vec<oxc::Statement<'a>> = Vec::new();
|
||||
|
||||
// Substitute every non-gated compiled function into its original in a single
|
||||
// program walk, each matched by its `scope_id` cell. In-place edits do not
|
||||
// change `program.body`, so the outlined-decl insertion and gating below
|
||||
// (which do restructure it) are unaffected by running first. Gated functions
|
||||
// are replaced by a conditional in the loop below, not edited in place.
|
||||
let replace_map: FxHashMap<ScopeId, &CodegenFunction<'a>> = replacements
|
||||
.iter()
|
||||
.filter(|r| r.gating.is_none())
|
||||
.map(|r| (r.fn_scope_id, &r.codegen_fn))
|
||||
.collect();
|
||||
if !replace_map.is_empty() {
|
||||
let mut visitor =
|
||||
OxcReplaceFnVisitor { ast, remaining: replace_map.len(), replacements: replace_map };
|
||||
oxc_ast_visit::VisitMut::visit_program(&mut visitor, program);
|
||||
}
|
||||
|
||||
for replacement in replacements {
|
||||
let mut sibling_outlined_decls: Vec<oxc::Statement<'a>> = Vec::new();
|
||||
let insert_as_sibling = replacement.original_kind == OriginalFnKind::FunctionDeclaration;
|
||||
@@ -2641,14 +2680,6 @@ fn ox_transform_program<'a>(
|
||||
|
||||
if let Some(ref gating_config) = replacement.gating {
|
||||
ox_apply_gated_conditional(ast, program, replacement, gating_config, context);
|
||||
} else {
|
||||
let mut visitor = OxcReplaceFnVisitor {
|
||||
ast,
|
||||
scope_id: replacement.fn_scope_id,
|
||||
codegen: &replacement.codegen_fn,
|
||||
done: false,
|
||||
};
|
||||
oxc_ast_visit::VisitMut::visit_program(&mut visitor, program);
|
||||
}
|
||||
|
||||
if !sibling_outlined_decls.is_empty() {
|
||||
|
||||
Reference in new issue
Block a user