Skip to content

Commit 45a3346

Browse files
committed
[compiler] Infer phi types, extend mutable ranges to account for Store effects
Redo of an earlier (pre-OSS) PR to infer types of phi nodes. There are a few pieces to this: 1. Update InferTypes to infer the type of `phi.id.type`, not the unused `phi.type`. 2. Update the algorithm to verify that all the phi types are actually equal, not just have the same kind. 3. Handle circular types by removing the cycle. However, that reveals another issue: InferMutableRanges currently infers the results of `Store` effects _after_ its fixpoint loop. That was fine when a Store could never occur on a phi (since they wouldn't have a type to get a function signature from). Now though, we can have Store effects occur on phis, and we need to ensure that this correctly updates the mutable range of the phi operands - recursively. See new test that fails without the fixpoint loop. ghstack-source-id: e254393 Pull Request resolved: #30796
1 parent 798002c commit 45a3346

16 files changed

+342
-168
lines changed

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,17 @@ export function inferMutableRanges(ir: HIRFunction): void {
5151
inferMutableLifetimes(ir, true);
5252

5353
// Re-infer mutable ranges for aliases
54-
inferMutableRangesForAlias(ir, aliases);
54+
prevAliases = aliases.canonicalize();
55+
while (true) {
56+
inferMutableRangesForAlias(ir, aliases);
57+
inferAliasForStores(ir, aliases);
58+
inferAliasForPhis(ir, aliases);
59+
const nextAliases = aliases.canonicalize();
60+
if (areEqualMaps(prevAliases, nextAliases)) {
61+
break;
62+
}
63+
prevAliases = nextAliases;
64+
}
5565
}
5666

5767
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {

compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -483,30 +483,135 @@ class Unifier {
483483
}
484484

485485
if (type.kind === 'Phi') {
486-
const operands = new Set(type.operands.map(i => this.get(i).kind));
487-
488-
CompilerError.invariant(operands.size > 0, {
486+
CompilerError.invariant(type.operands.length > 0, {
489487
reason: 'there should be at least one operand',
490488
description: null,
491489
loc: null,
492490
suggestions: null,
493491
});
494-
const kind = operands.values().next().value;
495492

496-
// there's only one unique type and it's not a type var
497-
if (operands.size === 1 && kind !== 'Type') {
498-
this.unify(v, type.operands[0]);
493+
let candidateType: Type | null = null;
494+
for (const operand of type.operands) {
495+
const resolved = this.get(operand);
496+
if (candidateType === null) {
497+
candidateType = resolved;
498+
} else if (!typeEquals(resolved, candidateType)) {
499+
candidateType = null;
500+
break;
501+
} // else same type, continue
502+
}
503+
504+
if (candidateType !== null) {
505+
this.unify(v, candidateType);
499506
return;
500507
}
501508
}
502509

503510
if (this.occursCheck(v, type)) {
511+
const resolvedType = this.tryResolveType(v, type);
512+
if (resolvedType !== null) {
513+
this.substitutions.set(v.id, resolvedType);
514+
return;
515+
}
504516
throw new Error('cycle detected');
505517
}
506518

507519
this.substitutions.set(v.id, type);
508520
}
509521

522+
tryResolveType(v: TypeVar, type: Type): Type | null {
523+
switch (type.kind) {
524+
case 'Phi': {
525+
/**
526+
* Resolve the type of the phi by recursively removing `v` as an operand.
527+
* For example we can end up with types like this:
528+
*
529+
* v = Phi [
530+
* T1
531+
* T2
532+
* Phi [
533+
* T3
534+
* Phi [
535+
* T4
536+
* v <-- cycle!
537+
* ]
538+
* ]
539+
* ]
540+
*
541+
* By recursively removing `v`, we end up with:
542+
*
543+
* v = Phi [
544+
* T1
545+
* T2
546+
* Phi [
547+
* T3
548+
* Phi [
549+
* T4
550+
* ]
551+
* ]
552+
* ]
553+
*
554+
* Which avoids the cycle
555+
*/
556+
const operands = [];
557+
for (const operand of type.operands) {
558+
if (operand.kind === 'Type' && operand.id === v.id) {
559+
continue;
560+
}
561+
const resolved = this.tryResolveType(v, operand);
562+
if (resolved === null) {
563+
return null;
564+
}
565+
operands.push(resolved);
566+
}
567+
return {kind: 'Phi', operands};
568+
}
569+
case 'Type': {
570+
const substitution = this.get(type);
571+
if (substitution !== type) {
572+
const resolved = this.tryResolveType(v, substitution);
573+
if (resolved !== null) {
574+
this.substitutions.set(type.id, resolved);
575+
}
576+
return resolved;
577+
}
578+
return type;
579+
}
580+
case 'Property': {
581+
const objectType = this.tryResolveType(v, this.get(type.objectType));
582+
if (objectType === null) {
583+
return null;
584+
}
585+
return {
586+
kind: 'Property',
587+
objectName: type.objectName,
588+
objectType,
589+
propertyName: type.propertyName,
590+
};
591+
}
592+
case 'Function': {
593+
const returnType = this.tryResolveType(v, this.get(type.return));
594+
if (returnType === null) {
595+
return null;
596+
}
597+
return {
598+
kind: 'Function',
599+
return: returnType,
600+
shapeId: type.shapeId,
601+
};
602+
}
603+
case 'ObjectMethod':
604+
case 'Object':
605+
case 'Primitive':
606+
case 'Poly': {
607+
return type;
608+
}
609+
default: {
610+
assertExhaustive(type, `Unexpected type kind '${(type as any).kind}'`);
611+
}
612+
}
613+
}
614+
510615
occursCheck(v: TypeVar, type: Type): boolean {
511616
if (typeEquals(v, type)) return true;
512617

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-push-effect.expect.md

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { c as _c } from "react/compiler-runtime"; // arrayInstance.push should h
2323
// - read on all args (rest parameter)
2424
// - mutate on receiver
2525
function Component(props) {
26-
const $ = _c(8);
26+
const $ = _c(7);
2727
let t0;
2828
if ($[0] !== props.x) {
2929
t0 = foo(props.x);
@@ -45,14 +45,7 @@ function Component(props) {
4545
let arr;
4646
if ($[4] !== x || $[5] !== y) {
4747
arr = [];
48-
let t2;
49-
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
50-
t2 = {};
51-
$[7] = t2;
52-
} else {
53-
t2 = $[7];
54-
}
55-
arr.push(t2);
48+
arr.push({});
5649
arr.push(x, y);
5750
$[4] = x;
5851
$[5] = y;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {makeArray} from 'shared-runtime';
6+
7+
function Component(props) {
8+
const x = {};
9+
let y;
10+
if (props.cond) {
11+
if (props.cond2) {
12+
y = [props.value];
13+
} else {
14+
y = [props.value2];
15+
}
16+
} else {
17+
y = [];
18+
}
19+
// This should be inferred as `<store> y` s.t. `x` can still
20+
// be independently memoized. *But* this also must properly
21+
// extend the mutable range of the array literals in the
22+
// if/else branches
23+
y.push(x);
24+
25+
return [x, y];
26+
}
27+
28+
export const FIXTURE_ENTRYPOINT = {
29+
fn: Component,
30+
params: [{cond: true, cond2: true, value: 42}],
31+
sequentialRenders: [
32+
{cond: true, cond2: true, value: 3.14},
33+
{cond: true, cond2: true, value: 42},
34+
{cond: true, cond2: true, value: 3.14},
35+
{cond: true, cond2: false, value2: 3.14},
36+
{cond: true, cond2: false, value2: 42},
37+
{cond: true, cond2: false, value2: 3.14},
38+
{cond: false},
39+
{cond: false},
40+
],
41+
};
42+
43+
```
44+
45+
## Code
46+
47+
```javascript
48+
import { c as _c } from "react/compiler-runtime";
49+
import { makeArray } from "shared-runtime";
50+
51+
function Component(props) {
52+
const $ = _c(3);
53+
let t0;
54+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
55+
t0 = {};
56+
$[0] = t0;
57+
} else {
58+
t0 = $[0];
59+
}
60+
const x = t0;
61+
let t1;
62+
if ($[1] !== props) {
63+
let y;
64+
if (props.cond) {
65+
if (props.cond2) {
66+
y = [props.value];
67+
} else {
68+
y = [props.value2];
69+
}
70+
} else {
71+
y = [];
72+
}
73+
74+
y.push(x);
75+
76+
t1 = [x, y];
77+
$[1] = props;
78+
$[2] = t1;
79+
} else {
80+
t1 = $[2];
81+
}
82+
return t1;
83+
}
84+
85+
export const FIXTURE_ENTRYPOINT = {
86+
fn: Component,
87+
params: [{ cond: true, cond2: true, value: 42 }],
88+
sequentialRenders: [
89+
{ cond: true, cond2: true, value: 3.14 },
90+
{ cond: true, cond2: true, value: 42 },
91+
{ cond: true, cond2: true, value: 3.14 },
92+
{ cond: true, cond2: false, value2: 3.14 },
93+
{ cond: true, cond2: false, value2: 42 },
94+
{ cond: true, cond2: false, value2: 3.14 },
95+
{ cond: false },
96+
{ cond: false },
97+
],
98+
};
99+
100+
```
101+
102+
### Eval output
103+
(kind: ok) [{},[3.14,"[[ cyclic ref *1 ]]"]]
104+
[{},[42,"[[ cyclic ref *1 ]]"]]
105+
[{},[3.14,"[[ cyclic ref *1 ]]"]]
106+
[{},[3.14,"[[ cyclic ref *1 ]]"]]
107+
[{},[42,"[[ cyclic ref *1 ]]"]]
108+
[{},[3.14,"[[ cyclic ref *1 ]]"]]
109+
[{},["[[ cyclic ref *1 ]]"]]
110+
[{},["[[ cyclic ref *1 ]]"]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {makeArray} from 'shared-runtime';
2+
3+
function Component(props) {
4+
const x = {};
5+
let y;
6+
if (props.cond) {
7+
if (props.cond2) {
8+
y = [props.value];
9+
} else {
10+
y = [props.value2];
11+
}
12+
} else {
13+
y = [];
14+
}
15+
// This should be inferred as `<store> y` s.t. `x` can still
16+
// be independently memoized. *But* this also must properly
17+
// extend the mutable range of the array literals in the
18+
// if/else branches
19+
y.push(x);
20+
21+
return [x, y];
22+
}
23+
24+
export const FIXTURE_ENTRYPOINT = {
25+
fn: Component,
26+
params: [{cond: true, cond2: true, value: 42}],
27+
sequentialRenders: [
28+
{cond: true, cond2: true, value: 3.14},
29+
{cond: true, cond2: true, value: 42},
30+
{cond: true, cond2: true, value: 3.14},
31+
{cond: true, cond2: false, value2: 3.14},
32+
{cond: true, cond2: false, value2: 42},
33+
{cond: true, cond2: false, value2: 3.14},
34+
{cond: false},
35+
{cond: false},
36+
],
37+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
## Input
33

44
```javascript
5+
import {makeArray} from 'shared-runtime';
6+
57
function Component(props) {
68
const x = {};
79
let y;
@@ -35,11 +37,20 @@ export const FIXTURE_ENTRYPOINT = {
3537

3638
```javascript
3739
import { c as _c } from "react/compiler-runtime";
40+
import { makeArray } from "shared-runtime";
41+
3842
function Component(props) {
39-
const $ = _c(2);
43+
const $ = _c(3);
4044
let t0;
41-
if ($[0] !== props) {
42-
const x = {};
45+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
46+
t0 = {};
47+
$[0] = t0;
48+
} else {
49+
t0 = $[0];
50+
}
51+
const x = t0;
52+
let t1;
53+
if ($[1] !== props) {
4354
let y;
4455
if (props.cond) {
4556
y = [props.value];
@@ -49,13 +60,13 @@ function Component(props) {
4960

5061
y.push(x);
5162

52-
t0 = [x, y];
53-
$[0] = props;
54-
$[1] = t0;
63+
t1 = [x, y];
64+
$[1] = props;
65+
$[2] = t1;
5566
} else {
56-
t0 = $[1];
67+
t1 = $[2];
5768
}
58-
return t0;
69+
return t1;
5970
}
6071

6172
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {makeArray} from 'shared-runtime';
2+
13
function Component(props) {
24
const x = {};
35
let y;

0 commit comments

Comments
 (0)