diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
index f249466431..d38a5f4f0a 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
@@ -33,6 +33,7 @@ import {
ScopeId,
TInstruction,
} from './HIR';
+import {printManualMemoDependency} from './PrintHIR';
const DEBUG_PRINT = false;
@@ -454,6 +455,25 @@ function collectNonNullsInBlocks(
assumedNonNullObjects.add(entry);
}
}
+ } else if (
+ fn.env.config.enablePreserveExistingMemoizationGuarantees &&
+ instr.value.kind === 'StartMemoize' &&
+ instr.value.deps != null
+ ) {
+ for (const dep of instr.value.deps) {
+ if (dep.root.kind === 'NamedLocal') {
+ const depNode = context.registry.getOrCreateProperty({
+ identifier: dep.root.value.identifier,
+ path: dep.path.slice(0, -1),
+ reactive: dep.root.value.reactive,
+ });
+ if (
+ isImmutableAtInstr(depNode.fullPath.identifier, instr.id, context)
+ ) {
+ assumedNonNullObjects.add(depNode);
+ }
+ }
+ }
}
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain-less-precise-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain-less-precise-deps.expect.md
new file mode 100644
index 0000000000..84c611dec3
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain-less-precise-deps.expect.md
@@ -0,0 +1,122 @@
+
+## Input
+
+```javascript
+// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
+
+import {useMemo} from 'react';
+import {identity, ValidateMemoization} from 'shared-runtime';
+
+function Component({x}) {
+ const object = useMemo(() => {
+ return identity({
+ callback: () => {
+ return identity(x.y.z); // accesses more levels of properties than the manual memo
+ },
+ });
+ // x.y as a manual dep only tells us that x is non-nullable, not that x.y is non-nullable
+ // we can only take a dep on x.y, not x.y.z
+ }, [x.y]);
+ const result = useMemo(() => {
+ return [object.callback()];
+ }, [object]);
+ return ;
+}
+
+const input1 = {x: {y: {z: 42}}};
+const input1b = {x: {y: {z: 42}}};
+const input2 = {x: {y: {z: 3.14}}};
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [input1],
+ sequentialRenders: [
+ input1,
+ input1,
+ input1b, // should reset even though .z didn't change
+ input1,
+ input2,
+ ],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
+
+import { useMemo } from "react";
+import { identity, ValidateMemoization } from "shared-runtime";
+
+function Component(t0) {
+ const $ = _c(11);
+ const { x } = t0;
+ let t1;
+ if ($[0] !== x.y) {
+ t1 = identity({ callback: () => identity(x.y.z) });
+ $[0] = x.y;
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const object = t1;
+ let t2;
+ if ($[2] !== object) {
+ t2 = object.callback();
+ $[2] = object;
+ $[3] = t2;
+ } else {
+ t2 = $[3];
+ }
+ let t3;
+ if ($[4] !== t2) {
+ t3 = [t2];
+ $[4] = t2;
+ $[5] = t3;
+ } else {
+ t3 = $[5];
+ }
+ const result = t3;
+ let t4;
+ if ($[6] !== x.y) {
+ t4 = [x.y];
+ $[6] = x.y;
+ $[7] = t4;
+ } else {
+ t4 = $[7];
+ }
+ let t5;
+ if ($[8] !== result || $[9] !== t4) {
+ t5 = ;
+ $[8] = result;
+ $[9] = t4;
+ $[10] = t5;
+ } else {
+ t5 = $[10];
+ }
+ return t5;
+}
+
+const input1 = { x: { y: { z: 42 } } };
+const input1b = { x: { y: { z: 42 } } };
+const input2 = { x: { y: { z: 3.14 } } };
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [input1],
+ sequentialRenders: [
+ input1,
+ input1,
+ input1b, // should reset even though .z didn't change
+ input1,
+ input2,
+ ],
+};
+
+```
+
+### Eval output
+(kind: ok)
{"inputs":[{"z":42}],"output":[42]}
+{"inputs":[{"z":42}],"output":[42]}
+{"inputs":[{"z":42}],"output":[42]}
+{"inputs":[{"z":42}],"output":[42]}
+{"inputs":[{"z":3.14}],"output":[3.14]}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain-less-precise-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain-less-precise-deps.js
new file mode 100644
index 0000000000..373fdc53fa
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain-less-precise-deps.js
@@ -0,0 +1,35 @@
+// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
+
+import {useMemo} from 'react';
+import {identity, ValidateMemoization} from 'shared-runtime';
+
+function Component({x}) {
+ const object = useMemo(() => {
+ return identity({
+ callback: () => {
+ return identity(x.y.z); // accesses more levels of properties than the manual memo
+ },
+ });
+ // x.y as a manual dep only tells us that x is non-nullable, not that x.y is non-nullable
+ // we can only take a dep on x.y, not x.y.z
+ }, [x.y]);
+ const result = useMemo(() => {
+ return [object.callback()];
+ }, [object]);
+ return ;
+}
+
+const input1 = {x: {y: {z: 42}}};
+const input1b = {x: {y: {z: 42}}};
+const input2 = {x: {y: {z: 3.14}}};
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [input1],
+ sequentialRenders: [
+ input1,
+ input1,
+ input1b, // should reset even though .z didn't change
+ input1,
+ input2,
+ ],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain.expect.md
new file mode 100644
index 0000000000..82c11f7783
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain.expect.md
@@ -0,0 +1,117 @@
+
+## Input
+
+```javascript
+// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
+
+import {useMemo} from 'react';
+import {identity, ValidateMemoization} from 'shared-runtime';
+
+function Component({x}) {
+ const object = useMemo(() => {
+ return identity({
+ callback: () => {
+ return identity(x.y.z);
+ },
+ });
+ }, [x.y.z]);
+ const result = useMemo(() => {
+ return [object.callback()];
+ }, [object]);
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{x: {y: {z: 42}}}],
+ sequentialRenders: [
+ {x: {y: {z: 42}}},
+ {x: {y: {z: 42}}},
+ {x: {y: {z: 3.14}}},
+ {x: {y: {z: 42}}},
+ {x: {y: {z: 3.14}}},
+ {x: {y: {z: 42}}},
+ ],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
+
+import { useMemo } from "react";
+import { identity, ValidateMemoization } from "shared-runtime";
+
+function Component(t0) {
+ const $ = _c(11);
+ const { x } = t0;
+ let t1;
+ if ($[0] !== x.y.z) {
+ t1 = identity({ callback: () => identity(x.y.z) });
+ $[0] = x.y.z;
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const object = t1;
+ let t2;
+ if ($[2] !== object) {
+ t2 = object.callback();
+ $[2] = object;
+ $[3] = t2;
+ } else {
+ t2 = $[3];
+ }
+ let t3;
+ if ($[4] !== t2) {
+ t3 = [t2];
+ $[4] = t2;
+ $[5] = t3;
+ } else {
+ t3 = $[5];
+ }
+ const result = t3;
+ let t4;
+ if ($[6] !== x.y.z) {
+ t4 = [x.y.z];
+ $[6] = x.y.z;
+ $[7] = t4;
+ } else {
+ t4 = $[7];
+ }
+ let t5;
+ if ($[8] !== result || $[9] !== t4) {
+ t5 = ;
+ $[8] = result;
+ $[9] = t4;
+ $[10] = t5;
+ } else {
+ t5 = $[10];
+ }
+ return t5;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ x: { y: { z: 42 } } }],
+ sequentialRenders: [
+ { x: { y: { z: 42 } } },
+ { x: { y: { z: 42 } } },
+ { x: { y: { z: 3.14 } } },
+ { x: { y: { z: 42 } } },
+ { x: { y: { z: 3.14 } } },
+ { x: { y: { z: 42 } } },
+ ],
+};
+
+```
+
+### Eval output
+(kind: ok) {"inputs":[42],"output":[42]}
+{"inputs":[42],"output":[42]}
+{"inputs":[3.14],"output":[3.14]}
+{"inputs":[42],"output":[42]}
+{"inputs":[3.14],"output":[3.14]}
+{"inputs":[42],"output":[42]}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain.js
new file mode 100644
index 0000000000..6b55e68bb0
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-deps-conditional-property-chain.js
@@ -0,0 +1,31 @@
+// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
+
+import {useMemo} from 'react';
+import {identity, ValidateMemoization} from 'shared-runtime';
+
+function Component({x}) {
+ const object = useMemo(() => {
+ return identity({
+ callback: () => {
+ return identity(x.y.z);
+ },
+ });
+ }, [x.y.z]);
+ const result = useMemo(() => {
+ return [object.callback()];
+ }, [object]);
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{x: {y: {z: 42}}}],
+ sequentialRenders: [
+ {x: {y: {z: 42}}},
+ {x: {y: {z: 42}}},
+ {x: {y: {z: 3.14}}},
+ {x: {y: {z: 42}}},
+ {x: {y: {z: 3.14}}},
+ {x: {y: {z: 42}}},
+ ],
+};