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 05f4ef1ae7..4ad86abbe7 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 0000000000..0f01fa0a76 --- /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 0000000000..656a54db5c --- /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 49c2506045..441b6ae314 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 36a68d07c4..4bdc7c7d92 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 d8e991dc46..2bbfc31a11 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 ffdc91c8c3..cdc9738d65 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 579905a649..a990ee04e5 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;