From b1893537f12a89b185ef5950a19c17cd13fc61f8 Mon Sep 17 00:00:00 2001 From: kostya-gromov Date: Thu, 4 Dec 2025 13:35:34 +0100 Subject: [PATCH 1/4] [compiler] Enhance constant propagation with JSX tag identifier collection --- .../src/Optimization/ConstantPropagation.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) 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 7330b63ddce..381f87a0d38 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts @@ -62,7 +62,12 @@ export function constantPropagation(fn: HIRFunction): void { function constantPropagationImpl(fn: HIRFunction, constants: Constants): void { while (true) { - const haveTerminalsChanged = applyConstantPropagation(fn, constants); + const jsxTagIdentifiers = collectJsxTagIdentifiers(fn); + const haveTerminalsChanged = applyConstantPropagation( + fn, + constants, + jsxTagIdentifiers, + ); if (!haveTerminalsChanged) { break; } @@ -106,6 +111,7 @@ function constantPropagationImpl(fn: HIRFunction, constants: Constants): void { function applyConstantPropagation( fn: HIRFunction, constants: Constants, + jsxTagIdentifiers: Set, ): boolean { let hasChanges = false; for (const [, block] of fn.body.blocks) { @@ -130,7 +136,7 @@ function applyConstantPropagation( continue; } const instr = block.instructions[i]!; - const value = evaluateInstruction(constants, instr); + const value = evaluateInstruction(constants, instr, jsxTagIdentifiers); if (value !== null) { constants.set(instr.lvalue.identifier.id, value); } @@ -239,6 +245,7 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null { function evaluateInstruction( constants: Constants, instr: Instruction, + jsxTagIdentifiers: Set, ): Constant | null { const value = instr.value; switch (value.kind) { @@ -591,6 +598,12 @@ function evaluateInstruction( return result; } case 'LoadLocal': { + if ( + instr.lvalue != null && + jsxTagIdentifiers.has(instr.lvalue.identifier.id) + ) { + return null; + } const placeValue = read(constants, value.place); if (placeValue !== null) { instr.value = placeValue; @@ -639,3 +652,19 @@ function read(constants: Constants, place: Place): Constant | null { type Constant = Primitive | LoadGlobal; type Constants = Map; + +function collectJsxTagIdentifiers(fn: HIRFunction): Set { + const identifiers = new Set(); + + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + if ( + instr.value.kind === 'JsxExpression' && + instr.value.tag.kind === 'Identifier' + ) { + identifiers.add(instr.value.tag.identifier.id); + } + } + } + return identifiers; +} From 061c07d06316a35c9e789755798a854dfe06f719 Mon Sep 17 00:00:00 2001 From: kostya-gromov Date: Thu, 4 Dec 2025 13:40:00 +0100 Subject: [PATCH 2/4] Updated tests --- .../jsx-local-tag-in-lambda.expect.md | 8 +-- .../jsx-tag-evaluation-order.expect.md | 10 ++-- .../repro-jsx-dynamic-tag-alias.expect.md | 56 +++++++++++++++++++ .../compiler/repro-jsx-dynamic-tag-alias.tsx | 12 ++++ 4 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md index a7d27bc3819..a41b7d9da14 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md @@ -25,10 +25,11 @@ import { c as _c } from "react/compiler-runtime"; import { Stringify } from "shared-runtime"; function useFoo() { const $ = _c(1); - - const callback = _temp; + const MyLocal = Stringify; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const callback = () => ; + t0 = callback(); $[0] = t0; } else { @@ -36,9 +37,6 @@ function useFoo() { } return t0; } -function _temp() { - return ; -} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md index b59db512231..8568df90556 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md @@ -30,11 +30,13 @@ import { StaticText1, StaticText2 } from "shared-runtime"; function Component(props) { const $ = _c(3); + let Tag = StaticText1; - const t0 = props.value; + const T0 = Tag; + const t0 = ((Tag = StaticText2), props.value); let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = ; + t1 = ; $[0] = t1; } else { t1 = $[0]; @@ -42,10 +44,10 @@ function Component(props) { let t2; if ($[1] !== t0) { t2 = ( - + {t0} {t1} - + ); $[1] = t0; $[2] = t2; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md new file mode 100644 index 00000000000..6a040cb5bb4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +import React from 'react'; + +const base = 'div'; + +const TestComponent: React.FC = () => { + const Comp = base; + return ; +}; + +export default function Home() { + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import React from "react"; + +const base = "div"; + +const TestComponent: React.FC = () => { + const $ = _c(1); + const Comp = base; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +}; + +export default function Home() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.tsx new file mode 100644 index 00000000000..c4d2604293e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const base = 'div'; + +const TestComponent: React.FC = () => { + const Comp = base; + return ; +}; + +export default function Home() { + return ; +} From 57d905817896c7e7b5881e480ba531d2f3e69f02 Mon Sep 17 00:00:00 2001 From: kostya-gromov Date: Fri, 5 Dec 2025 10:35:47 +0100 Subject: [PATCH 3/4] Improve JSX tag handling in constant propagation and update related tests --- .../src/Optimization/ConstantPropagation.ts | 15 +++++++++------ .../compiler/jsx-local-tag-in-lambda.expect.md | 9 ++++++--- .../compiler/jsx-tag-evaluation-order.expect.md | 8 ++++---- .../repro-jsx-dynamic-tag-alias.expect.md | 5 +++-- 4 files changed, 22 insertions(+), 15 deletions(-) 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 381f87a0d38..35a8bc21ce7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts @@ -23,6 +23,7 @@ import { markInstructionIds, markPredecessors, mergeConsecutiveBlocks, + promoteTemporaryJsxTag, reversePostorderBlocks, } from '../HIR'; import { @@ -598,14 +599,16 @@ function evaluateInstruction( return result; } case 'LoadLocal': { - if ( - instr.lvalue != null && - jsxTagIdentifiers.has(instr.lvalue.identifier.id) - ) { - return null; - } const placeValue = read(constants, value.place); if (placeValue !== null) { + if ( + instr.lvalue != null && + jsxTagIdentifiers.has(instr.lvalue.identifier.id) && + placeValue.kind === 'LoadGlobal' && + instr.lvalue.identifier.name == null + ) { + promoteTemporaryJsxTag(instr.lvalue.identifier); + } instr.value = placeValue; } return placeValue; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md index a41b7d9da14..d6da6da2c18 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md @@ -25,11 +25,10 @@ import { c as _c } from "react/compiler-runtime"; import { Stringify } from "shared-runtime"; function useFoo() { const $ = _c(1); - const MyLocal = Stringify; + + const callback = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const callback = () => ; - t0 = callback(); $[0] = t0; } else { @@ -37,6 +36,10 @@ function useFoo() { } return t0; } +function _temp() { + const T0 = Stringify; + return ; +} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md index 8568df90556..259fdc19947 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md @@ -30,13 +30,13 @@ import { StaticText1, StaticText2 } from "shared-runtime"; function Component(props) { const $ = _c(3); - let Tag = StaticText1; - const T0 = Tag; - const t0 = ((Tag = StaticText2), props.value); + const T0 = StaticText1; + const t0 = props.value; + const T1 = StaticText2; let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = ; + t1 = ; $[0] = t1; } else { t1 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md index 6a040cb5bb4..cc8790b2bde 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md @@ -27,10 +27,11 @@ const base = "div"; const TestComponent: React.FC = () => { const $ = _c(1); - const Comp = base; + + const T0 = base; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = ; + t0 = ; $[0] = t0; } else { t0 = $[0]; From 95b71d66ff81a282589f6519594e400f24149274 Mon Sep 17 00:00:00 2001 From: kostya-gromov Date: Wed, 10 Dec 2025 12:45:50 +0100 Subject: [PATCH 4/4] Added new logic under the PromoteUsedTemporaries file --- .../src/Optimization/ConstantPropagation.ts | 36 +---------------- .../ReactiveScopes/PromoteUsedTemporaries.ts | 40 ++++++++++++++++++- .../jsx-local-tag-in-lambda.expect.md | 3 +- .../jsx-tag-evaluation-order.expect.md | 8 ++-- .../repro-jsx-dynamic-tag-alias.expect.md | 4 +- 5 files changed, 46 insertions(+), 45 deletions(-) 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 35a8bc21ce7..7330b63ddce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts @@ -23,7 +23,6 @@ import { markInstructionIds, markPredecessors, mergeConsecutiveBlocks, - promoteTemporaryJsxTag, reversePostorderBlocks, } from '../HIR'; import { @@ -63,12 +62,7 @@ export function constantPropagation(fn: HIRFunction): void { function constantPropagationImpl(fn: HIRFunction, constants: Constants): void { while (true) { - const jsxTagIdentifiers = collectJsxTagIdentifiers(fn); - const haveTerminalsChanged = applyConstantPropagation( - fn, - constants, - jsxTagIdentifiers, - ); + const haveTerminalsChanged = applyConstantPropagation(fn, constants); if (!haveTerminalsChanged) { break; } @@ -112,7 +106,6 @@ function constantPropagationImpl(fn: HIRFunction, constants: Constants): void { function applyConstantPropagation( fn: HIRFunction, constants: Constants, - jsxTagIdentifiers: Set, ): boolean { let hasChanges = false; for (const [, block] of fn.body.blocks) { @@ -137,7 +130,7 @@ function applyConstantPropagation( continue; } const instr = block.instructions[i]!; - const value = evaluateInstruction(constants, instr, jsxTagIdentifiers); + const value = evaluateInstruction(constants, instr); if (value !== null) { constants.set(instr.lvalue.identifier.id, value); } @@ -246,7 +239,6 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null { function evaluateInstruction( constants: Constants, instr: Instruction, - jsxTagIdentifiers: Set, ): Constant | null { const value = instr.value; switch (value.kind) { @@ -601,14 +593,6 @@ function evaluateInstruction( case 'LoadLocal': { const placeValue = read(constants, value.place); if (placeValue !== null) { - if ( - instr.lvalue != null && - jsxTagIdentifiers.has(instr.lvalue.identifier.id) && - placeValue.kind === 'LoadGlobal' && - instr.lvalue.identifier.name == null - ) { - promoteTemporaryJsxTag(instr.lvalue.identifier); - } instr.value = placeValue; } return placeValue; @@ -655,19 +639,3 @@ function read(constants: Constants, place: Place): Constant | null { type Constant = Primitive | LoadGlobal; type Constants = Map; - -function collectJsxTagIdentifiers(fn: HIRFunction): Set { - const identifiers = new Set(); - - for (const [, block] of fn.body.blocks) { - for (const instr of block.instructions) { - if ( - instr.value.kind === 'JsxExpression' && - instr.value.tag.kind === 'Identifier' - ) { - identifiers.add(instr.value.tag.identifier.id); - } - } - } - return identifiers; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PromoteUsedTemporaries.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PromoteUsedTemporaries.ts index ce3ac13a9fe..02f1c1f3f6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PromoteUsedTemporaries.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PromoteUsedTemporaries.ts @@ -31,6 +31,39 @@ import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors'; * Phase 2: Promote identifiers which are used in a place that requires a named variable. */ class PromoteTemporaries extends ReactiveFunctionVisitor { + /* + * Only promote LoadGlobal temps when the JSX tag identifier's name differs + * from the referenced module-local binding. This preserves direct uses like + * while still emitting stable temps for aliases such as + * const Tag = StaticText1; return ;. + */ + override visitInstruction( + instruction: ReactiveInstruction, + state: State, + ): void { + const binding = + instruction.value.kind === 'LoadGlobal' + ? instruction.value.binding + : null; + const lvalue = instruction.lvalue; + const tagName = + lvalue != null + ? (state.tagNames.get(lvalue.identifier.declarationId) ?? null) + : null; + if ( + binding != null && + binding.kind === 'ModuleLocal' && + lvalue != null && + lvalue.identifier.name == null && + tagName !== null && + tagName !== binding.name && + state.tags.has(lvalue.identifier.declarationId) + ) { + promoteIdentifier(lvalue.identifier, state); + } + this.traverseInstruction(instruction, state); + } + override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void { for (const dep of scopeBlock.scope.dependencies) { const {identifier} = dep; @@ -172,6 +205,7 @@ class PromoteAllInstancedOfPromotedTemporaries extends ReactiveFunctionVisitor; type State = { tags: JsxExpressionTags; + tagNames: Map; promoted: Set; pruned: Map< DeclarationId, @@ -205,7 +239,10 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor { ): void { this.traverseValue(id, value, state); if (value.kind === 'JsxExpression' && value.tag.kind === 'Identifier') { - state.tags.add(value.tag.identifier.declarationId); + const identifier = value.tag.identifier; + state.tags.add(identifier.declarationId); + const name = identifier.name != null ? identifier.name.value : null; + state.tagNames.set(identifier.declarationId, name); } } @@ -432,6 +469,7 @@ class PromoteInterposedTemporaries extends ReactiveFunctionVisitor { export function promoteUsedTemporaries(fn: ReactiveFunction): void { const state: State = { tags: new Set(), + tagNames: new Map(), promoted: new Set(), pruned: new Map(), }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md index d6da6da2c18..a7d27bc3819 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md @@ -37,8 +37,7 @@ function useFoo() { return t0; } function _temp() { - const T0 = Stringify; - return ; + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md index 259fdc19947..b59db512231 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md @@ -31,12 +31,10 @@ import { StaticText1, StaticText2 } from "shared-runtime"; function Component(props) { const $ = _c(3); - const T0 = StaticText1; const t0 = props.value; - const T1 = StaticText2; let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = ; + t1 = ; $[0] = t1; } else { t1 = $[0]; @@ -44,10 +42,10 @@ function Component(props) { let t2; if ($[1] !== t0) { t2 = ( - + {t0} {t1} - + ); $[1] = t0; $[2] = t2; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md index cc8790b2bde..865180a9b36 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-jsx-dynamic-tag-alias.expect.md @@ -27,11 +27,9 @@ const base = "div"; const TestComponent: React.FC = () => { const $ = _c(1); - - const T0 = base; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = ; + t0 = ; $[0] = t0; } else { t0 = $[0];