[wip] Fix phi inference, expose InferMutableRange issue

> Update: this is now passing all tests. The approach is likely wrong, and even 
if it's fine it needs some cleanup. Putting up for review as folks (esp 
@gsathya) have time. 

## Background 

InferTypes was intended to infer types for phi identifiers, but by accident we 
ended up storing the inferred type on `phi.type` instead of `phi.id.type`, which 
is the type that usages of the phi will reference. Because of this, we weren't 
actually inferring types for several cases, for example if both if/else branches 
assign `x` to an array literal, we'd ideally like the corresponding phi id to be 
typed as a BuiltInArray: 

```javascript 

let x; 

let y = { ... }; 

if (cond) { 

x = []; 

} else { 

x = []; 

} 

// x should be BuiltnArray here. We inferred that on Phi.type but the x here 
wouldn't get that type previously 

x.push(y); 

``` 

## Circular Types 

I started by removing the `Phi.type` property and updating inference to store 
the result of phi unification on `phi.id.type` — but this revealed other issues. 

First was this can create circular types when there are loops. The solution is 
to basically allow circular types _for phis only_, and when we detect them we 
remove the cycle. Basically whenever we have a situation where we have some type 
variable X, and a type Y that is a (nested) phi type one of whose transitive 
operands contains X, we remove X from the transitive type and attempt to 
collapse the phi type upwards if all of its remaining operands are the same: 

``` 

X=Type(1) 

Y=Phi [ 

Type(2), 

Type(3) = Phi [ 

Type(1), // <-- cycle but we can prune this 

Type(2), 

Type(2), 

] 

] 

=> 

X=Type(1) 

Y=Phi [ 

Type(2), 

Type(3) = Phi [ // all remaining operands are the same, we can prune this 

Type(2), 

Type(2), 

] 

] 

=> 

X=Type(1) 

Y=Phi [ // all remaining operands are the same, we can prune this 

Type(2), 

Type(2), 

] 

=> 

X=Type(1) 

Y=Type(2) 

``` 

We have to do this not just doing unify(), but also in `get()` since there are 
cases where we don't know yet which type variables we can remove from a phi. 
Without also doing the pruning in get, we get an infinite loop. 

## Reactive Scope Alignment 

The above fixed the circular types, but exposed some new cases that can occur in 
terms of mutable ranges and ast structures: it wasn't possible before to have a 
Store on a phi node in practice, since that relied on type information which we 
didn't have for phis. 

The new validation that all instructions for a scope are part of that scope 
caught a couple issues, which were basically like this: 

``` 

[1] Sequence 

... 

[9] StoreLocal x@0[9:28] 

[10] ... 

``` 

Note that scope 0 starts at instruction 9, but that instruction is not at the 
block scope level. The first instruction at the block scope level that is within 
the range of scope 0 is instruction 10, which is after the scope should have 
started! So I also had to update AlignScopesToBlockScopes to handle the case of 
logical, conditional, and sequence expressions: we sometime need to adjust a 
scope start earlier in case they contain instructions that should start a scope.
This commit is contained in:
Joe Savona
2024-01-02 15:31:57 -08:00
parent bf859705b5
commit bd37fbe06a
13 changed files with 284 additions and 9 deletions
@@ -10,8 +10,10 @@ import {
Place,
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScope,
ScopeId,
makeInstructionId,
} from "../HIR/HIR";
import { getPlaceScope } from "./BuildReactiveBlocks";
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
@@ -79,6 +81,40 @@ class Visitor extends ReactiveFunctionVisitor<Context> {
state.visitScope(scope);
}
}
override visitInstruction(instr: ReactiveInstruction, state: Context): void {
switch (instr.value.kind) {
case "SequenceExpression":
case "ConditionalExpression":
case "LogicalExpression": {
const prevScopeCount = state.currentScopes().length;
this.traverseInstruction(instr, state);
/**
* These compound value types can have nested sequences of instructions
* with scopes that start "partway" through a block-level instruction.
* This would cause the start of the scope to not align with any block-level
* instruction and get skipped by the later BuildReactiveBlocks pass.
*
* Here we detect scopes created within compound instructions and align the
* start of these scopes to the outer instruction id to ensure the scopes
* aren't skipped.
*/
const scopes = state.currentScopes();
for (let i = prevScopeCount; i < scopes.length; i++) {
const scope = scopes[i];
scope.scope.range.start = makeInstructionId(
Math.min(instr.id, scope.scope.range.start)
);
}
break;
}
default: {
this.traverseInstruction(instr, state);
}
}
}
override visitBlock(block: ReactiveBlock, state: Context): void {
state.enter(() => {
this.traverseBlock(block, state);
@@ -108,6 +144,10 @@ class Context {
*/
#seenScopes: Set<ScopeId> = new Set();
currentScopes(): Array<PendingReactiveScope> {
return this.#blockScopes.at(-1) ?? [];
}
enter(fn: () => void): void {
this.#blockScopes.push([]);
fn();
@@ -0,0 +1,42 @@
## Input
```javascript
function Foo(props) {
let x;
true ? (x = []) : (x = {});
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Foo(props) {
const $ = useMemoCache(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
true ? (x = []) : (x = {});
$[0] = x;
} else {
x = $[0];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) []
@@ -0,0 +1,10 @@
function Foo(props) {
let x;
true ? (x = []) : (x = {});
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
@@ -0,0 +1,42 @@
## Input
```javascript
function Foo(props) {
let x;
true && (x = []);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Foo(props) {
const $ = useMemoCache(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
true && (x = []);
$[0] = x;
} else {
x = $[0];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) []
@@ -0,0 +1,10 @@
function Foo(props) {
let x;
true && (x = []);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
@@ -0,0 +1,42 @@
## Input
```javascript
function Foo(props) {
let x;
true && ((x = []), null);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Foo(props) {
const $ = useMemoCache(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
true && ((x = []), null);
$[0] = x;
} else {
x = $[0];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) []
@@ -0,0 +1,10 @@
function Foo(props) {
let x;
true && ((x = []), null);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
@@ -0,0 +1,42 @@
## Input
```javascript
function Foo(props) {
let x;
(x = []), null;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Foo(props) {
const $ = useMemoCache(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
(x = []), null;
$[0] = x;
} else {
x = $[0];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) []
@@ -0,0 +1,10 @@
function Foo(props) {
let x;
(x = []), null;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function foo(props) {
const $ = useMemoCache(2);
const $ = useMemoCache(4);
let x;
if ($[0] !== props.bar) {
x = [];
@@ -32,7 +32,13 @@ function foo(props) {
} else {
x = $[1];
}
props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
if ($[2] !== props) {
props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
$[2] = props;
$[3] = x;
} else {
x = $[3];
}
return x;
}
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function foo(props) {
const $ = useMemoCache(2);
const $ = useMemoCache(4);
let x;
if ($[0] !== props.bar) {
x = [];
@@ -32,7 +32,13 @@ function foo(props) {
} else {
x = $[1];
}
props.cond ? ((x = []), x.push(props.foo)) : null;
if ($[2] !== props) {
props.cond ? ((x = []), x.push(props.foo)) : null;
$[2] = props;
$[3] = x;
} else {
x = $[3];
}
return x;
}
@@ -19,13 +19,22 @@ function foo(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function foo(props) {
const $ = useMemoCache(2);
const $ = useMemoCache(5);
let x;
if ($[0] !== props) {
x = [];
x.push(props.bar);
props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
mut(x);
if ($[2] !== props || $[3] !== x) {
props.cond
? ((x = []), x.push(props.foo))
: ((x = []), x.push(props.bar));
mut(x);
$[2] = props;
$[3] = x;
$[4] = x;
} else {
x = $[4];
}
$[0] = props;
$[1] = x;
} else {
@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function foo(props) {
const $ = useMemoCache(2);
const $ = useMemoCache(4);
let x;
if ($[0] !== props.bar) {
x = [];
@@ -34,7 +34,13 @@ function foo(props) {
} else {
x = $[1];
}
props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
if ($[2] !== props) {
props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
$[2] = props;
$[3] = x;
} else {
x = $[3];
}
return x;
}