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;