From cd2537c6a6804793c73a30975a495fe22b5fd793 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 7 May 2025 17:45:38 -0700 Subject: [PATCH] [compiler][wip] Infer alias effects for function expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a stab at addressing a pattern that @mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships. From an example @mofeiz added: ```js const x = {}; const y = {}; const f = () => { const a = [y]; const b = x; // this sets y.x = x a[0].x = b; } f(); mutate(y.x); // which means this mutates x! ``` Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too. The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to: * Return the alias sets from InferMutableRanges * Augment them with capturing of the form above, handling cases such as the `a[0].x = b` * For each alias group, record a CaptureEffect for any group that contains 2+ context operands * Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions. This isn't quite right yet, just sharing early hacking. [ghstack-poisoned] --- .../src/HIR/HIR.ts | 4 + .../src/HIR/PrintHIR.ts | 23 +++- .../src/Inference/AnalyseFunctions.ts | 70 +++++++++- .../src/Inference/InferAlias.ts | 10 ++ .../src/Inference/InferFunctionEffects.ts | 130 ++++++++++-------- .../src/Inference/InferMutableRanges.ts | 5 +- .../src/Inference/InferReferenceEffects.ts | 71 ++++------ ...ay-map-captures-receiver-noAlias.expect.md | 26 +--- ...ring-fun-alias-captured-mutate-2.expect.md | 34 ++--- ...-fun-alias-captured-mutate-arr-2.expect.md | 34 ++--- ...g-func-alias-captured-mutate-arr.expect.md | 22 ++- ...able-reassigned-reactive-capture.expect.md | 6 +- ...eturned-inner-fn-mutates-context.expect.md | 14 +- 13 files changed, 261 insertions(+), 188 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 99b8c189ee0fd..991ac9e72d16c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -300,6 +300,10 @@ export type FunctionEffect = places: ReadonlySet; effect: Effect; loc: SourceLocation; + } + | { + kind: 'CaptureEffect'; + places: ReadonlySet; }; /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index c8182c9e72a7c..93acb4c9447ff 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -546,12 +546,23 @@ export function printInstructionValue(instrValue: ReactiveValue): string { const effects = instrValue.loweredFunc.func.effects ?.map(effect => { - if (effect.kind === 'ContextMutation') { - return `ContextMutation places=[${[...effect.places] - .map(place => printPlace(place)) - .join(', ')}] effect=${effect.effect}`; - } else { - return `GlobalMutation`; + switch (effect.kind) { + case 'ContextMutation': { + return `ContextMutation places=[${[...effect.places] + .map(place => printPlace(place)) + .join(', ')}] effect=${effect.effect}`; + } + case 'GlobalMutation': { + return 'GlobalMutation'; + } + case 'ReactMutation': { + return 'ReactMutation'; + } + case 'CaptureEffect': { + return `CaptureEffect places=[${[...effect.places] + .map(place => printPlace(place)) + .join(', ')}]`; + } } }) .join(', ') ?? ''; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts index a439b4cd01232..e2687ecc6560f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -11,6 +11,7 @@ import { HIRFunction, Identifier, LoweredFunction, + Place, isRefOrRefValue, makeInstructionId, } from '../HIR'; @@ -19,6 +20,8 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes'; import {rewriteInstructionKindsBasedOnReassignment} from '../SSA'; import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; +import DisjointSet from '../Utils/DisjointSet'; +import {eachInstructionValueOperand} from '../HIR/visitors'; export default function analyseFunctions(func: HIRFunction): void { for (const [_, block] of func.body.blocks) { @@ -26,8 +29,8 @@ export default function analyseFunctions(func: HIRFunction): void { switch (instr.value.kind) { case 'ObjectMethod': case 'FunctionExpression': { - lower(instr.value.loweredFunc.func); - infer(instr.value.loweredFunc); + const aliases = lower(instr.value.loweredFunc.func); + infer(instr.value.loweredFunc, aliases); /** * Reset mutable range for outer inferReferenceEffects @@ -44,11 +47,11 @@ export default function analyseFunctions(func: HIRFunction): void { } } -function lower(func: HIRFunction): void { +function lower(func: HIRFunction): DisjointSet { analyseFunctions(func); inferReferenceEffects(func, {isFunctionExpression: true}); deadCodeElimination(func); - inferMutableRanges(func); + const aliases = inferMutableRanges(func); rewriteInstructionKindsBasedOnReassignment(func); inferReactiveScopeVariables(func); func.env.logger?.debugLogIRs?.({ @@ -56,9 +59,49 @@ function lower(func: HIRFunction): void { name: 'AnalyseFunction (inner)', value: func, }); + inferAliasesForCapturing(func, aliases); + return aliases; } -function infer(loweredFunc: LoweredFunction): void { +/** + * The alias sets returned by InferMutableRanges() accounts only for aliases that + * are known to mutate together. Notably this skips cases where a value is captured + * into some other value, but neither is subsequently mutated. An example is pushing + * a mutable value onto an array, where neither the array or value are subsequently + * mutated. + * + * This function extends the aliases sets to account for such capturing, so that we + * can detect cases where one of the values in a set is mutated later (in an outer function) + * we can correctly infer them as mutating together. + */ +function inferAliasesForCapturing( + fn: HIRFunction, + aliases: DisjointSet, +): void { + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + let operands: Array | null = null; + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + operand.effect === Effect.Mutate || + operand.effect === Effect.Store || + operand.effect === Effect.Capture + ) { + operands ??= []; + operands.push(operand.identifier); + } + } + if (operands != null && operands.length > 1) { + aliases.union(operands); + } + } + } +} + +function infer( + loweredFunc: LoweredFunction, + aliases: DisjointSet, +): void { for (const operand of loweredFunc.func.context) { const identifier = operand.identifier; CompilerError.invariant(operand.effect === Effect.Unknown, { @@ -85,6 +128,23 @@ function infer(loweredFunc: LoweredFunction): void { operand.effect = Effect.Read; } } + const contextIdentifiers = new Map( + loweredFunc.func.context.map(place => [place.identifier, place]), + ); + for (const set of aliases.buildSets()) { + const contextOperands: Set = new Set( + [...set] + .map(identifier => contextIdentifiers.get(identifier)) + .filter(place => place != null) as Array, + ); + if (contextOperands.size > 1) { + loweredFunc.func.effects ??= []; + loweredFunc.func.effects?.push({ + kind: 'CaptureEffect', + places: contextOperands, + }); + } + } } function isMutatedOrReassigned(id: Identifier): boolean { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts index 80422c8391f46..153af0b31ce9d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts @@ -60,6 +60,16 @@ function inferInstr( alias = instrValue.value; break; } + case 'ObjectMethod': + case 'FunctionExpression': { + for (const effect of instrValue.loweredFunc.func.effects ?? []) { + if (effect.kind !== 'CaptureEffect') { + continue; + } + aliases.union([...effect.places].map(place => place.identifier)); + } + return; + } default: return; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts index a58ae440219b9..ba09bcba1e9a4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts @@ -95,45 +95,58 @@ function inheritFunctionEffects( return effects .flatMap(effect => { - if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') { - return [effect]; - } else { - const effects: Array = []; - CompilerError.invariant(effect.kind === 'ContextMutation', { - reason: 'Expected ContextMutation', - loc: null, - }); - /** - * Contextual effects need to be replayed against the current inference - * state, which may know more about the value to which the effect applied. - * The main cases are: - * 1. The mutated context value is _still_ a context value in the current scope, - * so we have to continue propagating the original context mutation. - * 2. The mutated context value is a mutable value in the current scope, - * so the context mutation was fine and we can skip propagating the effect. - * 3. The mutated context value is an immutable value in the current scope, - * resulting in a non-ContextMutation FunctionEffect. We propagate that new, - * more detailed effect to the current function context. - */ - for (const place of effect.places) { - if (state.isDefined(place)) { - const replayedEffect = inferOperandEffect(state, { - ...place, - loc: effect.loc, - effect: effect.effect, - }); - if (replayedEffect != null) { - if (replayedEffect.kind === 'ContextMutation') { - // Case 1, still a context value so propagate the original effect - effects.push(effect); - } else { - // Case 3, immutable value so propagate the more precise effect - effects.push(replayedEffect); - } - } // else case 2, local mutable value so this effect was fine + switch (effect.kind) { + case 'GlobalMutation': + case 'ReactMutation': { + return [effect]; + } + case 'ContextMutation': { + const effects: Array = []; + CompilerError.invariant(effect.kind === 'ContextMutation', { + reason: 'Expected ContextMutation', + loc: null, + }); + /** + * Contextual effects need to be replayed against the current inference + * state, which may know more about the value to which the effect applied. + * The main cases are: + * 1. The mutated context value is _still_ a context value in the current scope, + * so we have to continue propagating the original context mutation. + * 2. The mutated context value is a mutable value in the current scope, + * so the context mutation was fine and we can skip propagating the effect. + * 3. The mutated context value is an immutable value in the current scope, + * resulting in a non-ContextMutation FunctionEffect. We propagate that new, + * more detailed effect to the current function context. + */ + for (const place of effect.places) { + if (state.isDefined(place)) { + const replayedEffect = inferOperandEffect(state, { + ...place, + loc: effect.loc, + effect: effect.effect, + }); + if (replayedEffect != null) { + if (replayedEffect.kind === 'ContextMutation') { + // Case 1, still a context value so propagate the original effect + effects.push(effect); + } else { + // Case 3, immutable value so propagate the more precise effect + effects.push(replayedEffect); + } + } // else case 2, local mutable value so this effect was fine + } } + return effects; + } + case 'CaptureEffect': { + return []; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); } - return effects; } }) .filter((effect): effect is FunctionEffect => effect != null); @@ -298,26 +311,31 @@ export function inferTerminalFunctionEffects( export function transformFunctionEffectErrors( functionEffects: Array, ): Array { - return functionEffects.map(eff => { - switch (eff.kind) { - case 'ReactMutation': - case 'GlobalMutation': { - return eff.error; - } - case 'ContextMutation': { - return { - severity: ErrorSeverity.Invariant, - reason: `Unexpected ContextMutation in top-level function effects`, - loc: eff.loc, - }; + return functionEffects + .map(eff => { + switch (eff.kind) { + case 'ReactMutation': + case 'GlobalMutation': { + return eff.error; + } + case 'ContextMutation': { + return { + severity: ErrorSeverity.Invariant, + reason: `Unexpected ContextMutation in top-level function effects`, + loc: eff.loc, + }; + } + case 'CaptureEffect': { + return null; + } + default: + assertExhaustive( + eff, + `Unexpected function effect kind \`${(eff as any).kind}\``, + ); } - default: - assertExhaustive( - eff, - `Unexpected function effect kind \`${(eff as any).kind}\``, - ); - } - }); + }) + .filter(eff => eff != null) as Array; } function isEffectSafeOutsideRender(effect: FunctionEffect): boolean { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts index 624c302fbf7ee..df97d08da8ef9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts @@ -6,6 +6,7 @@ */ import {HIRFunction, Identifier} from '../HIR/HIR'; +import DisjointSet from '../Utils/DisjointSet'; import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions'; import {inferAliases} from './InferAlias'; import {inferAliasForPhis} from './InferAliasForPhis'; @@ -14,7 +15,7 @@ import {inferMutableLifetimes} from './InferMutableLifetimes'; import {inferMutableRangesForAlias} from './InferMutableRangesForAlias'; import {inferTryCatchAliases} from './InferTryCatchAliases'; -export function inferMutableRanges(ir: HIRFunction): void { +export function inferMutableRanges(ir: HIRFunction): DisjointSet { // Infer mutable ranges for non fields inferMutableLifetimes(ir, false); @@ -84,6 +85,8 @@ export function inferMutableRanges(ir: HIRFunction): void { } prevAliases = nextAliases; } + + return aliases; } function areEqualMaps(a: Map, b: Map): boolean { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts index 3dc532c87d6ea..2e6108c9b7ba5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -670,11 +670,7 @@ class InferenceState { for (const [value, kind] of this.#values) { const id = identify(value); result.values[id] = { - abstract: { - kind: kind.kind, - context: [...kind.context].map(printPlace), - reason: [...kind.reason], - }, + abstract: this.debugAbstractValue(kind), value: printInstructionValue(value), }; } @@ -684,6 +680,14 @@ class InferenceState { return result; } + debugAbstractValue(value: AbstractValue): any { + return { + kind: value.kind, + context: [...value.context].map(printPlace), + reason: [...value.reason], + }; + } + inferPhi(phi: Phi): void { const values: Set = new Set(); for (const [_, operand] of phi.operands) { @@ -909,19 +913,11 @@ function inferBlock( break; } case 'ArrayExpression': { - const contextRefOperands = getContextRefOperand(state, instrValue); - const valueKind: AbstractValue = - contextRefOperands.length > 0 - ? { - kind: ValueKind.Context, - reason: new Set([ValueReason.Other]), - context: new Set(contextRefOperands), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; + const valueKind: AbstractValue = { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + context: new Set(), + }; for (const element of instrValue.elements) { if (element.kind === 'Spread') { @@ -942,6 +938,7 @@ function inferBlock( let _: 'Hole' = element.kind; } } + state.initialize(instrValue, valueKind); state.define(instr.lvalue, instrValue); instr.lvalue.effect = Effect.Store; @@ -961,19 +958,11 @@ function inferBlock( break; } case 'ObjectExpression': { - const contextRefOperands = getContextRefOperand(state, instrValue); - const valueKind: AbstractValue = - contextRefOperands.length > 0 - ? { - kind: ValueKind.Context, - reason: new Set([ValueReason.Other]), - context: new Set(contextRefOperands), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; + const valueKind: AbstractValue = { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + context: new Set(), + }; for (const property of instrValue.properties) { switch (property.kind) { @@ -1818,7 +1807,9 @@ function inferBlock( state.isDefined(operand) && ((operand.identifier.type.kind === 'Function' && state.isFunctionExpression) || - state.kind(operand).kind === ValueKind.Context) + state.kind(operand).kind === ValueKind.Context || + (state.kind(operand).kind === ValueKind.Mutable && + state.isFunctionExpression)) ) { /** * Returned values should only be typed as 'frozen' if they are both (1) @@ -1845,22 +1836,6 @@ function inferBlock( ); } -function getContextRefOperand( - state: InferenceState, - instrValue: InstructionValue, -): Array { - const result = []; - for (const place of eachInstructionValueOperand(instrValue)) { - if (state.isDefined(place)) { - const kind = state.kind(place); - if (kind.kind === ValueKind.Context) { - result.push(...kind.context); - } - } - } - return result; -} - export function getFunctionCallSignature( env: Environment, type: Type, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md index 1680386c741a3..efd094c1a5360 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md @@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(6); + const $ = _c(2); let t0; if ($[0] !== props.a) { - t0 = { a: props.a }; + const item = { a: props.a }; + const items = [item]; + t0 = items.map(_temp); $[0] = props.a; $[1] = t0; } else { t0 = $[1]; } - const item = t0; - let t1; - if ($[2] !== item) { - t1 = [item]; - $[2] = item; - $[3] = t1; - } else { - t1 = $[3]; - } - const items = t1; - let t2; - if ($[4] !== items) { - t2 = items.map(_temp); - $[4] = items; - $[5] = t2; - } else { - t2 = $[5]; - } - const mapped = t2; + const mapped = t0; return mapped; } function _temp(item_0) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md index 061e64222eef0..c56f00e5c0cd7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md @@ -37,26 +37,26 @@ import { c as _c } from "react/compiler-runtime"; import { mutate } from "shared-runtime"; function Component(t0) { - const $ = _c(3); + const $ = _c(2); const { foo, bar } = t0; - let x; - if ($[0] !== bar || $[1] !== foo) { - x = { foo }; - const y = { bar }; - const f0 = function () { - const a = { y }; - const b = x; - a.x = b; - }; - - f0(); - mutate(y); - $[0] = bar; - $[1] = foo; - $[2] = x; + let t1; + if ($[0] !== foo) { + t1 = { foo }; + $[0] = foo; + $[1] = t1; } else { - x = $[2]; + t1 = $[1]; } + const x = t1; + const y = { bar }; + const f0 = function () { + const a = { y }; + const b = x; + a.x = b; + }; + + f0(); + mutate(y); return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md index fb44f2c7b8f24..25dead462d916 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md @@ -37,26 +37,26 @@ import { c as _c } from "react/compiler-runtime"; import { mutate } from "shared-runtime"; function Component(t0) { - const $ = _c(3); + const $ = _c(2); const { foo, bar } = t0; - let x; - if ($[0] !== bar || $[1] !== foo) { - x = { foo }; - const y = { bar }; - const f0 = function () { - const a = [y]; - const b = x; - a.x = b; - }; - - f0(); - mutate(y); - $[0] = bar; - $[1] = foo; - $[2] = x; + let t1; + if ($[0] !== foo) { + t1 = { foo }; + $[0] = foo; + $[1] = t1; } else { - x = $[2]; + t1 = $[1]; } + const x = t1; + const y = { bar }; + const f0 = function () { + const a = [y]; + const b = x; + a.x = b; + }; + + f0(); + mutate(y); return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md index e9cb3c4fd145b..7985f3b810763 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md @@ -35,11 +35,19 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; import { mutate } from "shared-runtime"; function Component(t0) { - const $ = _c(3); + const $ = _c(5); const { foo, bar } = t0; + let t1; + if ($[0] !== foo) { + t1 = { foo }; + $[0] = foo; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; let y; - if ($[0] !== bar || $[1] !== foo) { - const x = { foo }; + if ($[2] !== bar || $[3] !== x) { y = { bar }; const f0 = function () { const a = [y]; @@ -49,11 +57,11 @@ function Component(t0) { f0(); mutate(y); - $[0] = bar; - $[1] = foo; - $[2] = y; + $[2] = bar; + $[3] = x; + $[4] = y; } else { - y = $[2]; + y = $[4]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-reactive-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-reactive-capture.expect.md index 8578d269cbe2c..fed5ae4d097eb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-reactive-capture.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-reactive-capture.expect.md @@ -29,16 +29,16 @@ import { invoke } from "shared-runtime"; function Component(t0) { const $ = _c(2); - const { value } = t0; let x; - if ($[0] !== value) { + if ($[0] !== t0) { + const { value } = t0; x = null; const reassign = () => { x = value; }; invoke(reassign); - $[0] = value; + $[0] = t0; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-mutates-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-mutates-context.expect.md index 0da18e5f65669..7187b936b91b1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-mutates-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-mutates-context.expect.md @@ -54,10 +54,11 @@ import { Stringify } from "shared-runtime"; */ function Foo(t0) { "use memo"; - const $ = _c(3); - const { a, b } = t0; + const $ = _c(2); let t1; - if ($[0] !== a || $[1] !== b) { + if ($[0] !== t0) { + const { a, b } = t0; + const obj = {}; const updaterFactory = () => (newValue) => { obj.value = newValue; @@ -67,11 +68,10 @@ function Foo(t0) { const updater = updaterFactory(); updater(b); t1 = ; - $[0] = a; - $[1] = b; - $[2] = t1; + $[0] = t0; + $[1] = t1; } else { - t1 = $[2]; + t1 = $[1]; } return t1; }