diff --git a/compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts b/compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts
index 41454fca5d..e8e972be4a 100644
--- a/compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts
+++ b/compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts
@@ -23,7 +23,6 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from "../HIR/visitors";
-import { hasBackEdge } from "../Optimization/DeadCodeElimination";
import {
findDisjointMutableValues,
isMutable,
@@ -89,6 +88,25 @@ import { assertExhaustive } from "../Utils/utils";
* The algorithm uses a fixpoint iteration in order to propagate reactivity "forward" through
* the control-flow graph. We track whether each IdentifierId is reactive and terminate when
* there are no changes after a given pass over the CFG.
+ *
+ * Note that in Forget it's possible to create a "readonly" reference to a value where
+ * the reference is created within that value's mutable range:
+ *
+ * ```javascript
+ * const x = [];
+ * const z = [x];
+ * x.push(props.input);
+ *
+ * return
{z}
;
+ * ```
+ *
+ * Here `z` is never used to mutate the value, but it is aliasing `x` which
+ * is mutated after the creation of the alias. The pass needs to account for
+ * values which become reactive via mutability, and propagate this reactivity
+ * to these readonly aliases. Using forward data flow is insufficient since
+ * this information needs to propagate "backwards" from the `x.push(props.input)`
+ * to the previous `z = [x]` line. We use a fixpoint iteration even if the
+ * program has no back edges to accomplish this.
*/
export function inferReactivePlaces(fn: HIRFunction): void {
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
@@ -137,7 +155,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
return false;
}
- const hasLoop = hasBackEdge(fn);
do {
const identifierMapping = new Map();
for (const [, block] of fn.body.blocks) {
@@ -270,7 +287,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
reactiveIdentifiers.isReactive(operand);
}
}
- } while (reactiveIdentifiers.snapshot() && hasLoop);
+ } while (reactiveIdentifiers.snapshot());
}
/*
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactivity-via-readonly-alias-of-mutable-value.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactivity-via-readonly-alias-of-mutable-value.expect.md
new file mode 100644
index 0000000000..103f1f9c21
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactivity-via-readonly-alias-of-mutable-value.expect.md
@@ -0,0 +1,100 @@
+
+## Input
+
+```javascript
+function Component(props) {
+ const x = [];
+ const y = x;
+
+ // y isn't reactive yet when we first visit this, so z is initially non-reactive
+ const z = [y];
+
+ // then we realize y is reactive. we need a fixpoint to propagate this back to z
+ y.push(props.input);
+
+ // PruneNonReactiveDependencies partially propagates reactivity (for now) which
+ // we bypass with an indirection of storing into another variable
+ const a = [z];
+
+ // b's value is conditional on `a`, which is reactive per above
+ let b = 0;
+ if (a[0][0][0] === 42) {
+ b = 1;
+ }
+
+ return [b];
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ sequentialRenders: [
+ { input: 42 },
+ { input: 42 },
+ { input: "sathya" },
+ { input: "sathya" },
+ { input: 42 },
+ { input: "sathya" },
+ { input: 42 },
+ { input: "sathya" },
+ ],
+};
+
+```
+
+## Code
+
+```javascript
+import { unstable_useMemoCache as useMemoCache } from "react";
+function Component(props) {
+ const $ = useMemoCache(2);
+ const x = [];
+ const y = x;
+
+ const z = [y];
+
+ y.push(props.input);
+
+ const a = [z];
+
+ let b = 0;
+ if (a[0][0][0] === 42) {
+ b = 1;
+ }
+ let t0;
+ if ($[0] !== b) {
+ t0 = [b];
+ $[0] = b;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ return t0;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ sequentialRenders: [
+ { input: 42 },
+ { input: 42 },
+ { input: "sathya" },
+ { input: "sathya" },
+ { input: 42 },
+ { input: "sathya" },
+ { input: 42 },
+ { input: "sathya" },
+ ],
+};
+
+```
+
+### Eval output
+(kind: ok) [1]
+[1]
+[0]
+[0]
+[1]
+[0]
+[1]
+[0]
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactivity-via-readonly-alias-of-mutable-value.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactivity-via-readonly-alias-of-mutable-value.js
new file mode 100644
index 0000000000..c4165938b6
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactivity-via-readonly-alias-of-mutable-value.js
@@ -0,0 +1,37 @@
+function Component(props) {
+ const x = [];
+ const y = x;
+
+ // y isn't reactive yet when we first visit this, so z is initially non-reactive
+ const z = [y];
+
+ // then we realize y is reactive. we need a fixpoint to propagate this back to z
+ y.push(props.input);
+
+ // PruneNonReactiveDependencies partially propagates reactivity (for now) which
+ // we bypass with an indirection of storing into another variable
+ const a = [z];
+
+ // b's value is conditional on `a`, which is reactive per above
+ let b = 0;
+ if (a[0][0][0] === 42) {
+ b = 1;
+ }
+
+ return [b];
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ sequentialRenders: [
+ { input: 42 },
+ { input: 42 },
+ { input: "sathya" },
+ { input: "sathya" },
+ { input: 42 },
+ { input: "sathya" },
+ { input: 42 },
+ { input: "sathya" },
+ ],
+};