@samitouri / QOS-React-1 / commits / ac06829246

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;