From 8d6d9e4b0fbbb8765c58c30586040bc0ef007ba9 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 15:09:32 -0700 Subject: [PATCH 01/12] [compiler] update fixtures --- ...map-named-callback-cross-context.expect.md | 81 +++++++++---------- ...map-named-callback-cross-context.expect.md | 81 +++++++++---------- .../shared-hook-calls.expect.md | 81 ++++++++----------- .../shared-hook-calls.expect.md | 81 ++++++++----------- 4 files changed, 144 insertions(+), 180 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md index d24536d616..c1a6dfb3ea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md @@ -57,67 +57,62 @@ import { Stringify } from "shared-runtime"; * - cb1 is not assumed to be called since it's only used as a call operand */ function useFoo(t0) { - const $ = _c(14); - let arr1; - let arr2; + const $ = _c(13); + const { arr1, arr2 } = t0; let t1; - if ($[0] !== t0) { - ({ arr1, arr2 } = t0); - let t2; - if ($[4] !== arr1[0]) { - t2 = (e) => arr1[0].value + e.value; - $[4] = arr1[0]; - $[5] = t2; - } else { - t2 = $[5]; - } - const cb1 = t2; - t1 = () => arr1.map(cb1); - $[0] = t0; - $[1] = arr1; - $[2] = arr2; - $[3] = t1; + if ($[0] !== arr1[0]) { + t1 = (e) => arr1[0].value + e.value; + $[0] = arr1[0]; + $[1] = t1; } else { - arr1 = $[1]; - arr2 = $[2]; - t1 = $[3]; + t1 = $[1]; } - const getArrMap1 = t1; + const cb1 = t1; let t2; - if ($[6] !== arr2) { - t2 = (e_0) => arr2[0].value + e_0.value; - $[6] = arr2; - $[7] = t2; + if ($[2] !== arr1 || $[3] !== cb1) { + t2 = () => arr1.map(cb1); + $[2] = arr1; + $[3] = cb1; + $[4] = t2; } else { - t2 = $[7]; + t2 = $[4]; } - const cb2 = t2; + const getArrMap1 = t2; let t3; - if ($[8] !== arr1 || $[9] !== cb2) { - t3 = () => arr1.map(cb2); - $[8] = arr1; - $[9] = cb2; - $[10] = t3; + if ($[5] !== arr2) { + t3 = (e_0) => arr2[0].value + e_0.value; + $[5] = arr2; + $[6] = t3; } else { - t3 = $[10]; + t3 = $[6]; } - const getArrMap2 = t3; + const cb2 = t3; let t4; - if ($[11] !== getArrMap1 || $[12] !== getArrMap2) { - t4 = ( + if ($[7] !== arr1 || $[8] !== cb2) { + t4 = () => arr1.map(cb2); + $[7] = arr1; + $[8] = cb2; + $[9] = t4; + } else { + t4 = $[9]; + } + const getArrMap2 = t4; + let t5; + if ($[10] !== getArrMap1 || $[11] !== getArrMap2) { + t5 = ( ); - $[11] = getArrMap1; - $[12] = getArrMap2; - $[13] = t4; + $[10] = getArrMap1; + $[11] = getArrMap2; + $[12] = t5; } else { - t4 = $[13]; + t5 = $[12]; } - return t4; + return t5; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md index a36b862052..7bc2c193cf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md @@ -58,67 +58,62 @@ import { Stringify } from "shared-runtime"; * - cb1 is not assumed to be called since it's only used as a call operand */ function useFoo(t0) { - const $ = _c(14); - let arr1; - let arr2; + const $ = _c(13); + const { arr1, arr2 } = t0; let t1; - if ($[0] !== t0) { - ({ arr1, arr2 } = t0); - let t2; - if ($[4] !== arr1[0]) { - t2 = (e) => arr1[0].value + e.value; - $[4] = arr1[0]; - $[5] = t2; - } else { - t2 = $[5]; - } - const cb1 = t2; - t1 = () => arr1.map(cb1); - $[0] = t0; - $[1] = arr1; - $[2] = arr2; - $[3] = t1; + if ($[0] !== arr1[0]) { + t1 = (e) => arr1[0].value + e.value; + $[0] = arr1[0]; + $[1] = t1; } else { - arr1 = $[1]; - arr2 = $[2]; - t1 = $[3]; + t1 = $[1]; } - const getArrMap1 = t1; + const cb1 = t1; let t2; - if ($[6] !== arr2) { - t2 = (e_0) => arr2[0].value + e_0.value; - $[6] = arr2; - $[7] = t2; + if ($[2] !== arr1 || $[3] !== cb1) { + t2 = () => arr1.map(cb1); + $[2] = arr1; + $[3] = cb1; + $[4] = t2; } else { - t2 = $[7]; + t2 = $[4]; } - const cb2 = t2; + const getArrMap1 = t2; let t3; - if ($[8] !== arr1 || $[9] !== cb2) { - t3 = () => arr1.map(cb2); - $[8] = arr1; - $[9] = cb2; - $[10] = t3; + if ($[5] !== arr2) { + t3 = (e_0) => arr2[0].value + e_0.value; + $[5] = arr2; + $[6] = t3; } else { - t3 = $[10]; + t3 = $[6]; } - const getArrMap2 = t3; + const cb2 = t3; let t4; - if ($[11] !== getArrMap1 || $[12] !== getArrMap2) { - t4 = ( + if ($[7] !== arr1 || $[8] !== cb2) { + t4 = () => arr1.map(cb2); + $[7] = arr1; + $[8] = cb2; + $[9] = t4; + } else { + t4 = $[9]; + } + const getArrMap2 = t4; + let t5; + if ($[10] !== getArrMap1 || $[11] !== getArrMap2) { + t5 = ( ); - $[11] = getArrMap1; - $[12] = getArrMap2; - $[13] = t4; + $[10] = getArrMap1; + $[11] = getArrMap2; + $[12] = t5; } else { - t4 = $[13]; + t5 = $[12]; } - return t4; + return t5; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md index 3f361c2019..39bd61aaf3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md @@ -30,60 +30,47 @@ import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire @enabl import { fire } from "react"; function Component(t0) { - const $ = _c(13); - let bar; - let baz; - let foo; - if ($[0] !== t0) { - ({ bar, baz } = t0); - let t1; - if ($[4] !== bar) { - t1 = () => { - console.log(bar); - }; - $[4] = bar; - $[5] = t1; - } else { - t1 = $[5]; - } - foo = t1; - $[0] = t0; - $[1] = bar; - $[2] = baz; - $[3] = foo; + const $ = _c(9); + const { bar, baz } = t0; + let t1; + if ($[0] !== bar) { + t1 = () => { + console.log(bar); + }; + $[0] = bar; + $[1] = t1; } else { - bar = $[1]; - baz = $[2]; - foo = $[3]; + t1 = $[1]; } - const t1 = useFire(foo); - const t2 = useFire(baz); - let t3; - if ($[6] !== bar || $[7] !== t1 || $[8] !== t2) { - t3 = () => { - t1(bar); + const foo = t1; + const t2 = useFire(foo); + const t3 = useFire(baz); + let t4; + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) { + t4 = () => { + t2(bar); + t3(bar); + }; + $[2] = bar; + $[3] = t2; + $[4] = t3; + $[5] = t4; + } else { + t4 = $[5]; + } + useEffect(t4); + let t5; + if ($[6] !== bar || $[7] !== t2) { + t5 = () => { t2(bar); }; $[6] = bar; - $[7] = t1; - $[8] = t2; - $[9] = t3; + $[7] = t2; + $[8] = t5; } else { - t3 = $[9]; + t5 = $[8]; } - useEffect(t3); - let t4; - if ($[10] !== bar || $[11] !== t1) { - t4 = () => { - t1(bar); - }; - $[10] = bar; - $[11] = t1; - $[12] = t4; - } else { - t4 = $[12]; - } - useEffect(t4); + useEffect(t5); return null; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md index d98b3e03ab..92dbf9843a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md @@ -30,60 +30,47 @@ import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; function Component(t0) { - const $ = _c(13); - let bar; - let baz; - let foo; - if ($[0] !== t0) { - ({ bar, baz } = t0); - let t1; - if ($[4] !== bar) { - t1 = () => { - console.log(bar); - }; - $[4] = bar; - $[5] = t1; - } else { - t1 = $[5]; - } - foo = t1; - $[0] = t0; - $[1] = bar; - $[2] = baz; - $[3] = foo; + const $ = _c(9); + const { bar, baz } = t0; + let t1; + if ($[0] !== bar) { + t1 = () => { + console.log(bar); + }; + $[0] = bar; + $[1] = t1; } else { - bar = $[1]; - baz = $[2]; - foo = $[3]; + t1 = $[1]; } - const t1 = useFire(foo); - const t2 = useFire(baz); - let t3; - if ($[6] !== bar || $[7] !== t1 || $[8] !== t2) { - t3 = () => { - t1(bar); + const foo = t1; + const t2 = useFire(foo); + const t3 = useFire(baz); + let t4; + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) { + t4 = () => { + t2(bar); + t3(bar); + }; + $[2] = bar; + $[3] = t2; + $[4] = t3; + $[5] = t4; + } else { + t4 = $[5]; + } + useEffect(t4); + let t5; + if ($[6] !== bar || $[7] !== t2) { + t5 = () => { t2(bar); }; $[6] = bar; - $[7] = t1; - $[8] = t2; - $[9] = t3; + $[7] = t2; + $[8] = t5; } else { - t3 = $[9]; + t5 = $[8]; } - useEffect(t3); - let t4; - if ($[10] !== bar || $[11] !== t1) { - t4 = () => { - t1(bar); - }; - $[10] = bar; - $[11] = t1; - $[12] = t4; - } else { - t4 = $[12]; - } - useEffect(t4); + useEffect(t5); return null; } From 1fa18f62b2b9a9f5c2960662f09fb2e96789b1b9 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 02/12] [commit] Improve error for hoisting violations The previous error for hoisting violations pointed only to the variable declaration, but didn't show where the value was accessed before that declaration. We now track where each hoisted variable is first accessed and report two errors, one for the reference and one for the declaration. When we improve our diagnostic infra to support reporting errors at multiple locations we can merge these into a single conceptual error. --- .../Inference/InferMutationAliasingEffects.ts | 100 ++++++++++++------ .../error.invalid-hoisting-setstate.expect.md | 12 ++- ...rozen-hoisted-storecontext-const.expect.md | 16 +-- 3 files changed, 86 insertions(+), 42 deletions(-) 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 521f55026e..6bb078e8fa 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -38,6 +38,7 @@ import { import { eachInstructionValueLValue, eachInstructionValueOperand, + eachTerminalOperand, eachTerminalSuccessor, } from '../HIR/visitors'; import {Ok, Result} from '../Utils/Result'; @@ -221,8 +222,19 @@ export function inferMutationAliasingEffects( return Ok(undefined); } -function findHoistedContextDeclarations(fn: HIRFunction): Set { - const hoisted = new Set(); +function findHoistedContextDeclarations( + fn: HIRFunction, +): Map { + const hoisted = new Map(); + function visit(place: Place): void { + if ( + hoisted.has(place.identifier.declarationId) && + hoisted.get(place.identifier.declarationId) == null + ) { + // If this is the first load of the value, store the location + hoisted.set(place.identifier.declarationId, place); + } + } for (const block of fn.body.blocks.values()) { for (const instr of block.instructions) { if (instr.value.kind === 'DeclareContext') { @@ -232,10 +244,17 @@ function findHoistedContextDeclarations(fn: HIRFunction): Set { kind == InstructionKind.HoistedFunction || kind == InstructionKind.HoistedLet ) { - hoisted.add(instr.value.lvalue.place.identifier.declarationId); + hoisted.set(instr.value.lvalue.place.identifier.declarationId, null); + } + } else { + for (const operand of eachInstructionValueOperand(instr.value)) { + visit(operand); } } } + for (const operand of eachTerminalOperand(block.terminal)) { + visit(operand); + } } return hoisted; } @@ -248,12 +267,12 @@ class Context { catchHandlers: Map = new Map(); isFuctionExpression: boolean; fn: HIRFunction; - hoistedContextDeclarations: Set; + hoistedContextDeclarations: Map; constructor( isFunctionExpression: boolean, fn: HIRFunction, - hoistedContextDeclarations: Set, + hoistedContextDeclarations: Map, ) { this.isFuctionExpression = isFunctionExpression; this.fn = fn; @@ -901,48 +920,69 @@ function applyEffect( console.log(prettyFormat(state.debugAbstractValue(value))); } - let reason: string; - let description: string | null = null; - if ( mutationKind === 'mutate-frozen' && context.hoistedContextDeclarations.has( effect.value.identifier.declarationId, ) ) { - reason = `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`; - if ( + const description = effect.value.identifier.name !== null && effect.value.identifier.name.kind === 'named' - ) { - description = `Move the declaration of \`${effect.value.identifier.name.value}\` to before it is first referenced`; + ? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared` + : null; + const hoistedAccess = context.hoistedContextDeclarations.get( + effect.value.identifier.declarationId, + ); + if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) { + effects.push({ + kind: 'MutateFrozen', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`, + description, + loc: hoistedAccess.loc, + suggestions: null, + }, + }); } + + effects.push({ + kind: 'MutateFrozen', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`, + description, + loc: effect.value.loc, + suggestions: null, + }, + }); } else { - reason = getWriteErrorReason({ + const reason = getWriteErrorReason({ kind: value.kind, reason: value.reason, context: new Set(), }); - if ( + const description = effect.value.identifier.name !== null && effect.value.identifier.name.kind === 'named' - ) { - description = `Found mutation of \`${effect.value.identifier.name.value}\``; - } + ? `Found mutation of \`${effect.value.identifier.name.value}\`` + : null; + effects.push({ + kind: + value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason, + description, + loc: effect.value.loc, + suggestions: null, + }, + }); } - - effects.push({ - kind: - value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal', - place: effect.value, - error: { - severity: ErrorSeverity.InvalidReact, - reason, - description, - loc: effect.value.loc, - suggestions: null, - }, - }); } break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md index 3fcc84c9a4..1be37ef830 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md @@ -38,13 +38,15 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` - 19 | useEffect(() => setState(2), []); + 17 | * $2 = Function context=setState + 18 | */ +> 19 | useEffect(() => setState(2), []); + | ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19) + +InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21) 20 | -> 21 | const [state, setState] = useState(0); - | ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `setState` to before it is first referenced (21:21) + 21 | const [state, setState] = useState(0); 22 | return ; - 23 | } - 24 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md index 7bf3cd0cd3..a95ace1df5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md @@ -31,13 +31,15 @@ function Component({content, refetch}) { ## Error ``` - 17 | // This has to error: onRefetch needs to memoize with `content` as a - 18 | // dependency, but the dependency comes later -> 19 | const {data = null} = content; - | ^^^^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `data` to before it is first referenced (19:19) - 20 | - 21 | return ; - 22 | } + 9 | // TDZ violation! + 10 | const onRefetch = useCallback(() => { +> 11 | refetch(data); + | ^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `data` is accessed before it is declared (11:11) + +InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `data` is accessed before it is declared (19:19) + 12 | }, [refetch]); + 13 | + 14 | // The context variable gets frozen here since it's passed to a hook ``` \ No newline at end of file From 91e98346cba34f331592a6d2f9e6d103c7b0d317 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 03/12] [compiler] Fix infinite loop due to uncached applied signatures When we apply new aliasing signatures we can generate new temporaries, which causes the abstract memory model to not converge. The fix is to make sure we cache the applications of these signatures. --- .../src/Inference/AliasingEffects.ts | 15 +- .../Inference/InferMutationAliasingEffects.ts | 226 ++++++++++++------ .../repro-compiler-infinite-loop.expect.md | 54 +++++ .../repro-compiler-infinite-loop.js | 17 ++ 4 files changed, 237 insertions(+), 75 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts index 1a23a9cd3c..f844129e26 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts @@ -8,6 +8,7 @@ import {CompilerErrorDetailOptions} from '../CompilerError'; import { FunctionExpression, + GeneratedSource, Hole, IdentifierId, ObjectMethod, @@ -18,6 +19,7 @@ import { ValueReason, } from '../HIR'; import {FunctionSignature} from '../HIR/ObjectShape'; +import {printSourceLocation} from '../HIR/PrintHIR'; /** * `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or @@ -200,10 +202,19 @@ export function hashEffect(effect: AliasingEffect): string { return [effect.kind, effect.value.identifier.id, effect.reason].join(':'); } case 'Impure': - case 'Render': + case 'Render': { + return [effect.kind, effect.place.identifier.id].join(':'); + } case 'MutateFrozen': case 'MutateGlobal': { - return [effect.kind, effect.place.identifier.id].join(':'); + return [ + effect.kind, + effect.place.identifier.id, + effect.error.severity, + effect.error.reason, + effect.error.description, + printSourceLocation(effect.error.loc ?? GeneratedSource), + ].join(':'); } case 'Mutate': case 'MutateConditionally': 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 6bb078e8fa..93f00508b2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -50,12 +50,14 @@ import { } from './InferReferenceEffects'; import { assertExhaustive, + getOrInsertDefault, getOrInsertWith, Set_isSuperset, } from '../Utils/utils'; import { printAliasingEffect, printAliasingSignature, + printFunction, printIdentifier, printInstruction, printInstructionValue, @@ -195,12 +197,15 @@ export function inferMutationAliasingEffects( let count = 0; while (queuedStates.size !== 0) { count++; - if (count > 1000) { + if (count > 100) { console.log( 'oops infinite loop', fn.id, typeof fn.loc !== 'symbol' ? fn.loc?.filename : null, ); + if (DEBUG) { + console.log(printFunction(fn)); + } throw new Error('infinite loop'); } for (const [blockId, block] of fn.body.blocks) { @@ -212,6 +217,11 @@ export function inferMutationAliasingEffects( statesByBlock.set(blockId, incomingState); const state = incomingState.clone(); + if (DEBUG) { + console.log('*************'); + console.log(`bb${block.id}`); + console.log('*************'); + } inferBlock(context, state, block); for (const nextBlockId of eachTerminalSuccessor(block.terminal)) { @@ -264,7 +274,13 @@ class Context { instructionSignatureCache: Map = new Map(); effectInstructionValueCache: Map = new Map(); + applySignatureCache: Map< + AliasingSignature, + Map | null> + > = new Map(); catchHandlers: Map = new Map(); + functionSignatureCache: Map = + new Map(); isFuctionExpression: boolean; fn: HIRFunction; hoistedContextDeclarations: Map; @@ -279,6 +295,19 @@ class Context { this.hoistedContextDeclarations = hoistedContextDeclarations; } + cacheApplySignature( + signature: AliasingSignature, + effect: Extract, + f: () => Array | null, + ): Array | null { + const inner = getOrInsertDefault( + this.applySignatureCache, + signature, + new Map(), + ); + return getOrInsertWith(inner, effect, f); + } + internEffect(effect: AliasingEffect): AliasingEffect { const hash = hashEffect(effect); let interned = this.internedEffects.get(hash); @@ -352,11 +381,13 @@ function inferBlock( state.appendAlias(handlerParam, instr.lvalue); const kind = state.kind(instr.lvalue).kind; if (kind === ValueKind.Mutable || kind == ValueKind.Context) { - effects.push({ - kind: 'Alias', - from: instr.lvalue, - into: handlerParam, - }); + effects.push( + context.internEffect({ + kind: 'Alias', + from: instr.lvalue, + into: handlerParam, + }), + ); } } } @@ -365,11 +396,11 @@ function inferBlock( } else if (terminal.kind === 'return') { if (!context.isFuctionExpression) { terminal.effects = [ - { + context.internEffect({ kind: 'Freeze', value: terminal.value, reason: ValueReason.JsxCaptured, - }, + }), ]; } } @@ -546,20 +577,21 @@ function applyEffect( break; } case ValueKind.Frozen: { - effects.push({ - kind: 'ImmutableCapture', - from: effect.from, - into: effect.into, - }); + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + aliased, + effects, + ); break; } default: { - effects.push({ - // OK: recording information flow - kind: 'CreateFrom', // prev Alias - from: effect.from, - into: effect.into, - }); + effects.push(effect); } } break; @@ -658,11 +690,17 @@ function applyEffect( } case ValueKind.Frozen: { isMutableReferenceType = false; - effects.push({ - kind: 'ImmutableCapture', - from: effect.from, - into: effect.into, - }); + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + aliased, + effects, + ); break; } default: { @@ -684,11 +722,17 @@ function applyEffect( const fromKind = fromValue.kind; switch (fromKind) { case ValueKind.Frozen: { - effects.push({ - kind: 'ImmutableCapture', - from: effect.from, - into: effect.into, - }); + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + aliased, + effects, + ); let value = context.effectInstructionValueCache.get(effect); if (value == null) { value = { @@ -746,23 +790,33 @@ function applyEffect( * We're calling a locally declared function, we already know it's effects! * We just have to substitute in the args for the params */ - const signature = buildSignatureFromFunctionExpression( - state.env, - functionValues[0], - ); + const functionExpr = functionValues[0]; + let signature = context.functionSignatureCache.get(functionExpr); + if (signature == null) { + signature = buildSignatureFromFunctionExpression( + state.env, + functionExpr, + ); + context.functionSignatureCache.set(functionExpr, signature); + } if (DEBUG) { console.log( `constructed alias signature:\n${printAliasingSignature(signature)}`, ); } - const signatureEffects = computeEffectsForSignature( - state.env, + const signatureEffects = context.cacheApplySignature( signature, - effect.into, - effect.receiver, - effect.args, - functionValues[0].loweredFunc.func.context, - effect.loc, + effect, + () => + computeEffectsForSignature( + state.env, + signature, + effect.into, + effect.receiver, + effect.args, + functionExpr.loweredFunc.func.context, + effect.loc, + ), ); if (signatureEffects != null) { if (DEBUG) { @@ -781,18 +835,24 @@ function applyEffect( break; } } - const signatureEffects = - effect.signature?.aliasing != null - ? computeEffectsForSignature( + let signatureEffects = null; + if (effect.signature?.aliasing != null) { + const signature = effect.signature.aliasing; + signatureEffects = context.cacheApplySignature( + effect.signature.aliasing, + effect, + () => + computeEffectsForSignature( state.env, - effect.signature.aliasing, + signature, effect.into, effect.receiver, effect.args, [], effect.loc, - ) - : null; + ), + ); + } if (signatureEffects != null) { if (DEBUG) { console.log('apply aliasing signature effects'); @@ -935,30 +995,42 @@ function applyEffect( effect.value.identifier.declarationId, ); if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) { - effects.push({ + applyEffect( + context, + state, + { + kind: 'MutateFrozen', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`, + description, + loc: hoistedAccess.loc, + suggestions: null, + }, + }, + aliased, + effects, + ); + } + + applyEffect( + context, + state, + { kind: 'MutateFrozen', place: effect.value, error: { severity: ErrorSeverity.InvalidReact, - reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`, + reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`, description, - loc: hoistedAccess.loc, + loc: effect.value.loc, suggestions: null, }, - }); - } - - effects.push({ - kind: 'MutateFrozen', - place: effect.value, - error: { - severity: ErrorSeverity.InvalidReact, - reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`, - description, - loc: effect.value.loc, - suggestions: null, }, - }); + aliased, + effects, + ); } else { const reason = getWriteErrorReason({ kind: value.kind, @@ -970,18 +1042,26 @@ function applyEffect( effect.value.identifier.name.kind === 'named' ? `Found mutation of \`${effect.value.identifier.name.value}\`` : null; - effects.push({ - kind: - value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal', - place: effect.value, - error: { - severity: ErrorSeverity.InvalidReact, - reason, - description, - loc: effect.value.loc, - suggestions: null, + applyEffect( + context, + state, + { + kind: + value.kind === ValueKind.Frozen + ? 'MutateFrozen' + : 'MutateGlobal', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason, + description, + loc: effect.value.loc, + suggestions: null, + }, }, - }); + aliased, + effects, + ); } } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md new file mode 100644 index 0000000000..dfc4ed9883 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel + +import fbt from 'fbt'; + +component Component() { + const sections = Object.keys(items); + + for (let i = 0; i < sections.length; i += 3) { + chunks.push( + sections.slice(i, i + 3).map(section => { + return ; + }) + ); + } + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import fbt from "fbt"; + +function Component() { + const $ = _c(1); + const sections = Object.keys(items); + for (let i = 0; i < sections.length; i = i + 3, i) { + chunks.push(sections.slice(i, i + 3).map(_temp)); + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp(section) { + return ; +} + +``` + +### 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-compiler-infinite-loop.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js new file mode 100644 index 0000000000..d03a44618e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js @@ -0,0 +1,17 @@ +// @flow @enableNewMutationAliasingModel + +import fbt from 'fbt'; + +component Component() { + const sections = Object.keys(items); + + for (let i = 0; i < sections.length; i += 3) { + chunks.push( + sections.slice(i, i + 3).map(section => { + return ; + }) + ); + } + + return ; +} From 54f1c3c85b89426350d680b2ba1ee9c143a13512 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 04/12] [compiler] Repro for case of lost precision in new inference In comparing compilation output of the old/new inference models I found this case (heavily distilled into a fixture). Roughly speaking the scenario is: * Create a mutable object `x` * Extract part of that object and pass it to a hook/jsx so that _part_ becomes frozen * Mutate `x`, even indirectly. In the old model we can still independently memoize the value from the middle step, since we assume that part of the larger value is not changing. In the new model, the mutation from the later step effectively overrides the freeze effect in step 2, and considers the value to have changed later anyway. We've already rolled out and vetted the previous behavior, confirming that the heuristic of "that part of the mutable object is fozen now" is generally safe. I'll fix in a follow-up. --- ...jsx-captures-value-mutated-later.expect.md | 53 +++++++++++++++++++ .../repro-jsx-captures-value-mutated-later.js | 15 ++++++ 2 files changed, 68 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md new file mode 100644 index 0000000000..109219e03a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel + +import {identity, Stringify, useFragment} from 'shared-runtime'; + +component Example() { + const data = useFragment(); + + const {a, b} = identity(data); + + const el = ; + + identity(a.at(0)); + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import { identity, Stringify, useFragment } from "shared-runtime"; + +function Example() { + const $ = _c(2); + const data = useFragment(); + let t0; + if ($[0] !== data) { + const { a, b } = identity(data); + + const el = ; + + identity(a.at(0)); + + t0 = ; + $[0] = data; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +``` + +### 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-jsx-captures-value-mutated-later.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js new file mode 100644 index 0000000000..7ab6dbc30a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js @@ -0,0 +1,15 @@ +// @flow @enableNewMutationAliasingModel + +import {identity, Stringify, useFragment} from 'shared-runtime'; + +component Example() { + const data = useFragment(); + + const {a, b} = identity(data); + + const el = ; + + identity(a.at(0)); + + return ; +} From 82d774e3cc8ec4b18e7a31ed247d7f873ddba23e Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 15:10:01 -0700 Subject: [PATCH 05/12] [compiler] moduleTypeProvider support for aliasing signatures This allows us to type things like `nullthrows()` or `identity()` functions where the return type is polymorphic on the input. --- .../src/HIR/Globals.ts | 110 +++++++++++++++- .../src/HIR/TypeSchema.ts | 84 +++++++++++++ ...d-identity-function-frozen-input.expect.md | 117 ++++++++++++++++++ .../typed-identity-function-frozen-input.js | 39 ++++++ ...-identity-function-mutable-input.expect.md | 112 +++++++++++++++++ .../typed-identity-function-mutable-input.js | 35 ++++++ .../sprout/shared-runtime-type-provider.ts | 16 +++ .../snap/src/sprout/shared-runtime.ts | 4 + 8 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index c4c85be147..83f744cf68 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -5,7 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR'; +import { + Effect, + GeneratedSource, + makeIdentifierId, + Place, + ValueKind, + ValueReason, +} from './HIR'; import { BUILTIN_SHAPES, BuiltInArrayId, @@ -37,10 +44,15 @@ import { signatureArgument, } from './ObjectShape'; import {BuiltInType, ObjectType, PolyType} from './Types'; -import {TypeConfig} from './TypeSchema'; +import { + AliasingEffectConfig, + AliasingSignatureConfig, + TypeConfig, +} from './TypeSchema'; import {assertExhaustive} from '../Utils/utils'; import {isHookName} from './Environment'; import {CompilerError, SourceLocation} from '..'; +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; /* * This file exports types and defaults for JavaScript global objects. @@ -891,6 +903,10 @@ export function installTypeConfig( } } case 'function': { + const aliasing = + typeConfig.aliasing != null + ? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc) + : null; return addFunction(shapes, [], { positionalParams: typeConfig.positionalParams, restParam: typeConfig.restParam, @@ -906,9 +922,14 @@ export function installTypeConfig( noAlias: typeConfig.noAlias === true, mutableOnlyIfOperandsAreMutable: typeConfig.mutableOnlyIfOperandsAreMutable === true, + aliasing, }); } case 'hook': { + const aliasing = + typeConfig.aliasing != null + ? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc) + : null; return addHook(shapes, { hookKind: 'Custom', positionalParams: typeConfig.positionalParams ?? [], @@ -923,6 +944,7 @@ export function installTypeConfig( ), returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen, noAlias: typeConfig.noAlias === true, + aliasing, }); } case 'object': { @@ -965,6 +987,90 @@ export function installTypeConfig( } } +function parseAliasingSignatureConfig( + typeConfig: AliasingSignatureConfig, + moduleName: string, + loc: SourceLocation, +): AliasingSignature { + const lifetimes = new Map(); + function define(temp: string): Place { + CompilerError.invariant(!lifetimes.has(temp), { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`, + loc, + }); + const place = signatureArgument(lifetimes.size); + lifetimes.set(temp, place); + return place; + } + function lookup(temp: string): Place { + const place = lifetimes.get(temp); + CompilerError.invariant(place != null, { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`, + loc, + }); + return place; + } + const receiver = define(typeConfig.receiver); + const params = typeConfig.params.map(define); + const rest = typeConfig.rest != null ? define(typeConfig.rest) : null; + const returns = define(typeConfig.returns); + const temporaries = typeConfig.temporaries.map(define); + const effects = typeConfig.effects.map( + (effect: AliasingEffectConfig): AliasingEffect => { + switch (effect.kind) { + case 'Assign': { + return { + kind: 'Assign', + from: lookup(effect.from), + into: lookup(effect.into), + }; + } + case 'Create': { + return { + kind: 'Create', + into: lookup(effect.into), + reason: ValueReason.KnownReturnSignature, + value: effect.value, + }; + } + case 'Freeze': { + return { + kind: 'Freeze', + value: lookup(effect.value), + reason: ValueReason.KnownReturnSignature, + }; + } + case 'Impure': { + return { + kind: 'Impure', + place: lookup(effect.place), + error: CompilerError.throwTodo({ + reason: 'Support impure effect declarations', + loc: GeneratedSource, + }), + }; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); + } + } + }, + ); + return { + receiver: receiver.identifier.id, + params: params.map(p => p.identifier.id), + rest: rest != null ? rest.identifier.id : null, + returns: returns.identifier.id, + temporaries, + effects, + }; +} + export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType { // hooks that freeze args and return frozen value const frozenHooks = [ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 9aac2a264f..5ed39da0d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -31,6 +31,86 @@ export const ObjectTypeSchema: z.ZodType = z.object({ properties: ObjectPropertiesSchema.nullable(), }); +export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), { + message: "Placeholder names must start with '@'", +}); + +export type FreezeEffectConfig = { + kind: 'Freeze'; + value: string; +}; + +export const FreezeEffectSchema: z.ZodType = z.object({ + kind: z.literal('Freeze'), + value: LifetimeIdSchema, +}); + +export type CreateEffectConfig = { + kind: 'Create'; + into: string; + value: ValueKind; +}; + +export const CreateEffectSchema: z.ZodType = z.object({ + kind: z.literal('Create'), + into: LifetimeIdSchema, + value: ValueKindSchema, +}); + +export type AssignEffectConfig = { + kind: 'Assign'; + from: string; + into: string; +}; + +export const AssignEffectSchema: z.ZodType = z.object({ + kind: z.literal('Assign'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type ImpureEffectConfig = { + kind: 'Impure'; + place: string; +}; + +export const ImpureEffectSchema: z.ZodType = z.object({ + kind: z.literal('Impure'), + place: LifetimeIdSchema, +}); + +export type AliasingEffectConfig = + | FreezeEffectConfig + | CreateEffectConfig + | AssignEffectConfig + | ImpureEffectConfig; + +export const AliasingEffectSchema: z.ZodType = z.union([ + FreezeEffectSchema, + CreateEffectSchema, + AssignEffectSchema, + ImpureEffectSchema, +]); + +export type AliasingSignatureConfig = { + receiver: string; + params: Array; + rest: string | null; + returns: string; + effects: Array; + temporaries: Array; +}; + +export const AliasingSignatureSchema: z.ZodType = + z.object({ + receiver: LifetimeIdSchema, + params: z.array(LifetimeIdSchema), + rest: LifetimeIdSchema.nullable(), + returns: LifetimeIdSchema, + effects: z.array(AliasingEffectSchema), + temporaries: z.array(LifetimeIdSchema), + }); + export type FunctionTypeConfig = { kind: 'function'; positionalParams: Array; @@ -42,6 +122,7 @@ export type FunctionTypeConfig = { mutableOnlyIfOperandsAreMutable?: boolean | null | undefined; impure?: boolean | null | undefined; canonicalName?: string | null | undefined; + aliasing?: AliasingSignatureConfig | null | undefined; }; export const FunctionTypeSchema: z.ZodType = z.object({ kind: z.literal('function'), @@ -54,6 +135,7 @@ export const FunctionTypeSchema: z.ZodType = z.object({ mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(), impure: z.boolean().nullable().optional(), canonicalName: z.string().nullable().optional(), + aliasing: AliasingSignatureSchema.nullable().optional(), }); export type HookTypeConfig = { @@ -63,6 +145,7 @@ export type HookTypeConfig = { returnType: TypeConfig; returnValueKind?: ValueKind | null | undefined; noAlias?: boolean | null | undefined; + aliasing?: AliasingSignatureConfig | null | undefined; }; export const HookTypeSchema: z.ZodType = z.object({ kind: z.literal('hook'), @@ -71,6 +154,7 @@ export const HookTypeSchema: z.ZodType = z.object({ returnType: z.lazy(() => TypeSchema), returnValueKind: ValueKindSchema.nullable().optional(), noAlias: z.boolean().nullable().optional(), + aliasing: AliasingSignatureSchema.nullable().optional(), }); export type BuiltInTypeConfig = diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md new file mode 100644 index 0000000000..b15248df07 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md @@ -0,0 +1,117 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // freeze the value + useIdentity(x); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // but x2 is frozen so this downgrades to a read. + // x should *not* take b as a dependency + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = makeObject_Primitives(a); + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + + useIdentity(x); + + const x2 = typedIdentity(x); + + identity(x2, b); + let t2; + if ($[2] !== a) { + t2 = [a]; + $[2] = a; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== x) { + t3 = ; + $[4] = t2; + $[5] = x; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 0, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js new file mode 100644 index 0000000000..bcf6ecef02 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js @@ -0,0 +1,39 @@ +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // freeze the value + useIdentity(x); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // but x2 is frozen so this downgrades to a read. + // x should *not* take b as a dependency + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md new file mode 100644 index 0000000000..17fed05d93 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md @@ -0,0 +1,112 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // and x is still mutable so + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = makeObject_Primitives(a); + + const x2 = typedIdentity(x); + + identity(x2, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 0, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js new file mode 100644 index 0000000000..719c89d11d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js @@ -0,0 +1,35 @@ +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // and x is still mutable so + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index 4c1d77f2f8..449bc8e688 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -69,6 +69,22 @@ export function makeSharedRuntimeTypeProvider({ returnValueKind: ValueKindEnum.Mutable, noAlias: true, }, + typedIdentity: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'Assign', from: '@value', into: '@return'}], + }, + }, }, }; } else if (moduleName === 'ReactCompilerTest') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 569d31cbd4..c8bf9272a2 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -396,4 +396,8 @@ export function typedLog(...values: Array): void { console.log(...values); } +export function typedIdentity(value: T): T { + return value; +} + export default typedLog; From dba1d7f712730c019292ce6232822ce651da779c Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 06/12] [compiler] More readable alias signature declarations Now that we have support for defining aliasing signatures in moduleTypeProvider, which uses string names for receiver/args/returns/etc, we can reuse that same form for builtin declarations. The declarations are written in the unparsed form and than parsed/validated when registered (in the addFunction/addHook call). This also required flushing out configs/schemas for more effect types. --- .../src/HIR/Globals.ts | 131 +--------- .../src/HIR/HIR.ts | 14 ++ .../src/HIR/ObjectShape.ts | 231 ++++++++++++++---- .../src/HIR/TypeSchema.ts | 117 ++++++++- 4 files changed, 327 insertions(+), 166 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 83f744cf68..13f87528b2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -5,14 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { - Effect, - GeneratedSource, - makeIdentifierId, - Place, - ValueKind, - ValueReason, -} from './HIR'; +import {Effect, ValueKind, ValueReason} from './HIR'; import { BUILTIN_SHAPES, BuiltInArrayId, @@ -41,18 +34,12 @@ import { addFunction, addHook, addObject, - signatureArgument, } from './ObjectShape'; import {BuiltInType, ObjectType, PolyType} from './Types'; -import { - AliasingEffectConfig, - AliasingSignatureConfig, - TypeConfig, -} from './TypeSchema'; +import {TypeConfig} from './TypeSchema'; import {assertExhaustive} from '../Utils/utils'; import {isHookName} from './Environment'; import {CompilerError, SourceLocation} from '..'; -import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; /* * This file exports types and defaults for JavaScript global objects. @@ -658,35 +645,35 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useEffect', returnValueKind: ValueKind.Frozen, aliasing: { - receiver: makeIdentifierId(0), + receiver: '@receiver', params: [], - rest: makeIdentifierId(1), - returns: makeIdentifierId(2), - temporaries: [signatureArgument(3)], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], effects: [ // Freezes the function and deps { kind: 'Freeze', - value: signatureArgument(1), + value: '@rest', reason: ValueReason.Effect, }, // Internally creates an effect object that captures the function and deps { kind: 'Create', - into: signatureArgument(3), + into: '@effect', value: ValueKind.Frozen, reason: ValueReason.KnownReturnSignature, }, // The effect stores the function and dependencies { kind: 'Capture', - from: signatureArgument(1), - into: signatureArgument(3), + from: '@rest', + into: '@effect', }, // Returns undefined { kind: 'Create', - into: signatureArgument(2), + into: '@returns', value: ValueKind.Primitive, reason: ValueReason.KnownReturnSignature, }, @@ -903,10 +890,6 @@ export function installTypeConfig( } } case 'function': { - const aliasing = - typeConfig.aliasing != null - ? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc) - : null; return addFunction(shapes, [], { positionalParams: typeConfig.positionalParams, restParam: typeConfig.restParam, @@ -922,14 +905,10 @@ export function installTypeConfig( noAlias: typeConfig.noAlias === true, mutableOnlyIfOperandsAreMutable: typeConfig.mutableOnlyIfOperandsAreMutable === true, - aliasing, + aliasing: typeConfig.aliasing, }); } case 'hook': { - const aliasing = - typeConfig.aliasing != null - ? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc) - : null; return addHook(shapes, { hookKind: 'Custom', positionalParams: typeConfig.positionalParams ?? [], @@ -944,7 +923,7 @@ export function installTypeConfig( ), returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen, noAlias: typeConfig.noAlias === true, - aliasing, + aliasing: typeConfig.aliasing, }); } case 'object': { @@ -987,90 +966,6 @@ export function installTypeConfig( } } -function parseAliasingSignatureConfig( - typeConfig: AliasingSignatureConfig, - moduleName: string, - loc: SourceLocation, -): AliasingSignature { - const lifetimes = new Map(); - function define(temp: string): Place { - CompilerError.invariant(!lifetimes.has(temp), { - reason: `Invalid type configuration for module`, - description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`, - loc, - }); - const place = signatureArgument(lifetimes.size); - lifetimes.set(temp, place); - return place; - } - function lookup(temp: string): Place { - const place = lifetimes.get(temp); - CompilerError.invariant(place != null, { - reason: `Invalid type configuration for module`, - description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`, - loc, - }); - return place; - } - const receiver = define(typeConfig.receiver); - const params = typeConfig.params.map(define); - const rest = typeConfig.rest != null ? define(typeConfig.rest) : null; - const returns = define(typeConfig.returns); - const temporaries = typeConfig.temporaries.map(define); - const effects = typeConfig.effects.map( - (effect: AliasingEffectConfig): AliasingEffect => { - switch (effect.kind) { - case 'Assign': { - return { - kind: 'Assign', - from: lookup(effect.from), - into: lookup(effect.into), - }; - } - case 'Create': { - return { - kind: 'Create', - into: lookup(effect.into), - reason: ValueReason.KnownReturnSignature, - value: effect.value, - }; - } - case 'Freeze': { - return { - kind: 'Freeze', - value: lookup(effect.value), - reason: ValueReason.KnownReturnSignature, - }; - } - case 'Impure': { - return { - kind: 'Impure', - place: lookup(effect.place), - error: CompilerError.throwTodo({ - reason: 'Support impure effect declarations', - loc: GeneratedSource, - }), - }; - } - default: { - assertExhaustive( - effect, - `Unexpected effect kind '${(effect as any).kind}'`, - ); - } - } - }, - ); - return { - receiver: receiver.identifier.id, - params: params.map(p => p.identifier.id), - rest: rest != null ? rest.identifier.id : null, - returns: returns.identifier.id, - temporaries, - effects, - }; -} - export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType { // hooks that freeze args and return frozen value const frozenHooks = [ 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 33c978c145..15ee2f33a1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1453,6 +1453,20 @@ export const ValueKindSchema = z.enum([ ValueKind.Context, ]); +export const ValueReasonSchema = z.enum([ + ValueReason.Context, + ValueReason.Effect, + ValueReason.Global, + ValueReason.HookCaptured, + ValueReason.HookReturn, + ValueReason.JsxCaptured, + ValueReason.KnownReturnSignature, + ValueReason.Other, + ValueReason.ReactiveFunctionArgument, + ValueReason.ReducerState, + ValueReason.State, +]); + // The effect with which a value is modified. export enum Effect { // Default value: not allowed after lifetime inference diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index eb80e5c59d..4b67dc647f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -6,14 +6,18 @@ */ import {CompilerError} from '../CompilerError'; -import {AliasingSignature} from '../Inference/AliasingEffects'; +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; +import {assertExhaustive} from '../Utils/utils'; import { Effect, GeneratedSource, + Hole, makeDeclarationId, makeIdentifierId, makeInstructionId, Place, + SourceLocation, + SpreadPattern, ValueKind, ValueReason, } from './HIR'; @@ -25,6 +29,7 @@ import { PolyType, PrimitiveType, } from './Types'; +import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema'; /* * This file exports types and defaults for JavaScript object shapes. These are @@ -53,13 +58,20 @@ function createAnonId(): string { export function addFunction( registry: ShapeRegistry, properties: Iterable<[string, BuiltInType | PolyType]>, - fn: Omit, + fn: Omit & { + aliasing?: AliasingSignatureConfig | null | undefined; + }, id: string | null = null, isConstructor: boolean = false, ): FunctionType { const shapeId = id ?? createAnonId(); + const aliasing = + fn.aliasing != null + ? parseAliasingSignatureConfig(fn.aliasing, '', GeneratedSource) + : null; addShape(registry, shapeId, properties, { ...fn, + aliasing, hookKind: null, }); return { @@ -77,11 +89,18 @@ export function addFunction( */ export function addHook( registry: ShapeRegistry, - fn: FunctionSignature & {hookKind: HookKind}, + fn: Omit & { + hookKind: HookKind; + aliasing?: AliasingSignatureConfig | null | undefined; + }, id: string | null = null, ): FunctionType { const shapeId = id ?? createAnonId(); - addShape(registry, shapeId, [], fn); + const aliasing = + fn.aliasing != null + ? parseAliasingSignatureConfig(fn.aliasing, '', GeneratedSource) + : null; + addShape(registry, shapeId, [], {...fn, aliasing}); return { kind: 'Function', return: fn.returnType, @@ -90,6 +109,129 @@ export function addHook( }; } +function parseAliasingSignatureConfig( + typeConfig: AliasingSignatureConfig, + moduleName: string, + loc: SourceLocation, +): AliasingSignature { + const lifetimes = new Map(); + function define(temp: string): Place { + CompilerError.invariant(!lifetimes.has(temp), { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`, + loc, + }); + const place = signatureArgument(lifetimes.size); + lifetimes.set(temp, place); + return place; + } + function lookup(temp: string): Place { + const place = lifetimes.get(temp); + CompilerError.invariant(place != null, { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`, + loc, + }); + return place; + } + const receiver = define(typeConfig.receiver); + const params = typeConfig.params.map(define); + const rest = typeConfig.rest != null ? define(typeConfig.rest) : null; + const returns = define(typeConfig.returns); + const temporaries = typeConfig.temporaries.map(define); + const effects = typeConfig.effects.map( + (effect: AliasingEffectConfig): AliasingEffect => { + switch (effect.kind) { + case 'CreateFrom': + case 'Capture': + case 'Alias': + case 'Assign': { + const from = lookup(effect.from); + const into = lookup(effect.into); + return { + kind: effect.kind, + from, + into, + }; + } + case 'Mutate': + case 'MutateTransitiveConditionally': { + const value = lookup(effect.value); + return {kind: effect.kind, value}; + } + case 'Create': { + const into = lookup(effect.into); + return { + kind: 'Create', + into, + reason: effect.reason, + value: effect.value, + }; + } + case 'Freeze': { + const value = lookup(effect.value); + return { + kind: 'Freeze', + value, + reason: effect.reason, + }; + } + case 'Impure': { + const place = lookup(effect.place); + return { + kind: 'Impure', + place, + error: CompilerError.throwTodo({ + reason: 'Support impure effect declarations', + loc: GeneratedSource, + }), + }; + } + case 'Apply': { + const receiver = lookup(effect.receiver); + const fn = lookup(effect.function); + const args: Array = effect.args.map( + arg => { + if (typeof arg === 'string') { + return lookup(arg); + } else if (arg.kind === 'Spread') { + return {kind: 'Spread', place: lookup(arg.place)}; + } else { + return arg; + } + }, + ); + const into = lookup(effect.into); + return { + kind: 'Apply', + receiver, + function: fn, + mutatesFunction: effect.mutatesFunction, + args, + into, + loc, + signature: null, + }; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); + } + } + }, + ); + return { + receiver: receiver.identifier.id, + params: params.map(p => p.identifier.id), + rest: rest != null ? rest.identifier.id : null, + returns: returns.identifier.id, + temporaries, + effects, + }; +} + /* * Add an object to an existing ShapeRegistry. * @@ -192,8 +334,7 @@ export type FunctionSignature = { canonicalName?: string; - aliasing?: AliasingSignature | null; - todo_aliasing?: AliasingSignature | null; + aliasing?: AliasingSignature | null | undefined; }; /* @@ -320,24 +461,24 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [ calleeEffect: Effect.Store, returnValueKind: ValueKind.Primitive, aliasing: { - receiver: makeIdentifierId(0), + receiver: '@receiver', params: [], - rest: makeIdentifierId(1), - returns: makeIdentifierId(2), + rest: '@rest', + returns: '@returns', temporaries: [], effects: [ // Push directly mutates the array itself - {kind: 'Mutate', value: signatureArgument(0)}, + {kind: 'Mutate', value: '@receiver'}, // The arguments are captured into the array { kind: 'Capture', - from: signatureArgument(1), - into: signatureArgument(0), + from: '@rest', + into: '@receiver', }, // Returns the new length, a primitive { kind: 'Create', - into: signatureArgument(2), + into: '@returns', value: ValueKind.Primitive, reason: ValueReason.KnownReturnSignature, }, @@ -374,58 +515,56 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [ noAlias: true, mutableOnlyIfOperandsAreMutable: true, aliasing: { - receiver: makeIdentifierId(0), - params: [makeIdentifierId(1)], + receiver: '@receiver', + params: ['@callback'], rest: null, - returns: makeIdentifierId(2), + returns: '@returns', temporaries: [ // Temporary representing captured items of the receiver - signatureArgument(3), + '@item', // Temporary representing the result of the callback - signatureArgument(4), + '@callbackReturn', /* * Undefined `this` arg to the callback. Note the signature does not * support passing an explicit thisArg second param */ - signatureArgument(5), + '@thisArg', ], effects: [ // Map creates a new mutable array { kind: 'Create', - into: signatureArgument(2), + into: '@returns', value: ValueKind.Mutable, reason: ValueReason.KnownReturnSignature, }, // The first arg to the callback is an item extracted from the receiver array { kind: 'CreateFrom', - from: signatureArgument(0), - into: signatureArgument(3), + from: '@receiver', + into: '@item', }, // The undefined this for the callback { kind: 'Create', - into: signatureArgument(5), + into: '@thisArg', value: ValueKind.Primitive, reason: ValueReason.KnownReturnSignature, }, // calls the callback, returning the result into a temporary { kind: 'Apply', - receiver: signatureArgument(5), - args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)], - function: signatureArgument(1), - into: signatureArgument(4), - signature: null, + receiver: '@thisArg', + args: ['@item', {kind: 'Hole'}, '@receiver'], + function: '@callback', + into: '@callbackReturn', mutatesFunction: false, - loc: GeneratedSource, }, // captures the result of the callback into the return array { kind: 'Capture', - from: signatureArgument(4), - into: signatureArgument(2), + from: '@callbackReturn', + into: '@returns', }, ], }, @@ -577,28 +716,28 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [ // returnValueKind is technically dependent on the ValueKind of the set itself returnValueKind: ValueKind.Mutable, aliasing: { - receiver: makeIdentifierId(0), + receiver: '@receiver', params: [], - rest: makeIdentifierId(1), - returns: makeIdentifierId(2), + rest: '@rest', + returns: '@returns', temporaries: [], effects: [ // Set.add returns the receiver Set { kind: 'Assign', - from: signatureArgument(0), - into: signatureArgument(2), + from: '@receiver', + into: '@returns', }, // Set.add mutates the set itself { kind: 'Mutate', - value: signatureArgument(0), + value: '@receiver', }, // Captures the rest params into the set { kind: 'Capture', - from: signatureArgument(1), - into: signatureArgument(0), + from: '@rest', + into: '@receiver', }, ], }, @@ -1303,30 +1442,30 @@ export const DefaultNonmutatingHook = addHook( hookKind: 'Custom', returnValueKind: ValueKind.Frozen, aliasing: { - receiver: makeIdentifierId(0), + receiver: '@receiver', params: [], - rest: makeIdentifierId(1), - returns: makeIdentifierId(2), + rest: '@rest', + returns: '@returns', temporaries: [], effects: [ // Freeze the arguments { kind: 'Freeze', - value: signatureArgument(1), + value: '@rest', reason: ValueReason.HookCaptured, }, // Returns a frozen value { kind: 'Create', - into: signatureArgument(2), + into: '@returns', value: ValueKind.Frozen, reason: ValueReason.HookReturn, }, // May alias any arguments into the return { kind: 'Alias', - from: signatureArgument(1), - into: signatureArgument(2), + from: '@rest', + into: '@returns', }, ], }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 5ed39da0d9..5945e3a078 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -8,7 +8,12 @@ import {isValidIdentifier} from '@babel/types'; import {z} from 'zod'; import {Effect, ValueKind} from '..'; -import {EffectSchema, ValueKindSchema} from './HIR'; +import { + EffectSchema, + ValueKindSchema, + ValueReason, + ValueReasonSchema, +} from './HIR'; export type ObjectPropertiesConfig = {[key: string]: TypeConfig}; export const ObjectPropertiesSchema: z.ZodType = z @@ -38,23 +43,48 @@ export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), { export type FreezeEffectConfig = { kind: 'Freeze'; value: string; + reason: ValueReason; }; export const FreezeEffectSchema: z.ZodType = z.object({ kind: z.literal('Freeze'), value: LifetimeIdSchema, + reason: ValueReasonSchema, }); +export type MutateEffectConfig = { + kind: 'Mutate'; + value: string; +}; + +export const MutateEffectSchema: z.ZodType = z.object({ + kind: z.literal('Mutate'), + value: LifetimeIdSchema, +}); + +export type MutateTransitiveConditionallyConfig = { + kind: 'MutateTransitiveConditionally'; + value: string; +}; + +export const MutateTransitiveConditionallySchema: z.ZodType = + z.object({ + kind: z.literal('MutateTransitiveConditionally'), + value: LifetimeIdSchema, + }); + export type CreateEffectConfig = { kind: 'Create'; into: string; value: ValueKind; + reason: ValueReason; }; export const CreateEffectSchema: z.ZodType = z.object({ kind: z.literal('Create'), into: LifetimeIdSchema, value: ValueKindSchema, + reason: ValueReasonSchema, }); export type AssignEffectConfig = { @@ -69,6 +99,77 @@ export const AssignEffectSchema: z.ZodType = z.object({ into: LifetimeIdSchema, }); +export type AliasEffectConfig = { + kind: 'Alias'; + from: string; + into: string; +}; + +export const AliasEffectSchema: z.ZodType = z.object({ + kind: z.literal('Alias'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type CaptureEffectConfig = { + kind: 'Capture'; + from: string; + into: string; +}; + +export const CaptureEffectSchema: z.ZodType = z.object({ + kind: z.literal('Capture'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type CreateFromEffectConfig = { + kind: 'CreateFrom'; + from: string; + into: string; +}; + +export const CreateFromEffectSchema: z.ZodType = + z.object({ + kind: z.literal('CreateFrom'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, + }); + +export type ApplyArgConfig = + | string + | {kind: 'Spread'; place: string} + | {kind: 'Hole'}; + +export const ApplyArgSchema: z.ZodType = z.union([ + LifetimeIdSchema, + z.object({ + kind: z.literal('Spread'), + place: LifetimeIdSchema, + }), + z.object({ + kind: z.literal('Hole'), + }), +]); + +export type ApplyEffectConfig = { + kind: 'Apply'; + receiver: string; + function: string; + mutatesFunction: boolean; + args: Array; + into: string; +}; + +export const ApplyEffectSchema: z.ZodType = z.object({ + kind: z.literal('Apply'), + receiver: LifetimeIdSchema, + function: LifetimeIdSchema, + mutatesFunction: z.boolean(), + args: z.array(ApplyArgSchema), + into: LifetimeIdSchema, +}); + export type ImpureEffectConfig = { kind: 'Impure'; place: string; @@ -82,14 +183,26 @@ export const ImpureEffectSchema: z.ZodType = z.object({ export type AliasingEffectConfig = | FreezeEffectConfig | CreateEffectConfig + | CreateFromEffectConfig | AssignEffectConfig - | ImpureEffectConfig; + | AliasEffectConfig + | CaptureEffectConfig + | ImpureEffectConfig + | MutateEffectConfig + | MutateTransitiveConditionallyConfig + | ApplyEffectConfig; export const AliasingEffectSchema: z.ZodType = z.union([ FreezeEffectSchema, CreateEffectSchema, + CreateFromEffectSchema, AssignEffectSchema, + AliasEffectSchema, + CaptureEffectSchema, ImpureEffectSchema, + MutateEffectSchema, + MutateTransitiveConditionallySchema, + ApplyEffectSchema, ]); export type AliasingSignatureConfig = { From c33109e72094fcff43661428b7d175d34cc20bce Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 07/12] [compiler] Rename InferFunctionExprAliasingEffectsSignature --- .../src/Inference/AnalyseFunctions.ts | 4 ++-- ....ts => InferFunctionExpressionAliasingEffectsSignature.ts} | 2 +- .../src/Inference/InferMutationAliasingRanges.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename compiler/packages/babel-plugin-react-compiler/src/Inference/{InferMutationAliasingFunctionEffects.ts => InferFunctionExpressionAliasingEffectsSignature.ts} (98%) 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 c8726f4e4b..2bd46abc1f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -22,7 +22,7 @@ import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; import {assertExhaustive} from '../Utils/utils'; import {inferMutationAliasingEffects} from './InferMutationAliasingEffects'; -import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects'; +import {inferFunctionExpressionAliasingEffectsSignature} from './InferFunctionExpressionAliasingEffectsSignature'; import {inferMutationAliasingRanges} from './InferMutationAliasingRanges'; export default function analyseFunctions(func: HIRFunction): void { @@ -71,7 +71,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void { inferMutationAliasingRanges(fn, {isFunctionExpression: true}); rewriteInstructionKindsBasedOnReassignment(fn); inferReactiveScopeVariables(fn); - const effects = inferMutationAliasingFunctionEffects(fn); + const effects = inferFunctionExpressionAliasingEffectsSignature(fn); fn.env.logger?.debugLogIRs?.({ kind: 'hir', name: 'AnalyseFunction (inner)', diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingFunctionEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts similarity index 98% rename from compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingFunctionEffects.ts rename to compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts index 678c958ad9..818f4dae67 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingFunctionEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts @@ -28,7 +28,7 @@ import {AliasingEffect} from './AliasingEffects'; * This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render) * from instructions within the function up to the function itself. */ -export function inferMutationAliasingFunctionEffects( +export function inferFunctionExpressionAliasingEffectsSignature( fn: HIRFunction, ): Array | null { const effects: Array = []; 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 64f8cf2431..e0fb84fe5a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -25,7 +25,7 @@ import { import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; import {printFunction} from '../HIR'; import {printIdentifier, printPlace} from '../HIR/PrintHIR'; -import {MutationKind} from './InferMutationAliasingFunctionEffects'; +import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature'; import {Result} from '../Utils/Result'; const DEBUG = false; From 730cf6a89092ce670250d0d5504f5a8ec2aad09b Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 08/12] [compiler] Docs describing new inference model Start of docs describing the effects and the inference rules. --- .../Inference/MUTABILITY_ALIASING_MODEL.md | 524 ++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md 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 new file mode 100644 index 0000000000..f17f345d24 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md @@ -0,0 +1,524 @@ +# The Mutability & Aliasing Model + +This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes). + +To build an intuition, consider the following example: + +```js +function Component() { + // a is created and mutated over the course of these two instructions: + const a = {}; + mutate(a); + + // b and c are created and mutated together — mutate might modify b via c + const b = {}; + const c = {b}; + mutate(c); + + // does not modify a/b/c + return +} +``` + +The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c. + +In code, the mutability and aliasing model is compromised of the following phases: + +* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. +* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values. +* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values. + +Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions. + +## Mutation and Aliasing Effects + +The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time + +### Creation Effects + +#### Create + +```js +{ + kind: 'Create'; + into: Place; + value: ValueKind; + reason: ValueReason; +} +``` + +Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`. + +#### CreateFunction + +```js +{ + kind: 'CreateFunction'; + captures: Array; + function: FunctionExpression | ObjectMethod; + into: Place; +} +``` + +Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`. + +#### Apply + +```js +{ + kind: 'Apply'; + receiver: Place; + function: Place; // same as receiver for function calls + mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default + args: Array; + into: Place; // where result is stored + signature: FunctionSignature | null; +} +``` + +Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine: + +* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments. +* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects. +* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those. +* Else fall back to inferring a generic set of effects. + +The generic fallback is to assume: +- The return value may alias any of the arguments (Alias param -> return) +- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param) +- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M) + +### Aliasing Effects + +These effects describe data-flow only, separately from mutation or other state-changing semantics. + +#### Assign + +```js +{ + kind: 'Assign'; + from: Place; + into: Place; +} +``` + +Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value. + +> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect + +#### Alias + +```js +{ + kind: 'Alias'; + from: Place; + into: Place; +} +``` + +Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value. + +#### Capture + +```js +{ + kind: 'Capture'; + from: Place; + into: Place; +} +``` + +Describes that a reference to one variable (from) is stored within another value (into). Examples include: +- An array expression captures the items of the array (`array = [capturedValue]`) +- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`) +- Property assignment captures the value onto the object (`object.property = capturedValue`) + +#### CreateFrom + +```js +{ + kind: 'CreateFrom'; + from: Place; + into: Place; +} +``` + +This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include: + +- Indexing into an array (`createdFrom = array[0]`) +- Reading an object property (`createdFrom = object.property`) +- Getting a Map key (`createdFrom = map.get(key)`) + +#### ImmutableCapture + +Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis. + +### State-Changing Effects + +The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`). + +#### Freeze + +```js +{ + kind: 'Freeze', + // The reference being frozen + value: Place; + // The reason the value is frozen (passed to a hook, passed to jsx, etc) + reason: ValueReason; +} +``` + +Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update: + +```js +// INVALID DO NOT DO THIS +function Component(props) { + const array = useArray(props.value); + // OOPS! this value is memoized, the array won't get re-created + // when `props.value` changes, so we might just keep pushing new + // values to the same array on every render! + array.push(props.otherValue); +} + +function useArray(a) { + return useMemo(() => [a], [a]); +} +``` + +The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example: + +```js +const x = {}; +const y = []; +x.y = y; +freeze(y); // y _reference_ is frozen +x.y.push(props.value); // but y is still considered mutable bc of this +``` + +#### Mutate (and MutateConditionally) + +```js +{ + kind: 'Mutate'; + value: Place; +} +``` + +Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include: + +- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!) +- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties. + +This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures. + +`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness. + + + +#### MutateTransitiveConditionally (and MutateTransitive) + +`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them. + +There is also `MutateTransitive` for completeness, but this is not generally used. + +### Side Effects + +Finally, there are a few effects that describe error, or potential error, conditions: + +- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated. +- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect. +- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect. + - TODO: we could probably merge this and MutateGlobal +- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs. + + +## Rules + +### Mutation of Alias Mutates the Source Value + +``` +Alias a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = maybeIdentity(b); // Alias a <- b +a.property = value; // a could be b, so this mutates b +``` + +### Mutation of Assignment Mutates the Source Value + +``` +Assign a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = b; +a.property = value // a _is_ b, this mutates b +``` + +### Mutation of CreateFrom Mutates the Source Value + +``` +CreateFrom a <- b +Mutate A +=> +MutateTransitive b +``` + +Example: + +```js +const a = b[index]; +a.property = value // the contents of b are transitively mutated +``` + + +### Mutation of Capture Does *Not* Mutate the Source Value + +``` +Capture a <- b +Mutate a +!=> +~Mutate b~ +``` + +Example: + +```js +const a = {}; +a.b = b; +a.property = value; // mutates a, not b +``` + +### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source + +``` +Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b +MutateTransitive a +=> +MutateTransitive b +``` + +Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then: + +```js +const a = b; // assign +mutate(a); // clearly can transitively mutate b + +const a = maybeIdentity(b); // alias +mutate(a); // clearly can transitively mutate b + +const a = b[index]; // createfrom +mutate(a); // clearly can transitively mutate b + +const a = {}; +a.b = b; // capture +mutate(a); // can transitively mutate b +``` + +### Freeze Does Not Freeze the Value + +Freeze does not freeze the value itself: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +!=> +~Freeze x~ +``` + +This means that subsequent mutations of the original value are valid: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +Mutate x +=> +Mutate x (mutation is ok) +``` + +As well as mutations through other assignments/aliases/captures/createfroms of the original value: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x +Mutate z +=> +Mutate x (mutation is ok) +``` + +### Freeze Freezes The Reference + +Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate. + +Conditional mutations of the reference are no-ops: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +MutateConditional y +=> +(no mutation) +``` + +And known mutations of the reference are errors: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +MutateConditional y +=> +MutateFrozen y error=... +``` + +### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture + +A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables. + +In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example: + +```js +const f = (x) => { + const y = [x]; // capture y <- x + const z = y[0]; // createfrom z <- y + return z; // assign return <- z +} +// return <- x +``` + +In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source. + +#### Each of the effects is transitive with itself + +``` +Assign b <- a +Assign c <- b +=> +Assign c <- a +``` + +``` +Alias b <- a +Alias c <- b +=> +Alias c <- a +``` + +``` +CreateFrom b <- a +CreateFrom c <- b +=> +CreateFrom c <- a +``` + +``` +Capture b <- a +Capture c <- b +=> +Capture c <- a +``` + +#### Alias > Assign + +``` +Assign b <- a +Alias c <- b +=> +Alias c <- a +``` + +``` +Alias b <- a +Assign c <- b +=> +Alias c <- a +``` + +### CreateFrom > Assign/Alias + +Intuition: + +``` +CreateFrom b <- a +Alias c <- b OR Assign c <- b +=> +CreateFrom c <- a +``` + +``` +Alias b <- a OR Assign b <- a +CreateFrom c <- b +=> +CreateFrom c <- a +``` + +### Capture > Assign/Alias + +Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture. + +``` +Capture b <- a +Alias c <- b OR Assign c <- b +=> +Capture c <- a +``` + +``` +Alias b <- a OR Assign b <- a +Capture c <- b +=> +Capture c <- a +``` + +### Capture And CreateFrom + +Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations: + +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 +``` + +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. + +``` +Capture b <- a +CreateFrom c <- b +=> +Alias c <- a +``` + +Meanwhile the opposite direction preservers 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 +``` + +``` +Capture b <- a +CreateFrom c <- b +=> +Capture b <- a +``` \ No newline at end of file From 4e4c353e2ad7177cdce64dfd5409f2e73c84666b Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 09/12] [compiler] Tests for different orders of createfrom/capture w/wo function expressions Adds some typed helpers to represent aliasing, assign, capture, createfrom, and mutate effects along with representative runtime behavior, and then adds tests to demonstrate that we model capture->createfrom and createfrom->capture correctly. There is one case (createfrom->capture in a lambda) where we infer a less precise effect, but in the more conservative direction (we include more code/deps than necesssary rather than fewer). --- .../Inference/MUTABILITY_ALIASING_MODEL.md | 24 ++- ...-control-flow-sensitive-mutation.expect.md | 166 ++++++++++++++++++ .../todo-control-flow-sensitive-mutation.tsx | 46 +++++ ...tivity-createfrom-capture-lambda.expect.md | 116 ++++++++++++ ...transitivity-createfrom-capture-lambda.tsx | 33 ++++ ...ity-add-captured-array-to-itself.expect.md | 153 ++++++++++++++++ ...nsitivity-add-captured-array-to-itself.tsx | 34 ++++ ...tivity-capture-createfrom-lambda.expect.md | 115 ++++++++++++ ...transitivity-capture-createfrom-lambda.tsx | 32 ++++ .../transitivity-capture-createfrom.expect.md | 106 +++++++++++ .../transitivity-capture-createfrom.tsx | 28 +++ .../transitivity-createfrom-capture.expect.md | 103 +++++++++++ .../transitivity-createfrom-capture.tsx | 28 +++ ...ansitivity-phi-assign-or-capture.expect.md | 122 +++++++++++++ .../transitivity-phi-assign-or-capture.tsx | 32 ++++ .../babel-plugin-react-compiler/src/index.ts | 1 + compiler/packages/snap/src/compiler.ts | 10 +- compiler/packages/snap/src/runner-worker.ts | 5 + .../sprout/shared-runtime-type-provider.ts | 113 +++++++++++- .../snap/src/sprout/shared-runtime.ts | 29 ++- 20 files changed, 1289 insertions(+), 7 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx 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 f17f345d24..0360b1aa5e 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 @@ -271,9 +271,9 @@ a.property = value // a _is_ b, this mutates b ``` CreateFrom a <- b -Mutate A +Mutate a => -MutateTransitive b +Mutate b ``` Example: @@ -301,6 +301,26 @@ a.b = b; a.property = value; // mutates a, not b ``` +### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture + +``` +Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b +Mutate b +=> +Mutate a +``` + +A derived value changes when it's source value is mutated. + +Example: + +```js +const x = {}; +const y = [x]; +x.y = true; // this changes the value within `y` ie mutates y +``` + + ### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md new file mode 100644 index 0000000000..d3555c0b27 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md @@ -0,0 +1,166 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b, c}: {a: number; b: number; c: number}) { + const x = useMemo(() => [{value: a}], [a, b, c]); + if (b === 0) { + // This object should only depend on c, it cannot be affected by the later mutation + x.push({value: c}); + } else { + // This mutation shouldn't affect the object in the consequent + mutate(x); + } + + return ( + <> + ; + {/* TODO: should only depend on c */} + + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0, c: 0}], + sequentialRenders: [ + {a: 0, b: 0, c: 0}, + {a: 0, b: 1, c: 0}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 1, c: 1}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 0, c: 0}, + {a: 0, b: 0, c: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(22); + const { a, b, c } = t0; + let t1; + let x; + if ($[0] !== a || $[1] !== b || $[2] !== c) { + t1 = [{ value: a }]; + x = t1; + if (b === 0) { + x.push({ value: c }); + } else { + mutate(x); + } + $[0] = a; + $[1] = b; + $[2] = c; + $[3] = x; + $[4] = t1; + } else { + x = $[3]; + t1 = $[4]; + } + let t2; + if ($[5] !== a || $[6] !== b || $[7] !== c) { + t2 = [a, b, c]; + $[5] = a; + $[6] = b; + $[7] = c; + $[8] = t2; + } else { + t2 = $[8]; + } + let t3; + if ($[9] !== t2 || $[10] !== x) { + t3 = ; + $[9] = t2; + $[10] = x; + $[11] = t3; + } else { + t3 = $[11]; + } + let t4; + if ($[12] !== a || $[13] !== b || $[14] !== c) { + t4 = [a, b, c]; + $[12] = a; + $[13] = b; + $[14] = c; + $[15] = t4; + } else { + t4 = $[15]; + } + let t5; + if ($[16] !== t4 || $[17] !== x[0]) { + t5 = ; + $[16] = t4; + $[17] = x[0]; + $[18] = t5; + } else { + t5 = $[18]; + } + let t6; + if ($[19] !== t3 || $[20] !== t5) { + t6 = ( + <> + {t3};{t5}; + + ); + $[19] = t3; + $[20] = t5; + $[21] = t6; + } else { + t6 = $[21]; + } + return t6; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0, c: 0 }], + sequentialRenders: [ + { a: 0, b: 0, c: 0 }, + { a: 0, b: 1, c: 0 }, + { a: 1, b: 1, c: 0 }, + { a: 1, b: 1, c: 1 }, + { a: 1, b: 1, c: 0 }, + { a: 1, b: 0, c: 0 }, + { a: 0, b: 0, c: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}
;
{"inputs":[0,0,0],"output":{"value":0}}
; +
{"inputs":[0,1,0],"output":[{"value":0},"joe"]}
;
{"inputs":[0,1,0],"output":{"value":0}}
; +
{"inputs":[1,1,0],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,0],"output":{"value":1}}
; +
{"inputs":[1,1,1],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,1],"output":{"value":1}}
; +
{"inputs":[1,1,0],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,0],"output":{"value":1}}
; +
{"inputs":[1,0,0],"output":[{"value":1},{"value":0}]}
;
{"inputs":[1,0,0],"output":{"value":1}}
; +
{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}
;
{"inputs":[0,0,0],"output":{"value":0}}
; \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx new file mode 100644 index 0000000000..80386c46af --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx @@ -0,0 +1,46 @@ +import {useMemo} from 'react'; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b, c}: {a: number; b: number; c: number}) { + const x = useMemo(() => [{value: a}], [a, b, c]); + if (b === 0) { + // This object should only depend on c, it cannot be affected by the later mutation + x.push({value: c}); + } else { + // This mutation shouldn't affect the object in the consequent + mutate(x); + } + + return ( + <> + ; + {/* TODO: should only depend on c */} + + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0, c: 0}], + sequentialRenders: [ + {a: 0, b: 0, c: 0}, + {a: 0, b: 1, c: 0}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 1, c: 1}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 0, c: 0}, + {a: 0, b: 0, c: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md new file mode 100644 index 0000000000..9210bef3fb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md @@ -0,0 +1,116 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + const z = f(); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + // TODO: this *should* only depend on `a` + 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 { + typedCapture, + typedCreateFrom, + typedMutate, + 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 = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + + const z_0 = f(); + + typedMutate(z_0, 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}]}
+
{"inputs":[0,1],"output":[{"a":0}]}
+
{"inputs":[1,1],"output":[{"a":1}]}
+
{"inputs":[0,0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx new file mode 100644 index 0000000000..9e10bec1b4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx @@ -0,0 +1,33 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + const z = f(); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + // TODO: this *should* only depend on `a` + 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/transitivity-add-captured-array-to-itself.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md new file mode 100644 index 0000000000..2813e072e2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md @@ -0,0 +1,153 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const o: any = useMemo(() => ({a}), [a]); + const x: Array = useMemo(() => [o], [o, b]); + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(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 { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(20); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const o = t1; + let t3; + let x; + if ($[2] !== b || $[3] !== o) { + t3 = [o]; + x = t3; + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + $[2] = b; + $[3] = o; + $[4] = x; + $[5] = t3; + } else { + x = $[4]; + t3 = $[5]; + } + let t4; + if ($[6] !== a) { + t4 = [a]; + $[6] = a; + $[7] = t4; + } else { + t4 = $[7]; + } + let t5; + if ($[8] !== o || $[9] !== t4) { + t5 = ; + $[8] = o; + $[9] = t4; + $[10] = t5; + } else { + t5 = $[10]; + } + let t6; + if ($[11] !== a || $[12] !== b) { + t6 = [a, b]; + $[11] = a; + $[12] = b; + $[13] = t6; + } else { + t6 = $[13]; + } + let t7; + if ($[14] !== t6 || $[15] !== x) { + t7 = ; + $[14] = t6; + $[15] = x; + $[16] = t7; + } else { + t7 = $[16]; + } + let t8; + if ($[17] !== t5 || $[18] !== t7) { + t8 = ( + <> + {t5};{t7}; + + ); + $[17] = t5; + $[18] = t7; + $[19] = t8; + } else { + t8 = $[19]; + } + return t8; +} + +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],"output":{"a":0}}
;
{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}
; +
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,1],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],1]}
; +
{"inputs":[1],"output":{"a":1}}
;
{"inputs":[1,1],"output":[{"a":1},[["[[ cyclic ref *2 ]]"]],1]}
; +
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}
; \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx new file mode 100644 index 0000000000..7d7ec91bc5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx @@ -0,0 +1,34 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const o: any = useMemo(() => ({a}), [a]); + const x: Array = useMemo(() => [o], [o, b]); + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(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/transitivity-capture-createfrom-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md new file mode 100644 index 0000000000..85c356f894 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md @@ -0,0 +1,115 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + const z = f(); + // mutates x + typedMutate(z, 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 { + typedCapture, + typedCreateFrom, + typedMutate, + 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 = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + + const z_0 = f(); + + typedMutate(z_0, 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,"property":0}}
+
{"inputs":[0,1],"output":{"a":0,"property":1}}
+
{"inputs":[1,1],"output":{"a":1,"property":1}}
+
{"inputs":[0,0],"output":{"a":0,"property":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx new file mode 100644 index 0000000000..cee7901a6e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx @@ -0,0 +1,32 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + const z = f(); + // mutates x + typedMutate(z, 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/transitivity-capture-createfrom.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md new file mode 100644 index 0000000000..af15d568ce --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md @@ -0,0 +1,106 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const y = typedCapture(x); + const z = typedCreateFrom(y); + // mutates x + typedMutate(z, 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 { + typedCapture, + typedCreateFrom, + typedMutate, + 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 y = typedCapture(x); + const z = typedCreateFrom(y); + + typedMutate(z, 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,"property":0}}
+
{"inputs":[0,1],"output":{"a":0,"property":1}}
+
{"inputs":[1,1],"output":{"a":1,"property":1}}
+
{"inputs":[0,0],"output":{"a":0,"property":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx new file mode 100644 index 0000000000..df89c3413e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const y = typedCapture(x); + const z = typedCreateFrom(y); + // mutates x + typedMutate(z, 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/transitivity-createfrom-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md new file mode 100644 index 0000000000..25dbcdf3ac --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md @@ -0,0 +1,103 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const y = typedCreateFrom(x); + const z = typedCapture(y); + // does not mutate x, so x should not depend on b + typedMutate(z, 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 { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = [{ a }]; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const x = t1; + const y = typedCreateFrom(x); + const z = typedCapture(y); + + typedMutate(z, b); + let t3; + if ($[2] !== a) { + t3 = [a]; + $[2] = a; + $[3] = t3; + } else { + t3 = $[3]; + } + let t4; + if ($[4] !== t3 || $[5] !== x) { + t4 = ; + $[4] = t3; + $[5] = x; + $[6] = t4; + } else { + t4 = $[6]; + } + return t4; +} + +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],"output":[{"a":0}]}
+
{"inputs":[0],"output":[{"a":0}]}
+
{"inputs":[1],"output":[{"a":1}]}
+
{"inputs":[0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx new file mode 100644 index 0000000000..a8a02564bd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const y = typedCreateFrom(x); + const z = typedCapture(y); + // does not mutate x, so x should not depend on b + typedMutate(z, 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/transitivity-phi-assign-or-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md new file mode 100644 index 0000000000..b5350352cf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md @@ -0,0 +1,122 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a, b]); + let z: any; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + // could mutate x + typedMutate(z, 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 { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(12); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + let x; + if ($[2] !== b || $[3] !== t2) { + t1 = [t2]; + x = t1; + let z; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + + typedMutate(z, b); + $[2] = b; + $[3] = t2; + $[4] = x; + $[5] = t1; + } else { + x = $[4]; + t1 = $[5]; + } + let t3; + if ($[6] !== a || $[7] !== b) { + t3 = [a, b]; + $[6] = a; + $[7] = b; + $[8] = t3; + } else { + t3 = $[8]; + } + let t4; + if ($[9] !== t3 || $[10] !== x) { + t4 = ; + $[9] = t3; + $[10] = x; + $[11] = t4; + } else { + t4 = $[11]; + } + return t4; +} + +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}]}
+
{"inputs":[0,1],"output":[{"a":0}]}
+
{"inputs":[1,1],"output":[{"a":1}]}
+
{"inputs":[0,0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx new file mode 100644 index 0000000000..e583bfa83a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx @@ -0,0 +1,32 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a, b]); + let z: any; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + // could mutate x + typedMutate(z, 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/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index cbae672e50..bbd814b2b6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -30,6 +30,7 @@ export { export { Effect, ValueKind, + ValueReason, printHIR, printFunctionWithOutlined, validateEnvironmentConfig, diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index 7241ed5149..a159359773 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -18,7 +18,11 @@ import type { CompilerReactTarget, CompilerPipelineValue, } from 'babel-plugin-react-compiler/src/Entrypoint'; -import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR'; +import type { + Effect, + ValueKind, + ValueReason, +} from 'babel-plugin-react-compiler/src/HIR'; import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; import * as HermesParser from 'hermes-parser'; import invariant from 'invariant'; @@ -42,6 +46,7 @@ function makePluginOptions( debugIRLogger: (value: CompilerPipelineValue) => void, EffectEnum: typeof Effect, ValueKindEnum: typeof ValueKind, + ValueReasonEnum: typeof ValueReason, ): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] { // TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false let validatePreserveExistingMemoizationGuarantees = false; @@ -77,6 +82,7 @@ function makePluginOptions( moduleTypeProvider: makeSharedRuntimeTypeProvider({ EffectEnum, ValueKindEnum, + ValueReasonEnum, }), assertValidMutableRanges: true, validatePreserveExistingMemoizationGuarantees, @@ -209,6 +215,7 @@ export async function transformFixtureInput( debugIRLogger: (value: CompilerPipelineValue) => void, EffectEnum: typeof Effect, ValueKindEnum: typeof ValueKind, + ValueReasonEnum: typeof ValueReason, ): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> { // Extract the first line to quickly check for custom test directives const firstLine = input.substring(0, input.indexOf('\n')); @@ -237,6 +244,7 @@ export async function transformFixtureInput( debugIRLogger, EffectEnum, ValueKindEnum, + ValueReasonEnum, ); const forgetResult = transformFromAstSync(inputAst, input, { filename: virtualFilepath, diff --git a/compiler/packages/snap/src/runner-worker.ts b/compiler/packages/snap/src/runner-worker.ts index 2478e6a545..fd4763b203 100644 --- a/compiler/packages/snap/src/runner-worker.ts +++ b/compiler/packages/snap/src/runner-worker.ts @@ -24,6 +24,7 @@ import type { CompilerPipelineValue, Effect, ValueKind, + ValueReason, } from 'babel-plugin-react-compiler/src'; import chalk from 'chalk'; @@ -78,6 +79,9 @@ async function compile( const ValueKindEnum = importedCompilerPlugin[ 'ValueKind' ] as typeof ValueKind; + const ValueReasonEnum = importedCompilerPlugin[ + 'ValueReason' + ] as typeof ValueReason; const printFunctionWithOutlined = importedCompilerPlugin[ PRINT_HIR_IMPORT ] as typeof PrintFunctionWithOutlined; @@ -128,6 +132,7 @@ async function compile( debugIRLogger, EffectEnum, ValueKindEnum, + ValueReasonEnum, ); if (result.kind === 'err') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index 449bc8e688..58b007c1c7 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -5,15 +5,21 @@ * LICENSE file in the root directory of this source tree. */ -import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src'; +import type { + Effect, + ValueKind, + ValueReason, +} from 'babel-plugin-react-compiler/src'; import type {TypeConfig} from 'babel-plugin-react-compiler/src/HIR/TypeSchema'; export function makeSharedRuntimeTypeProvider({ EffectEnum, ValueKindEnum, + ValueReasonEnum, }: { EffectEnum: typeof Effect; ValueKindEnum: typeof ValueKind; + ValueReasonEnum: typeof ValueReason; }) { return function sharedRuntimeTypeProvider( moduleName: string, @@ -85,6 +91,111 @@ export function makeSharedRuntimeTypeProvider({ effects: [{kind: 'Assign', from: '@value', into: '@return'}], }, }, + typedAssign: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'Assign', from: '@value', into: '@return'}], + }, + }, + typedAlias: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Mutable, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Alias', from: '@value', into: '@return'}, + ], + }, + }, + typedCapture: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Array'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Mutable, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Capture', from: '@value', into: '@return'}, + ], + }, + }, + typedCreateFrom: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'CreateFrom', from: '@value', into: '@return'}], + }, + }, + typedMutate: { + kind: 'function', + positionalParams: [EffectEnum.Read, EffectEnum.Capture], + restParam: null, + calleeEffect: EffectEnum.Store, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + aliasing: { + receiver: '@receiver', + params: ['@object', '@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Primitive, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Mutate', value: '@object'}, + {kind: 'Capture', from: '@value', into: '@object'}, + ], + }, + }, }, }; } else if (moduleName === 'ReactCompilerTest') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index c8bf9272a2..1e0ab108db 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -269,10 +269,12 @@ export function ValidateMemoization({ inputs, output: rawOutput, onlyCheckCompiled = false, + alwaysCheck = false, }: { inputs: Array; output: any; - onlyCheckCompiled: boolean; + onlyCheckCompiled?: boolean; + alwaysCheck?: boolean; }): React.ReactElement { 'use no forget'; // Wrap rawOutput as it might be a function, which useState would invoke. @@ -280,8 +282,9 @@ export function ValidateMemoization({ const [previousInputs, setPreviousInputs] = React.useState(inputs); const [previousOutput, setPreviousOutput] = React.useState(output); if ( - onlyCheckCompiled && - (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget' + alwaysCheck || + (onlyCheckCompiled && + (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget') ) { if ( inputs.length !== previousInputs.length || @@ -400,4 +403,24 @@ export function typedIdentity(value: T): T { return value; } +export function typedAssign(x: T): T { + return x; +} + +export function typedAlias(x: T): T { + return x; +} + +export function typedCapture(x: T): Array { + return [x]; +} + +export function typedCreateFrom(array: Array): T { + return array[0]; +} + +export function typedMutate(x: any, v: any = null): void { + x.property = v; +} + export default typedLog; From 4132421c31b619b91497e807e6898855fc6a19a6 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 10/12] [compiler] Fix By accident we were only ever checking the compiled output, but the intention was in general to be able to compare memoization with/without forget. --- ...-control-flow-sensitive-mutation.expect.md | 13 ++---- .../todo-control-flow-sensitive-mutation.tsx | 9 +--- ...tivity-createfrom-capture-lambda.expect.md | 4 +- ...transitivity-createfrom-capture-lambda.tsx | 2 +- ...ity-add-captured-array-to-itself.expect.md | 8 ++-- ...nsitivity-add-captured-array-to-itself.tsx | 4 +- ...tivity-capture-createfrom-lambda.expect.md | 4 +- ...transitivity-capture-createfrom-lambda.tsx | 2 +- .../transitivity-capture-createfrom.expect.md | 4 +- .../transitivity-capture-createfrom.tsx | 2 +- .../transitivity-createfrom-capture.expect.md | 4 +- .../transitivity-createfrom-capture.tsx | 2 +- ...ansitivity-phi-assign-or-capture.expect.md | 4 +- .../transitivity-phi-assign-or-capture.tsx | 2 +- ...d-identity-function-frozen-input.expect.md | 42 ++++++++++--------- .../typed-identity-function-frozen-input.js | 3 +- .../compiler/weakmap-constructor.expect.md | 36 +++++++++++++--- .../fixtures/compiler/weakmap-constructor.js | 19 +++++++-- .../compiler/weakset-constructor.expect.md | 36 +++++++++++++--- .../fixtures/compiler/weakset-constructor.js | 19 +++++++-- .../snap/src/sprout/shared-runtime.ts | 4 +- 21 files changed, 145 insertions(+), 78 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md index d3555c0b27..a3cf9f638a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md @@ -23,14 +23,9 @@ function Component({a, b, c}: {a: number; b: number; c: number}) { return ( <> - ; + ; {/* TODO: should only depend on c */} - - ; + ; ); } @@ -98,7 +93,7 @@ function Component(t0) { } let t3; if ($[9] !== t2 || $[10] !== x) { - t3 = ; + t3 = ; $[9] = t2; $[10] = x; $[11] = t3; @@ -117,7 +112,7 @@ function Component(t0) { } let t5; if ($[16] !== t4 || $[17] !== x[0]) { - t5 = ; + t5 = ; $[16] = t4; $[17] = x[0]; $[18] = t5; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx index 80386c46af..61f8c47e45 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx @@ -19,14 +19,9 @@ function Component({a, b, c}: {a: number; b: number; c: number}) { return ( <> - ; + ; {/* TODO: should only depend on c */} - - ; + ; ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md index 9210bef3fb..9dba055973 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md @@ -22,7 +22,7 @@ function Component({a, b}) { typedMutate(z, b); // TODO: this *should* only depend on `a` - return ; + return ; } export const FIXTURE_ENTRYPOINT = { @@ -86,7 +86,7 @@ function Component(t0) { } let t3; if ($[7] !== t2 || $[8] !== x) { - t3 = ; + t3 = ; $[7] = t2; $[8] = x; $[9] = t3; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx index 9e10bec1b4..d6bd1690f6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx @@ -18,7 +18,7 @@ function Component({a, b}) { typedMutate(z, b); // TODO: this *should* only depend on `a` - return ; + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md index 2813e072e2..fb3c8d0a89 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md @@ -20,8 +20,8 @@ function Component({a, b}) { return ( <> - ; - ; + ; + ; ); } @@ -92,7 +92,7 @@ function Component(t0) { } let t5; if ($[8] !== o || $[9] !== t4) { - t5 = ; + t5 = ; $[8] = o; $[9] = t4; $[10] = t5; @@ -110,7 +110,7 @@ function Component(t0) { } let t7; if ($[14] !== t6 || $[15] !== x) { - t7 = ; + t7 = ; $[14] = t6; $[15] = x; $[16] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx index 7d7ec91bc5..d81c069e33 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx @@ -16,8 +16,8 @@ function Component({a, b}) { return ( <> - ; - ; + ; + ; ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md index 85c356f894..bfb4ede0ad 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md @@ -21,7 +21,7 @@ function Component({a, b}: {a: number; b: number}) { // mutates x typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { @@ -85,7 +85,7 @@ function Component(t0) { } let t3; if ($[7] !== t2 || $[8] !== x) { - t3 = ; + t3 = ; $[7] = t2; $[8] = x; $[9] = t3; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx index cee7901a6e..72289eb833 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx @@ -17,7 +17,7 @@ function Component({a, b}: {a: number; b: number}) { // mutates x typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md index af15d568ce..bd0a76965d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md @@ -17,7 +17,7 @@ function Component({a, b}: {a: number; b: number}) { // mutates x typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { @@ -76,7 +76,7 @@ function Component(t0) { } let t3; if ($[7] !== t2 || $[8] !== x) { - t3 = ; + t3 = ; $[7] = t2; $[8] = x; $[9] = t3; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx index df89c3413e..d06ad11eb5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx @@ -13,7 +13,7 @@ function Component({a, b}: {a: number; b: number}) { // mutates x typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md index 25dbcdf3ac..742a233e80 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md @@ -17,7 +17,7 @@ function Component({a, b}) { // does not mutate x, so x should not depend on b typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { @@ -73,7 +73,7 @@ function Component(t0) { } let t4; if ($[4] !== t3 || $[5] !== x) { - t4 = ; + t4 = ; $[4] = t3; $[5] = x; $[6] = t4; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx index a8a02564bd..32d65e61e0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx @@ -13,7 +13,7 @@ function Component({a, b}) { // does not mutate x, so x should not depend on b typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md index b5350352cf..c50fe47aed 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md @@ -21,7 +21,7 @@ function Component({a, b}) { // could mutate x typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { @@ -92,7 +92,7 @@ function Component(t0) { } let t4; if ($[9] !== t3 || $[10] !== x) { - t4 = ; + t4 = ; $[9] = t3; $[10] = x; $[11] = t4; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx index e583bfa83a..90b7597694 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx @@ -17,7 +17,7 @@ function Component({a, b}) { // could mutate x typedMutate(z, b); - return ; + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md index b15248df07..dc8bedaf83 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md @@ -4,6 +4,7 @@ ```javascript // @enableNewMutationAliasingModel +import {useMemo} from 'react'; import { identity, makeObject_Primitives, @@ -14,7 +15,7 @@ import { function Component({a, b}) { // create a mutable value with input `a` - const x = makeObject_Primitives(a); + const x = useMemo(() => makeObject_Primitives(a), [a]); // freeze the value useIdentity(x); @@ -49,6 +50,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useMemo } from "react"; import { identity, makeObject_Primitives, @@ -61,13 +63,15 @@ function Component(t0) { const $ = _c(7); const { a, b } = t0; let t1; + let t2; if ($[0] !== a) { - t1 = makeObject_Primitives(a); + t2 = makeObject_Primitives(a); $[0] = a; - $[1] = t1; + $[1] = t2; } else { - t1 = $[1]; + t2 = $[1]; } + t1 = t2; const x = t1; useIdentity(x); @@ -75,24 +79,24 @@ function Component(t0) { const x2 = typedIdentity(x); identity(x2, b); - let t2; - if ($[2] !== a) { - t2 = [a]; - $[2] = a; - $[3] = t2; - } else { - t2 = $[3]; - } let t3; - if ($[4] !== t2 || $[5] !== x) { - t3 = ; - $[4] = t2; - $[5] = x; - $[6] = t3; + if ($[2] !== a) { + t3 = [a]; + $[2] = a; + $[3] = t3; } else { - t3 = $[6]; + t3 = $[3]; } - return t3; + let t4; + if ($[4] !== t3 || $[5] !== x) { + t4 = ; + $[4] = t3; + $[5] = x; + $[6] = t4; + } else { + t4 = $[6]; + } + return t4; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js index bcf6ecef02..d0f677ee4d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js @@ -1,5 +1,6 @@ // @enableNewMutationAliasingModel +import {useMemo} from 'react'; import { identity, makeObject_Primitives, @@ -10,7 +11,7 @@ import { function Component({a, b}) { // create a mutable value with input `a` - const x = makeObject_Primitives(a); + const x = useMemo(() => makeObject_Primitives(a), [a]); // freeze the value useIdentity(x); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md index aebaedf6a8..a6def457a4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -13,9 +14,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } @@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; import { ValidateMemoization } from "shared-runtime"; function Component(t0) { @@ -76,7 +90,9 @@ function Component(t0) { } let t2; if ($[7] !== map || $[8] !== t1) { - t2 = ; + t2 = ( + + ); $[7] = map; $[8] = t1; $[9] = t2; @@ -94,7 +110,13 @@ function Component(t0) { } let t4; if ($[13] !== mapAlias || $[14] !== t3) { - t4 = ; + t4 = ( + + ); $[13] = mapAlias; $[14] = t3; $[15] = t4; @@ -119,7 +141,9 @@ function Component(t0) { } let t7; if ($[20] !== t5 || $[21] !== t6) { - t7 = ; + t7 = ( + + ); $[20] = t5; $[21] = t6; $[22] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js index ecdec6c9e9..d005c9f271 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -9,9 +10,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md index 5ebf32d533..94e0c7f055 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -13,9 +14,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } @@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; import { ValidateMemoization } from "shared-runtime"; function Component(t0) { @@ -76,7 +90,9 @@ function Component(t0) { } let t2; if ($[7] !== set || $[8] !== t1) { - t2 = ; + t2 = ( + + ); $[7] = set; $[8] = t1; $[9] = t2; @@ -94,7 +110,13 @@ function Component(t0) { } let t4; if ($[13] !== setAlias || $[14] !== t3) { - t4 = ; + t4 = ( + + ); $[13] = setAlias; $[14] = t3; $[15] = t4; @@ -119,7 +141,9 @@ function Component(t0) { } let t7; if ($[20] !== t5 || $[21] !== t6) { - t7 = ; + t7 = ( + + ); $[20] = t5; $[21] = t6; $[22] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js index 8c0a7deb18..9114233812 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -9,9 +10,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 1e0ab108db..f37ca82709 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -269,12 +269,10 @@ export function ValidateMemoization({ inputs, output: rawOutput, onlyCheckCompiled = false, - alwaysCheck = false, }: { inputs: Array; output: any; onlyCheckCompiled?: boolean; - alwaysCheck?: boolean; }): React.ReactElement { 'use no forget'; // Wrap rawOutput as it might be a function, which useState would invoke. @@ -282,7 +280,7 @@ export function ValidateMemoization({ const [previousInputs, setPreviousInputs] = React.useState(inputs); const [previousOutput, setPreviousOutput] = React.useState(output); if ( - alwaysCheck || + !onlyCheckCompiled || (onlyCheckCompiled && (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget') ) { From 85dce8e4802db6075908b3d7ea7378a703c53994 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 13:32:54 -0700 Subject: [PATCH 11/12] [compiler] Preserve Create effects, guarantee effects initialize once Ensures that effects are well-formed with respect to the rules: * For a given instruction, each place is only initialized once (w one of Create, CreateFrom, Assign) * Ensures that Alias targets are already initialized within the same instruction (should have a Create before them) * Preserves Create and similar instructions * Avoids duplicate instructions when inferring effects of function expressions --- .../src/Inference/AnalyseFunctions.ts | 14 ++- .../Inference/InferMutationAliasingEffects.ts | 102 +++++++++++++----- 2 files changed, 88 insertions(+), 28 deletions(-) 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 2bd46abc1f..eed8946c81 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -20,10 +20,11 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes'; import {rewriteInstructionKindsBasedOnReassignment} from '../SSA'; import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; -import {assertExhaustive} from '../Utils/utils'; +import {assertExhaustive, retainWhere} 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) { @@ -81,6 +82,17 @@ function lowerWithMutationAliasing(fn: HIRFunction): void { 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; + }); + } /** * Phase 2: populate the Effect of each context variable to use in inferring 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 93f00508b2..0ef421ad6b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -362,6 +362,11 @@ function inferBlock( } else if (terminal.kind === 'maybe-throw') { const handlerParam = context.catchHandlers.get(terminal.handler); if (handlerParam != null) { + CompilerError.invariant(state.kind(handlerParam) != null, { + reason: + 'Expected catch binding to be intialized with a DeclareLocal Catch instruction', + loc: terminal.loc, + }); const effects: Array = []; for (const instr of block.instructions) { if ( @@ -476,14 +481,14 @@ function applySignature( * Track which values we've already aliased once, so that we can switch to * appendAlias() for subsequent aliases into the same value */ - const aliased = new Set(); + const initialized = new Set(); if (DEBUG) { console.log(printInstruction(instruction)); } for (const effect of signature.effects) { - applyEffect(context, state, effect, aliased, effects); + applyEffect(context, state, effect, initialized, effects); } if (DEBUG) { console.log( @@ -508,7 +513,7 @@ function applyEffect( context: Context, state: InferenceState, _effect: AliasingEffect, - aliased: Set, + initialized: Set, effects: Array, ): void { const effect = context.internEffect(_effect); @@ -524,6 +529,13 @@ function applyEffect( break; } case 'Create': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + let value = context.effectInstructionValueCache.get(effect); if (value == null) { value = { @@ -538,6 +550,7 @@ function applyEffect( reason: new Set([effect.reason]), }); state.define(effect.into, value); + effects.push(effect); break; } case 'ImmutableCapture': { @@ -555,6 +568,13 @@ function applyEffect( break; } case 'CreateFrom': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + const fromValue = state.kind(effect.from); let value = context.effectInstructionValueCache.get(effect); if (value == null) { @@ -573,10 +593,21 @@ function applyEffect( switch (fromValue.kind) { case ValueKind.Primitive: case ValueKind.Global: { - // no need to track this data flow + effects.push({ + kind: 'Create', + value: fromValue.kind, + into: effect.into, + reason: [...fromValue.reason][0] ?? ValueReason.Other, + }); break; } case ValueKind.Frozen: { + effects.push({ + kind: 'Create', + value: fromValue.kind, + into: effect.into, + reason: [...fromValue.reason][0] ?? ValueReason.Other, + }); applyEffect( context, state, @@ -585,7 +616,7 @@ function applyEffect( from: effect.from, into: effect.into, }, - aliased, + initialized, effects, ); break; @@ -597,6 +628,13 @@ function applyEffect( break; } case 'CreateFunction': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + effects.push(effect); /** * We consider the function mutable if it has any mutable context variables or @@ -653,7 +691,7 @@ function applyEffect( from: capture, into: effect.into, }, - aliased, + initialized, effects, ); } @@ -661,6 +699,14 @@ function applyEffect( } case 'Alias': case 'Capture': { + CompilerError.invariant( + effect.kind === 'Capture' || initialized.has(effect.into.identifier.id), + { + reason: `Expected destination value to already be initialized within this instruction for Alias effect`, + description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`, + loc: effect.into.loc, + }, + ); /* * Capture describes potential information flow: storing a pointer to one value * within another. If the destination is not mutable, or the source value has @@ -698,7 +744,7 @@ function applyEffect( from: effect.from, into: effect.into, }, - aliased, + initialized, effects, ); break; @@ -714,6 +760,13 @@ function applyEffect( break; } case 'Assign': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + /* * Alias represents potential pointer aliasing. If the type is a global, * a primitive (copy-on-write semantics) then we can prune the effect @@ -730,7 +783,7 @@ function applyEffect( from: effect.from, into: effect.into, }, - aliased, + initialized, effects, ); let value = context.effectInstructionValueCache.get(effect); @@ -768,12 +821,7 @@ function applyEffect( break; } default: { - if (aliased.has(effect.into.identifier.id)) { - state.appendAlias(effect.into, effect.from); - } else { - aliased.add(effect.into.identifier.id); - state.alias(effect.into, effect.from); - } + state.assign(effect.into, effect.from); effects.push(effect); break; } @@ -826,11 +874,11 @@ function applyEffect( context, state, {kind: 'MutateTransitiveConditionally', value: effect.function}, - aliased, + initialized, effects, ); for (const signatureEffect of signatureEffects) { - applyEffect(context, state, signatureEffect, aliased, effects); + applyEffect(context, state, signatureEffect, initialized, effects); } break; } @@ -858,7 +906,7 @@ function applyEffect( console.log('apply aliasing signature effects'); } for (const signatureEffect of signatureEffects) { - applyEffect(context, state, signatureEffect, aliased, effects); + applyEffect(context, state, signatureEffect, initialized, effects); } } else if (effect.signature != null) { if (DEBUG) { @@ -873,7 +921,7 @@ function applyEffect( effect.loc, ); for (const legacyEffect of legacyEffects) { - applyEffect(context, state, legacyEffect, aliased, effects); + applyEffect(context, state, legacyEffect, initialized, effects); } } else { if (DEBUG) { @@ -888,7 +936,7 @@ function applyEffect( value: ValueKind.Mutable, reason: ValueReason.Other, }, - aliased, + initialized, effects, ); /* @@ -911,21 +959,21 @@ function applyEffect( kind: 'MutateTransitiveConditionally', value: operand, }, - aliased, + initialized, effects, ); } const mutateIterator = arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null; if (mutateIterator) { - applyEffect(context, state, mutateIterator, aliased, effects); + applyEffect(context, state, mutateIterator, initialized, effects); } applyEffect( context, state, // OK: recording information flow {kind: 'Alias', from: operand, into: effect.into}, - aliased, + initialized, effects, ); for (const otherArg of [ @@ -953,7 +1001,7 @@ function applyEffect( from: operand, into: other, }, - aliased, + initialized, effects, ); } @@ -1009,7 +1057,7 @@ function applyEffect( suggestions: null, }, }, - aliased, + initialized, effects, ); } @@ -1028,7 +1076,7 @@ function applyEffect( suggestions: null, }, }, - aliased, + initialized, effects, ); } else { @@ -1059,7 +1107,7 @@ function applyEffect( suggestions: null, }, }, - aliased, + initialized, effects, ); } @@ -1166,7 +1214,7 @@ class InferenceState { } // Updates the value at @param place to point to the same value as @param value. - alias(place: Place, value: Place): void { + assign(place: Place, value: Place): void { const values = this.#variables.get(value.identifier.id); CompilerError.invariant(values != null, { reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`, From c27abf4da49e8e79e7a8b4ba722d86e50f72d457 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 18 Jun 2025 15:10:11 -0700 Subject: [PATCH 12/12] [compiler] Cleanup debugging code Removes unnecessary debugging code in the new inference passes now that they've stabilized more. --- .../Inference/InferMutationAliasingEffects.ts | 70 ++---------- .../Inference/InferMutationAliasingRanges.ts | 105 ++---------------- 2 files changed, 17 insertions(+), 158 deletions(-) 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 0ef421ad6b..b6c9812918 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -57,7 +57,6 @@ import { import { printAliasingEffect, printAliasingSignature, - printFunction, printIdentifier, printInstruction, printInstructionValue, @@ -194,19 +193,15 @@ export function inferMutationAliasingEffects( hoistedContextDeclarations, ); - let count = 0; + let iterationCount = 0; while (queuedStates.size !== 0) { - count++; - if (count > 100) { - console.log( - 'oops infinite loop', - fn.id, - typeof fn.loc !== 'symbol' ? fn.loc?.filename : null, - ); - if (DEBUG) { - console.log(printFunction(fn)); - } - throw new Error('infinite loop'); + iterationCount++; + if (iterationCount > 100) { + CompilerError.invariant(false, { + reason: `[InferMutationAliasingEffects] Potential infinite loop`, + description: `A value, temporary place, or effect was not cached properly`, + loc: fn.loc, + }); } for (const [blockId, block] of fn.body.blocks) { const incomingState = queuedStates.get(blockId); @@ -217,11 +212,6 @@ export function inferMutationAliasingEffects( statesByBlock.set(blockId, incomingState); const state = incomingState.clone(); - if (DEBUG) { - console.log('*************'); - console.log(`bb${block.id}`); - console.log('*************'); - } inferBlock(context, state, block); for (const nextBlockId of eachTerminalSuccessor(block.terminal)) { @@ -867,9 +857,6 @@ function applyEffect( ), ); if (signatureEffects != null) { - if (DEBUG) { - console.log('apply function expression effects'); - } applyEffect( context, state, @@ -902,16 +889,10 @@ function applyEffect( ); } if (signatureEffects != null) { - if (DEBUG) { - console.log('apply aliasing signature effects'); - } for (const signatureEffect of signatureEffects) { applyEffect(context, state, signatureEffect, initialized, effects); } } else if (effect.signature != null) { - if (DEBUG) { - console.log('apply legacy signature effects'); - } const legacyEffects = computeEffectsForLegacySignature( state, effect.signature, @@ -924,9 +905,6 @@ function applyEffect( applyEffect(context, state, legacyEffect, initialized, effects); } } else { - if (DEBUG) { - console.log('default effects'); - } applyEffect( context, state, @@ -1292,9 +1270,6 @@ class InferenceState { kind: ValueKind.Frozen, reason: new Set([reason]), }); - if (DEBUG) { - console.log(`freeze value: ${printInstructionValue(value)} ${reason}`); - } if ( value.kind === 'FunctionExpression' && (this.env.config.enablePreserveExistingMemoizationGuarantees || @@ -2334,17 +2309,6 @@ function computeEffectsForSignature( // Too many args and there is no rest param to hold them (args.length > signature.params.length && signature.rest == null) ) { - if (DEBUG) { - if (signature.params.length > args.length) { - console.log( - `not enough args: ${args.length} args for ${signature.params.length} params`, - ); - } else { - console.log( - `too many args: ${args.length} args for ${signature.params.length} params, with no rest param`, - ); - } - } return null; } // Build substitutions @@ -2359,9 +2323,6 @@ function computeEffectsForSignature( continue; } else if (params == null || i >= params.length || arg.kind === 'Spread') { if (signature.rest == null) { - if (DEBUG) { - console.log(`no rest value to hold param`); - } return null; } const place = arg.kind === 'Identifier' ? arg : arg.place; @@ -2469,23 +2430,14 @@ function computeEffectsForSignature( case 'Apply': { const applyReceiver = substitutions.get(effect.receiver.identifier.id); if (applyReceiver == null || applyReceiver.length !== 1) { - if (DEBUG) { - console.log(`too many substitutions for receiver`); - } return null; } const applyFunction = substitutions.get(effect.function.identifier.id); if (applyFunction == null || applyFunction.length !== 1) { - if (DEBUG) { - console.log(`too many substitutions for function`); - } return null; } const applyInto = substitutions.get(effect.into.identifier.id); if (applyInto == null || applyInto.length !== 1) { - if (DEBUG) { - console.log(`too many substitutions for into`); - } return null; } const applyArgs: Array = []; @@ -2495,18 +2447,12 @@ function computeEffectsForSignature( } else if (arg.kind === 'Identifier') { const applyArg = substitutions.get(arg.identifier.id); if (applyArg == null || applyArg.length !== 1) { - if (DEBUG) { - console.log(`too many substitutions for arg`); - } return null; } applyArgs.push(applyArg[0]); } else { const applyArg = substitutions.get(arg.place.identifier.id); if (applyArg == null || applyArg.length !== 1) { - if (DEBUG) { - console.log(`too many substitutions for arg`); - } return null; } applyArgs.push({kind: 'Spread', place: applyArg[0]}); 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 e0fb84fe5a..864eb29d8e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import prettyFormat from 'pretty-format'; import {CompilerError, SourceLocation} from '..'; import { BlockId, @@ -23,14 +22,9 @@ import { eachTerminalOperand, } from '../HIR/visitors'; import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; -import {printFunction} from '../HIR'; -import {printIdentifier, printPlace} from '../HIR/PrintHIR'; import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature'; import {Result} from '../Utils/Result'; -const DEBUG = false; -const VERBOSE = false; - /** * 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, @@ -56,10 +50,6 @@ export function inferMutationAliasingRanges( fn: HIRFunction, {isFunctionExpression}: {isFunctionExpression: boolean}, ): Result { - if (VERBOSE) { - console.log(); - console.log(printFunction(fn)); - } /** * 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. @@ -115,20 +105,6 @@ export function inferMutationAliasingRanges( seenBlocks.add(block.id); for (const instr of block.instructions) { - if ( - instr.value.kind === 'FunctionExpression' || - instr.value.kind === 'ObjectMethod' - ) { - state.create(instr.lvalue, { - kind: 'Function', - function: instr.value.loweredFunc.func, - }); - } else { - for (const lvalue of eachInstructionLValue(instr)) { - state.create(lvalue, {kind: 'Object'}); - } - } - if (instr.effects == null) continue; for (const effect of instr.effects) { if (effect.kind === 'Create') { @@ -141,6 +117,15 @@ export function inferMutationAliasingRanges( } else if (effect.kind === 'CreateFrom') { state.createFrom(index++, effect.from, effect.into); } else if (effect.kind === 'Assign') { + /** + * TODO: Invariant that the node is not initialized yet + * + * InferFunctionExpressionAliasingEffectSignatures currently infers + * Assign effects in some places that should be Alias, leading to + * Assign effects that reinitialize a value. The end result appears to + * be fine, but we should fix that inference pass so that we add the + * invariant here. + */ if (!state.nodes.has(effect.into.identifier)) { state.create(effect.into, {kind: 'Object'}); } @@ -216,10 +201,6 @@ export function inferMutationAliasingRanges( } } - if (VERBOSE) { - console.log(state.debug()); - console.log(pretty(mutations)); - } for (const mutation of mutations) { state.mutate( mutation.index, @@ -234,9 +215,6 @@ export function inferMutationAliasingRanges( for (const render of renders) { state.render(render.index, render.place.identifier, errors); } - if (DEBUG) { - console.log(pretty([...state.nodes.keys()])); - } fn.aliasingEffects ??= []; for (const param of [...fn.context, ...fn.params]) { const place = param.kind === 'Identifier' ? param : param.place; @@ -458,9 +436,6 @@ export function inferMutationAliasingRanges( } } - if (VERBOSE) { - console.log(printFunction(fn)); - } return errors.asResult(); } @@ -511,11 +486,6 @@ class AliasingState { const fromNode = this.nodes.get(from.identifier); const toNode = this.nodes.get(into.identifier); if (fromNode == null || toNode == null) { - if (VERBOSE) { - console.log( - `skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`, - ); - } return; } fromNode.edges.push({index, node: into.identifier, kind: 'alias'}); @@ -528,11 +498,6 @@ class AliasingState { const fromNode = this.nodes.get(from.identifier); const toNode = this.nodes.get(into.identifier); if (fromNode == null || toNode == null) { - if (VERBOSE) { - console.log( - `skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`, - ); - } return; } fromNode.edges.push({index, node: into.identifier, kind: 'capture'}); @@ -545,11 +510,6 @@ class AliasingState { const fromNode = this.nodes.get(from.identifier); const toNode = this.nodes.get(into.identifier); if (fromNode == null || toNode == null) { - if (VERBOSE) { - console.log( - `skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`, - ); - } return; } fromNode.edges.push({index, node: into.identifier, kind: 'alias'}); @@ -604,11 +564,6 @@ class AliasingState { loc: SourceLocation, errors: CompilerError, ): void { - if (DEBUG) { - console.log( - `mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`, - ); - } const seen = new Set(); const queue: Array<{ place: Identifier; @@ -623,18 +578,8 @@ class AliasingState { seen.add(current); const node = this.nodes.get(current); if (node == null) { - if (DEBUG) { - console.log( - `no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`, - ); - } continue; } - if (DEBUG) { - console.log( - ` mutate $${node.id.id} transitive=${transitive} direction=${direction}`, - ); - } node.id.mutableRange.end = makeInstructionId( Math.max(node.id.mutableRange.end, end), ); @@ -701,37 +646,5 @@ class AliasingState { } } } - if (DEBUG) { - const nodes = new Map(); - for (const id of seen) { - const node = this.nodes.get(id); - nodes.set(id.id, node); - } - console.log(pretty(nodes)); - } - } - - debug(): string { - return pretty(this.nodes); } } - -export function pretty(v: any): string { - return prettyFormat(v, { - plugins: [ - { - test: v => - v !== null && typeof v === 'object' && v.kind === 'Identifier', - serialize: v => printPlace(v), - }, - { - test: v => - v !== null && - typeof v === 'object' && - typeof v.declarationId === 'number', - serialize: v => - `${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`, - }, - ], - }); -}