Skip to content

Commit 4739c58

Browse files
authored
feat(react-compiler): improve SWC parity for early-return and hooks validation (#11738)
## Summary This draft continues the React Compiler SWC parity port inside `crates/swc_ecma_react_compiler`. Current upstream fixture status (continue mode): - `FAILED_FIXTURE`: **1029 / 1718** (was **1040 / 1718**) - `fixture mismatch`: **835** (was **842**) - `missing_diagnostic`: **163** (was **167**) - `missing_error_fragment`: **31** (unchanged) Early-return focused cluster: - `early-return*` subset: **11/15 -> 7/15** failures ## What Changed - Wire `@enablePropagateDepsInHIR` fixture pragma into environment/options and propagate `enable_forest` through the entrypoint. - Improve reactive lowering for `if` tails that return conditionally (early-return sentinel path), so components without a terminal `return` can still be memoized correctly. - Add targeted nested memoization parity for member-based single-element arrays in nested scopes, while guarding against over-memoization on local member deps. - Improve nested temp naming behavior across `if` branches to better match expected fixture output. - Add normalization to prune empty `else {}` blocks. - Extend Rules of Hooks validation to catch hook calls that happen after a conditional early return path. ## Files - `crates/swc_ecma_react_compiler/src/entrypoint/program.rs` - `crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rs` - `crates/swc_ecma_react_compiler/src/validation/validate_hooks_usage.rs` - `crates/swc_ecma_react_compiler/tests/fixture.rs` ## Validation Executed locally: - `cargo fmt --all` - `cargo clippy --all --all-targets -- -D warnings` - `cargo test -p swc_ecma_react_compiler --test fixture fixture_cases_local -- --nocapture` - `cargo test -p swc_ecma_react_compiler --test fixture fixture_cases_upstream_phase1 -- --nocapture` - `REACT_COMPILER_FIXTURE_CONTINUE_ON_FAIL=1 REACT_COMPILER_FIXTURE_ALLOW_FAILURE=1 cargo test -p swc_ecma_react_compiler --test fixture fixture_cases_upstream -- --nocapture` Note: - `cargo test -p swc_ecma_react_compiler` still fails due remaining upstream parity gaps (`fixture_cases_upstream`). ## Scope Note This draft is intentionally crate-parity-focused and does not expand `@swc/core` / `@swc/react-compiler` exposure.
1 parent 7e2cf8d commit 4739c58

File tree

4 files changed

+340
-46
lines changed

4 files changed

+340
-46
lines changed

crates/swc_ecma_react_compiler/src/entrypoint/program.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ pub fn compile_fn(
329329
optimization::outline_functions(&mut hir);
330330
}
331331

332-
let reactive = reactive_scopes::build_reactive_function(&hir);
332+
let mut reactive = reactive_scopes::build_reactive_function(&hir);
333+
reactive.enable_forest = opts.environment.enable_forest;
333334

334335
if opts
335336
.environment

crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rs

Lines changed: 215 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub struct ReactiveFunction {
4747
pub is_async: bool,
4848
pub is_generator: bool,
4949
pub fn_type: ReactFunctionType,
50+
pub enable_forest: bool,
5051
}
5152

5253
#[derive(Clone)]
@@ -130,6 +131,7 @@ fn function_to_reactive(
130131
is_async: function.is_async,
131132
is_generator: function.is_generator,
132133
fn_type,
134+
enable_forest: false,
133135
}
134136
}
135137

@@ -1665,6 +1667,7 @@ fn memoize_reactive_function(reactive: &mut ReactiveFunction) -> (u32, u32, u32,
16651667
let has_destructuring_default_alloc =
16661668
body_contains_destructuring_default_alloc_literal(&reactive.body);
16671669
if reactive.fn_type == ReactFunctionType::Component
1670+
&& !reactive.enable_forest
16681671
&& !has_identity_sensitive_work
16691672
&& !has_destructuring_default_alloc
16701673
{
@@ -1770,7 +1773,10 @@ fn memoize_reactive_function(reactive: &mut ReactiveFunction) -> (u32, u32, u32,
17701773
Some(Stmt::Return(return_stmt)) if return_stmt.arg.is_some()
17711774
)
17721775
);
1773-
if !can_memoize_try_tail {
1776+
let can_memoize_if_tail_with_return = stmts.last().is_some_and(|stmt| {
1777+
matches!(stmt, Stmt::If(_)) && contains_return_stmt_in_stmts(std::slice::from_ref(stmt))
1778+
});
1779+
if !can_memoize_try_tail && !can_memoize_if_tail_with_return {
17741780
transformed.extend(stmts);
17751781
reactive.body.stmts = transformed;
17761782
return (0, 0, 0, 0, 0);
@@ -3457,6 +3463,143 @@ fn memoize_reactive_function(reactive: &mut ReactiveFunction) -> (u32, u32, u32,
34573463
}
34583464
}
34593465
}
3466+
} else if matches!(tail.last(), Some(Stmt::If(_))) && contains_return_stmt_in_stmts(&tail) {
3467+
prune_empty_stmts(&mut tail);
3468+
prune_noop_identifier_exprs(&mut tail);
3469+
prune_unused_underscore_jsx_decls(&mut tail);
3470+
promote_immutable_lets_to_const(&mut tail);
3471+
normalize_static_string_members_in_stmts(&mut tail);
3472+
inline_const_literal_indices_in_stmts(&mut tail);
3473+
normalize_compound_assignments_in_stmts(&mut tail);
3474+
normalize_reactive_labels(&mut tail);
3475+
normalize_if_break_blocks(&mut tail);
3476+
normalize_if_return_blocks(&mut tail);
3477+
lower_function_decls_to_const_in_stmts(&mut tail);
3478+
flatten_hoistable_blocks_in_stmts(&mut tail, &mut reserved);
3479+
flatten_hoistable_blocks_in_nested_functions(&mut tail);
3480+
lower_iife_call_args_in_stmts(&mut tail, &mut reserved, &mut next_temp);
3481+
inline_trivial_iifes_in_stmts(&mut tail);
3482+
flatten_hoistable_blocks_in_stmts(&mut tail, &mut reserved);
3483+
flatten_hoistable_blocks_in_nested_functions(&mut tail);
3484+
strip_runtime_call_type_args_in_stmts(&mut tail);
3485+
prune_unused_pure_var_decls(&mut tail);
3486+
prune_unused_function_like_decl_stmts(&mut tail);
3487+
3488+
let mut local_bindings = HashSet::new();
3489+
for stmt in &tail {
3490+
collect_stmt_bindings_including_nested_blocks(stmt, &mut local_bindings);
3491+
}
3492+
let non_optional_member_dep_keys =
3493+
collect_non_optional_member_dependency_keys_from_stmts(
3494+
&tail,
3495+
&known_bindings,
3496+
&local_bindings,
3497+
);
3498+
let mixed_optional_member_dep_keys = collect_mixed_member_dependency_keys_from_stmts(
3499+
&tail,
3500+
&known_bindings,
3501+
&local_bindings,
3502+
);
3503+
let conditional_only_non_optional_member_dep_keys =
3504+
collect_conditional_only_non_optional_member_dependency_keys_from_stmts(
3505+
&tail,
3506+
&known_bindings,
3507+
&local_bindings,
3508+
);
3509+
let mut deps = collect_dependencies_from_stmts(&tail, &known_bindings, &local_bindings);
3510+
let called_fn_deps =
3511+
collect_called_local_function_capture_dependencies(&tail, &known_bindings);
3512+
for dep in called_fn_deps {
3513+
if !deps.iter().any(|existing| existing.key == dep.key) {
3514+
deps.push(dep);
3515+
}
3516+
}
3517+
let inline_fn_capture_deps =
3518+
collect_stmt_function_capture_dependencies(&tail, &known_bindings);
3519+
for dep in inline_fn_capture_deps {
3520+
if !deps.iter().any(|existing| existing.key == dep.key) {
3521+
deps.push(dep);
3522+
}
3523+
}
3524+
deps = reduce_dependencies(deps);
3525+
deps = normalize_optional_member_dependencies(
3526+
deps,
3527+
&non_optional_member_dep_keys,
3528+
&mixed_optional_member_dep_keys,
3529+
&conditional_only_non_optional_member_dep_keys,
3530+
);
3531+
deps = reduce_nested_member_dependencies(deps);
3532+
3533+
let temp = fresh_temp_ident(&mut next_temp, &mut reserved);
3534+
let label = Ident::new_no_ctxt("bb0".into(), DUMMY_SP);
3535+
let (mut rewritten_stmts, has_early_return) =
3536+
rewrite_returns_for_labeled_block(tail, &label, &temp);
3537+
3538+
if has_early_return {
3539+
let nested_slot_start = next_slot + deps.len() as u32 + 1;
3540+
let (nested_slots, nested_blocks, nested_values) = if deps.is_empty() {
3541+
(0, 0, 0)
3542+
} else {
3543+
inject_nested_call_memoization_into_stmts(
3544+
&mut rewritten_stmts,
3545+
&known_bindings,
3546+
&cache_ident,
3547+
nested_slot_start,
3548+
&mut reserved,
3549+
&mut next_temp,
3550+
false,
3551+
)
3552+
};
3553+
3554+
let mut with_header = Vec::with_capacity(rewritten_stmts.len() + 2);
3555+
with_header.push(assign_stmt(
3556+
AssignTarget::from(temp.clone()),
3557+
early_return_sentinel_expr(),
3558+
));
3559+
with_header.push(Stmt::Labeled(LabeledStmt {
3560+
span: DUMMY_SP,
3561+
label,
3562+
body: Box::new(Stmt::Block(BlockStmt {
3563+
span: DUMMY_SP,
3564+
ctxt: Default::default(),
3565+
stmts: rewritten_stmts,
3566+
})),
3567+
}));
3568+
3569+
let value_slot = next_slot + deps.len() as u32;
3570+
transformed.extend(build_memoized_block(
3571+
&cache_ident,
3572+
next_slot,
3573+
&deps,
3574+
&temp,
3575+
with_header,
3576+
true,
3577+
));
3578+
transformed.push(Stmt::If(IfStmt {
3579+
span: DUMMY_SP,
3580+
test: Box::new(Expr::Bin(swc_ecma_ast::BinExpr {
3581+
span: DUMMY_SP,
3582+
op: op!("!=="),
3583+
left: Box::new(Expr::Ident(temp.clone())),
3584+
right: early_return_sentinel_expr(),
3585+
})),
3586+
cons: Box::new(Stmt::Block(BlockStmt {
3587+
span: DUMMY_SP,
3588+
ctxt: Default::default(),
3589+
stmts: vec![Stmt::Return(swc_ecma_ast::ReturnStmt {
3590+
span: DUMMY_SP,
3591+
arg: Some(Box::new(Expr::Ident(temp))),
3592+
})],
3593+
})),
3594+
alt: None,
3595+
}));
3596+
3597+
next_slot = value_slot + 1 + nested_slots;
3598+
memo_blocks += 1 + nested_blocks;
3599+
memo_values += 1 + nested_values;
3600+
} else {
3601+
transformed.extend(rewritten_stmts);
3602+
}
34603603
} else if let [Stmt::Try(try_stmt)] = tail.as_mut_slice() {
34613604
if try_stmt.finalizer.is_some() {
34623605
transformed.extend(tail);
@@ -3713,6 +3856,7 @@ fn memoize_reactive_function(reactive: &mut ReactiveFunction) -> (u32, u32, u32,
37133856
}
37143857

37153858
prune_unused_object_pattern_bindings_in_stmts(&mut transformed);
3859+
prune_empty_else_blocks_in_stmts(&mut transformed);
37163860
normalize_empty_jsx_elements_to_self_closing_in_stmts(&mut transformed);
37173861
reactive.body.stmts = transformed;
37183862

@@ -4856,6 +5000,7 @@ fn normalize_non_ident_params_without_memoization(reactive: &mut ReactiveFunctio
48565000
normalize_switch_case_blocks_in_stmts(&mut stmts);
48575001
prune_trivial_do_while_break_stmts(&mut stmts);
48585002
normalize_reactive_labels(&mut stmts);
5003+
prune_empty_else_blocks_in_stmts(&mut stmts);
48595004
if param_prologue.is_empty() {
48605005
reactive.body.stmts = stmts;
48615006
return;
@@ -4869,6 +5014,7 @@ fn normalize_non_ident_params_without_memoization(reactive: &mut ReactiveFunctio
48695014
transformed.extend(stmts.drain(..directive_end));
48705015
transformed.extend(param_prologue);
48715016
transformed.extend(stmts);
5017+
prune_empty_else_blocks_in_stmts(&mut transformed);
48725018
reactive.body.stmts = transformed;
48735019
}
48745020

@@ -10416,8 +10562,37 @@ fn inject_nested_call_memoization_into_stmts(
1041610562
&& !binding_mutated_via_member_assignment_after(remaining, binding.id.sym.as_ref())
1041710563
&& !binding_maybe_mutated_via_alias_after(remaining, binding.id.sym.as_ref())
1041810564
{
10419-
let nested_deps = collect_identifier_dependencies_for_nested_expr(init_expr);
10565+
let local_bindings = HashSet::new();
10566+
let nested_deps = collect_dependencies_from_expr(
10567+
init_expr,
10568+
&nested_known_bindings,
10569+
&local_bindings,
10570+
);
10571+
let has_local_member_dep = nested_deps.iter().any(|dep| {
10572+
let Some((base, _)) = dep.key.split_once('.') else {
10573+
return false;
10574+
};
10575+
binding_declared_in_stmts(&out, base)
10576+
});
10577+
let dep_bases_are_stable = nested_deps.iter().all(|dep| {
10578+
let base = dep
10579+
.key
10580+
.split_once('.')
10581+
.map(|(base, _)| base)
10582+
.unwrap_or(dep.key.as_str());
10583+
!binding_reassigned_after(remaining, base)
10584+
&& !binding_mutated_via_member_call_after(remaining, base)
10585+
&& !binding_mutated_via_member_assignment_after(remaining, base)
10586+
&& !binding_maybe_mutated_via_alias_after(remaining, base)
10587+
&& !binding_passed_to_potentially_mutating_call_after(remaining, base)
10588+
&& !binding_maybe_mutated_in_called_iife_after(remaining, base)
10589+
});
1042010590
if !nested_deps.is_empty() {
10591+
if has_local_member_dep || !dep_bases_are_stable {
10592+
out.push(stmt.clone());
10593+
mark_stmt_bindings_unstable(&stmt, &mut nested_known_bindings);
10594+
continue;
10595+
}
1042110596
let result_temp = fresh_temp_ident(next_temp, reserved);
1042210597
let mut nested_compute = vec![assign_stmt(
1042310598
AssignTarget::from(result_temp.clone()),
@@ -11060,16 +11235,22 @@ fn inject_nested_call_memoization_into_stmt_children(
1106011235
match stmt {
1106111236
Stmt::Block(block) => inject_stmt_list(&mut block.stmts),
1106211237
Stmt::If(if_stmt) => {
11238+
let branch_temp_start = *next_temp;
11239+
let branch_reserved_start = reserved.clone();
11240+
let mut cons_next_temp = branch_temp_start;
11241+
let mut cons_reserved = branch_reserved_start.clone();
1106311242
inject_nested_call_memoization_into_stmt_children(
1106411243
&mut if_stmt.cons,
1106511244
known_bindings,
1106611245
cache_ident,
1106711246
cursor,
1106811247
added_blocks,
1106911248
added_values,
11070-
reserved,
11071-
next_temp,
11249+
&mut cons_reserved,
11250+
&mut cons_next_temp,
1107211251
);
11252+
let mut alt_next_temp = branch_temp_start;
11253+
let mut alt_reserved = branch_reserved_start.clone();
1107311254
if let Some(alt) = &mut if_stmt.alt {
1107411255
inject_nested_call_memoization_into_stmt_children(
1107511256
alt,
@@ -11078,10 +11259,12 @@ fn inject_nested_call_memoization_into_stmt_children(
1107811259
cursor,
1107911260
added_blocks,
1108011261
added_values,
11081-
reserved,
11082-
next_temp,
11262+
&mut alt_reserved,
11263+
&mut alt_next_temp,
1108311264
);
1108411265
}
11266+
*next_temp = cons_next_temp.max(alt_next_temp);
11267+
*reserved = branch_reserved_start;
1108511268
}
1108611269
Stmt::Labeled(labeled) => inject_nested_call_memoization_into_stmt_children(
1108711270
&mut labeled.body,
@@ -11180,47 +11363,16 @@ fn mark_stmt_bindings_unstable(stmt: &Stmt, known_bindings: &mut HashMap<String,
1118011363
}
1118111364
}
1118211365

11183-
fn collect_identifier_dependencies_for_nested_expr(expr: &Expr) -> Vec<ReactiveDependency> {
11184-
struct Collector {
11185-
seen: HashSet<String>,
11186-
deps: Vec<ReactiveDependency>,
11187-
}
11188-
11189-
impl Visit for Collector {
11190-
fn visit_arrow_expr(&mut self, _: &ArrowExpr) {
11191-
// Skip nested functions.
11192-
}
11193-
11194-
fn visit_function(&mut self, _: &Function) {
11195-
// Skip nested functions.
11196-
}
11197-
11198-
fn visit_ident(&mut self, ident: &Ident) {
11199-
let key = ident.sym.to_string();
11200-
if self.seen.insert(key.clone()) {
11201-
self.deps.push(ReactiveDependency {
11202-
key,
11203-
expr: Box::new(Expr::Ident(ident.clone())),
11204-
});
11205-
}
11206-
}
11207-
}
11208-
11209-
let mut collector = Collector {
11210-
seen: HashSet::new(),
11211-
deps: Vec::new(),
11212-
};
11213-
expr.visit_with(&mut collector);
11214-
collector
11215-
.deps
11216-
.sort_by(|left, right| left.key.cmp(&right.key));
11217-
collector.deps
11218-
}
11219-
1122011366
fn is_simple_nested_array_initializer(expr: &Expr) -> bool {
1122111367
let Expr::Array(array) = expr else {
1122211368
return false;
1122311369
};
11370+
if array.elems.len() == 1 {
11371+
let Some(Some(element)) = array.elems.first() else {
11372+
return false;
11373+
};
11374+
return element.spread.is_none() && matches!(&*element.expr, Expr::Member(_));
11375+
}
1122411376
if array.elems.len() < 2 {
1122511377
return false;
1122611378
}
@@ -18234,6 +18386,26 @@ fn normalize_if_return_blocks(stmts: &mut [Stmt]) {
1823418386
}
1823518387
}
1823618388

18389+
fn prune_empty_else_blocks_in_stmts(stmts: &mut [Stmt]) {
18390+
struct EmptyElsePruner;
18391+
18392+
impl VisitMut for EmptyElsePruner {
18393+
fn visit_mut_if_stmt(&mut self, if_stmt: &mut IfStmt) {
18394+
if_stmt.visit_mut_children_with(self);
18395+
18396+
if matches!(if_stmt.alt.as_deref(), Some(Stmt::Block(block)) if block.stmts.is_empty())
18397+
{
18398+
if_stmt.alt = None;
18399+
}
18400+
}
18401+
}
18402+
18403+
let mut pruner = EmptyElsePruner;
18404+
for stmt in stmts {
18405+
stmt.visit_mut_with(&mut pruner);
18406+
}
18407+
}
18408+
1823718409
fn normalize_switch_case_blocks_in_stmts(stmts: &mut [Stmt]) {
1823818410
struct SwitchCaseNormalizer;
1823918411

0 commit comments

Comments
 (0)