Update on "[compiler] Override type provider for hook-like names"

If an imported name is hook-like but a type provider provides a non-hook type, we now override the returned value and treat it as a generic custom hook. This is meant as an extra check to prevent miscompilation.

[ghstack-poisoned]
This commit is contained in:
Joe Savona
2024-09-04 13:26:07 -07:00
10 changed files with 274 additions and 61 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,
+1 -1
View File
@@ -134,7 +134,7 @@ export const alwaysThrottleRetries = true;
export const passChildrenWhenCloningPersistedNodes = false;
export const enableServerComponentLogs = __EXPERIMENTAL__;
export const enableServerComponentLogs = true;
/**
* Enables a new Fiber flag used in persisted mode to reduce the number