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 15ee2f33a18af..2869ab94d996d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1770,6 +1770,10 @@ export function isUseStateType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState'; } +export function isJsxType(type: Type): boolean { + return type.kind === 'Object' && type.shapeId === 'BuiltInJsx'; +} + export function isRefOrRefValue(id: Identifier): boolean { return isUseRefType(id) || isRefValueType(id); } 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 eed8946c81496..7052cb2dd8a52 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -20,11 +20,9 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes'; import {rewriteInstructionKindsBasedOnReassignment} from '../SSA'; import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; -import {assertExhaustive, retainWhere} from '../Utils/utils'; +import {assertExhaustive} from '../Utils/utils'; import {inferMutationAliasingEffects} from './InferMutationAliasingEffects'; -import {inferFunctionExpressionAliasingEffectsSignature} from './InferFunctionExpressionAliasingEffectsSignature'; import {inferMutationAliasingRanges} from './InferMutationAliasingRanges'; -import {hashEffect} from './AliasingEffects'; export default function analyseFunctions(func: HIRFunction): void { for (const [_, block] of func.body.blocks) { @@ -69,30 +67,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void { analyseFunctions(fn); inferMutationAliasingEffects(fn, {isFunctionExpression: true}); deadCodeElimination(fn); - inferMutationAliasingRanges(fn, {isFunctionExpression: true}); + const functionEffects = inferMutationAliasingRanges(fn, { + isFunctionExpression: true, + }).unwrap(); rewriteInstructionKindsBasedOnReassignment(fn); inferReactiveScopeVariables(fn); - const effects = inferFunctionExpressionAliasingEffectsSignature(fn); - fn.env.logger?.debugLogIRs?.({ - kind: 'hir', - name: 'AnalyseFunction (inner)', - value: fn, - }); - if (effects != null) { - fn.aliasingEffects ??= []; - fn.aliasingEffects?.push(...effects); - } - if (fn.aliasingEffects != null) { - const seen = new Set(); - retainWhere(fn.aliasingEffects, effect => { - const hash = hashEffect(effect); - if (seen.has(hash)) { - return false; - } - seen.add(hash); - return true; - }); - } + fn.aliasingEffects = functionEffects; /** * Phase 2: populate the Effect of each context variable to use in inferring @@ -100,7 +80,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void { * effects to decide if the function may be mutable or not. */ const capturedOrMutated = new Set(); - for (const effect of effects ?? []) { + for (const effect of functionEffects) { switch (effect.kind) { case 'Assign': case 'Alias': @@ -152,6 +132,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void { operand.effect = Effect.Read; } } + + fn.env.logger?.debugLogIRs?.({ + kind: 'hir', + name: 'AnalyseFunction (inner)', + value: fn, + }); } function lower(func: HIRFunction): void { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts deleted file mode 100644 index 818f4dae67385..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts +++ /dev/null @@ -1,206 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR'; -import {getOrInsertDefault} from '../Utils/utils'; -import {AliasingEffect} from './AliasingEffects'; - -/** - * This function tracks data flow within an inner function expression in order to - * compute a set of data-flow aliasing effects describing data flow between the function's - * params, context variables, and return value. - * - * For example, consider the following function expression: - * - * ``` - * (x) => { return [x, y] } - * ``` - * - * This function captures both param `x` and context variable `y` into the return value. - * Unlike our previous inference which counted this as a mutation of x and y, we want to - * build a signature for the function that describes the data flow. We would infer - * `Capture x -> return, Capture y -> return` effects for this function. - * - * This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render) - * from instructions within the function up to the function itself. - */ -export function inferFunctionExpressionAliasingEffectsSignature( - fn: HIRFunction, -): Array | null { - const effects: Array = []; - - /** - * Map used to identify tracked variables: params, context vars, return value - * This is used to detect mutation/capturing/aliasing of params/context vars - */ - const tracked = new Map(); - tracked.set(fn.returns.identifier.id, fn.returns); - for (const operand of [...fn.context, ...fn.params]) { - const place = operand.kind === 'Identifier' ? operand : operand.place; - tracked.set(place.identifier.id, place); - } - - /** - * Track capturing/aliasing of context vars and params into each other and into the return. - * We don't need to track locals and intermediate values, since we're only concerned with effects - * as they relate to arguments visible outside the function. - * - * For each aliased identifier we track capture/alias/createfrom and then merge this with how - * the value is used. Eg capturing an alias => capture. See joinEffects() helper. - */ - type AliasedIdentifier = { - kind: AliasingKind; - place: Place; - }; - const dataFlow = new Map>(); - - /* - * Check for aliasing of tracked values. Also joins the effects of how the value is - * used (@param kind) with the aliasing type of each value - */ - function lookup( - place: Place, - kind: AliasedIdentifier['kind'], - ): Array | null { - if (tracked.has(place.identifier.id)) { - return [{kind, place}]; - } - return ( - dataFlow.get(place.identifier.id)?.map(aliased => ({ - kind: joinEffects(aliased.kind, kind), - place: aliased.place, - })) ?? null - ); - } - - // todo: fixpoint - for (const block of fn.body.blocks.values()) { - for (const phi of block.phis) { - const operands: Array = []; - for (const operand of phi.operands.values()) { - const inputs = lookup(operand, 'Alias'); - if (inputs != null) { - operands.push(...inputs); - } - } - if (operands.length !== 0) { - dataFlow.set(phi.place.identifier.id, operands); - } - } - for (const instr of block.instructions) { - if (instr.effects == null) continue; - for (const effect of instr.effects) { - if ( - effect.kind === 'Assign' || - effect.kind === 'Capture' || - effect.kind === 'Alias' || - effect.kind === 'CreateFrom' - ) { - const from = lookup(effect.from, effect.kind); - if (from == null) { - continue; - } - const into = lookup(effect.into, 'Alias'); - if (into == null) { - getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push( - ...from, - ); - } else { - for (const aliased of into) { - getOrInsertDefault( - dataFlow, - aliased.place.identifier.id, - [], - ).push(...from); - } - } - } else if ( - effect.kind === 'Create' || - effect.kind === 'CreateFunction' - ) { - getOrInsertDefault(dataFlow, effect.into.identifier.id, [ - {kind: 'Alias', place: effect.into}, - ]); - } else if ( - effect.kind === 'MutateFrozen' || - effect.kind === 'MutateGlobal' || - effect.kind === 'Impure' || - effect.kind === 'Render' - ) { - effects.push(effect); - } - } - } - if (block.terminal.kind === 'return') { - const from = lookup(block.terminal.value, 'Alias'); - if (from != null) { - getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push( - ...from, - ); - } - } - } - - // Create aliasing effects based on observed data flow - let hasReturn = false; - for (const [into, from] of dataFlow) { - const input = tracked.get(into); - if (input == null) { - continue; - } - for (const aliased of from) { - if ( - aliased.place.identifier.id === input.identifier.id || - !tracked.has(aliased.place.identifier.id) - ) { - continue; - } - const effect = {kind: aliased.kind, from: aliased.place, into: input}; - effects.push(effect); - if ( - into === fn.returns.identifier.id && - (aliased.kind === 'Assign' || aliased.kind === 'CreateFrom') - ) { - hasReturn = true; - } - } - } - // TODO: more precise return effect inference - if (!hasReturn) { - effects.unshift({ - kind: 'Create', - into: fn.returns, - value: - fn.returnType.kind === 'Primitive' - ? ValueKind.Primitive - : ValueKind.Mutable, - reason: ValueReason.KnownReturnSignature, - }); - } - - return effects; -} - -export enum MutationKind { - None = 0, - Conditional = 1, - Definite = 2, -} - -type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign'; -function joinEffects( - effect1: AliasingKind, - effect2: AliasingKind, -): AliasingKind { - if (effect1 === 'Capture' || effect2 === 'Capture') { - return 'Capture'; - } else if (effect1 === 'Assign' || effect2 === 'Assign') { - return 'Assign'; - } else { - return 'Alias'; - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index b6c9812918ff9..b91b606d507e6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -822,7 +822,8 @@ function applyEffect( const functionValues = state.values(effect.function); if ( functionValues.length === 1 && - functionValues[0].kind === 'FunctionExpression' + functionValues[0].kind === 'FunctionExpression' && + functionValues[0].loweredFunc.func.aliasingEffects != null ) { /* * We're calling a locally declared function, we already know it's effects! @@ -2126,8 +2127,6 @@ function computeEffectsForLegacySignature( const mutateIterator = conditionallyMutateIterator(place); if (mutateIterator != null) { effects.push(mutateIterator); - // TODO: should we always push to captures? - captures.push(place); } effects.push({ kind: 'Capture', diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts index 864eb29d8ee80..11401ae715670 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -13,7 +13,10 @@ import { Identifier, IdentifierId, InstructionId, + isJsxType, makeInstructionId, + ValueKind, + ValueReason, Place, } from '../HIR/HIR'; import { @@ -22,34 +25,58 @@ import { eachTerminalOperand, } from '../HIR/visitors'; import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; -import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature'; -import {Result} from '../Utils/Result'; +import {Err, Ok, Result} from '../Utils/Result'; +import {AliasingEffect} from './AliasingEffects'; /** - * Infers mutable ranges for all values in the program, using previously inferred - * mutation/aliasing effects. This pass builds a data flow graph using the effects, - * tracking an abstract notion of "when" each effect occurs relative to the others. - * It then walks each mutation effect against the graph, updating the range of each - * node that would be reachable at the "time" that the effect occurred. + * This pass builds an abstract model of the heap and interprets the effects of the + * given function in order to determine the following: + * - The mutable ranges of all identifiers in the function + * - The externally-visible effects of the function, such as mutations of params and + * context-vars, aliasing between params/context-vars/return-value, and impure side + * effects. + * - The legacy `Effect` to store on each Place. + * + * This pass builds a data flow graph using the effects, tracking an abstract notion + * of "when" each effect occurs relative to the others. It then walks each mutation + * effect against the graph, updating the range of each node that would be reachable + * at the "time" that the effect occurred. * * This pass also validates against invalid effects: any function that is reachable * by being called, or via a Render effect, is validated against mutating globals * or calling impure code. * * Note that this function also populates the outer function's aliasing effects with - * any mutations that apply to its params or context variables. For example, a - * function expression such as the following: + * any mutations that apply to its params or context variables. + * + * ## Example + * A function expression such as the following: * * ``` * (x) => { x.y = true } * ``` * * Would populate a `Mutate x` aliasing effect on the outer function. + * + * ## Returned Function Effects + * + * The function returns (if successful) a list of externally-visible effects. + * This is determined by simulating a conditional, transitive mutation against + * each param, context variable, and return value in turn, and seeing which other + * such values are affected. If they're affected, they must be captured, so we + * record a Capture. + * + * The only tricky bit is the return value, which could _alias_ (or even assign) + * one or more of the params/context-vars rather than just capturing. So we have + * to do a bit more tracking for returns. */ export function inferMutationAliasingRanges( fn: HIRFunction, {isFunctionExpression}: {isFunctionExpression: boolean}, -): Result { +): Result, CompilerError> { + // The set of externally-visible effects + const functionEffects: Array = []; + /** * Part 1: Infer mutable ranges for values. We build an abstract model of * values, the alias/capture edges between them, and the set of mutations. @@ -168,8 +195,10 @@ export function inferMutationAliasingRanges( effect.kind === 'Impure' ) { errors.push(effect.error); + functionEffects.push(effect); } else if (effect.kind === 'Render') { renders.push({index: index++, place: effect.place}); + functionEffects.push(effect); } } } @@ -215,7 +244,6 @@ export function inferMutationAliasingRanges( for (const render of renders) { state.render(render.index, render.place.identifier, errors); } - fn.aliasingEffects ??= []; for (const param of [...fn.context, ...fn.params]) { const place = param.kind === 'Identifier' ? param : param.place; const node = state.nodes.get(place.identifier); @@ -226,13 +254,13 @@ export function inferMutationAliasingRanges( if (node.local != null) { if (node.local.kind === MutationKind.Conditional) { mutated = true; - fn.aliasingEffects.push({ + functionEffects.push({ kind: 'MutateConditionally', value: {...place, loc: node.local.loc}, }); } else if (node.local.kind === MutationKind.Definite) { mutated = true; - fn.aliasingEffects.push({ + functionEffects.push({ kind: 'Mutate', value: {...place, loc: node.local.loc}, }); @@ -241,13 +269,13 @@ export function inferMutationAliasingRanges( if (node.transitive != null) { if (node.transitive.kind === MutationKind.Conditional) { mutated = true; - fn.aliasingEffects.push({ + functionEffects.push({ kind: 'MutateTransitiveConditionally', value: {...place, loc: node.transitive.loc}, }); } else if (node.transitive.kind === MutationKind.Definite) { mutated = true; - fn.aliasingEffects.push({ + functionEffects.push({ kind: 'MutateTransitive', value: {...place, loc: node.transitive.loc}, }); @@ -436,7 +464,82 @@ export function inferMutationAliasingRanges( } } - return errors.asResult(); + /** + * Part 3 + * Finish populating the externally visible effects. Above we bubble-up the side effects + * (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables. + * Here we populate an effect to create the return value as well as populating alias/capture + * effects for how data flows between the params, context vars, and return. + */ + functionEffects.push({ + kind: 'Create', + into: fn.returns, + value: + fn.returnType.kind === 'Primitive' + ? ValueKind.Primitive + : isJsxType(fn.returnType) + ? ValueKind.Frozen + : ValueKind.Mutable, + reason: ValueReason.KnownReturnSignature, + }); + /** + * Determine precise data-flow effects by simulating transitive mutations of the params/ + * captures and seeing what other params/context variables are affected. Anything that + * would be transitively mutated needs a capture relationship. + */ + const tracked: Array = []; + const ignoredErrors = new CompilerError(); + for (const param of [...fn.params, ...fn.context, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + tracked.push(place); + } + for (const into of tracked) { + const mutationIndex = index++; + state.mutate( + mutationIndex, + into.identifier, + null, + true, + MutationKind.Conditional, + into.loc, + ignoredErrors, + ); + for (const from of tracked) { + if ( + from.identifier.id === into.identifier.id || + from.identifier.id === fn.returns.identifier.id + ) { + continue; + } + const fromNode = state.nodes.get(from.identifier); + CompilerError.invariant(fromNode != null, { + reason: `Expected a node to exist for all parameters and context variables`, + loc: into.loc, + }); + if (fromNode.lastMutated === mutationIndex) { + if (into.identifier.id === fn.returns.identifier.id) { + // The return value could be any of the params/context variables + functionEffects.push({ + kind: 'Alias', + from, + into, + }); + } else { + // Otherwise params/context-vars can only capture each other + functionEffects.push({ + kind: 'Capture', + from, + into, + }); + } + } + } + } + + if (errors.hasErrors() && !isFunctionExpression) { + return Err(errors); + } + return Ok(functionEffects); } function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { @@ -452,6 +555,12 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { } } +export enum MutationKind { + None = 0, + Conditional = 1, + Definite = 2, +} + type Node = { id: Identifier; createdFrom: Map; @@ -460,6 +569,7 @@ type Node = { edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>; transitive: {kind: MutationKind; loc: SourceLocation} | null; local: {kind: MutationKind; loc: SourceLocation} | null; + lastMutated: number; value: | {kind: 'Object'} | {kind: 'Phi'} @@ -477,6 +587,7 @@ class AliasingState { edges: [], transitive: null, local: null, + lastMutated: 0, value, }); } @@ -558,7 +669,8 @@ class AliasingState { mutate( index: number, start: Identifier, - end: InstructionId, + // Null is used for simulated mutations + end: InstructionId | null, transitive: boolean, kind: MutationKind, loc: SourceLocation, @@ -580,9 +692,12 @@ class AliasingState { if (node == null) { continue; } - node.id.mutableRange.end = makeInstructionId( - Math.max(node.id.mutableRange.end, end), - ); + node.lastMutated = Math.max(node.lastMutated, index); + if (end != null) { + node.id.mutableRange.end = makeInstructionId( + Math.max(node.id.mutableRange.end, end), + ); + } if ( node.value.kind === 'Function' && node.transitive == null && diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md index 0360b1aa5e571..60ef16a6245f3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md @@ -514,9 +514,9 @@ Intuition: these effects are inverses of each other (capturing into an object, e Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. ```js -const y = [x]; // capture -const z = y[0]; // createfrom -mutate(z); // this clearly can mutate x, so the result must be one of Assign/Alias/CreateFrom +const b = [a]; // capture +const c = b[0]; // createfrom +mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom ``` We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias. @@ -528,17 +528,17 @@ CreateFrom c <- b Alias c <- a ``` -Meanwhile the opposite direction preservers the capture, because the result is not the same as the source: +Meanwhile the opposite direction preserves the capture, because the result is not the same as the source: ```js -const y = x[0]; // createfrom -const z = [y]; // capture -mutate(z); // does not mutate x, so the result must be Capture +const b = a[0]; // createfrom +const c = [b]; // capture +mutate(c); // does not mutate a, so the result must be Capture ``` ``` -Capture b <- a -CreateFrom c <- b +CreateFrom b <- a +Capture c <- b => -Capture b <- a +Capture c <- a ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md new file mode 100644 index 0000000000000..326cd9f7e0d90 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +import {Stringify, mutate} from 'shared-runtime'; + +function Component({foo, bar}) { + let x = {foo}; + let y = {bar}; + const f0 = function () { + let a = {y}; + let b = {x}; + a.y.x = b; + }; + f0(); + mutate(y); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 2, bar: 3}], + sequentialRenders: [ + {foo: 2, bar: 3}, + {foo: 2, bar: 3}, + {foo: 2, bar: 4}, + {foo: 3, bar: 4}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, mutate } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { foo, bar } = t0; + let t1; + if ($[0] !== bar || $[1] !== foo) { + const x = { foo }; + const y = { bar }; + const f0 = function () { + const a = { y }; + const b = { x }; + a.y.x = b; + }; + + f0(); + mutate(y); + t1 = ; + $[0] = bar; + $[1] = foo; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 2, bar: 3 }], + sequentialRenders: [ + { foo: 2, bar: 3 }, + { foo: 2, bar: 3 }, + { foo: 2, bar: 4 }, + { foo: 3, bar: 4 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}
+
{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}
+
{"x":{"bar":4,"x":{"x":{"foo":2}},"wat0":"joe"}}
+
{"x":{"bar":4,"x":{"x":{"foo":3}},"wat0":"joe"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js new file mode 100644 index 0000000000000..5aa39d3ffb36b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js @@ -0,0 +1,25 @@ +import {Stringify, mutate} from 'shared-runtime'; + +function Component({foo, bar}) { + let x = {foo}; + let y = {bar}; + const f0 = function () { + let a = {y}; + let b = {x}; + a.y.x = b; + }; + f0(); + mutate(y); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 2, bar: 3}], + sequentialRenders: [ + {foo: 2, bar: 3}, + {foo: 2, bar: 3}, + {foo: 2, bar: 4}, + {foo: 3, bar: 4}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md new file mode 100644 index 0000000000000..c24c16b50dc1a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md @@ -0,0 +1,97 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + return identity(x); + }; + const x2 = f(); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { identity, ValidateMemoization } from "shared-runtime"; + +function Component(t0) { + const $ = _c(10); + const { a, b } = t0; + let t1; + let x; + if ($[0] !== a || $[1] !== b) { + t1 = { a }; + x = t1; + const f = () => identity(x); + + const x2 = f(); + x2.b = b; + $[0] = a; + $[1] = b; + $[2] = x; + $[3] = t1; + } else { + x = $[2]; + t1 = $[3]; + } + let t2; + if ($[4] !== a || $[5] !== b) { + t2 = [a, b]; + $[4] = a; + $[5] = b; + $[6] = t2; + } else { + t2 = $[6]; + } + let t3; + if ($[7] !== t2 || $[8] !== x) { + t3 = ; + $[7] = t2; + $[8] = x; + $[9] = t3; + } else { + t3 = $[9]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":0}}
+
{"inputs":[0,1],"output":{"a":0,"b":1}}
+
{"inputs":[1,1],"output":{"a":1,"b":1}}
+
{"inputs":[0,0],"output":{"a":0,"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js new file mode 100644 index 0000000000000..c7770ffcdce2b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js @@ -0,0 +1,24 @@ +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + return identity(x); + }; + const x2 = f(); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md new file mode 100644 index 0000000000000..59403c64951b8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md @@ -0,0 +1,92 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const x2 = identity(x); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { identity, ValidateMemoization } from "shared-runtime"; + +function Component(t0) { + const $ = _c(10); + const { a, b } = t0; + let t1; + let x; + if ($[0] !== a || $[1] !== b) { + t1 = { a }; + x = t1; + const x2 = identity(x); + x2.b = b; + $[0] = a; + $[1] = b; + $[2] = x; + $[3] = t1; + } else { + x = $[2]; + t1 = $[3]; + } + let t2; + if ($[4] !== a || $[5] !== b) { + t2 = [a, b]; + $[4] = a; + $[5] = b; + $[6] = t2; + } else { + t2 = $[6]; + } + let t3; + if ($[7] !== t2 || $[8] !== x) { + t3 = ; + $[7] = t2; + $[8] = x; + $[9] = t3; + } else { + t3 = $[9]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":0}}
+
{"inputs":[0,1],"output":{"a":0,"b":1}}
+
{"inputs":[1,1],"output":{"a":1,"b":1}}
+
{"inputs":[0,0],"output":{"a":0,"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js new file mode 100644 index 0000000000000..bd928634a29bf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js @@ -0,0 +1,21 @@ +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const x2 = identity(x); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md new file mode 100644 index 0000000000000..9d168c9e5c1ea --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +function Component() { + const x = {}; + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = {}; + $[0] = t0; + } else { + t0 = $[0]; + } + const x = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + + t1 = ; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js new file mode 100644 index 0000000000000..6e67ed7bab695 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js @@ -0,0 +1,14 @@ +function Component() { + const x = {}; + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md new file mode 100644 index 0000000000000..73cf419be14d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +function Component({a, b}) { + const y = {a}; + const x = {b}; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + // z is a phi with a backedge, and we don't realize it could be x, + // and therefore fail to record a Capture x <- y effect for this + // function expression + z.y = y; + }; + f(); + mutate(x); + return
{x}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const y = { a }; + const x = { b }; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + + z.y = y; + }; + + f(); + mutate(x); + t1 =
{x}
; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js new file mode 100644 index 0000000000000..31a51b45aa384 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js @@ -0,0 +1,17 @@ +function Component({a, b}) { + const y = {a}; + const x = {b}; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + // z is a phi with a backedge, and we don't realize it could be x, + // and therefore fail to record a Capture x <- y effect for this + // function expression + z.y = y; + }; + f(); + mutate(x); + return
{x}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md new file mode 100644 index 0000000000000..28fc8b601f7ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:true + +export const App = () => { + const [selected, setSelected] = useState(new Set()); + const onSelectedChange = (value: string) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + // This should not count as a mutation of `selected` + newSelected.delete(value); + } else { + // This should not count as a mutation of `selected` + newSelected.add(value); + } + setSelected(newSelected); + }; + + return ; +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:true + +export const App = () => { + const $ = _c(6); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = new Set(); + $[0] = t0; + } else { + t0 = $[0]; + } + const [selected, setSelected] = useState(t0); + let t1; + if ($[1] !== selected) { + t1 = (value) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + newSelected.delete(value); + } else { + newSelected.add(value); + } + + setSelected(newSelected); + }; + $[1] = selected; + $[2] = t1; + } else { + t1 = $[2]; + } + const onSelectedChange = t1; + let t2; + if ($[3] !== onSelectedChange || $[4] !== selected) { + t2 = ; + $[3] = onSelectedChange; + $[4] = selected; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +}; + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js new file mode 100644 index 0000000000000..c5a404a66c371 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js @@ -0,0 +1,18 @@ +// @enableNewMutationAliasingModel:true + +export const App = () => { + const [selected, setSelected] = useState(new Set()); + const onSelectedChange = (value: string) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + // This should not count as a mutation of `selected` + newSelected.delete(value); + } else { + // This should not count as a mutation of `selected` + newSelected.add(value); + } + setSelected(newSelected); + }; + + return ; +};