Summary
Perry compiles a module's entire top level into a single LLVM function (@main). For a large minified bundle that function is enormous — the Claude Code cli.js entry is ~68 MB of IR, 439k instructions, with ~13,170 GC-root slots. Essentially every backend algorithm is super-linear in single-function size, so one giant function is simultaneously pathological for:
#8583 works around the GC half by spilling the giant function's roots to the shadow frame so RS4GC skips it. That is a correct stopgap but a heuristic bandage: it leaves a threshold to calibrate, does nothing for the ISel/emit cost, and keeps producing the giant function that every other backend pass then struggles with. module.rs's own comment already flags this: the 68 MB IIFE is "irreducible without structured intra-function outlining."
Proposal
Outline the module-entry body (and, more generally, any oversized generated function) into many smaller functions in codegen, before LLVM sees it:
- chunk the top-level statement stream (
codegen/entry.rs::compile_module_entry) into ordered range-functions called in sequence;
- promote every module-level
let that crosses a chunk boundary to a global — the mechanism already exists (module_globals_emit.rs globalizes lets that escape into closures);
- preserve evaluation order and TDZ semantics across the chunk boundaries.
Why this is the architecturally correct fix (not a workaround)
Scope / risk
Larger than #8583's series and touches GC root analysis, entry lowering, and global promotion — hence a separate tracker. The secret-tests/claude-code-build/split-bundle-modules.mjs / compose-bundle-entry.mjs tooling is a source-level approximation of the same idea and can inform chunk boundaries.
Follow-up to #8583. Related backend pathology context: #8421 (removed the oversized-function -O0 fallback), #4880, #8128.
Summary
Perry compiles a module's entire top level into a single LLVM function (
@main). For a large minified bundle that function is enormous — the Claude Codecli.jsentry is ~68 MB of IR, 439k instructions, with ~13,170 GC-root slots. Essentially every backend algorithm is super-linear in single-function size, so one giant function is simultaneously pathological for:rewrite-statepoints-for-gc— relocation fan-out islive_roots × safepoints; on@mainthat expands 439k → 6.5M instructions and the subsequent-Osnever finishes (this is the root cause behind [perf][regression] Large minified bundle becomes a multi-hour compile after RS4GC/opt-tier changes (83 CGUs, ~17 GiB RSS) #8583).-O0/FastISel fallback was removed in fix: make compile output TypeScript-developer friendly #8421).#8583 works around the GC half by spilling the giant function's roots to the shadow frame so RS4GC skips it. That is a correct stopgap but a heuristic bandage: it leaves a threshold to calibrate, does nothing for the ISel/emit cost, and keeps producing the giant function that every other backend pass then struggles with.
module.rs's own comment already flags this: the 68 MB IIFE is "irreducible without structured intra-function outlining."Proposal
Outline the module-entry body (and, more generally, any oversized generated function) into many smaller functions in codegen, before LLVM sees it:
codegen/entry.rs::compile_module_entry) into ordered range-functions called in sequence;letthat crosses a chunk boundary to a global — the mechanism already exists (module_globals_emit.rsglobalizes lets that escape into closures);Why this is the architecturally correct fix (not a workaround)
Scope / risk
Larger than #8583's series and touches GC root analysis, entry lowering, and global promotion — hence a separate tracker. The
secret-tests/claude-code-build/split-bundle-modules.mjs/compose-bundle-entry.mjstooling is a source-level approximation of the same idea and can inform chunk boundaries.Follow-up to #8583. Related backend pathology context: #8421 (removed the oversized-function
-O0fallback), #4880, #8128.