Skip to content

Commit ac06829

Browse files
authored
feat(compiler): Implement constant propagation for template literals (facebook#33139)
New take on facebook#29716 ## Summary Template literals consisting entirely of constant values will be inlined to a string literal, effectively replacing the backticks with a double quote. This is done primarily to make the resulting instruction a string literal, so it can be processed further in constant propatation. So this is now correctly simplified to `true`: ```js `` === "" // now true `a${1}` === "a1" // now true ``` If a template string literal can only partially be comptime-evaluated, it is not that useful for dead code elimination or further constant folding steps and thus, is left as-is in that case. Same is true if the literal contains an array, object, symbol or function. ## How did you test this change? See added tests.
1 parent 38ef655 commit ac06829

8 files changed

+268
-9
lines changed

compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts

+67
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,73 @@ function evaluateInstruction(
509509
}
510510
return null;
511511
}
512+
case 'TemplateLiteral': {
513+
if (value.subexprs.length === 0) {
514+
const result: InstructionValue = {
515+
kind: 'Primitive',
516+
value: value.quasis.map(q => q.cooked).join(''),
517+
loc: value.loc,
518+
};
519+
instr.value = result;
520+
return result;
521+
}
522+
523+
if (value.subexprs.length !== value.quasis.length - 1) {
524+
return null;
525+
}
526+
527+
if (value.quasis.some(q => q.cooked === undefined)) {
528+
return null;
529+
}
530+
531+
let quasiIndex = 0;
532+
let resultString = value.quasis[quasiIndex].cooked as string;
533+
++quasiIndex;
534+
535+
for (const subExpr of value.subexprs) {
536+
const subExprValue = read(constants, subExpr);
537+
if (!subExprValue || subExprValue.kind !== 'Primitive') {
538+
return null;
539+
}
540+
541+
const expressionValue = subExprValue.value;
542+
if (
543+
typeof expressionValue !== 'number' &&
544+
typeof expressionValue !== 'string' &&
545+
typeof expressionValue !== 'boolean' &&
546+
!(typeof expressionValue === 'object' && expressionValue === null)
547+
) {
548+
// value is not supported (function, object) or invalid (symbol), or something else
549+
return null;
550+
}
551+
552+
const suffix = value.quasis[quasiIndex].cooked;
553+
++quasiIndex;
554+
555+
if (suffix === undefined) {
556+
return null;
557+
}
558+
559+
/*
560+
* Spec states that concat calls ToString(argument) internally on its parameters
561+
* -> we don't have to implement ToString(argument) ourselves and just use the engine implementation
562+
* Refs:
563+
* - https://tc39.es/ecma262/2024/#sec-tostring
564+
* - https://tc39.es/ecma262/2024/#sec-string.prototype.concat
565+
* - https://tc39.es/ecma262/2024/#sec-template-literals-runtime-semantics-evaluation
566+
*/
567+
resultString = resultString.concat(expressionValue as string, suffix);
568+
}
569+
570+
const result: InstructionValue = {
571+
kind: 'Primitive',
572+
value: resultString,
573+
loc: value.loc,
574+
};
575+
576+
instr.value = result;
577+
return result;
578+
}
512579
case 'LoadLocal': {
513580
const placeValue = read(constants, value.place);
514581
if (placeValue !== null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {Stringify, identity} from 'shared-runtime';
6+
7+
function foo() {
8+
try {
9+
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
10+
} catch {}
11+
12+
return (
13+
<Stringify
14+
value={[
15+
`` === '',
16+
`\n` === '\n',
17+
`a\nb`,
18+
`\n`,
19+
`a${1}b`,
20+
` abc \u0041\n\u000a\ŧ`,
21+
`abc${1}def`,
22+
`abc${1}def${2}`,
23+
`abc${1}def${2}ghi`,
24+
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
25+
`1${2}${Math.sin(0)}`,
26+
`${NaN}`,
27+
`${Infinity}`,
28+
`${-Infinity}`,
29+
`${Number.MAX_SAFE_INTEGER}`,
30+
`${Number.MIN_SAFE_INTEGER}`,
31+
`${Number.MAX_VALUE}`,
32+
`${Number.MIN_VALUE}`,
33+
`${-0}`,
34+
`
35+
`,
36+
`${{}}`,
37+
`${[1, 2, 3]}`,
38+
`${true}`,
39+
`${false}`,
40+
`${null}`,
41+
`${undefined}`,
42+
`123456789${0}`,
43+
`${0}123456789`,
44+
`${0}123456789${0}`,
45+
`${0}1234${5}6789${0}`,
46+
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
47+
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
48+
`${`${`${`${0}`}`}`}`,
49+
`${`${`${`${''}`}`}`}`,
50+
`${`${`${`${identity('')}`}`}`}`,
51+
]}
52+
/>
53+
);
54+
}
55+
56+
export const FIXTURE_ENTRYPOINT = {
57+
fn: foo,
58+
params: [],
59+
isComponent: false,
60+
};
61+
62+
```
63+
64+
## Code
65+
66+
```javascript
67+
import { c as _c } from "react/compiler-runtime";
68+
import { Stringify, identity } from "shared-runtime";
69+
70+
function foo() {
71+
const $ = _c(1);
72+
try {
73+
identity(`${Symbol("0")}`);
74+
} catch {}
75+
let t0;
76+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
77+
t0 = (
78+
<Stringify
79+
value={[
80+
true,
81+
true,
82+
83+
"a\nb",
84+
"\n",
85+
"a1b",
86+
" abc A\n\n\u0167",
87+
"abc1def",
88+
"abc1def2",
89+
"abc1def2ghi",
90+
"a4bcde6f",
91+
`1${2}${Math.sin(0)}`,
92+
`${NaN}`,
93+
`${Infinity}`,
94+
`${-Infinity}`,
95+
`${Number.MAX_SAFE_INTEGER}`,
96+
`${Number.MIN_SAFE_INTEGER}`,
97+
`${Number.MAX_VALUE}`,
98+
`${Number.MIN_VALUE}`,
99+
"0",
100+
"\n ",
101+
102+
`${{}}`,
103+
`${[1, 2, 3]}`,
104+
"true",
105+
"false",
106+
"null",
107+
`${undefined}`,
108+
"1234567890",
109+
"0123456789",
110+
"01234567890",
111+
"01234567890",
112+
"0123401234567890123456789067890",
113+
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
114+
"0",
115+
"",
116+
`${`${`${`${identity("")}`}`}`}`,
117+
]}
118+
/>
119+
);
120+
$[0] = t0;
121+
} else {
122+
t0 = $[0];
123+
}
124+
return t0;
125+
}
126+
127+
export const FIXTURE_ENTRYPOINT = {
128+
fn: foo,
129+
params: [],
130+
isComponent: false,
131+
};
132+
133+
```
134+
135+
### Eval output
136+
(kind: ok) <div>{"value":[true,true,"a\nb","\n","a1b"," abc A\n\nŧ","abc1def","abc1def2","abc1def2ghi","a4bcde6f","120","NaN","Infinity","-Infinity","9007199254740991","-9007199254740991","1.7976931348623157e+308","5e-324","0","\n ","[object Object]","1,2,3","true","false","null","undefined","1234567890","0123456789","01234567890","01234567890","0123401234567890123456789067890","012340123456789067890","0","",""]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {Stringify, identity} from 'shared-runtime';
2+
3+
function foo() {
4+
try {
5+
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
6+
} catch {}
7+
8+
return (
9+
<Stringify
10+
value={[
11+
`` === '',
12+
`\n` === '\n',
13+
`a\nb`,
14+
`\n`,
15+
`a${1}b`,
16+
` abc \u0041\n\u000a\ŧ`,
17+
`abc${1}def`,
18+
`abc${1}def${2}`,
19+
`abc${1}def${2}ghi`,
20+
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
21+
`1${2}${Math.sin(0)}`,
22+
`${NaN}`,
23+
`${Infinity}`,
24+
`${-Infinity}`,
25+
`${Number.MAX_SAFE_INTEGER}`,
26+
`${Number.MIN_SAFE_INTEGER}`,
27+
`${Number.MAX_VALUE}`,
28+
`${Number.MIN_VALUE}`,
29+
`${-0}`,
30+
`
31+
`,
32+
`${{}}`,
33+
`${[1, 2, 3]}`,
34+
`${true}`,
35+
`${false}`,
36+
`${null}`,
37+
`${undefined}`,
38+
`123456789${0}`,
39+
`${0}123456789`,
40+
`${0}123456789${0}`,
41+
`${0}1234${5}6789${0}`,
42+
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
43+
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
44+
`${`${`${`${0}`}`}`}`,
45+
`${`${`${`${''}`}`}`}`,
46+
`${`${`${`${identity('')}`}`}`}`,
47+
]}
48+
/>
49+
);
50+
}
51+
52+
export const FIXTURE_ENTRYPOINT = {
53+
fn: foo,
54+
params: [],
55+
isComponent: false,
56+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/labeled-break-within-label-switch.expect.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ function useHook(cond) {
4040
log = [];
4141
switch (CONST_STRING0) {
4242
case CONST_STRING0: {
43-
log.push(`@A`);
43+
log.push("@A");
4444
bb0: {
4545
if (cond) {
4646
break bb0;
4747
}
4848

49-
log.push(`@B`);
49+
log.push("@B");
5050
}
5151

52-
log.push(`@C`);
52+
log.push("@C");
5353
}
5454
}
5555
$[0] = cond;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-assignment-to-scope-declarations.expect.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function foo(name) {
9797
const t0 = `${name}!`;
9898
let t1;
9999
if ($[0] !== t0) {
100-
t1 = { status: `<status>`, text: t0 };
100+
t1 = { status: "<status>", text: t0 };
101101
$[0] = t0;
102102
$[1] = t1;
103103
} else {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-both-mixed-local-and-scope-declaration.expect.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function foo(name) {
102102
const t0 = `${name}!`;
103103
let t1;
104104
if ($[0] !== t0) {
105-
t1 = { status: `<status>`, text: t0 };
105+
t1 = { status: "<status>", text: t0 };
106106
$[0] = t0;
107107
$[1] = t1;
108108
} else {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/template-literal.expect.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function componentB(props) {
2020
```javascript
2121
function componentA(props) {
2222
let t = `hello ${props.a}, ${props.b}!`;
23-
t = t + ``;
23+
t = t + "";
2424
return t;
2525
}
2626

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unlabeled-break-within-label-switch.expect.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ function useHook(cond) {
4040
log = [];
4141
bb0: switch (CONST_STRING0) {
4242
case CONST_STRING0: {
43-
log.push(`@A`);
43+
log.push("@A");
4444
if (cond) {
4545
break bb0;
4646
}
4747

48-
log.push(`@B`);
48+
log.push("@B");
4949

50-
log.push(`@C`);
50+
log.push("@C");
5151
}
5252
}
5353
$[0] = cond;

0 commit comments

Comments
 (0)