Date: Thu, 8 May 2025 18:24:22 +0200
Subject: [PATCH 4/4] 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.
---
.../src/Optimization/ConstantPropagation.ts | 67 +++++++++
...ant-propagation-template-literal.expect.md | 136 ++++++++++++++++++
.../constant-propagation-template-literal.js | 56 ++++++++
...abeled-break-within-label-switch.expect.md | 6 +-
...assignment-to-scope-declarations.expect.md | 2 +-
...ixed-local-and-scope-declaration.expect.md | 2 +-
.../compiler/template-literal.expect.md | 2 +-
...abeled-break-within-label-switch.expect.md | 6 +-
8 files changed, 268 insertions(+), 9 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts
index 05f4ef1ae7ffd..4ad86abbe73cb 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts
@@ -509,6 +509,73 @@ function evaluateInstruction(
}
return null;
}
+ case 'TemplateLiteral': {
+ if (value.subexprs.length === 0) {
+ const result: InstructionValue = {
+ kind: 'Primitive',
+ value: value.quasis.map(q => q.cooked).join(''),
+ loc: value.loc,
+ };
+ instr.value = result;
+ return result;
+ }
+
+ if (value.subexprs.length !== value.quasis.length - 1) {
+ return null;
+ }
+
+ if (value.quasis.some(q => q.cooked === undefined)) {
+ return null;
+ }
+
+ let quasiIndex = 0;
+ let resultString = value.quasis[quasiIndex].cooked as string;
+ ++quasiIndex;
+
+ for (const subExpr of value.subexprs) {
+ const subExprValue = read(constants, subExpr);
+ if (!subExprValue || subExprValue.kind !== 'Primitive') {
+ return null;
+ }
+
+ const expressionValue = subExprValue.value;
+ if (
+ typeof expressionValue !== 'number' &&
+ typeof expressionValue !== 'string' &&
+ typeof expressionValue !== 'boolean' &&
+ !(typeof expressionValue === 'object' && expressionValue === null)
+ ) {
+ // value is not supported (function, object) or invalid (symbol), or something else
+ return null;
+ }
+
+ const suffix = value.quasis[quasiIndex].cooked;
+ ++quasiIndex;
+
+ if (suffix === undefined) {
+ return null;
+ }
+
+ /*
+ * Spec states that concat calls ToString(argument) internally on its parameters
+ * -> we don't have to implement ToString(argument) ourselves and just use the engine implementation
+ * Refs:
+ * - https://tc39.es/ecma262/2024/#sec-tostring
+ * - https://tc39.es/ecma262/2024/#sec-string.prototype.concat
+ * - https://tc39.es/ecma262/2024/#sec-template-literals-runtime-semantics-evaluation
+ */
+ resultString = resultString.concat(expressionValue as string, suffix);
+ }
+
+ const result: InstructionValue = {
+ kind: 'Primitive',
+ value: resultString,
+ loc: value.loc,
+ };
+
+ instr.value = result;
+ return result;
+ }
case 'LoadLocal': {
const placeValue = read(constants, value.place);
if (placeValue !== null) {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.expect.md
new file mode 100644
index 0000000000000..0f01fa0a769ea
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.expect.md
@@ -0,0 +1,136 @@
+
+## Input
+
+```javascript
+import {Stringify, identity} from 'shared-runtime';
+
+function foo() {
+ try {
+ identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
+ } catch {}
+
+ return (
+
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: foo,
+ params: [],
+ isComponent: false,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { Stringify, identity } from "shared-runtime";
+
+function foo() {
+ const $ = _c(1);
+ try {
+ identity(`${Symbol("0")}`);
+ } catch {}
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = (
+
+ );
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: foo,
+ params: [],
+ isComponent: false,
+};
+
+```
+
+### Eval output
+(kind: ok) {"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","",""]}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.js
new file mode 100644
index 0000000000000..656a54db5c713
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-template-literal.js
@@ -0,0 +1,56 @@
+import {Stringify, identity} from 'shared-runtime';
+
+function foo() {
+ try {
+ identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
+ } catch {}
+
+ return (
+
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: foo,
+ params: [],
+ isComponent: false,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/labeled-break-within-label-switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/labeled-break-within-label-switch.expect.md
index 49c2506045e84..441b6ae3147d8 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/labeled-break-within-label-switch.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/labeled-break-within-label-switch.expect.md
@@ -40,16 +40,16 @@ function useHook(cond) {
log = [];
switch (CONST_STRING0) {
case CONST_STRING0: {
- log.push(`@A`);
+ log.push("@A");
bb0: {
if (cond) {
break bb0;
}
- log.push(`@B`);
+ log.push("@B");
}
- log.push(`@C`);
+ log.push("@C");
}
}
$[0] = cond;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-assignment-to-scope-declarations.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-assignment-to-scope-declarations.expect.md
index 36a68d07c46c9..4bdc7c7d92c63 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-assignment-to-scope-declarations.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-assignment-to-scope-declarations.expect.md
@@ -97,7 +97,7 @@ function foo(name) {
const t0 = `${name}!`;
let t1;
if ($[0] !== t0) {
- t1 = { status: ``, text: t0 };
+ t1 = { status: "", text: t0 };
$[0] = t0;
$[1] = t1;
} else {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-both-mixed-local-and-scope-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-both-mixed-local-and-scope-declaration.expect.md
index d8e991dc46a08..2bbfc31a11ad3 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-both-mixed-local-and-scope-declaration.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequential-destructuring-both-mixed-local-and-scope-declaration.expect.md
@@ -102,7 +102,7 @@ function foo(name) {
const t0 = `${name}!`;
let t1;
if ($[0] !== t0) {
- t1 = { status: ``, text: t0 };
+ t1 = { status: "", text: t0 };
$[0] = t0;
$[1] = t1;
} else {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/template-literal.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/template-literal.expect.md
index ffdc91c8c32c6..cdc9738d65792 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/template-literal.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/template-literal.expect.md
@@ -20,7 +20,7 @@ function componentB(props) {
```javascript
function componentA(props) {
let t = `hello ${props.a}, ${props.b}!`;
- t = t + ``;
+ t = t + "";
return t;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unlabeled-break-within-label-switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unlabeled-break-within-label-switch.expect.md
index 579905a649bfc..a990ee04e57bb 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unlabeled-break-within-label-switch.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unlabeled-break-within-label-switch.expect.md
@@ -40,14 +40,14 @@ function useHook(cond) {
log = [];
bb0: switch (CONST_STRING0) {
case CONST_STRING0: {
- log.push(`@A`);
+ log.push("@A");
if (cond) {
break bb0;
}
- log.push(`@B`);
+ log.push("@B");
- log.push(`@C`);
+ log.push("@C");
}
}
$[0] = cond;