Repository navigation
Conversation
…bol the function keeps A compiled function expression keeps the symbol that the parser gave its name. Codegen made a new symbol for each read of that name, which nothing declares: the bundler's renamer numbered it, so `Tree3` threw, or was another `Tree` of the bundle. Lowering now gets `PendingCompile::own_name` and returns the identifier that the reads of it became. Codegen prints the parser's symbol for that declaration.
|
Updated 2:15 AM PT - Nov 10th, 2026
✅ @robobun, your commit b288f8fd725d6c3af506df2fb5b2e76408be08b9 passed in 🧪 To try this PR locally: bunx bun-pr 48444That installs a local version of the PR into your bun-48444 --bun |
|
Status: the fix is pushed, and the diff is green where CI ran. How I reproduced it: a stand-in
CI on b288f8f (build 139645): 184 of 188 jobs passed. The other 4 are the PR: #48444 |
|
No actionable comments were generated in the recent review. WalkthroughThe React Compiler now carries a function expression’s own-name binding through lowering and codegen. Codegen resolves matching references to the parser symbol. Regression coverage was added, and the transpiler cache format version increased to 52. ChangesFunction-expression own-name handling
Suggested reviewers: Merge Risk:
|
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I also checked that every caller of the changed lowering::lower / Codegen::new signatures is updated (lint.rs, compile_outlined_fn pass None, which is right since neither compiles a function that keeps its own name), that the EXPECTED_VERSION bump in RuntimeTranspilerCache.rs is present for the changed output, and that the outlined-function path sharing cx.cg cannot hit own_name (outlined functions capture no context, so no own-name read reaches them). I did not run the new test matrix.
Extended reasoning...
The change threads a function expression's own-name Ref from program.rs through pipeline.rs and lowering into codegen, where identifier resolution maps the matching declaration to the parser's symbol instead of a fresh local; it touches no security-sensitive surface. The one confirmed inline finding concerns the no-renamer runtime path, which the PR description itself acknowledges as still wrong; the rest of the signature and cache-version mechanics check out by reading.
|
Another branch fixed this same read and is not opened as a PR, because this PR owns the bug. I built the Two things still differ from the run without the flag on that build. No open PR has them.
The first three are the second Downsides bullet here. The review thread above held that fix back for a nested function with the same name. #48484 now leaves that function as written. 5be1a24 on
Checked on a debug build of 2596ac5 with the
Not built: #48475. A component in a |
|
Follow-up for the second Downsides bullet: under The reservation exists as one commit on top of #48484: cd1729b on What the commit does
Measured (debug builds of #48484 and of the commit,
The rows are the parameter Two questions for a maintainer
|
Problem
bun build --react-compilermiscompiles a function expression that reads its own name in a callback:memo(function Tree() { … [0].map(n => <Tree />) }). The output throwsReferenceError: Tree3 is not definedor renders anotherTree. Since 1.4.0. No user reported it.Context::ref_for_id,src/react_compiler/codegen.rs:470). Nothing declares this one: the function keeps the parser's symbol (react-compiler: register the locals of a compiled function in its own scope #42463).Fix
PendingCompile::own_namebecame. Codegen prints the parser's symbol for it.test/bundler/transpiler/react-compiler.test.ts(5 new cases fail on main),test/aot/react-compiler.test.ts, upstream fixtures, lint suites.Background
Downsides
bun test --react-compiler, a function with a block local of its own name now reads that local, silently. Main throwsName_0 is not defined.argumentsin a callback threw. It now renders stalearguments, until react-compiler: do not compile a function that reads the implicitarguments#46496.Notes
Repro.
a.jsx, over a stand-inreactwhosememoreturns the function, whosejsxDEV(type, props, key)returns{ type, props, key }, and whosereact/compiler-runtimeexportsc(n):bun build ./a.jsx --outdir=out && bun out/a.jsprintsR function,0. With--react-compiler, 1.4.3, 1.4.4 and main printR ReferenceError: Tree3 is not defined. The built text holdsvar Tree = memo(function Tree2(t0) {andjsxDEV(Tree3, ....Forms by release (release binaries, and a debug build of main):
const)Tree3 is not defined--minifyTree is not definedTree is not definedb is not definedconst Tree = memo(function Row…<Row />…)Row is not definedwith--minifyRow2 is not definedconst Tree = memo(function Tree…)Tree{"A":"C","B":"C","C":"B"}at exit 0Treeunder another nameTree4 is not defined[0].map(() => typeof Tree)"undefined"with--minify"undefined"A 1.4.3 canary (367d939) prints what 1.4.4 prints for the repro. Before #42463 the new symbol was a module-level one and printed under its bare name, so it was whatever top-level thing had that name. Since #42463 it is a local of the function, and the renamer numbers it.
Which functions are hit. Only a function that the compiler goes on to compile. The compile set does not change here. A read of the own name in the body itself stops the compiler (
[InferMutationAliasingEffects] Expected value kind to be initialized), and so does the usualitems.map(item => <Tree … />)over a prop since #44805 item 5 restored upstream'sExpected a node for all identifiers. The existing testreact-compiler/FunctionExpressionThatNamesItselfpins those as not compiled and is unchanged. What is left is a callback passed to a method of an array that the function makes itself: an array literal, a spread copy,Object.keys.Upstream. babel-plugin-react-compiler 1.0.0 compiles the repro (
CompileSuccess) and prints<Tree key={n} x={n} />insidefunction Tree(t0). Upstream never binds the name: it prints the function id and the read as the same text. react/react#38100 (bail on such a read) and react/react#37511 (initialize the binding, oxc port) are open. If a sync ports #38100, these functions stop being compiled and the(t0)assertions of the new test flip. 1.0.0 does not compiletypeof TreeorTree.lengthin a callback (TodoUnsupported declaration type for hoisting). The port never had that check, so those are compiled here, and the new test says so.Measured, main 2596ac5 against this change (debug builds unless said):
Host::new_localcalls: the repro 7 -> 6. The 1,806 upstream fixtures 8,816 -> 8,816 (gdb breakpoint counts).resolve_binding_with_loccalls 13,393 vs 13,393,ref_for_idcalls 25,856 vs 25,856, lowered HIR dumps that differ 0 of 1,583.diff -rq).size). Symbols ofreact_compiler: +437 bytes (nm -S).--minify-identifiers,--target=bun): the own-name forms 7 -> 0. The 4 sibling files below are still flagged.bun test --react-compilerprobes that are right: 7 -> 9 of 16. With--aot: 6 -> 16 of 16.bun build --target=browser11,910 -> 418,bun test --react-compiler7,228 -> 3,134. With--minify-identifiers418, with--target=bun --minify-syntax268 (main not run there). Cells that main gets right and this change gets wrong: 0. The 418 are all nested function expressions (below).Still wrong, not changed here.
function cb(n) { return typeof cb_0 + n; }). Same mint, other carrier: its name is printed by its text. Since 1.4.0.namespacethat reads an exported member from a callback (UI2 is not defined). Main only.argumentsread from a callback. react-compiler: do not compile a function that reads the implicitarguments#46496 is open for it, after [compiler] Bail on use of arguments object react/react#37645.memo(function c({ x, c }) {…})builds tofunction c(t0) { let $ = c(6);and throws aRangeError. The cache call lands on the function. On 1.4.4 too. Another mechanism: the name's symbol is linked to the parameter that the new body drops.const Row = memo(function Row…)prints asfunction Row2in a bundle, so.nameisRow2. Cosmetic, older than this change. The new test matchesfunction Tree\d*\(t0\)for that reason.bun test --react-compilerno renamer runs, and a symbol prints under the name it was made with. A local that has the text of the own name then takes the read: a block local of that name, or a temporaryt1in a function namedt1. Main prints the read asName_0, which is not bound: a tag or a member read throws there, andtypeofgives"undefined". With this change the read takes the value of that local, with no error.bun buildand--aotare right for these. The rule belongs in RenameVariables' reserved names, after the nested name above is printed through its binding.Removed after the self-review. A first version also bound the own name ahead of the parameters in lowering, and renamed a local with that text in codegen, for the no-renamer rows above. It broke
memo(function C() { [x].map(function C(k) { return typeof C + k; }); … }), which 1.4.4 and main get right when the component renders no children: lowering then gave the nested function's reads the nameC_0, and that function is printed by its text. That form is now a row of the test.Merge order. #48308 has a parameter
own_nameand a comment "A read of the function's own name is a local that nothing declares: it keeps its spelling". After this change that is no longer true. Whichever lands second removes that exemption. #48269, #48308, #48310 and this change each setEXPECTED_VERSIONto 52. #48310 and this change each add a file to thepure-componentsprogram, on the same two lines of itsmain.tsx: keep both.Suites run on a debug build.
test/bundler/transpiler/react-compiler.test.ts(150 pass),react-compiler-fixtures.test.ts(3,293 pass),test/cli/lint/react-compiler.test.ts,test/cli/lint/conformance.test.ts, the--react-compilercases oftest/cli/test/bun-test.test.ts,test/aot/react-compiler*.test.ts, andbun run rust:test -p bun_aot react_compiler(52 pass).no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/transpiler/react-compiler.test.ts