InferReactivePlaces account for immutable aliases of mutably aliased values

I found this by adding logic to reject inputs where reactivity gets newly 
propagated in PruneNonReactiveDependencies. It's possible to create a readonly 
alias to a mutable value such that we don't know the value is reactive yet when 
the alias is created. Thus we need to do a fixpoint iteration even if there are 
no loops in order to be able to revisit such aliases and reflow the reactivity 
forward. Example: 

```javascript 

const x = []; 

const y = x; 

const z = [y]; // y isn't reactive yet when we first visit this, so z is 
initially non-reactive 

y.push(props.value); // then we realize y is reactive. we need a fixpoint to 
propagate this back to z 

const a = [z]; // need an indirection to get past the partial propagation in 
PruneNonReactiveDependencies 

let b = 0; 

if (a[0][0]) { 

b = 1; 

} 

return [b]; 

``` 

Existing fixtures don't change because the basic reactivity propagation in 
PruneNonReactiveDependencies is enough to make common cases work. I confirmed 
that the new fixture does not work on previous PR in the stack.
This commit is contained in:
Joe Savona
2024-01-22 15:35:55 -08:00
parent 0894e35d94
commit 57163f0a52
3 changed files with 157 additions and 3 deletions
@@ -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 <div>{z}</div>;
* ```
*
* 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<Identifier, Identifier>();
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());
}
/*
@@ -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]
@@ -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" },
],
};