@samitouri / QOS-React / commits / 665de2ed28

[compiler] Improve name hints for outlined functions (#34434)

The previous PR added name hints for anonymous functions, but didn't handle the case of outlined functions. Here we do some cleanup around function `id` and name hints: * Make `HIRFunction.id` a ValidatedIdentifierName, which involved some cleanup of the validation helpers * Add `HIRFunction.nameHint: string` as a place to store the generated name hints which are not valid identifiers * Update Codegen to always use the `id` as the actual function name, and only use nameHint as part of generating the object+property wrapper for debug purposes. This ensures we don't conflate synthesized hints with real function names. Then, we also update OutlineFunctions to use the function name _or_ the nameHint as the input to generating a unique identifier. This isn't quite as nice as the object form since we lose our formatting, but it's a simple step that gives more context to the developer than `_temp` does. Switching to output the object+property lookup form for outlined functions is a bit more involved, let's do that in a follow-up.

Joseph Savona committed Sep 9, 2025 at 12:14 UTC 665de2ed283205fccecda649ae2d66f62983f15f
12 files changed +154 -63
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+9 -9
@@ -325,6 +325,15 @@ function runWithEnvironment(
325 outlineJSX(hir);
326 }
327
328 + if (env.config.enableNameAnonymousFunctions) {
329 + nameAnonymousFunctions(hir);
330 + log({
331 + kind: 'hir',
332 + name: 'NameAnonymousFunctions',
333 + value: hir,
334 + });
335 + }
336 +
337 if (env.config.enableFunctionOutlining) {
338 outlineFunctions(hir, fbtOperands);
339 log({kind: 'hir', name: 'OutlineFunctions', value: hir});
@@ -415,15 +424,6 @@ function runWithEnvironment(
424 });
425 }
426
418 - if (env.config.enableNameAnonymousFunctions) {
419 - nameAnonymousFunctions(hir);
420 - log({
421 - kind: 'hir',
422 - name: 'NameAnonymougFunctions',
423 - value: hir,
424 - });
425 - }
426 -
427 const reactiveFunction = buildReactiveFunction(hir);
428 log({
429 kind: 'reactive',
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+15 -8
@@ -47,6 +47,7 @@ import {
47 makePropertyLiteral,
48 makeType,
49 promoteTemporary,
50 + validateIdentifierName,
51 } from './HIR';
52 import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
53 import {BuiltInArrayId} from './ObjectShape';
@@ -213,6 +214,16 @@ export function lower(
214 );
215 }
216
217 + let validatedId: HIRFunction['id'] = null;
218 + if (id != null) {
219 + const idResult = validateIdentifierName(id);
220 + if (idResult.isErr()) {
221 + builder.errors.merge(idResult.unwrapErr());
222 + } else {
223 + validatedId = idResult.unwrap().value;
224 + }
225 + }
226 +
227 if (builder.errors.hasAnyErrors()) {
228 return Err(builder.errors);
229 }
@@ -234,7 +245,8 @@ export function lower(
245 );
246
247 return Ok({
237 - id,
248 + id: validatedId,
249 + nameHint: null,
250 params,
251 fnType: bindings == null ? env.fnType : 'Other',
252 returnTypeAnnotation: null, // TODO: extract the actual return type node if present
@@ -3563,19 +3575,14 @@ function lowerFunctionToValue(
3575 ): InstructionValue {
3576 const exprNode = expr.node;
3577 const exprLoc = exprNode.loc ?? GeneratedSource;
3566 - let name: string | null = null;
3567 - if (expr.isFunctionExpression()) {
3568 - name = expr.get('id')?.node?.name ?? null;
3569 - } else if (expr.isFunctionDeclaration()) {
3570 - name = expr.get('id')?.node?.name ?? null;
3571 - }
3578 const loweredFunc = lowerFunction(builder, expr);
3579 if (!loweredFunc) {
3580 return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
3581 }
3582 return {
3583 kind: 'FunctionExpression',
3578 - name,
3584 + name: loweredFunc.func.id,
3585 + nameHint: null,
3586 type: expr.node.type,
3587 loc: exprLoc,
3588 loweredFunc,
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+46 -33
@@ -7,7 +7,11 @@
7
8 import {BindingKind} from '@babel/traverse';
9 import * as t from '@babel/types';
10 -import {CompilerError} from '../CompilerError';
10 +import {
11 + CompilerDiagnostic,
12 + CompilerError,
13 + ErrorCategory,
14 +} from '../CompilerError';
15 import {assertExhaustive} from '../Utils/utils';
16 import {Environment, ReactFunctionType} from './Environment';
17 import type {HookKind} from './ObjectShape';
@@ -54,7 +58,8 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
58 */
59 export type ReactiveFunction = {
60 loc: SourceLocation;
57 - id: string | null;
61 + id: ValidIdentifierName | null;
62 + nameHint: string | null;
63 params: Array<Place | SpreadPattern>;
64 generator: boolean;
65 async: boolean;
@@ -276,7 +281,8 @@ export type ReactiveTryTerminal = {
281 // A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
282 export type HIRFunction = {
283 loc: SourceLocation;
279 - id: string | null;
284 + id: ValidIdentifierName | null;
285 + nameHint: string | null;
286 fnType: ReactFunctionType;
287 env: Environment;
288 params: Array<Place | SpreadPattern>;
@@ -1124,7 +1130,8 @@ export type JsxAttribute =
1130
1131 export type FunctionExpression = {
1132 kind: 'FunctionExpression';
1127 - name: string | null;
1133 + name: ValidIdentifierName | null;
1134 + nameHint: string | null;
1135 loweredFunc: LoweredFunction;
1136 type:
1137 | 'ArrowFunctionExpression'
@@ -1301,11 +1308,41 @@ export function forkTemporaryIdentifier(
1308
1309 export function validateIdentifierName(
1310 name: string,
1304 -): Result<ValidIdentifierName, null> {
1305 - if (isReservedWord(name) || !t.isValidIdentifier(name)) {
1306 - return Err(null);
1311 +): Result<ValidatedIdentifier, CompilerError> {
1312 + if (isReservedWord(name)) {
1313 + const error = new CompilerError();
1314 + error.pushDiagnostic(
1315 + CompilerDiagnostic.create({
1316 + category: ErrorCategory.Syntax,
1317 + reason: 'Expected a non-reserved identifier name',
1318 + description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
1319 + suggestions: null,
1320 + }).withDetails({
1321 + kind: 'error',
1322 + loc: GeneratedSource,
1323 + message: 'reserved word',
1324 + }),
1325 + );
1326 + return Err(error);
1327 + } else if (!t.isValidIdentifier(name)) {
1328 + const error = new CompilerError();
1329 + error.pushDiagnostic(
1330 + CompilerDiagnostic.create({
1331 + category: ErrorCategory.Syntax,
1332 + reason: `Expected a valid identifier name`,
1333 + description: `\`${name}\` is not a valid JavaScript identifier`,
1334 + suggestions: null,
1335 + }).withDetails({
1336 + kind: 'error',
1337 + loc: GeneratedSource,
1338 + message: 'reserved word',
1339 + }),
1340 + );
1341 }
1308 - return Ok(makeIdentifierName(name).value);
1342 + return Ok({
1343 + kind: 'named',
1344 + value: name as ValidIdentifierName,
1345 + });
1346 }
1347
1348 /**
@@ -1314,31 +1351,7 @@ export function validateIdentifierName(
1351 * original source code.
1352 */
1353 export function makeIdentifierName(name: string): ValidatedIdentifier {
1317 - if (isReservedWord(name)) {
1318 - CompilerError.throwInvalidJS({
1319 - reason: 'Expected a non-reserved identifier name',
1320 - loc: GeneratedSource,
1321 - description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
1322 - suggestions: null,
1323 - });
1324 - } else {
1325 - CompilerError.invariant(t.isValidIdentifier(name), {
1326 - reason: `Expected a valid identifier name`,
1327 - description: `\`${name}\` is not a valid JavaScript identifier`,
1328 - details: [
1329 - {
1330 - kind: 'error',
1331 - loc: GeneratedSource,
1332 - message: null,
1333 - },
1334 - ],
1335 - suggestions: null,
1336 - });
1337 - }
1338 - return {
1339 - kind: 'named',
1340 - value: name as ValidIdentifierName,
1341 - };
1354 + return validateIdentifierName(name).unwrap();
1355 }
1356
1357 /**
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+3
@@ -56,6 +56,9 @@ export function printFunction(fn: HIRFunction): string {
56 } else {
57 definition += '<<anonymous>>';
58 }
59 + if (fn.nameHint != null) {
60 + definition += ` ${fn.nameHint}`;
61 + }
62 if (fn.params.length !== 0) {
63 definition +=
64 '(' +
compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts
+2
@@ -249,6 +249,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
249 const fn: HIRFunction = {
250 loc: GeneratedSource,
251 id: null,
252 + nameHint: null,
253 fnType: 'Other',
254 env,
255 params: [obj],
@@ -275,6 +276,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
276 value: {
277 kind: 'FunctionExpression',
278 name: null,
279 + nameHint: null,
280 loweredFunc: {
281 func: fn,
282 },
compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts
+3 -1
@@ -31,7 +31,9 @@ export function outlineFunctions(
31 ) {
32 const loweredFunc = value.loweredFunc.func;
33
34 - const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
34 + const id = fn.env.generateGloballyUniqueIdentifierName(
35 + loweredFunc.id ?? loweredFunc.nameHint,
36 + );
37 loweredFunc.id = id.value;
38
39 fn.env.outlineFunction(loweredFunc, null);
compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts
+1
@@ -364,6 +364,7 @@ function emitOutlinedFn(
364 const fn: HIRFunction = {
365 loc: GeneratedSource,
366 id: null,
367 + nameHint: null,
368 fnType: 'Other',
369 env,
370 params: [propsObj],
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts
+1
@@ -44,6 +44,7 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
44 return {
45 loc: fn.loc,
46 id: fn.id,
47 + nameHint: fn.nameHint,
48 params: fn.params,
49 generator: fn.generator,
50 async: fn.async,
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+6 -11
@@ -43,7 +43,6 @@ import {
43 ValidIdentifierName,
44 getHookKind,
45 makeIdentifierName,
46 - validateIdentifierName,
46 } from '../HIR/HIR';
47 import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
48 import {eachPatternOperand} from '../HIR/visitors';
@@ -62,6 +61,7 @@ export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
61 export type CodegenFunction = {
62 type: 'CodegenFunction';
63 id: t.Identifier | null;
64 + nameHint: string | null;
65 params: t.FunctionDeclaration['params'];
66 body: t.BlockStatement;
67 generator: boolean;
@@ -384,6 +384,7 @@ function codegenReactiveFunction(
384 type: 'CodegenFunction',
385 loc: fn.loc,
386 id: fn.id !== null ? t.identifier(fn.id) : null,
387 + nameHint: fn.nameHint,
388 params,
389 body,
390 generator: fn.generator,
@@ -2328,10 +2329,6 @@ function codegenInstructionValue(
2329 reactiveFunction,
2330 ).unwrap();
2331
2331 - const validatedName =
2332 - instrValue.name != null
2333 - ? validateIdentifierName(instrValue.name)
2334 - : Err(null);
2332 if (instrValue.type === 'ArrowFunctionExpression') {
2333 let body: t.BlockStatement | t.Expression = fn.body;
2334 if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2343,9 +2340,7 @@ function codegenInstructionValue(
2340 value = t.arrowFunctionExpression(fn.params, body, fn.async);
2341 } else {
2342 value = t.functionExpression(
2346 - validatedName
2347 - .map<t.Identifier | null>(name => t.identifier(name))
2348 - .unwrapOr(null),
2343 + instrValue.name != null ? t.identifier(instrValue.name) : null,
2344 fn.params,
2345 fn.body,
2346 fn.generator,
@@ -2354,10 +2349,10 @@ function codegenInstructionValue(
2349 }
2350 if (
2351 cx.env.config.enableNameAnonymousFunctions &&
2357 - validatedName.isErr() &&
2358 - instrValue.name != null
2352 + instrValue.name == null &&
2353 + instrValue.nameHint != null
2354 ) {
2360 - const name = instrValue.name;
2355 + const name = instrValue.nameHint;
2356 value = t.memberExpression(
2357 t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
2358 t.stringLiteral(name),
compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts
+2 -1
@@ -26,7 +26,8 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
26 * nesting depth.
27 */
28 const name = `${prefix}${node.generatedName}]`;
29 - node.fn.name = name;
29 + node.fn.nameHint = name;
30 + node.fn.loweredFunc.func.nameHint = name;
31 }
32 /**
33 * Whether or not we generated a name for the function at this node,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions-outline.expect.md new
+52
@@ -0,0 +1,52 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNameAnonymousFunctions
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Component(props) {
9 + const onClick = () => {
10 + console.log('hello!');
11 + };
12 + return <div onClick={onClick} />;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{value: 42}],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
26 +import { Stringify } from "shared-runtime";
27 +
28 +function Component(props) {
29 + const $ = _c(1);
30 + const onClick = _ComponentOnClick;
31 + let t0;
32 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33 + t0 = <div onClick={onClick} />;
34 + $[0] = t0;
35 + } else {
36 + t0 = $[0];
37 + }
38 + return t0;
39 +}
40 +function _ComponentOnClick() {
41 + console.log("hello!");
42 +}
43 +
44 +export const FIXTURE_ENTRYPOINT = {
45 + fn: Component,
46 + params: [{ value: 42 }],
47 +};
48 +
49 +```
50 +
51 +### Eval output
52 +(kind: ok) <div></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions-outline.js new
+14
@@ -0,0 +1,14 @@
1 +// @enableNameAnonymousFunctions
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Component(props) {
5 + const onClick = () => {
6 + console.log('hello!');
7 + };
8 + return <div onClick={onClick} />;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [{value: 42}],
14 +};