Skip to content

[compiler] Transitively hoist function declarations referenced from hoisted callers#36541

Open
tejasupmanyu wants to merge 2 commits into
facebook:mainfrom
tejasupmanyu:fix/compiler-named-function-hoisting
Open

[compiler] Transitively hoist function declarations referenced from hoisted callers#36541
tejasupmanyu wants to merge 2 commits into
facebook:mainfrom
tejasupmanyu:fix/compiler-named-function-hoisting

Conversation

@tejasupmanyu
Copy link
Copy Markdown

Summary

BuildHIR hoists a function declaration F when F is referenced before its lexical position in a block, but the analysis is shallow: it doesn't walk F's body to find other function declarations in the same block that F calls. The codegen lowers a non-hoisted function declaration that's captured by memoization into a const foo = ... binding initialized at the original lexical position. When F runs ahead of a callee's lexical position (because F itself was hoisted, or invoked from one), the callee's const is still in the TDZ and the call throws ReferenceError: Cannot access 'X' before initialization.

Repro from the linked issue (smaller form):

function Component({data}) {
  const result = compute(data);     // compute hoists as a function decl
  function helper(x) { return x > 0; }
  function compute(arr) {
    return arr.filter(helper);      // helper is in TDZ here
  }
  return <div>{result.length}</div>;
}

Compiled output before this change (TDZ at runtime):

result = compute(data);
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
  t1 = function helper(x) { return x > 0; };
  $[2] = t1;
} else { t1 = $[2]; }
const helper = t1;                  // ← TDZ when compute(data) calls helper
function compute(arr) { return arr.filter(helper); }

Compiled output with this change (both hoist as function declarations):

result = compute(data);
function helper(x) { return x > 0; }
function compute(arr) { return arr.filter(helper); }

Reported on multiple real projects, including Comcast/react-data-grid.

Fixes #33689

Fix

After the per-statement hoist analysis populates willHoist, walk the body of each hoisted function declaration and add references to other hoistable function declarations from the same block scope. Iterate to a fixpoint so chains (a → b → c) and mutually recursive groups are handled.

Cases unaffected: when the call site appears after the declarations, nothing is added to willHoist, and the existing memoization output is preserved.

Test plan

  • Added transitive-hoist-function-declaration.js regression fixture. Eval output <div>{"result":[1,2]}</div> confirms the compiled function runs end-to-end without TDZ.
  • Manually verified the variants from the issue (mutual recursion a ↔ b; chain a → b → c; call placed after declarations — no over-hoisting).
  • Full snap suite: 1720/1720 pass on this branch.
  • yarn workspace babel-plugin-react-compiler lint clean.
  • Pre-existing error.todo-functiondecl-hoisting and error.todo-valid-functiondecl-hoisting fixtures still pass (their expected errors are unchanged — the fix only expands willHoist, doesn't change downstream pruning).

…oisted callers

When a function declaration `F` in a block is referenced before its
lexical position, `BuildHIR` hoists `F` (DeclareContext HoistedFunction).
But the algorithm is shallow: it doesn't walk `F`'s body to hoist other
function declarations from the same block that `F` calls. The compiler
rewrites a non-hoisted function declaration whose value is captured by
memoization into a `const foo = ...` binding initialized at the original
lexical position. If `F` runs ahead of a callee's lexical position
(because `F` was hoisted, or invoked from a hoisted function), the
callee's `const` binding is still in the TDZ at the call site, throwing
`ReferenceError: Cannot access 'X' before initialization` at runtime.

Repro from the linked issue:

  function Component({data}) {
    const result = compute(data);    // compute is hoisted
    function helper(x) { return x > 0; }
    function compute(arr) {
      return arr.filter(helper);     // helper is in TDZ here
    }
    return <div>{result.length}</div>;
  }

Fix: after the per-statement hoist analysis populates `willHoist`, walk
the body of each hoisted function declaration and add references to
other hoistable function declarations from the same block. Iterate to a
fixpoint so chains of calls (a -> b -> c) are handled.

Cases unaffected: when the call site appears after the declarations, no
hoisting kicks in; the existing memoization output is preserved.

Fixes facebook#33689
@meta-cla meta-cla Bot added the CLA Signed label May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compiler Bug]: Not handling named functions calling named functions when defined in a weird order

1 participant