InferReactivePlaces accounts for mutable aliasing

Fixes T175227223. When inferring reactivity, mutation of a value with a reactive 
input marks the mutable value as reactive. However, we also need to account for 
aliases: 

```javascript 

const x = []; 

const y = x; 

y.push(props.value); 

``` 

Previously we would have only considered `y` reactive here, but `x` also becomes 
reactive. 

The implementation extracts out a helper from InferReactiveScopeVariables that 
builds a `DisjointSet<Identifier>` of disjoint sets of mutably aliased values. 
InferReactivePlaces then treats all instances of each mutable alias group as 
equivalent for reactivity purposes.
This commit is contained in:
Joe Savona
2024-01-19 16:03:58 -08:00
parent a272cf9b0c
commit 8be56418d3
8 changed files with 369 additions and 112 deletions
@@ -24,7 +24,11 @@ import {
eachTerminalOperand,
} from "../HIR/visitors";
import { hasBackEdge } from "../Optimization/DeadCodeElimination";
import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables";
import {
findDisjointMutableValues,
isMutable,
} from "../ReactiveScopes/InferReactiveScopeVariables";
import DisjointSet from "../Utils/DisjointSet";
import { assertExhaustive } from "../Utils/utils";
/*
@@ -87,7 +91,7 @@ import { assertExhaustive } from "../Utils/utils";
* there are no changes after a given pass over the CFG.
*/
export function inferReactivePlaces(fn: HIRFunction): void {
const reactiveIdentifiers = new ReactivityMap();
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
for (const param of fn.params) {
const place = param.kind === "Identifier" ? param : param.place;
reactiveIdentifiers.markReactive(place);
@@ -328,15 +332,33 @@ class ReactivityMap {
hasChanges: boolean = false;
reactive: Set<IdentifierId> = new Set();
/**
* Sets of mutably aliased identifiers — these are the same foundation for determining
* reactive scopes a few passes later. The actual InferReactiveScopeVariables pass runs
* after LeaveSSA, which artificially merges mutable ranges in cases such as declarations
* that are later reassigned. Here we use only the underlying sets of mutably aliased values.
*
* Any identifier that has a mapping in this disjoint set will be treated as a stand in for
* its canonical identifier in all cases, so that any reactivity flowing into one identifier of
* an alias group will effectively make the whole alias group (all its identifiers) reactive.
*/
aliasedIdentifiers: DisjointSet<Identifier>;
constructor(aliasedIdentifiers: DisjointSet<Identifier>) {
this.aliasedIdentifiers = aliasedIdentifiers;
}
isReactive(place: Place): boolean {
const reactive = this.reactive.has(place.identifier.id);
const reactive = this.isReactiveIdentifier(place.identifier);
if (reactive) {
place.reactive = true;
}
return reactive;
}
isReactiveIdentifier(identifier: Identifier): boolean {
isReactiveIdentifier(inputIdentifier: Identifier): boolean {
const identifier =
this.aliasedIdentifiers.find(inputIdentifier) ?? inputIdentifier;
return this.reactive.has(identifier.id);
}
@@ -345,7 +367,9 @@ class ReactivityMap {
this.markReactiveIdentifier(place.identifier);
}
markReactiveIdentifier(identifier: Identifier): void {
markReactiveIdentifier(inputIdentifier: Identifier): void {
const identifier =
this.aliasedIdentifiers.find(inputIdentifier) ?? inputIdentifier;
if (!this.reactive.has(identifier.id)) {
this.hasChanges = true;
this.reactive.add(identifier.id);
@@ -84,100 +84,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
* Represents the set of reactive scopes as disjoint sets of identifiers
* that mutate together.
*/
const scopeIdentifiers = new DisjointSet<Identifier>();
for (const [_, block] of fn.body.blocks) {
/*
* If a phi is mutated after creation, then we need to alias all of its operands such that they
* are assigned to the same scope.
*/
for (const phi of block.phis) {
if (
// The phi was reset because it was not mutated after creation
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
phi.id.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id)
) {
for (const [, phiId] of phi.operands) {
scopeIdentifiers.union([phi.id, phiId]);
}
}
}
block.phis.clear();
for (const instr of block.instructions) {
const operands: Array<Identifier> = [];
const range = instr.lvalue.identifier.mutableRange;
if (range.end > range.start + 1 || mayAllocate(fn.env, instr)) {
operands.push(instr.lvalue!.identifier);
}
if (
instr.value.kind === "StoreLocal" ||
instr.value.kind === "StoreContext"
) {
if (
instr.value.lvalue.place.identifier.mutableRange.end >
instr.value.lvalue.place.identifier.mutableRange.start + 1
) {
operands.push(instr.value.lvalue.place.identifier);
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === "Destructure") {
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
if (
place.identifier.mutableRange.end >
place.identifier.mutableRange.start + 1
) {
operands.push(place.identifier);
}
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === "MethodCall") {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
operand.identifier.mutableRange.start > 0
) {
operands.push(operand.identifier);
}
}
/*
* Ensure that the ComputedLoad to resolve the method is in the same scope as the
* call itself
*/
operands.push(instr.value.property.identifier);
} else {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
operand.identifier.mutableRange.start > 0
) {
operands.push(operand.identifier);
}
}
}
if (operands.length !== 0) {
scopeIdentifiers.union(operands);
}
}
}
const scopeIdentifiers = findDisjointMutableValues(fn);
// Maps each scope (by its identifying member) to a ScopeId value
const scopes: Map<Identifier, ReactiveScope> = new Map();
@@ -278,3 +185,102 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
}
}
}
export function findDisjointMutableValues(
fn: HIRFunction
): DisjointSet<Identifier> {
const scopeIdentifiers = new DisjointSet<Identifier>();
for (const [_, block] of fn.body.blocks) {
/*
* If a phi is mutated after creation, then we need to alias all of its operands such that they
* are assigned to the same scope.
*/
for (const phi of block.phis) {
if (
// The phi was reset because it was not mutated after creation
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
phi.id.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id)
) {
for (const [, phiId] of phi.operands) {
scopeIdentifiers.union([phi.id, phiId]);
}
}
}
for (const instr of block.instructions) {
const operands: Array<Identifier> = [];
const range = instr.lvalue.identifier.mutableRange;
if (range.end > range.start + 1 || mayAllocate(fn.env, instr)) {
operands.push(instr.lvalue!.identifier);
}
if (
instr.value.kind === "StoreLocal" ||
instr.value.kind === "StoreContext"
) {
if (
instr.value.lvalue.place.identifier.mutableRange.end >
instr.value.lvalue.place.identifier.mutableRange.start + 1
) {
operands.push(instr.value.lvalue.place.identifier);
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === "Destructure") {
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
if (
place.identifier.mutableRange.end >
place.identifier.mutableRange.start + 1
) {
operands.push(place.identifier);
}
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === "MethodCall") {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
operand.identifier.mutableRange.start > 0
) {
operands.push(operand.identifier);
}
}
/*
* Ensure that the ComputedLoad to resolve the method is in the same scope as the
* call itself
*/
operands.push(instr.value.property.identifier);
} else {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
operand.identifier.mutableRange.start > 0
) {
operands.push(operand.identifier);
}
}
}
if (operands.length !== 0) {
scopeIdentifiers.union(operands);
}
}
}
return scopeIdentifiers;
}
@@ -0,0 +1,84 @@
## Input
```javascript
function Component(props) {
const x = [];
const y = x;
y.push(props.input);
return [x[0]];
}
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(4);
let x;
if ($[0] !== props.input) {
x = [];
const y = x;
y.push(props.input);
$[0] = props.input;
$[1] = x;
} else {
x = $[1];
}
const t0 = x[0];
let t1;
if ($[2] !== t0) {
t1 = [t0];
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
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) [42]
[42]
["sathya"]
["sathya"]
[42]
["sathya"]
[42]
["sathya"]
@@ -0,0 +1,22 @@
function Component(props) {
const x = [];
const y = x;
y.push(props.input);
return [x[0]];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [
{ input: 42 },
{ input: 42 },
{ input: "sathya" },
{ input: "sathya" },
{ input: 42 },
{ input: "sathya" },
{ input: 42 },
{ input: "sathya" },
],
};
@@ -0,0 +1,91 @@
## Input
```javascript
function Component(props) {
const x = [];
const f = (arg) => {
const y = x;
y.push(arg);
};
f(props.input);
return [x[0]];
}
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(4);
let x;
if ($[0] !== props.input) {
x = [];
const f = (arg) => {
const y = x;
y.push(arg);
};
f(props.input);
$[0] = props.input;
$[1] = x;
} else {
x = $[1];
}
const t0 = x[0];
let t1;
if ($[2] !== t0) {
t1 = [t0];
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
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) [42]
[42]
["sathya"]
["sathya"]
[42]
["sathya"]
[42]
["sathya"]
@@ -0,0 +1,25 @@
function Component(props) {
const x = [];
const f = (arg) => {
const y = x;
y.push(arg);
};
f(props.input);
return [x[0]];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [
{ input: 42 },
{ input: 42 },
{ input: "sathya" },
{ input: "sathya" },
{ input: 42 },
{ input: "sathya" },
{ input: 42 },
{ input: "sathya" },
],
};
@@ -42,7 +42,7 @@ import {
import { mutate } from "shared-runtime";
function Component(props) {
const $ = useMemoCache(5);
const $ = useMemoCache(6);
const x = [{ ...props.value }];
let t0;
let t1;
@@ -66,21 +66,23 @@ function Component(props) {
y = item;
return <span key={item.id}>{item.text}</span>;
});
let t3;
if ($[2] !== onClick || $[3] !== t2) {
t3 = (
const t3 = mutate(y);
let t4;
if ($[2] !== onClick || $[3] !== t2 || $[4] !== t3) {
t4 = (
<div onClick={onClick}>
{t2}
{mutate(y)}
{t3}
</div>
);
$[2] = onClick;
$[3] = t2;
$[4] = t3;
$[5] = t4;
} else {
t3 = $[4];
t4 = $[5];
}
return t3;
return t4;
}
export const FIXTURE_ENTRYPOINT = {
@@ -20,7 +20,7 @@ function HomeDiscoStoreItemTileRating(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function HomeDiscoStoreItemTileRating(props) {
const $ = useMemoCache(3);
const $ = useMemoCache(4);
const item = useFragment();
let count;
if ($[0] !== item) {
@@ -34,14 +34,17 @@ function HomeDiscoStoreItemTileRating(props) {
} else {
count = $[1];
}
let t0;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Text>{count}</Text>;
const t0 = count;
let t1;
if ($[2] !== t0) {
t1 = <Text>{t0}</Text>;
$[2] = t0;
$[3] = t1;
} else {
t0 = $[2];
t1 = $[3];
}
return t0;
return t1;
}
```