feat(compiler): Implement constant propagation for template literals (#33139)
New take on #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.
Niklas Mollenhauer committed
May 8, 2025 at 18:24 UTC
ac06829246287751e6b74bd99cd1b39d1aeba06a
8 files changed
+268
-9
compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts
+67
@@ -509,6 +509,73 @@ function evaluateInstruction(
509
}
510
return null;
511
}
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
+ }
579
case 'LoadLocal': {
580
const placeValue = read(constants, value.place);
581
if (placeValue !== null) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.expect.md
new
+136
@@ -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>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.js
new
+56
@@ -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
@@ -40,16 +40,16 @@ function useHook(cond) {
40
log = [];
41
switch (CONST_STRING0) {
42
case CONST_STRING0: {
43
- log.push(`@A`);
43
+ log.push("@A");
44
bb0: {
45
if (cond) {
46
break bb0;
47
}
48
49
- log.push(`@B`);
49
+ log.push("@B");
50
}
51
52
- log.push(`@C`);
52
+ log.push("@C");
53
}
54
}
55
$[0] = cond;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-assignment-to-scope-declarations.expect.md
+1
-1
@@ -97,7 +97,7 @@ function foo(name) {
97
const t0 = `${name}!`;
98
let t1;
99
if ($[0] !== t0) {
100
- t1 = { status: `<status>`, text: t0 };
100
+ t1 = { status: "<status>", text: t0 };
101
$[0] = t0;
102
$[1] = t1;
103
} else {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-both-mixed-local-and-scope-declaration.expect.md
+1
-1
@@ -102,7 +102,7 @@ function foo(name) {
102
const t0 = `${name}!`;
103
let t1;
104
if ($[0] !== t0) {
105
- t1 = { status: `<status>`, text: t0 };
105
+ t1 = { status: "<status>", text: t0 };
106
$[0] = t0;
107
$[1] = t1;
108
} else {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/template-literal.expect.md
+1
-1
@@ -20,7 +20,7 @@ function componentB(props) {
20
```javascript
21
function componentA(props) {
22
let t = `hello ${props.a}, ${props.b}!`;
23
- t = t + ``;
23
+ t = t + "";
24
return t;
25
}
26
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unlabeled-break-within-label-switch.expect.md
+3
-3
@@ -40,14 +40,14 @@ function useHook(cond) {
40
log = [];
41
bb0: switch (CONST_STRING0) {
42
case CONST_STRING0: {
43
- log.push(`@A`);
43
+ log.push("@A");
44
if (cond) {
45
break bb0;
46
}
47
48
- log.push(`@B`);
48
+ log.push("@B");
49
50
- log.push(`@C`);
50
+ log.push("@C");
51
}
52
}
53
$[0] = cond;