dillon1000/react

Commit

[compiler] Count loop reassignments as the enclosing scope reassigning the variable (#36732)

A counter initialized before a memo scope and incremented inside it
(`a++` or `a = a + 1` in a `for` body) was emitted as a scope
*dependency*, compared at its pre-loop value (constant every render)
while the cache stored its post-loop value. The memo could never hit, so
the scope recomputed on every render.

Root cause: the phi-union rule in
`InferReactiveScopeVariables.findDisjointMutableValues` only unioned a
phi into the scope when the phi value was mutated after creation, which
range-extension only does for object mutation. Primitive reassignments
around a loop back-edge never extend ranges, so the counter's SSA
versions stayed outside the scope and downstream dependency propagation
classified the pre-loop read as a dep.

The fix unions a phi with its operands and declaration when any operand
is defined at or after the phi's block, i.e. the value is reassigned
around a loop back-edge. This matches the shape the compiler already
produced for non-primitive loop reassignment (`x = [...x, i]`).
Implemented identically in the TypeScript compiler and the Rust port.

Both `a++` and `a = a + 1` variants are pinned by fixtures; the first
commit documents the previously-wrong codegen, the second fixes it
(counter becomes a scope output, dep on `count` only).

Corpus delta beyond the new fixtures is 4 fixtures, all strict
improvements with byte-identical eval output: `for-in-statement-break`,
`for-in-statement-continue`, `for-in-statement-type-inference` (loops
previously re-ran every render, now memoized), and `sequence-expression`
(two memo blocks collapse into one).

Known limitation, unchanged from before: conditional reassignment in a
loop (`for (...) { if (c) a++; }`) routes through a join phi that this
rule cannot see; that shape behaves as it did before this change.

Verification: TS snap 1806/1806, Rust snap 1806/1806, cargo workspace
green, scoped TS-vs-Rust HIR parity harness green.

Closes #34971
Browse files
Changed paths10 files
First-parent comparison
M compiler/crates/react_compiler_inference/src/infer_reactive_scope_variables.rs ModifiedM compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts ModifiedM compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-break.expect.md ModifiedM compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-continue.expect.md ModifiedM compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-type-inference.expect.md ModifiedA compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.expect.md AddedA compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.js AddedA compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.expect.md AddedA compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.js AddedM compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequence-expression.expect.md Modified
Patch

Files changed

Rendering syntax-highlighted changes…