@samitouri / QOS-React-1 / commits / bd37fbe06a

[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.

Joe Savona committed Jan 2, 2024 at 15:31 UTC bd37fbe06acca8e11a00ea48752fb5e86146f42f
13 files changed +284 -9
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts
+40
@@ -10,8 +10,10 @@ import {
10 Place,
11 ReactiveBlock,
12 ReactiveFunction,
13 + ReactiveInstruction,
14 ReactiveScope,
15 ScopeId,
16 + makeInstructionId,
17 } from "../HIR/HIR";
18 import { getPlaceScope } from "./BuildReactiveBlocks";
19 import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
@@ -79,6 +81,40 @@ class Visitor extends ReactiveFunctionVisitor<Context> {
81 state.visitScope(scope);
82 }
83 }
84 +
85 + override visitInstruction(instr: ReactiveInstruction, state: Context): void {
86 + switch (instr.value.kind) {
87 + case "SequenceExpression":
88 + case "ConditionalExpression":
89 + case "LogicalExpression": {
90 + const prevScopeCount = state.currentScopes().length;
91 + this.traverseInstruction(instr, state);
92 +
93 + /**
94 + * These compound value types can have nested sequences of instructions
95 + * with scopes that start "partway" through a block-level instruction.
96 + * This would cause the start of the scope to not align with any block-level
97 + * instruction and get skipped by the later BuildReactiveBlocks pass.
98 + *
99 + * Here we detect scopes created within compound instructions and align the
100 + * start of these scopes to the outer instruction id to ensure the scopes
101 + * aren't skipped.
102 + */
103 + const scopes = state.currentScopes();
104 + for (let i = prevScopeCount; i < scopes.length; i++) {
105 + const scope = scopes[i];
106 + scope.scope.range.start = makeInstructionId(
107 + Math.min(instr.id, scope.scope.range.start)
108 + );
109 + }
110 + break;
111 + }
112 + default: {
113 + this.traverseInstruction(instr, state);
114 + }
115 + }
116 + }
117 +
118 override visitBlock(block: ReactiveBlock, state: Context): void {
119 state.enter(() => {
120 this.traverseBlock(block, state);
@@ -108,6 +144,10 @@ class Context {
144 */
145 #seenScopes: Set<ScopeId> = new Set();
146
147 + currentScopes(): Array<PendingReactiveScope> {
148 + return this.#blockScopes.at(-1) ?? [];
149 + }
150 +
151 enter(fn: () => void): void {
152 this.#blockScopes.push([]);
153 fn();
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-conditional.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo(props) {
6 + let x;
7 + true ? (x = []) : (x = {});
8 + return x;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Foo,
13 + params: [{}],
14 +};
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +import { unstable_useMemoCache as useMemoCache } from "react";
22 +function Foo(props) {
23 + const $ = useMemoCache(1);
24 + let x;
25 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26 + true ? (x = []) : (x = {});
27 + $[0] = x;
28 + } else {
29 + x = $[0];
30 + }
31 + return x;
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Foo,
36 + params: [{}],
37 +};
38 +
39 +```
40 +
41 +### Eval output
42 +(kind: ok) []
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-conditional.js new
+10
@@ -0,0 +1,10 @@
1 +function Foo(props) {
2 + let x;
3 + true ? (x = []) : (x = {});
4 + return x;
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Foo,
9 + params: [{}],
10 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-logical-no-sequence.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo(props) {
6 + let x;
7 + true && (x = []);
8 + return x;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Foo,
13 + params: [{}],
14 +};
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +import { unstable_useMemoCache as useMemoCache } from "react";
22 +function Foo(props) {
23 + const $ = useMemoCache(1);
24 + let x;
25 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26 + true && (x = []);
27 + $[0] = x;
28 + } else {
29 + x = $[0];
30 + }
31 + return x;
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Foo,
36 + params: [{}],
37 +};
38 +
39 +```
40 +
41 +### Eval output
42 +(kind: ok) []
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-logical-no-sequence.js new
+10
@@ -0,0 +1,10 @@
1 +function Foo(props) {
2 + let x;
3 + true && (x = []);
4 + return x;
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Foo,
9 + params: [{}],
10 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-logical.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo(props) {
6 + let x;
7 + true && ((x = []), null);
8 + return x;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Foo,
13 + params: [{}],
14 +};
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +import { unstable_useMemoCache as useMemoCache } from "react";
22 +function Foo(props) {
23 + const $ = useMemoCache(1);
24 + let x;
25 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26 + true && ((x = []), null);
27 + $[0] = x;
28 + } else {
29 + x = $[0];
30 + }
31 + return x;
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Foo,
36 + params: [{}],
37 +};
38 +
39 +```
40 +
41 +### Eval output
42 +(kind: ok) []
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-logical.js new
+10
@@ -0,0 +1,10 @@
1 +function Foo(props) {
2 + let x;
3 + true && ((x = []), null);
4 + return x;
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Foo,
9 + params: [{}],
10 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-sequence.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo(props) {
6 + let x;
7 + (x = []), null;
8 + return x;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Foo,
13 + params: [{}],
14 +};
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +import { unstable_useMemoCache as useMemoCache } from "react";
22 +function Foo(props) {
23 + const $ = useMemoCache(1);
24 + let x;
25 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26 + (x = []), null;
27 + $[0] = x;
28 + } else {
29 + x = $[0];
30 + }
31 + return x;
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Foo,
36 + params: [{}],
37 +};
38 +
39 +```
40 +
41 +### Eval output
42 +(kind: ok) []
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/memoize-value-block-value-sequence.js new
+10
@@ -0,0 +1,10 @@
1 +function Foo(props) {
2 + let x;
3 + (x = []), null;
4 + return x;
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Foo,
9 + params: [{}],
10 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md
+8 -2
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
22 ```javascript
23 import { unstable_useMemoCache as useMemoCache } from "react";
24 function foo(props) {
25 - const $ = useMemoCache(2);
25 + const $ = useMemoCache(4);
26 let x;
27 if ($[0] !== props.bar) {
28 x = [];
@@ -32,7 +32,13 @@ function foo(props) {
32 } else {
33 x = $[1];
34 }
35 - props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
35 + if ($[2] !== props) {
36 + props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
37 + $[2] = props;
38 + $[3] = x;
39 + } else {
40 + x = $[3];
41 + }
42 return x;
43 }
44
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md
+8 -2
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
22 ```javascript
23 import { unstable_useMemoCache as useMemoCache } from "react";
24 function foo(props) {
25 - const $ = useMemoCache(2);
25 + const $ = useMemoCache(4);
26 let x;
27 if ($[0] !== props.bar) {
28 x = [];
@@ -32,7 +32,13 @@ function foo(props) {
32 } else {
33 x = $[1];
34 }
35 - props.cond ? ((x = []), x.push(props.foo)) : null;
35 + if ($[2] !== props) {
36 + props.cond ? ((x = []), x.push(props.foo)) : null;
37 + $[2] = props;
38 + $[3] = x;
39 + } else {
40 + x = $[3];
41 + }
42 return x;
43 }
44
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md
+12 -3
@@ -19,13 +19,22 @@ function foo(props) {
19 ```javascript
20 import { unstable_useMemoCache as useMemoCache } from "react";
21 function foo(props) {
22 - const $ = useMemoCache(2);
22 + const $ = useMemoCache(5);
23 let x;
24 if ($[0] !== props) {
25 x = [];
26 x.push(props.bar);
27 - props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
28 - mut(x);
27 + if ($[2] !== props || $[3] !== x) {
28 + props.cond
29 + ? ((x = []), x.push(props.foo))
30 + : ((x = []), x.push(props.bar));
31 + mut(x);
32 + $[2] = props;
33 + $[3] = x;
34 + $[4] = x;
35 + } else {
36 + x = $[4];
37 + }
38 $[0] = props;
39 $[1] = x;
40 } else {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md
+8 -2
@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
24 ```javascript
25 import { unstable_useMemoCache as useMemoCache } from "react";
26 function foo(props) {
27 - const $ = useMemoCache(2);
27 + const $ = useMemoCache(4);
28 let x;
29 if ($[0] !== props.bar) {
30 x = [];
@@ -34,7 +34,13 @@ function foo(props) {
34 } else {
35 x = $[1];
36 }
37 - props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
37 + if ($[2] !== props) {
38 + props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
39 + $[2] = props;
40 + $[3] = x;
41 + } else {
42 + x = $[3];
43 + }
44 return x;
45 }
46