mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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());
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
+100
@@ -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]
|
||||
+37
@@ -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" },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user