First-class representation of builtin jsx tags

We previously represented JsxExpressions using builtin tags - `<div>`, `<b>` etc 
- by lowering the tag name to a Primitive with the string name of the tag. 
However, by lowering into an independent value, it was possible that the lowered 
tag name could be grouped into a different memo slot, such that we ended up with 
output like: 

```javascript 

let t0; 

if (c_1) { 

... 

t0 = "div" 

... 

} else { ... } 

return <t0>{children}</t0> 

``` 

This is obviously wrong. It's also wrong to rename `t0` -> `T0`, because React 
treats that as a custom component, not a builtin. The right thing is to 
explicitly model builtin components, which this PR does by making 
`JsxExpression.tag` be a union of Place | BuiltinTag.
This commit is contained in:
Joe Savona
2023-04-26 11:27:35 -07:00
parent 9aea37ff5a
commit 894cf6e37b
13 changed files with 189 additions and 178 deletions
+6 -13
View File
@@ -17,6 +17,7 @@ import {
ArrayPattern,
BlockId,
BranchTerminal,
BuiltinTag,
Case,
Effect,
GeneratedSource,
@@ -2035,7 +2036,7 @@ function lowerJsxElementName(
exprPath: NodePath<
t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName
>
): Place {
): Place | BuiltinTag {
const exprNode = exprPath.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
if (exprPath.isJSXIdentifier()) {
@@ -2047,19 +2048,11 @@ function lowerJsxElementName(
loc: exprLoc,
});
} else {
if (tag.indexOf(":") !== -1) {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) JSXIdentifier to have no colons, got '${tag}'`,
severity: ErrorSeverity.InvalidInput,
nodePath: exprPath,
});
}
const place = lowerValueToTemporary(builder, {
kind: "Primitive",
value: tag,
return {
kind: "BuiltinTag",
name: tag,
loc: exprLoc,
});
return place;
};
}
} else if (exprPath.isJSXMemberExpression()) {
return lowerJsxMemberExpression(builder, exprPath);
+7 -1
View File
@@ -608,7 +608,7 @@ export type InstructionValue =
}
| {
kind: "JsxExpression";
tag: Place;
tag: Place | BuiltinTag;
props: Array<JsxAttribute>;
children: Array<Place> | null; // null === no children
loc: SourceLocation;
@@ -752,6 +752,12 @@ export type LoadGlobal = {
loc: SourceLocation;
};
export type BuiltinTag = {
kind: "BuiltinTag";
name: string;
loc: SourceLocation;
};
/*
* Range in which an identifier is mutable. Start and End refer to Instruction.id.
*
+7 -5
View File
@@ -317,18 +317,20 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
propItems.push(`...${printPlace(attribute.argument)}`);
}
}
const tag =
instrValue.tag.kind === "Identifier"
? printPlace(instrValue.tag)
: instrValue.tag.name;
const props = propItems.length !== 0 ? " " + propItems.join(" ") : "";
if (instrValue.children !== null) {
const children = instrValue.children.map((child) => {
return `{${printPlace(child)}}`;
});
value = `JSX <${printPlace(instrValue.tag)}${props}${
value = `JSX <${tag}${props}${
props.length > 0 ? " " : ""
}>${children.join("")}</${printPlace(instrValue.tag)}>`;
}>${children.join("")}</${tag}>`;
} else {
value = `JSX <${printPlace(instrValue.tag)}${props}${
props.length > 0 ? " " : ""
}/>`;
value = `JSX <${tag}${props}${props.length > 0 ? " " : ""}/>`;
}
break;
}
+6 -2
View File
@@ -110,7 +110,9 @@ export function* eachInstructionValueOperand(
break;
}
case "JsxExpression": {
yield instrValue.tag;
if (instrValue.tag.kind === "Identifier") {
yield instrValue.tag;
}
for (const attribute of instrValue.props) {
switch (attribute.kind) {
case "JsxAttribute": {
@@ -370,7 +372,9 @@ export function mapInstructionOperands(
break;
}
case "JsxExpression": {
instrValue.tag = fn(instrValue.tag);
if (instrValue.tag.kind === "Identifier") {
instrValue.tag = fn(instrValue.tag);
}
for (const attribute of instrValue.props) {
switch (attribute.kind) {
case "JsxAttribute": {
@@ -752,7 +752,10 @@ function codegenInstructionValue(
for (const attribute of instrValue.props) {
attributes.push(codegenJsxAttribute(cx, attribute));
}
let tagValue = codegenPlace(cx, instrValue.tag);
let tagValue =
instrValue.tag.kind === "Identifier"
? codegenPlace(cx, instrValue.tag)
: t.stringLiteral(instrValue.tag.name);
let tag: t.JSXIdentifier | t.JSXNamespacedName | t.JSXMemberExpression;
if (tagValue.type === "Identifier") {
tag = createJsxIdentifier(instrValue.tag.loc, tagValue.name);
@@ -10,9 +10,13 @@ import {
makeInstructionId,
ReactiveFunction,
ReactiveInstruction,
ReactiveValue,
} from "../HIR";
import { eachInstructionValueOperand } from "../HIR/visitors";
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
import {
eachReactiveValueOperand,
ReactiveFunctionVisitor,
visitReactiveFunction,
} from "./visitors";
/**
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/).
@@ -57,15 +61,14 @@ class Transform extends ReactiveFunctionVisitor<void> {
// Record references to `fbt` as a global
this.fbtValues.add(lvalue.identifier.id);
} else if (
(value.kind === "JsxExpression" &&
this.fbtValues.has(value.tag.identifier.id)) ||
isFbtJsxExpression(this.fbtValues, value) ||
(value.kind === "CallExpression" &&
this.fbtValues.has(value.callee.identifier.id))
) {
// if the JSX element's tag was `fbt`, mark all its operands
// to ensure that they end up in the same scope as the jsx element
// itself.
for (const operand of eachInstructionValueOperand(value)) {
for (const operand of eachReactiveValueOperand(value)) {
operand.identifier.scope = lvalue.identifier.scope;
operand.identifier.mutableRange.end =
lvalue.identifier.mutableRange.end;
@@ -81,3 +84,15 @@ class Transform extends ReactiveFunctionVisitor<void> {
}
}
}
function isFbtJsxExpression(
fbtValues: Set<IdentifierId>,
value: ReactiveValue
): boolean {
return (
value.kind === "JsxExpression" &&
((value.tag.kind === "Identifier" &&
fbtValues.has(value.tag.identifier.id)) ||
(value.tag.kind === "BuiltinTag" && value.tag.name === "fbt"))
);
}
@@ -56,7 +56,7 @@ class CollectJsxTagsVisitor extends ReactiveFunctionVisitor<JsxExpressionTags> {
value: ReactiveValue,
state: JsxExpressionTags
): void {
if (value.kind === "JsxExpression") {
if (value.kind === "JsxExpression" && value.tag.kind === "Identifier") {
state.add(value.tag.identifier.id);
}
}
@@ -381,7 +381,9 @@ function computeMemoizationInputs(
}
case "JsxExpression": {
const operands: Array<Place> = [];
operands.push(value.tag);
if (value.tag.kind === "Identifier") {
operands.push(value.tag);
}
for (const prop of value.props) {
if (prop.kind === "JsxAttribute") {
operands.push(prop.place);
@@ -14,27 +14,23 @@ function Component(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
const $ = useMemoCache(3);
let T0;
let t1;
const $ = useMemoCache(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const maybeMutable = new MaybeMutable();
T0 = "div";
t1 = maybeMutate(maybeMutable);
$[0] = T0;
t0 = maybeMutate(maybeMutable);
$[0] = t0;
} else {
t0 = $[0];
}
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <div>{t0}</div>;
$[1] = t1;
} else {
T0 = $[0];
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <T0>{t1}</T0>;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
return t1;
}
```
@@ -19,7 +19,7 @@ function Component(props) {
## Error
```
[ReactForget] InvalidInput: InferReferenceEffects: inferred mutation of known immutable value. Found mutation of $28:TObject<BuiltInArray> (frozen) (7:7)
[ReactForget] InvalidInput: InferReferenceEffects: inferred mutation of known immutable value. Found mutation of $27:TObject<BuiltInArray> (frozen) (7:7)
```
@@ -24,107 +24,101 @@ function Component(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
const $ = useMemoCache(23);
const $ = useMemoCache(21);
const item = useFragment(FRAGMENT, props.item);
useFreeze(item);
const c_0 = $[0] !== item;
let T1;
let t2;
let T3;
let t4;
let t1;
let T2;
let t3;
let t0;
let t5;
let T6;
let t7;
let t4;
let T5;
let t6;
if (c_0) {
const count = new MaybeMutable(item);
T6 = View;
t7 = "\n ";
T3 = View;
t4 = "\n ";
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
T5 = View;
t6 = "\n ";
T2 = View;
t3 = "\n ";
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <span>Text</span>;
$[9] = t0;
$[8] = t0;
} else {
t0 = $[9];
t0 = $[8];
}
t5 = "\n ";
T1 = "span";
t2 = maybeMutate(count);
t4 = "\n ";
t1 = maybeMutate(count);
$[0] = item;
$[1] = T1;
$[2] = t2;
$[3] = T3;
$[4] = t4;
$[5] = t0;
$[6] = t5;
$[7] = T6;
$[8] = t7;
$[1] = t1;
$[2] = T2;
$[3] = t3;
$[4] = t0;
$[5] = t4;
$[6] = T5;
$[7] = t6;
} else {
T1 = $[1];
t2 = $[2];
T3 = $[3];
t4 = $[4];
t0 = $[5];
t5 = $[6];
T6 = $[7];
t7 = $[8];
t1 = $[1];
T2 = $[2];
t3 = $[3];
t0 = $[4];
t4 = $[5];
T5 = $[6];
t6 = $[7];
}
const c_10 = $[10] !== T1;
const c_11 = $[11] !== t2;
let t8;
if (c_10 || c_11) {
t8 = <T1>{t2}</T1>;
$[10] = T1;
$[11] = t2;
$[12] = t8;
const c_9 = $[9] !== t1;
let t7;
if (c_9) {
t7 = <span>{t1}</span>;
$[9] = t1;
$[10] = t7;
} else {
t8 = $[12];
t7 = $[10];
}
const c_13 = $[13] !== T3;
const c_11 = $[11] !== T2;
const c_12 = $[12] !== t3;
const c_13 = $[13] !== t0;
const c_14 = $[14] !== t4;
const c_15 = $[15] !== t0;
const c_16 = $[16] !== t5;
const c_17 = $[17] !== t8;
let t9;
if (c_13 || c_14 || c_15 || c_16 || c_17) {
t9 = (
<T3>
{t4}
const c_15 = $[15] !== t7;
let t8;
if (c_11 || c_12 || c_13 || c_14 || c_15) {
t8 = (
<T2>
{t3}
{t0}
{t5}
{t8}
</T3>
);
$[13] = T3;
$[14] = t4;
$[15] = t0;
$[16] = t5;
$[17] = t8;
$[18] = t9;
} else {
t9 = $[18];
}
const c_19 = $[19] !== T6;
const c_20 = $[20] !== t7;
const c_21 = $[21] !== t9;
let t10;
if (c_19 || c_20 || c_21) {
t10 = (
<T6>
{t4}
{t7}
{t9}
</T6>
</T2>
);
$[19] = T6;
$[20] = t7;
$[21] = t9;
$[22] = t10;
$[11] = T2;
$[12] = t3;
$[13] = t0;
$[14] = t4;
$[15] = t7;
$[16] = t8;
} else {
t10 = $[22];
t8 = $[16];
}
return t10;
const c_17 = $[17] !== T5;
const c_18 = $[18] !== t6;
const c_19 = $[19] !== t8;
let t9;
if (c_17 || c_18 || c_19) {
t9 = (
<T5>
{t6}
{t8}
</T5>
);
$[17] = T5;
$[18] = t6;
$[19] = t8;
$[20] = t9;
} else {
t9 = $[20];
}
return t9;
}
```
@@ -21,52 +21,62 @@ function Component(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
const $ = useMemoCache(12);
let T1;
let t2;
let T3;
let t4;
const $ = useMemoCache(11);
let t1;
let T2;
let t3;
let t0;
let t5;
let T6;
let t7;
let t4;
let T5;
let t6;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const count = new MaybeMutable();
T6 = View;
t7 = "\n ";
T3 = View;
t4 = "\n ";
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
T5 = View;
t6 = "\n ";
T2 = View;
t3 = "\n ";
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <span>Text</span>;
$[8] = t0;
$[7] = t0;
} else {
t0 = $[8];
t0 = $[7];
}
t5 = "\n ";
T1 = "span";
t2 = maybeMutate(count);
$[0] = T1;
$[1] = t2;
$[2] = T3;
$[3] = t4;
$[4] = t0;
$[5] = t5;
$[6] = T6;
$[7] = t7;
t4 = "\n ";
t1 = maybeMutate(count);
$[0] = t1;
$[1] = T2;
$[2] = t3;
$[3] = t0;
$[4] = t4;
$[5] = T5;
$[6] = t6;
} else {
T1 = $[0];
t2 = $[1];
T3 = $[2];
t4 = $[3];
t0 = $[4];
t5 = $[5];
T6 = $[6];
t7 = $[7];
t1 = $[0];
T2 = $[1];
t3 = $[2];
t0 = $[3];
t4 = $[4];
T5 = $[5];
t6 = $[6];
}
let t7;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <span>{t1}</span>;
$[8] = t7;
} else {
t7 = $[8];
}
let t8;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <T1>{t2}</T1>;
t8 = (
<T2>
{t3}
{t0}
{t4}
{t7}
</T2>
);
$[9] = t8;
} else {
t8 = $[9];
@@ -74,30 +84,16 @@ function Component(props) {
let t9;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t9 = (
<T3>
{t4}
{t0}
{t5}
<T5>
{t6}
{t8}
</T3>
</T5>
);
$[10] = t9;
} else {
t9 = $[10];
}
let t10;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t10 = (
<T6>
{t7}
{t9}
</T6>
);
$[11] = t10;
} else {
t10 = $[11];
}
return t10;
return t9;
}
```