Mutation within a reactively controlled block propagates reactivity

In InferReactivePlaces, we already account for reactively controlled values: 
where a value is never assigned a non-reactive value, but _which_ value is 
assigned is based on a reactive condition (the test conditions of an if, switch, 
loop, etc). 

This PR extends that reactively-controlled inference to mutation that is 
conditioned upon a reactive value. From the test case: 

```javascript 

let x = []; 

if (props.cond) { 

// This mutation has no reactive inputs. 

// *But* the mutation conditionally occurs based on props.cond which is reactive 

x.push(1); 

} 

let y = false; 

if (x[0]) { // therefore the value observed here is reactive 

y = true; 

} 

// so the value of y here is reactive via the reactive control dependency x[0] 

return [y]; 

```
This commit is contained in:
Joe Savona
2024-01-19 10:03:51 -08:00
parent 34c89458f2
commit c8323f3b42
5 changed files with 218 additions and 43 deletions
@@ -97,10 +97,48 @@ export function inferReactivePlaces(fn: HIRFunction): void {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isReactiveControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case "if":
case "branch": {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
break;
}
case "switch": {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (
case_.test !== null &&
reactiveIdentifiers.isReactive(case_.test)
) {
return true;
}
}
break;
}
}
}
return false;
}
const hasLoop = hasBackEdge(fn);
do {
const identifierMapping = new Map<Identifier, Identifier>();
for (const [, block] of fn.body.blocks) {
let hasReactiveControl = isReactiveControlledBlock(block.id);
for (const phi of block.phis) {
if (reactiveIdentifiers.isReactiveIdentifier(phi.id)) {
// Already marked reactive on a previous pass
@@ -116,48 +154,10 @@ export function inferReactivePlaces(fn: HIRFunction): void {
if (isPhiReactive) {
reactiveIdentifiers.markReactiveIdentifier(phi.id);
} else {
// check to see if it has a reactive control dependency
for (const [pred, _operand] of phi.operands) {
let controlBlocks = postDominatorFrontierCache.get(pred);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, pred);
postDominatorFrontierCache.set(pred, controlBlocks);
}
control: for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case "if":
case "branch": {
if (
reactiveIdentifiers.isReactive(controlBlock.terminal.test)
) {
// control dependency is reactive
reactiveIdentifiers.markReactiveIdentifier(phi.id);
break control;
}
break;
}
case "switch": {
if (
reactiveIdentifiers.isReactive(controlBlock.terminal.test)
) {
// control dependency is reactive
reactiveIdentifiers.markReactiveIdentifier(phi.id);
break control;
}
for (const case_ of controlBlock.terminal.cases) {
if (
case_.test !== null &&
reactiveIdentifiers.isReactive(case_.test)
) {
// control dependency is reactive
reactiveIdentifiers.markReactiveIdentifier(phi.id);
break control;
}
}
break;
}
}
for (const [pred] of phi.operands) {
if (isReactiveControlledBlock(pred)) {
reactiveIdentifiers.markReactiveIdentifier(phi.id);
break;
}
}
}
@@ -197,7 +197,8 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
reactiveIdentifiers.markReactive(lvalue);
}
}
if (hasReactiveInput || hasReactiveControl) {
for (const operand of eachInstructionValueOperand(value)) {
switch (operand.effect) {
case Effect.Capture:
@@ -0,0 +1,63 @@
## Input
```javascript
function Component(props) {
// x is mutated conditionally based on a reactive value,
// so it needs to be considered reactive
let x = [];
if (props.cond) {
x.push(1);
}
// Since x is reactive, y is now reactively controlled too:
let y = false;
if (x[0]) {
y = true;
}
// Thus this value should be reactive on `y`:
return [y];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
const $ = useMemoCache(2);
const x = [];
if (props.cond) {
x.push(1);
}
let y = false;
if (x[0]) {
y = true;
}
let t0;
if ($[0] !== y) {
t0 = [y];
$[0] = y;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
};
```
### Eval output
(kind: ok) [true]
@@ -0,0 +1,20 @@
function Component(props) {
// x is mutated conditionally based on a reactive value,
// so it needs to be considered reactive
let x = [];
if (props.cond) {
x.push(1);
}
// Since x is reactive, y is now reactively controlled too:
let y = false;
if (x[0]) {
y = true;
}
// Thus this value should be reactive on `y`:
return [y];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
};
@@ -0,0 +1,68 @@
## Input
```javascript
function Component(props) {
// x is mutated conditionally based on a reactive value,
// so it needs to be considered reactive
let x = [];
if (props.cond) {
x.push(1);
}
// Since x is reactive, y is now reactively controlled too:
let y = false;
switch (x[0]) {
case 1: {
y = true;
break;
}
}
// Thus this value should be reactive on `y`:
return [y];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
const $ = useMemoCache(2);
const x = [];
if (props.cond) {
x.push(1);
}
let y = false;
switch (x[0]) {
case 1: {
y = true;
}
}
let t0;
if ($[0] !== y) {
t0 = [y];
$[0] = y;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
};
```
### Eval output
(kind: ok) [true]
@@ -0,0 +1,23 @@
function Component(props) {
// x is mutated conditionally based on a reactive value,
// so it needs to be considered reactive
let x = [];
if (props.cond) {
x.push(1);
}
// Since x is reactive, y is now reactively controlled too:
let y = false;
switch (x[0]) {
case 1: {
y = true;
break;
}
}
// Thus this value should be reactive on `y`:
return [y];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
};