[compiler] Type inference for tagged template literals

At Meta we have a pattern of using tagged template literals for features that are compiled away:

```
// Relay:
graphql`...graphql text...`
```

In many cases these tags produce a primitive value, and we can get even more optimal output if we can tell the compiler about these types. The new moduleTypeProvider gives us the ability to declare such types, this PR extends the compiler to use this type information for TaggedTemplateExpression values.

ghstack-source-id: 3cd6511b7f4e708bcb86f3f3fde5773bc51c7197
Pull Request resolved: https://github.com/facebook/react/pull/30869
This commit is contained in:
Joe Savona
2024-09-04 13:28:33 -07:00
parent d37cc3a8b2
commit 2d1b4a6a6d
9 changed files with 273 additions and 60 deletions
@@ -1180,18 +1180,6 @@ function inferBlock(
};
break;
}
case 'TaggedTemplateExpression': {
valueKind = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = {
kind: Effect.ConditionallyMutate,
reason: ValueReason.Other,
};
break;
}
case 'TemplateLiteral': {
/*
* template literal (with no tag function) always produces
@@ -1312,6 +1300,47 @@ function inferBlock(
instr.lvalue.effect = Effect.Store;
continue;
}
case 'TaggedTemplateExpression': {
const operands = [...eachInstructionValueOperand(instrValue)];
if (operands.length !== 1) {
// future-proofing to make sure we update this case when we support interpolation
CompilerError.throwTodo({
reason: 'Support tagged template expressions with interpolations',
loc: instrValue.loc,
});
}
const signature = getFunctionCallSignature(
env,
instrValue.tag.identifier.type,
);
let calleeEffect =
signature?.calleeEffect ?? Effect.ConditionallyMutate;
const returnValueKind: AbstractValue =
signature !== null
? {
kind: signature.returnValueKind,
reason: new Set([
signature.returnValueReason ??
ValueReason.KnownReturnSignature,
]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
state.referenceAndRecordEffects(
instrValue.tag,
calleeEffect,
ValueReason.Other,
functionEffects,
);
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.ConditionallyMutate;
continue;
}
case 'CallExpression': {
const signature = getFunctionCallSignature(
env,
@@ -227,6 +227,7 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case 'StoreGlobal': {
return false;
}
case 'TaggedTemplateExpression':
case 'CallExpression':
case 'MethodCall': {
return instruction.lvalue.identifier.type.kind !== 'Primitive';
@@ -241,8 +242,7 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case 'ObjectExpression':
case 'UnsupportedNode':
case 'ObjectMethod':
case 'FunctionExpression':
case 'TaggedTemplateExpression': {
case 'FunctionExpression': {
return true;
}
default: {
@@ -671,12 +671,11 @@ function computeMemoizationInputs(
],
};
}
case 'CallExpression': {
case 'TaggedTemplateExpression': {
const signature = getFunctionCallSignature(
env,
value.callee.identifier.type,
value.tag.identifier.type,
);
const operands = [...eachReactiveValueOperand(value)];
let lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
@@ -687,6 +686,33 @@ function computeMemoizationInputs(
rvalues: [],
};
}
const operands = [...eachReactiveValueOperand(value)];
lvalues.push(
...operands
.filter(operand => isMutableEffect(operand.effect, operand.loc))
.map(place => ({place, level: MemoizationLevel.Memoized})),
);
return {
lvalues,
rvalues: operands,
};
}
case 'CallExpression': {
const signature = getFunctionCallSignature(
env,
value.callee.identifier.type,
);
let lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
}
if (signature?.noAlias === true) {
return {
lvalues,
rvalues: [],
};
}
const operands = [...eachReactiveValueOperand(value)];
lvalues.push(
...operands
.filter(operand => isMutableEffect(operand.effect, operand.loc))
@@ -702,7 +728,6 @@ function computeMemoizationInputs(
env,
value.property.identifier.type,
);
const operands = [...eachReactiveValueOperand(value)];
let lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
@@ -713,6 +738,7 @@ function computeMemoizationInputs(
rvalues: [],
};
}
const operands = [...eachReactiveValueOperand(value)];
lvalues.push(
...operands
.filter(operand => isMutableEffect(operand.effect, operand.loc))
@@ -726,7 +752,6 @@ function computeMemoizationInputs(
case 'RegExpLiteral':
case 'ObjectMethod':
case 'FunctionExpression':
case 'TaggedTemplateExpression':
case 'ArrayExpression':
case 'NewExpression':
case 'ObjectExpression':
@@ -250,6 +250,7 @@ function* generateInstructionTypes(
}
case 'CallExpression': {
const returnType = makeType();
/*
* TODO: callee could be a hook or a function, so this type equation isn't correct.
* We should change Hook to a subtype of Function or change unifier logic.
@@ -258,8 +259,25 @@ function* generateInstructionTypes(
yield equation(value.callee.identifier.type, {
kind: 'Function',
shapeId: null,
return: left,
return: returnType,
});
yield equation(left, returnType);
break;
}
case 'TaggedTemplateExpression': {
const returnType = makeType();
/*
* TODO: callee could be a hook or a function, so this type equation isn't correct.
* We should change Hook to a subtype of Function or change unifier logic.
* (see https://github.com/facebook/react-forget/pull/1427)
*/
yield equation(value.tag.identifier.type, {
kind: 'Function',
shapeId: null,
return: returnType,
});
yield equation(left, returnType);
break;
}
@@ -392,7 +410,6 @@ function* generateInstructionTypes(
case 'MetaProperty':
case 'ComputedStore':
case 'ComputedLoad':
case 'TaggedTemplateExpression':
case 'Await':
case 'GetIterator':
case 'IteratorNext':
@@ -161,6 +161,14 @@ function getContextReassignment(
if (signature?.noAlias) {
operands = [value.receiver, value.property];
}
} else if (value.kind === 'TaggedTemplateExpression') {
const signature = getFunctionCallSignature(
fn.env,
value.tag.identifier.type,
);
if (signature?.noAlias) {
operands = [value.tag];
}
}
for (const operand of operands) {
CompilerError.invariant(operand.effect !== Effect.Unknown, {
@@ -63,67 +63,63 @@ function useFragment(_arg1, _arg2) {
}
function Component(props) {
const $ = _c(9);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = graphql`
const $ = _c(8);
const post = useFragment(
graphql`
fragment F on T {
id
}
`;
$[0] = t0;
} else {
t0 = $[0];
}
const post = useFragment(t0, props.post);
let t1;
if ($[1] !== post) {
`,
props.post,
);
let t0;
if ($[0] !== post) {
const allUrls = [];
const { media: t2, comments: t3, urls: t4 } = post;
const media = t2 === undefined ? null : t2;
const { media: t1, comments: t2, urls: t3 } = post;
const media = t1 === undefined ? null : t1;
let t4;
if ($[2] !== t2) {
t4 = t2 === undefined ? [] : t2;
$[2] = t2;
$[3] = t4;
} else {
t4 = $[3];
}
const comments = t4;
let t5;
if ($[3] !== t3) {
if ($[4] !== t3) {
t5 = t3 === undefined ? [] : t3;
$[3] = t3;
$[4] = t5;
$[4] = t3;
$[5] = t5;
} else {
t5 = $[4];
t5 = $[5];
}
const comments = t5;
const urls = t5;
let t6;
if ($[5] !== t4) {
t6 = t4 === undefined ? [] : t4;
$[5] = t4;
$[6] = t6;
} else {
t6 = $[6];
}
const urls = t6;
let t7;
if ($[7] !== comments.length) {
t7 = (e) => {
if ($[6] !== comments.length) {
t6 = (e) => {
if (!comments.length) {
return;
}
console.log(comments.length);
};
$[7] = comments.length;
$[8] = t7;
$[6] = comments.length;
$[7] = t6;
} else {
t7 = $[8];
t6 = $[7];
}
const onClick = t7;
const onClick = t6;
allUrls.push(...urls);
t1 = <Stringify media={media} allUrls={allUrls} onClick={onClick} />;
$[1] = post;
$[2] = t1;
t0 = <Stringify media={media} allUrls={allUrls} onClick={onClick} />;
$[0] = post;
$[1] = t0;
} else {
t1 = $[2];
t0 = $[1];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -0,0 +1,106 @@
## Input
```javascript
import {graphql} from 'shared-runtime';
export function Component({a, b}) {
const fragment = graphql`
fragment Foo on User {
name
}
`;
return <div>{fragment}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 1, b: 2},
{a: 2, b: 2},
{a: 3, b: 2},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { graphql } from "shared-runtime";
export function Component(t0) {
const $ = _c(1);
const fragment = graphql`
fragment Foo on User {
name
}
`;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <div>{fragment}</div>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 1, b: 2 },
{ a: 2, b: 2 },
{ a: 3, b: 2 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>
fragment Foo on User {
name
}
</div>
<div>
fragment Foo on User {
name
}
</div>
<div>
fragment Foo on User {
name
}
</div>
<div>
fragment Foo on User {
name
}
</div>
<div>
fragment Foo on User {
name
}
</div>
<div>
fragment Foo on User {
name
}
</div>
<div>
fragment Foo on User {
name
}
</div>
@@ -0,0 +1,24 @@
import {graphql} from 'shared-runtime';
export function Component({a, b}) {
const fragment = graphql`
fragment Foo on User {
name
}
`;
return <div>{fragment}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 1, b: 2},
{a: 2, b: 2},
{a: 3, b: 2},
{a: 0, b: 0},
],
};
@@ -32,6 +32,14 @@ export function makeSharedRuntimeTypeProvider({
returnType: {kind: 'type', name: 'Primitive'},
returnValueKind: ValueKindEnum.Primitive,
},
graphql: {
kind: 'function',
calleeEffect: EffectEnum.Read,
positionalParams: [],
restParam: EffectEnum.Read,
returnType: {kind: 'type', name: 'Primitive'},
returnValueKind: ValueKindEnum.Primitive,
},
typedArrayPush: {
kind: 'function',
calleeEffect: EffectEnum.Read,