[compiler] Special-case phi inference for mixed readonly type

This allows us to handle common operations such as `useFragment(...).edges.nodes ?? []` where we have a `Phi(MixedReadonly, Array)`. The underlying pattern remains general-purpose and not Relay-specific, and any API that returns transitively "mixed" data (primitives, arrays, plain objects) can benefit from the same type refinement.

[ghstack-poisoned]
This commit is contained in:
Joe Savona
2024-08-22 17:35:03 -07:00
parent f450193c7d
commit d3ed495294
4 changed files with 125 additions and 44 deletions
@@ -14,7 +14,9 @@ import {
Identifier,
IdentifierId,
Instruction,
isArrayType,
makeType,
ObjectType,
PropType,
Type,
typeEquals,
@@ -25,6 +27,7 @@ import {
BuiltInArrayId,
BuiltInFunctionId,
BuiltInJsxId,
BuiltInMixedReadonlyId,
BuiltInObjectId,
BuiltInPropsId,
BuiltInRefValueId,
@@ -496,8 +499,23 @@ class Unifier {
if (candidateType === null) {
candidateType = resolved;
} else if (!typeEquals(resolved, candidateType)) {
candidateType = null;
break;
function isArrayOrMixedReadonly(ty: Type): ty is ObjectType {
return (
ty.kind === 'Object' &&
(ty.shapeId === BuiltInArrayId ||
ty.shapeId === BuiltInMixedReadonlyId)
);
}
if (
isArrayOrMixedReadonly(resolved) &&
isArrayOrMixedReadonly(candidateType)
) {
candidateType =
resolved.shapeId === BuiltInArrayId ? resolved : candidateType;
} else {
candidateType = null;
break;
}
} // else same type, continue
}
@@ -1,42 +0,0 @@
## Input
```javascript
// @flow @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {useFragment} from 'shared-runtime';
function Component() {
const data = useFragment();
const nodes = data.nodes ?? [];
const flatMap = nodes.flatMap(node => node.items);
const filtered = flatMap.filter(item => item != null);
const map = useMemo(() => filtered.map(), [filtered]);
const index = filtered.findIndex(x => x === null);
return (
<div>
{map}
{index}
</div>
);
}
```
## Error
```
8 | const flatMap = nodes.flatMap(node => node.items);
9 | const filtered = flatMap.filter(item => item != null);
> 10 | const map = useMemo(() => filtered.map(), [filtered]);
| ^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (10:10)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (10:10)
11 | const index = filtered.findIndex(x => x === null);
12 |
13 | return (
```
@@ -0,0 +1,105 @@
## Input
```javascript
// @flow @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {useFragment} from 'shared-runtime';
function Component() {
const data = useFragment();
const nodes = data.nodes ?? [];
const flatMap = nodes.flatMap(node => node.items);
const filtered = flatMap.filter(item => item != null);
const map = useMemo(() => filtered.map(), [filtered]);
const index = filtered.findIndex(x => x === null);
return (
<div>
{map}
{index}
</div>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import { useFragment } from "shared-runtime";
function Component() {
const $ = _c(11);
const data = useFragment();
let t0;
if ($[0] !== data.nodes) {
t0 = data.nodes ?? [];
$[0] = data.nodes;
$[1] = t0;
} else {
t0 = $[1];
}
const nodes = t0;
let t1;
if ($[2] !== nodes) {
t1 = nodes.flatMap(_temp);
$[2] = nodes;
$[3] = t1;
} else {
t1 = $[3];
}
const flatMap = t1;
let t2;
if ($[4] !== flatMap) {
t2 = flatMap.filter(_temp2);
$[4] = flatMap;
$[5] = t2;
} else {
t2 = $[5];
}
const filtered = t2;
let t3;
let t4;
if ($[6] !== filtered) {
t4 = filtered.map();
$[6] = filtered;
$[7] = t4;
} else {
t4 = $[7];
}
t3 = t4;
const map = t3;
const index = filtered.findIndex(_temp3);
let t5;
if ($[8] !== map || $[9] !== index) {
t5 = (
<div>
{map}
{index}
</div>
);
$[8] = map;
$[9] = index;
$[10] = t5;
} else {
t5 = $[10];
}
return t5;
}
function _temp3(x) {
return x === null;
}
function _temp2(item) {
return item != null;
}
function _temp(node) {
return node.items;
}
```
### Eval output
(kind: exception) Fixture not implemented