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" }, + ], +};