diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index e5005d02c4ef7..7f8640122b942 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects'; import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges'; import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects'; +import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -414,6 +415,15 @@ function runWithEnvironment( }); } + if (env.config.enableNameAnonymousFunctions) { + nameAnonymousFunctions(hir); + log({ + kind: 'hir', + name: 'NameAnonymougFunctions', + value: hir, + }); + } + const reactiveFunction = buildReactiveFunction(hir); log({ kind: 'reactive', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 02607f27b4e60..4d0461fe105b0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -3566,6 +3566,8 @@ function lowerFunctionToValue( let name: string | null = null; if (expr.isFunctionExpression()) { name = expr.get('id')?.node?.name ?? null; + } else if (expr.isFunctionDeclaration()) { + name = expr.get('id')?.node?.name ?? null; } const loweredFunc = lowerFunction(builder, expr); if (!loweredFunc) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index e38a49cb56a06..8e6816a3d5128 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -261,6 +261,8 @@ export const EnvironmentConfigSchema = z.object({ enableFire: z.boolean().default(false), + enableNameAnonymousFunctions: z.boolean().default(false), + /** * Enables inference and auto-insertion of effect dependencies. Takes in an array of * configurable module and import pairs to allow for user-land experimentation. For example, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 582ef93cf34f2..fa502f821d0b7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -15,6 +15,7 @@ import {Type, makeType} from './Types'; import {z} from 'zod'; import type {AliasingEffect} from '../Inference/AliasingEffects'; import {isReservedWord} from '../Utils/Keyword'; +import {Err, Ok, Result} from '../Utils/Result'; /* * ******************************************************************************************* @@ -1298,6 +1299,15 @@ export function forkTemporaryIdentifier( }; } +export function validateIdentifierName( + name: string, +): Result { + if (isReservedWord(name) || !t.isValidIdentifier(name)) { + return Err(null); + } + return Ok(makeIdentifierName(name).value); +} + /** * Creates a valid identifier name. This should *not* be used for synthesizing * identifier names: only call this method for identifier names that appear in the diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 9d6f46c2374a7..28d8afd84b4fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -43,6 +43,7 @@ import { ValidIdentifierName, getHookKind, makeIdentifierName, + validateIdentifierName, } from '../HIR/HIR'; import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR'; import {eachPatternOperand} from '../HIR/visitors'; @@ -2326,6 +2327,11 @@ function codegenInstructionValue( ), reactiveFunction, ).unwrap(); + + const validatedName = + instrValue.name != null + ? validateIdentifierName(instrValue.name) + : Err(null); if (instrValue.type === 'ArrowFunctionExpression') { let body: t.BlockStatement | t.Expression = fn.body; if (body.body.length === 1 && loweredFunc.directives.length == 0) { @@ -2337,14 +2343,28 @@ function codegenInstructionValue( value = t.arrowFunctionExpression(fn.params, body, fn.async); } else { value = t.functionExpression( - fn.id ?? - (instrValue.name != null ? t.identifier(instrValue.name) : null), + validatedName + .map(name => t.identifier(name)) + .unwrapOr(null), fn.params, fn.body, fn.generator, fn.async, ); } + if ( + cx.env.config.enableNameAnonymousFunctions && + validatedName.isErr() && + instrValue.name != null + ) { + const name = instrValue.name; + value = t.memberExpression( + t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]), + t.stringLiteral(name), + true, + false, + ); + } break; } case 'TaggedTemplateExpression': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts new file mode 100644 index 0000000000000..5fed3af0f733f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts @@ -0,0 +1,173 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + FunctionExpression, + getHookKind, + HIRFunction, + IdentifierId, +} from '../HIR'; + +export function nameAnonymousFunctions(fn: HIRFunction): void { + if (fn.id == null) { + return; + } + const parentName = fn.id; + const functions = nameAnonymousFunctionsImpl(fn); + function visit(node: Node, prefix: string): void { + if (node.generatedName != null) { + /** + * Note that we don't generate a name for functions that already had one, + * so we'll only add the prefix to anonymous functions regardless of + * nesting depth. + */ + const name = `${prefix}${node.generatedName}]`; + node.fn.name = name; + } + /** + * Whether or not we generated a name for the function at this node, + * traverse into its nested functions to assign them names + */ + const nextPrefix = `${prefix}${node.generatedName ?? node.fn.name ?? ''} > `; + for (const inner of node.inner) { + visit(inner, nextPrefix); + } + } + for (const node of functions) { + visit(node, `${parentName}[`); + } +} + +type Node = { + fn: FunctionExpression; + generatedName: string | null; + inner: Array; +}; + +function nameAnonymousFunctionsImpl(fn: HIRFunction): Array { + // Functions that we track to generate names for + const functions: Map = new Map(); + // Tracks temporaries that read from variables/globals/properties + const names: Map = new Map(); + // Tracks all function nodes to bubble up for later renaming + const nodes: Array = []; + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {lvalue, value} = instr; + switch (value.kind) { + case 'LoadGlobal': { + names.set(lvalue.identifier.id, value.binding.name); + break; + } + case 'LoadContext': + case 'LoadLocal': { + const name = value.place.identifier.name; + if (name != null && name.kind === 'named') { + names.set(lvalue.identifier.id, name.value); + } + break; + } + case 'PropertyLoad': { + const objectName = names.get(value.object.identifier.id); + if (objectName != null) { + names.set( + lvalue.identifier.id, + `${objectName}.${String(value.property)}`, + ); + } + break; + } + case 'FunctionExpression': { + const inner = nameAnonymousFunctionsImpl(value.loweredFunc.func); + const node: Node = { + fn: value, + generatedName: null, + inner, + }; + /** + * Bubble-up all functions, even if they're named, so that we can + * later generate names for any inner anonymous functions + */ + nodes.push(node); + if (value.name == null) { + // but only generate names for anonymous functions + functions.set(lvalue.identifier.id, node); + } + break; + } + case 'StoreContext': + case 'StoreLocal': { + const node = functions.get(value.value.identifier.id); + const variableName = value.lvalue.place.identifier.name; + if ( + node != null && + variableName != null && + variableName.kind === 'named' + ) { + node.generatedName = variableName.value; + functions.delete(value.value.identifier.id); + } + break; + } + case 'CallExpression': + case 'MethodCall': { + const callee = + value.kind === 'MethodCall' ? value.property : value.callee; + const hookKind = getHookKind(fn.env, callee.identifier); + let calleeName: string | null = null; + if (hookKind != null && hookKind !== 'Custom') { + calleeName = hookKind; + } else { + calleeName = names.get(callee.identifier.id) ?? '(anonymous)'; + } + let fnArgCount = 0; + for (const arg of value.args) { + if (arg.kind === 'Identifier' && functions.has(arg.identifier.id)) { + fnArgCount++; + } + } + for (let i = 0; i < value.args.length; i++) { + const arg = value.args[i]!; + if (arg.kind === 'Spread') { + continue; + } + const node = functions.get(arg.identifier.id); + if (node != null) { + const generatedName = + fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`; + node.generatedName = generatedName; + functions.delete(arg.identifier.id); + } + } + break; + } + case 'JsxExpression': { + for (const attr of value.props) { + if (attr.kind === 'JsxSpreadAttribute') { + continue; + } + const node = functions.get(attr.place.identifier.id); + if (node != null) { + const elementName = + value.tag.kind === 'BuiltinTag' + ? value.tag.name + : (names.get(value.tag.identifier.id) ?? null); + const propName = + elementName == null + ? attr.name + : `<${elementName}>.${attr.name}`; + node.generatedName = `${propName}`; + functions.delete(attr.place.identifier.id); + } + } + break; + } + } + } + } + return nodes; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md new file mode 100644 index 0000000000000..88e270647d3e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md @@ -0,0 +1,272 @@ + +## Input + +```javascript +// @enableNameAnonymousFunctions + +import {useEffect} from 'react'; +import {identity, Stringify, useIdentity} from 'shared-runtime'; +import * as SharedRuntime from 'shared-runtime'; + +function Component(props) { + function named() { + const inner = () => props.named; + return inner(); + } + const namedVariable = function () { + return props.namedVariable; + }; + const methodCall = SharedRuntime.identity(() => props.methodCall); + const call = identity(() => props.call); + const builtinElementAttr =
props.builtinElementAttr} />; + const namedElementAttr = props.namedElementAttr} />; + const hookArgument = useIdentity(() => props.hookArgument); + useEffect(() => { + console.log(props.useEffect); + JSON.stringify(null, null, () => props.useEffect); + const g = () => props.useEffect; + console.log(g()); + }, [props.useEffect]); + return ( + <> + {named()} + {namedVariable()} + {methodCall()} + {call()} + {builtinElementAttr} + {namedElementAttr} + {hookArgument()} + + ); +} + +export const TODO_FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + named: '', + namedVariable: '', + methodCall: '', + call: '', + builtinElementAttr: '', + namedElementAttr: '', + hookArgument: '', + useEffect: '', + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions + +import { useEffect } from "react"; +import { identity, Stringify, useIdentity } from "shared-runtime"; +import * as SharedRuntime from "shared-runtime"; + +function Component(props) { + const $ = _c(31); + let t0; + if ($[0] !== props.named) { + t0 = function named() { + const inner = { "Component[named > inner]": () => props.named }[ + "Component[named > inner]" + ]; + return inner(); + }; + $[0] = props.named; + $[1] = t0; + } else { + t0 = $[1]; + } + const named = t0; + let t1; + if ($[2] !== props.namedVariable) { + t1 = { + "Component[namedVariable]": function () { + return props.namedVariable; + }, + }["Component[namedVariable]"]; + $[2] = props.namedVariable; + $[3] = t1; + } else { + t1 = $[3]; + } + const namedVariable = t1; + let t2; + if ($[4] !== props.methodCall) { + t2 = { "Component[SharedRuntime.identity()]": () => props.methodCall }[ + "Component[SharedRuntime.identity()]" + ]; + $[4] = props.methodCall; + $[5] = t2; + } else { + t2 = $[5]; + } + const methodCall = SharedRuntime.identity(t2); + let t3; + if ($[6] !== props.call) { + t3 = { "Component[identity()]": () => props.call }["Component[identity()]"]; + $[6] = props.call; + $[7] = t3; + } else { + t3 = $[7]; + } + const call = identity(t3); + let t4; + if ($[8] !== props.builtinElementAttr) { + t4 = ( +
.onClick]": () => props.builtinElementAttr }[ + "Component[
.onClick]" + ] + } + /> + ); + $[8] = props.builtinElementAttr; + $[9] = t4; + } else { + t4 = $[9]; + } + const builtinElementAttr = t4; + let t5; + if ($[10] !== props.namedElementAttr) { + t5 = ( + .onClick]": () => props.namedElementAttr }[ + "Component[.onClick]" + ] + } + /> + ); + $[10] = props.namedElementAttr; + $[11] = t5; + } else { + t5 = $[11]; + } + const namedElementAttr = t5; + let t6; + if ($[12] !== props.hookArgument) { + t6 = { "Component[useIdentity()]": () => props.hookArgument }[ + "Component[useIdentity()]" + ]; + $[12] = props.hookArgument; + $[13] = t6; + } else { + t6 = $[13]; + } + const hookArgument = useIdentity(t6); + let t7; + let t8; + if ($[14] !== props.useEffect) { + t7 = { + "Component[useEffect()]": () => { + console.log(props.useEffect); + JSON.stringify( + null, + null, + { + "Component[useEffect() > JSON.stringify()]": () => props.useEffect, + }["Component[useEffect() > JSON.stringify()]"], + ); + const g = { "Component[useEffect() > g]": () => props.useEffect }[ + "Component[useEffect() > g]" + ]; + console.log(g()); + }, + }["Component[useEffect()]"]; + t8 = [props.useEffect]; + $[14] = props.useEffect; + $[15] = t7; + $[16] = t8; + } else { + t7 = $[15]; + t8 = $[16]; + } + useEffect(t7, t8); + let t9; + if ($[17] !== named) { + t9 = named(); + $[17] = named; + $[18] = t9; + } else { + t9 = $[18]; + } + let t10; + if ($[19] !== namedVariable) { + t10 = namedVariable(); + $[19] = namedVariable; + $[20] = t10; + } else { + t10 = $[20]; + } + const t11 = methodCall(); + const t12 = call(); + let t13; + if ($[21] !== hookArgument) { + t13 = hookArgument(); + $[21] = hookArgument; + $[22] = t13; + } else { + t13 = $[22]; + } + let t14; + if ( + $[23] !== builtinElementAttr || + $[24] !== namedElementAttr || + $[25] !== t10 || + $[26] !== t11 || + $[27] !== t12 || + $[28] !== t13 || + $[29] !== t9 + ) { + t14 = ( + <> + {t9} + {t10} + {t11} + {t12} + {builtinElementAttr} + {namedElementAttr} + {t13} + + ); + $[23] = builtinElementAttr; + $[24] = namedElementAttr; + $[25] = t10; + $[26] = t11; + $[27] = t12; + $[28] = t13; + $[29] = t9; + $[30] = t14; + } else { + t14 = $[30]; + } + return t14; +} + +export const TODO_FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + named: "", + namedVariable: "", + methodCall: "", + call: "", + builtinElementAttr: "", + namedElementAttr: "", + hookArgument: "", + useEffect: "", + }, + ], +}; + +``` + +### 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/name-anonymous-functions.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js new file mode 100644 index 0000000000000..ff4f1f6017d98 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js @@ -0,0 +1,53 @@ +// @enableNameAnonymousFunctions + +import {useEffect} from 'react'; +import {identity, Stringify, useIdentity} from 'shared-runtime'; +import * as SharedRuntime from 'shared-runtime'; + +function Component(props) { + function named() { + const inner = () => props.named; + return inner(); + } + const namedVariable = function () { + return props.namedVariable; + }; + const methodCall = SharedRuntime.identity(() => props.methodCall); + const call = identity(() => props.call); + const builtinElementAttr =
props.builtinElementAttr} />; + const namedElementAttr = props.namedElementAttr} />; + const hookArgument = useIdentity(() => props.hookArgument); + useEffect(() => { + console.log(props.useEffect); + JSON.stringify(null, null, () => props.useEffect); + const g = () => props.useEffect; + console.log(g()); + }, [props.useEffect]); + return ( + <> + {named()} + {namedVariable()} + {methodCall()} + {call()} + {builtinElementAttr} + {namedElementAttr} + {hookArgument()} + + ); +} + +export const TODO_FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + named: '', + namedVariable: '', + methodCall: '', + call: '', + builtinElementAttr: '', + namedElementAttr: '', + hookArgument: '', + useEffect: '', + }, + ], +};