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 f17f345d24b6a..0360b1aa5e571 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 0000000000000..d3555c0b2711a --- /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 0000000000000..80386c46af86d --- /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 0000000000000..9210bef3fb42c --- /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 0000000000000..9e10bec1b4b87 --- /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 0000000000000..2813e072e2a12 --- /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 0000000000000..7d7ec91bc5936 --- /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 0000000000000..85c356f894bff --- /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 0000000000000..cee7901a6eefa --- /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 0000000000000..af15d568cebee --- /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 0000000000000..df89c3413e1d8 --- /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 0000000000000..25dbcdf3ac09d --- /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 0000000000000..a8a02564bd6c7 --- /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 0000000000000..b5350352cfd21 --- /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 0000000000000..e583bfa83aea0 --- /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 cbae672e50674..bbd814b2b60e3 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 7241ed51492bc..a159359773f31 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 2478e6a545b72..fd4763b20322e 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 449bc8e68816a..58b007c1c7355 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 c8bf9272a2ec1..1e0ab108db3f1 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;