mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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:
+40
@@ -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();
|
||||
|
||||
+42
@@ -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) []
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
function Foo(props) {
|
||||
let x;
|
||||
true ? (x = []) : (x = {});
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
+42
@@ -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) []
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
function Foo(props) {
|
||||
let x;
|
||||
true && (x = []);
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
+42
@@ -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) []
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
function Foo(props) {
|
||||
let x;
|
||||
true && ((x = []), null);
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
+42
@@ -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) []
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
function Foo(props) {
|
||||
let x;
|
||||
(x = []), null;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
+8
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+12
-3
@@ -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 {
|
||||
|
||||
+8
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user