Skip to content

feat(react-compiler): continue swc parity for dependency handling#11747

Merged
kdy1 merged 7 commits intomainfrom
kdy1/react-compiler-swc-port-parity-20260329
Mar 29, 2026
Merged

feat(react-compiler): continue swc parity for dependency handling#11747
kdy1 merged 7 commits intomainfrom
kdy1/react-compiler-swc-port-parity-20260329

Conversation

@kdy1
Copy link
Copy Markdown
Member

@kdy1 kdy1 commented Mar 29, 2026

Summary

  • Continue SWC React Compiler port work in crates/swc_ecma_react_compiler.
  • Refine reactive dependency collection/normalization around conditional and optional-call paths.
  • Improve optional call callee handling for both Expr::Member and Expr::OptChain(Member) in dependency collectors.
  • Extend nested memoization splitting for push(...) arguments when the argument is a simple non-hook call with concrete dependencies.

Key Changes

  • crates/swc_ecma_react_compiler/src/options.rs
    • Keep validate_no_void_use_memo default aligned with current fixture behavior (false).
  • crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rs
    • Better conditional dependency normalization for member paths.
    • Add dedup for dependency reductions to avoid duplicate keys.
    • Add switch-branch dependency handling parity improvements.
    • Improve optional-call dependency collapsing to object-level dependencies where needed.
    • Add targeted nested call memoization for push(...) call arguments.

Fixture Progress (Upstream)

  • Previous measured baseline in this thread: 832 failed
  • Current measured after this branch: 822 failed
  • reduce-reactive-deps: 28 -> 21
  • fixture_cases_upstream_phase1: still passing

Verification

  • cargo fmt --all
  • 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

@kdy1 kdy1 requested a review from a team as a code owner March 29, 2026 09:02
Copilot AI review requested due to automatic review settings March 29, 2026 09:02
kodiakhq[bot]
kodiakhq Bot previously approved these changes Mar 29, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 29, 2026

⚠️ No Changeset found

Latest commit: 5682468

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@claude

This comment has been minimized.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6d52c01c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rs Outdated
Comment thread crates/swc_ecma_react_compiler/src/validation/validate_no_eval_unsupported.rs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Continues the SWC React Compiler parity effort by refining reactive dependency collection/normalization (especially around optional/conditional paths), expanding memoization/hoisting behavior, and adding additional validation hooks/diagnostics to better match upstream behavior.

Changes:

  • Refines reactive dependency collection/normalization and memoization injection logic (optional-call handling, conditional branch key collection, deduping, JSX hoisting tweaks).
  • Adds new validation passes (BuildHIR TODO diagnostics, eval unsupported, reorderable default params) and wires them into the compilation pipeline.
  • Adjusts compilation option handling (effective opts derivation; auto-enable reanimated typing support when reanimated is imported).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/swc_ecma_react_compiler/src/validation/validate_source_locations.rs Replaces stub with TODO-style diagnostics for source-location validation.
crates/swc_ecma_react_compiler/src/validation/validate_reorderable_default_params.rs New validator for default-param reorder safety around function/arrow defaults.
crates/swc_ecma_react_compiler/src/validation/validate_no_eval_unsupported.rs New validator to flag unsupported eval() usage.
crates/swc_ecma_react_compiler/src/validation/validate_locals_not_reassigned_after_render.rs Adds additional detection for reassignments via function initializers/closures.
crates/swc_ecma_react_compiler/src/validation/validate_build_hir_todos.rs Introduces a large “upstream parity” validator emitting TODO/invariant-style diagnostics.
crates/swc_ecma_react_compiler/src/validation/mod.rs Registers and exports new validation modules.
crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rs Updates dependency collection logic, normalization, memoization injection, JSX hoisting, and switch normalization.
crates/swc_ecma_react_compiler/src/options.rs Updates comment on validate_no_void_use_memo default behavior.
crates/swc_ecma_react_compiler/src/entrypoint/program.rs Uses derived effective options, auto-enables reanimated typing support, and adds program-level BuildHIR TODO validation/diagnostics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +12 to +30
let mut details = Vec::new();

// Upstream validates many source location invariants and may emit repeated
// TODO diagnostics for a single function. We preserve that behavior shape by
// emitting multiple diagnostics when source-location validation is enabled.
for _ in 0..20 {
details.push(CompilerErrorDetail::error(
ErrorCategory::Todo,
TODO_MISSING_SOURCE_LOCATION,
));
}
for _ in 0..2 {
details.push(CompilerErrorDetail::error(
ErrorCategory::Todo,
TODO_WRONG_SOURCE_NODE_TYPE,
));
}

Err(CompilerError { details })
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_source_locations currently returns Err with a fixed set of TODO diagnostics regardless of the input HIR. If this flag is ever enabled, compilation will always fail even when source locations are valid, which makes the option effectively unusable. Consider either (a) implementing a minimal real validation check and only emitting diagnostics when an invariant is violated, or (b) changing this stub to return Ok(()) until the validator can be implemented, while logging a single TODO diagnostic behind a separate debug/fixture-only gate.

Suggested change
let mut details = Vec::new();
// Upstream validates many source location invariants and may emit repeated
// TODO diagnostics for a single function. We preserve that behavior shape by
// emitting multiple diagnostics when source-location validation is enabled.
for _ in 0..20 {
details.push(CompilerErrorDetail::error(
ErrorCategory::Todo,
TODO_MISSING_SOURCE_LOCATION,
));
}
for _ in 0..2 {
details.push(CompilerErrorDetail::error(
ErrorCategory::Todo,
TODO_WRONG_SOURCE_NODE_TYPE,
));
}
Err(CompilerError { details })
// TODO: Implement real source-location validation. For now, this is a no-op
// to avoid failing compilation unconditionally when the validator is enabled.
Ok(())

Copilot uses AI. Check for mistakes.
Comment on lines +4692 to +4696
match &*arrow.body {
swc_ecma_ast::BlockStmtOrExpr::BlockStmt(block) => Some(block.stmts.clone()),
swc_ecma_ast::BlockStmtOrExpr::Expr(expr) => Some(vec![Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: expr.clone(),
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline_use_memo_effect_stmt replaces an expression-statement useMemo(() => { ... }, deps) with the factory’s body statements (block.stmts.clone()). This changes JavaScript semantics: return inside the factory would become a return from the outer function, and any let/const declarations or temporaries inside the factory would leak into the outer scope (shadowing/TDZ differences). Consider preserving scoping/control flow by rewriting to an explicit factory invocation (e.g. call the factory/IIFE) or only performing this transform when the factory body is guaranteed to be side-effect-only and contains no control-flow statements/declarations that would escape its scope.

Copilot uses AI. Check for mistakes.
Comment on lines +550 to +560
fn visit_call_expr(&mut self, call: &CallExpr) {
let callee_name = self.callee_name(&call.callee);
if self.conditional_depth > 0 || self.is_hook_callee(&call.callee) {
let mut detail = CompilerErrorDetail::error(
ErrorCategory::Hooks,
ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER,
);
detail.loc = Some(call.span);
self.errors.push(detail);
self.emitted_conditional_hook = true;
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_build_hir_todos currently emits ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER when conditional_depth > 0 || is_hook_callee(...). This flags (1) any hook call even when not conditional, and (2) any call expression inside a conditional even when it’s not a hook call. That will produce many false-positive diagnostics when validate_build_hir_todos_program is run. This should likely require both conditional_depth > 0 and is_hook_callee(...), and/or honor emitted_conditional_hook to avoid repeated emissions.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +111
if let Err(err) = validation::validate_build_hir_todos_program(program) {
report.diagnostics.extend(err.details);
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_build_hir_todos_program is invoked unconditionally during compile_program, and any emitted diagnostics are appended to report.diagnostics even if the user hasn’t enabled a corresponding validation option. If this validator is intended only for fixture parity / debug builds, consider gating it behind an explicit EnvironmentConfig flag (similar to other validations) to avoid surprising new diagnostics in normal compilation runs.

Copilot uses AI. Check for mistakes.
enable_treat_ref_like_identifiers_as_refs: true,
enable_treat_set_identifiers_as_state_setters: false,
// Upstream defaults this check to off unless explicitly enabled.
// Upstream defaults this check to on.
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says “Upstream defaults this check to on.” but the actual default remains validate_no_void_use_memo: false. If the repository intentionally diverges from upstream for fixture parity, the comment should reflect that (e.g., upstream default vs SWC default) to avoid misleading future changes; otherwise, adjust the default to match the stated upstream behavior.

Suggested change
// Upstream defaults this check to on.
// Upstream defaults this check to on, but SWC keeps it off by default
// (for fixture parity / backwards compatibility).

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +121
Expr::Fn(fn_expr) => {
let mut shadowed = HashSet::new();
for param in &fn_expr.function.params {
collect_pat_bindings(&param.pat, &mut shadowed);
}
if let Some(body) = &fn_expr.function.body {
collect_block_bindings(body, &mut shadowed);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

captures_outer_names treats any Ident matching an outer param name as a capture unless it appears in shadowed, but for Expr::Fn it does not add the function expression’s own name (fn_expr.ident) to shadowed. Named function expressions can legally shadow an outer parameter of the same name, so this can produce false positives. Consider inserting fn_expr.ident (when present) into the shadowed set before visiting the body.

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +239
if let swc_ecma_ast::BlockStmtOrExpr::BlockStmt(body) = &*arrow.body {
collect_block_bindings(body, &mut shadowed);
let mut finder = Finder {
target,
shadowed,
found: None,
};
body.visit_with(&mut finder);
finder.found
} else {
None
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function_initializer_self_reassign_span skips arrow functions with expression bodies (BlockStmtOrExpr::Expr) by returning None. That means patterns like const x = () => x++ or const x = () => (x = 1) won’t be reported even though they are self-reassigning initializers. Consider visiting the expression body with the same Finder logic (similar to the Expr::Fn branch) so both block and expression-bodied arrows are handled consistently.

Suggested change
if let swc_ecma_ast::BlockStmtOrExpr::BlockStmt(body) = &*arrow.body {
collect_block_bindings(body, &mut shadowed);
let mut finder = Finder {
target,
shadowed,
found: None,
};
body.visit_with(&mut finder);
finder.found
} else {
None
match &*arrow.body {
swc_ecma_ast::BlockStmtOrExpr::BlockStmt(body) => {
collect_block_bindings(body, &mut shadowed);
let mut finder = Finder {
target,
shadowed,
found: None,
};
body.visit_with(&mut finder);
finder.found
}
swc_ecma_ast::BlockStmtOrExpr::Expr(expr) => {
let mut finder = Finder {
target,
shadowed,
found: None,
};
expr.visit_with(&mut finder);
finder.found
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 29, 2026

Binary Sizes

File Size
swc.linux-x64-gnu.node 28M (29077448 bytes)

Commit: e13ad30

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 29, 2026

Merging this PR will not alter performance

✅ 219 untouched benchmarks


Comparing kdy1/react-compiler-swc-port-parity-20260329 (5682468) with main (0e2075e)

Open in CodSpeed

@kdy1
Copy link
Copy Markdown
Member Author

kdy1 commented Mar 29, 2026

Addressed review feedback from #pullrequestreview-4026543374 in 5682468d78.

  • Prevented unsafe useMemo callback inlining from hoisting callback control flow into component scope.
  • Updated validate_no_eval_unsupported to respect lexical shadowing so only unshadowed/global eval(...) is rejected.

Resolved both review threads.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 29, 2026

PR Review: feat(react-compiler): continue swc parity for dependency handling

Overall this is solid incremental progress on the React Compiler port (832 → 822 failures). Here's detailed feedback:


Code Duplication

1. BranchCollector is duplicated almost identically between collect_branch_member_keys_from_stmt and collect_branch_member_keys_from_expr.

The two BranchCollector structs (starting around the collect_conditional_only_non_optional_member_dependency_keys_from_stmts changes) share ~95% of their implementation — same fields, same Visit trait impls for visit_member_expr, visit_opt_chain_expr, visit_if_stmt, visit_cond_expr, visit_switch_stmt, visit_bin_expr. Consider extracting a single generic BranchCollector and having both entry points construct and call into it.

2. collect_pat_bindings is duplicated across 4 files:

  • validate_build_hir_todos.rs
  • validate_no_eval_unsupported.rs
  • validate_reorderable_default_params.rs
  • validate_locals_not_reassigned_after_render.rs

Same for collect_block_bindings. These are identical utility functions that should be extracted into a shared module (e.g., validation/utils.rs or a crate-level utils module).

3. visit_opt_call logic is duplicated between collect_dependencies_from_expr and collect_dependencies_from_stmts visitors.

The new optional-call handling (both the Expr::Member and Expr::OptChain(Member) branches) is copy-pasted between the two dependency collection visitors. If the logic diverges in the future this will be a maintenance burden.


Potential Bugs / Correctness

4. validate_source_locations now always returns Err with 22 hardcoded dummy diagnostics:

for _ in 0..20 {
    details.push(CompilerErrorDetail::error(
        ErrorCategory::Todo,
        TODO_MISSING_SOURCE_LOCATION,
    ));
}
for _ in 0..2 {
    details.push(CompilerErrorDetail::error(
        ErrorCategory::Todo,
        TODO_WRONG_SOURCE_NODE_TYPE,
    ));
}
Err(CompilerError { details })

This unconditionally fails for every function. Is this intentional for fixture-matching purposes? If so, a comment explaining this would help. The previous implementation returned Ok(()).

5. Comment/value mismatch in options.rs:

// Upstream defaults this check to on.
validate_no_void_use_memo: false,

The comment says "on" but the value is false. If upstream defaults it to on, should this be true?

6. visit_call_expr in BuildHirTodoFinder has an overly broad conditional hook check:

if self.conditional_depth > 0 || self.is_hook_callee(&call.callee) {
    // emits ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER
}

This fires for any function call when conditional_depth > 0, not just hook calls. A regular console.log() inside an if block would trigger "Hooks must always be called in a consistent order." Is this intentional for upstream parity, or should it be self.conditional_depth > 0 && self.is_hook_callee(&call.callee)?

7. Intentional double-emission of errors needs documentation:

Several places intentionally emit the same error twice:

  • visit_pat: TODO_LOCAL_FBT_VARIABLE emitted twice
  • visit_param: TODO_DESTRUCTURING_CONTEXT_VARIABLES emitted twice
  • visit_member_expr: ERROR_CANNOT_ACCESS_REFS_DURING_RENDER emitted twice
  • visit_call_expr: SKIP_PRESERVE_MEMOIZATION emitted twice for useMemo/useCallback

If these are for upstream output parity, a brief comment like // Upstream emits this diagnostic twice would help future readers understand this isn't accidental.


Naming

8. should_hoist_single_dependency_array_expr no longer checks for a single-element array:

The function was changed from single-element to multi-element array validation, but the name still says "single_dependency". Consider renaming to should_hoist_dependency_array_expr to match the new behavior.


Performance

9. BranchCollector allocations in deeply nested conditionals:

Each branch of every if/cond/switch creates a new BranchCollector with fresh HashSet allocations. In deeply nested conditional code this could become expensive. Not a blocker but worth noting — if this shows up in profiles, the collectors could be pooled or reused.

10. The dedup_by after sort_by in reduce_dependencies / reduce_nested_member_dependencies is correctdedup_by removes consecutive duplicates, and the sort ensures duplicates are adjacent. Good fix.


Style (minor)

11. validate_build_hir_todos.rs at 1413 lines is quite large for a single validation file, especially compared to the other validation modules. The file handles dozens of different error categories. Consider whether some of the logic (e.g., find_use_before_declaration, has_top_level_reassignment, has_nested_function_reference and their supporting types) could be split into sub-modules as they grow.


Summary

The core dependency handling improvements (conditional/optional-call normalization, dedup, switch-branch parity) look correct and well-motivated. The main concerns are:

  1. Significant code duplication across validation files and dependency collectors
  2. The validate_source_locations always-fail change needs clarification
  3. The comment/value mismatch in options.rs
  4. The broad conditional hook check in BuildHirTodoFinder

Nice progress on the fixture pass rate!

@kdy1 kdy1 enabled auto-merge (squash) March 29, 2026 09:27
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5682468d78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4700 to +4704
if !use_memo_callback_block_is_safe_to_inline(block) {
return None;
}
Some(block.stmts.clone())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve callback scope when inlining unused useMemo

Inlining useMemo callback blocks by returning block.stmts.clone() causes those statements to be spliced into the parent render body, which removes the callback’s function scope. That changes semantics for local declarations inside the callback (let/const/var), because bindings that were previously private to the callback can now collide with or leak into render scope. For example, useMemo(() => { const tmp = 1; ... }, deps) should keep tmp scoped to the callback, but this rewrite makes it a top-level render binding.

Useful? React with 👍 / 👎.

Comment on lines +552 to +555
if self.conditional_depth > 0 || self.is_hook_callee(&call.callee) {
let mut detail = CompilerErrorDetail::error(
ErrorCategory::Hooks,
ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate hook-order diagnostic on conditional context

This check reports the Rules-of-Hooks ordering error whenever the callee is a hook, even if the hook call is top-level and valid, because the condition is conditional_depth > 0 || is_hook_callee(...). Since validate_build_hir_todos_program is invoked for every program, ordinary components that correctly call hooks will still accumulate false-positive hook-order diagnostics in report.diagnostics.

Useful? React with 👍 / 👎.

@kdy1 kdy1 merged commit 83688c8 into main Mar 29, 2026
49 checks passed
@kdy1 kdy1 deleted the kdy1/react-compiler-swc-port-parity-20260329 branch March 29, 2026 09:49
@github-actions github-actions Bot modified the milestones: Planned, 1.15.24 Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants