Compare commits

..

2 Commits

Author SHA1 Message Date
Mofei Zhang
8ba22587b5 [compiler][optim] infer mixedReadOnly for numeric and computed properties
Expand type inference to infer mixedReadOnly types for numeric and computed property accesses.
```js
function Component({idx})
  const data = useFragment(...)
  // we want to type `posts` correctly as Array
  const posts = data.viewers[idx].posts.slice(0, 5);
  // ...
}
```
2025-03-13 11:52:25 -04:00
Mofei Zhang
7606b29048 [compiler][optim] Add shape for Array.from
(see title)
2025-03-13 11:52:25 -04:00
15 changed files with 546 additions and 41 deletions

View File

@@ -1126,9 +1126,32 @@ export class Environment {
);
}
getFallthroughPropertyType(
receiver: Type,
_property: Type,
): BuiltInType | PolyType | null {
let shapeId = null;
if (receiver.kind === 'Object' || receiver.kind === 'Function') {
shapeId = receiver.shapeId;
}
if (shapeId !== null) {
const shape = this.#shapes.get(shapeId);
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
loc: null,
suggestions: null,
});
return shape.properties.get('*') ?? null;
}
return null;
}
getPropertyType(
receiver: Type,
property: string,
property: string | number,
): BuiltInType | PolyType | null {
let shapeId = null;
if (receiver.kind === 'Object' || receiver.kind === 'Function') {
@@ -1146,17 +1169,19 @@ export class Environment {
loc: null,
suggestions: null,
});
let value =
shape.properties.get(property) ?? shape.properties.get('*') ?? null;
if (value === null && isHookName(property)) {
value = this.#getCustomHookType();
if (typeof property === 'string') {
return (
shape.properties.get(property) ??
shape.properties.get('*') ??
(isHookName(property) ? this.#getCustomHookType() : null)
);
} else {
return shape.properties.get('*') ?? null;
}
return value;
} else if (isHookName(property)) {
} else if (typeof property === 'string' && isHookName(property)) {
return this.#getCustomHookType();
} else {
return null;
}
return null;
}
getFunctionSignature(type: FunctionType): FunctionSignature | null {

View File

@@ -119,8 +119,8 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
],
/*
* https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.from
* Array.from(arrayLike, optionalFn, optionalThis) not added because
* the Effect of `arrayLike` is polymorphic i.e.
* Array.from(arrayLike, optionalFn, optionalThis)
* Note that the Effect of `arrayLike` is polymorphic i.e.
* - Effect.read if
* - it does not have an @iterator property and is array-like
* (i.e. has a length property)
@@ -128,6 +128,20 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
* - Effect.mutate if it is a self-mutative iterator (e.g. a generator
* function)
*/
[
'from',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [
Effect.ConditionallyMutate,
Effect.ConditionallyMutate,
Effect.ConditionallyMutate,
],
restParam: Effect.Read,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
}),
],
[
'of',
// Array.of(element0, ..., elementN)

View File

@@ -60,7 +60,15 @@ export type PropType = {
kind: 'Property';
objectType: Type;
objectName: string;
propertyName: PropertyLiteral;
propertyName:
| {
kind: 'literal';
value: PropertyLiteral;
}
| {
kind: 'computed';
value: Type;
};
};
export type ObjectMethod = {

View File

@@ -307,11 +307,26 @@ function* generateInstructionTypes(
kind: 'Property',
objectType: value.object.identifier.type,
objectName: getName(names, value.object.identifier.id),
propertyName: value.property,
propertyName: {
kind: 'literal',
value: value.property,
},
});
break;
}
case 'ComputedLoad': {
yield equation(left, {
kind: 'Property',
objectType: value.object.identifier.type,
objectName: getName(names, value.object.identifier.id),
propertyName: {
kind: 'computed',
value: value.property.identifier.type,
},
});
break;
}
case 'MethodCall': {
const returnType = makeType();
yield equation(value.property.identifier.type, {
@@ -336,7 +351,10 @@ function* generateInstructionTypes(
kind: 'Property',
objectType: value.value.identifier.type,
objectName: getName(names, value.value.identifier.id),
propertyName: makePropertyLiteral(propertyName),
propertyName: {
kind: 'literal',
value: makePropertyLiteral(propertyName),
},
});
} else {
break;
@@ -353,7 +371,10 @@ function* generateInstructionTypes(
kind: 'Property',
objectType: value.value.identifier.type,
objectName: getName(names, value.value.identifier.id),
propertyName: makePropertyLiteral(property.key.name),
propertyName: {
kind: 'literal',
value: makePropertyLiteral(property.key.name),
},
});
}
}
@@ -410,7 +431,6 @@ function* generateInstructionTypes(
case 'RegExpLiteral':
case 'MetaProperty':
case 'ComputedStore':
case 'ComputedLoad':
case 'Await':
case 'GetIterator':
case 'IteratorNext':
@@ -454,12 +474,13 @@ class Unifier {
return;
}
const objectType = this.get(tB.objectType);
let propertyType;
if (typeof tB.propertyName === 'number') {
propertyType = null;
} else {
propertyType = this.env.getPropertyType(objectType, tB.propertyName);
}
const propertyType =
tB.propertyName.kind === 'literal'
? this.env.getPropertyType(objectType, tB.propertyName.value)
: this.env.getFallthroughPropertyType(
objectType,
tB.propertyName.value,
);
if (propertyType !== null) {
this.unify(tA, propertyType);
}
@@ -677,7 +698,11 @@ class Unifier {
const RefLikeNameRE = /^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/;
function isRefLikeName(t: PropType): boolean {
return RefLikeNameRE.test(t.objectName) && t.propertyName === 'current';
return (
t.propertyName.kind === 'literal' &&
RefLikeNameRE.test(t.objectName) &&
t.propertyName.value === 'current'
);
}
function tryUnionTypes(ty1: Type, ty2: Type): Type | null {

View File

@@ -0,0 +1,91 @@
## Input
```javascript
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism).
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, (x, idx) => ({...x, id: idx}));
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useIdentity, Stringify } from "shared-runtime";
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism).
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component(t0) {
const $ = _c(4);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr, _temp);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(x, idx) {
return { ...x, id: idx };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
};
```
### Eval output
(kind: ok) <div>{"children":{"value":5,"id":2}}</div>
<div>{"children":{"value":6,"id":2}}</div>
<div>{"children":{"value":6,"id":2}}</div>

View File

@@ -0,0 +1,26 @@
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism).
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, (x, idx) => ({...x, id: idx}));
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};

View File

@@ -0,0 +1,88 @@
## Input
```javascript
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism)
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useIdentity, Stringify } from "shared-runtime";
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism)
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component(t0) {
const $ = _c(4);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
};
```
### Eval output
(kind: ok) <div>{"children":{"value":5}}</div>
<div>{"children":{"value":6}}</div>
<div>{"children":{"value":6}}</div>

View File

@@ -0,0 +1,26 @@
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism)
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};

View File

@@ -0,0 +1,64 @@
## Input
```javascript
import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
function Component(t0) {
const $ = _c(4);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
};
```
### Eval output
(kind: ok) <div>{"children":{"value":5,"wat0":"joe"}}</div>
<div>{"children":{"value":6,"wat0":"joe"}}</div>
<div>{"children":{"value":6,"wat0":"joe"}}</div>

View File

@@ -0,0 +1,14 @@
import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};

View File

@@ -0,0 +1,61 @@
## Input
```javascript
import {useFragment} from 'shared-runtime';
/**
* React compiler should infer that the returned value is a primitive and avoid
* memoizing it.
*/
function useRelayData({query, idx}) {
'use memo';
const data = useFragment('', query);
return data.a[idx].toString();
}
export const FIXTURE_ENTRYPOINT = {
fn: useRelayData,
params: [{query: '', idx: 0}],
sequentialRenders: [
{query: '', idx: 0},
{query: '', idx: 0},
{query: '', idx: 1},
],
};
```
## Code
```javascript
import { useFragment } from "shared-runtime";
/**
* React compiler should infer that the returned value is a primitive and avoid
* memoizing it.
*/
function useRelayData(t0) {
"use memo";
const { query, idx } = t0;
const data = useFragment("", query);
return data.a[idx].toString();
}
export const FIXTURE_ENTRYPOINT = {
fn: useRelayData,
params: [{ query: "", idx: 0 }],
sequentialRenders: [
{ query: "", idx: 0 },
{ query: "", idx: 0 },
{ query: "", idx: 1 },
],
};
```
### Eval output
(kind: ok) "1"
"1"
"2"

View File

@@ -0,0 +1,21 @@
import {useFragment} from 'shared-runtime';
/**
* React compiler should infer that the returned value is a primitive and avoid
* memoizing it.
*/
function useRelayData({query, idx}) {
'use memo';
const data = useFragment('', query);
return data.a[idx].toString();
}
export const FIXTURE_ENTRYPOINT = {
fn: useRelayData,
params: [{query: '', idx: 0}],
sequentialRenders: [
{query: '', idx: 0},
{query: '', idx: 0},
{query: '', idx: 1},
],
};

View File

@@ -68,21 +68,29 @@ function Validate({ x, input }) {
}
function useFoo(input) {
"use memo";
const $ = _c(3);
const $ = _c(5);
const x = Array.from([{}]);
useIdentity();
x.push([input]);
let t0;
if ($[0] !== input || $[1] !== x) {
t0 = <Validate x={x} input={input} />;
if ($[0] !== input) {
t0 = [input];
$[0] = input;
$[1] = x;
$[2] = t0;
$[1] = t0;
} else {
t0 = $[2];
t0 = $[1];
}
return t0;
x.push(t0);
let t1;
if ($[2] !== input || $[3] !== x) {
t1 = <Validate x={x} input={input} />;
$[2] = input;
$[3] = x;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -37,6 +37,12 @@ function useFoo({val1, val2}) {
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{val1: 1, val2: 2}],
params: [
{val1: 1, val2: 2},
{val1: 1, val2: 2},
{val1: 1, val2: 3},
{val1: 4, val2: 2},
],
};
```
@@ -71,29 +77,51 @@ function Validate({ x, val1, val2 }) {
}
function useFoo(t0) {
"use memo";
const $ = _c(4);
const $ = _c(8);
const { val1, val2 } = t0;
const x = Array.from([]);
useIdentity();
x.push([val1]);
x.push([val2]);
let t1;
if ($[0] !== val1 || $[1] !== val2 || $[2] !== x) {
t1 = <Validate x={x} val1={val1} val2={val2} />;
if ($[0] !== val1) {
t1 = [val1];
$[0] = val1;
$[1] = val2;
$[2] = x;
$[3] = t1;
$[1] = t1;
} else {
t1 = $[3];
t1 = $[1];
}
return t1;
x.push(t1);
let t2;
if ($[2] !== val2) {
t2 = [val2];
$[2] = val2;
$[3] = t2;
} else {
t2 = $[3];
}
x.push(t2);
let t3;
if ($[4] !== val1 || $[5] !== val2 || $[6] !== x) {
t3 = <Validate x={x} val1={val1} val2={val2} />;
$[4] = val1;
$[5] = val2;
$[6] = x;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ val1: 1, val2: 2 }],
params: [
{ val1: 1, val2: 2 },
{ val1: 1, val2: 2 },
{ val1: 1, val2: 3 },
{ val1: 4, val2: 2 },
],
};
```

View File

@@ -33,4 +33,10 @@ function useFoo({val1, val2}) {
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{val1: 1, val2: 2}],
params: [
{val1: 1, val2: 2},
{val1: 1, val2: 2},
{val1: 1, val2: 3},
{val1: 4, val2: 2},
],
};