Summary
#8595 ("structured intra-function outlining of oversized generated functions (module-entry IIFE)") landed and works — but it only splits hir.init, the module top level. A CommonJS module's body is not in hir.init: Perry lowers it into a factory closure. So for a large CJS bundle the outliner sees an essentially empty entry, does nothing, and the pathology #8595 was filed to remove is still there in full.
Concretely: typescript@5.9.3's lib/_tsc.js (5.9 MB, CJS) compiles into one 6.30 MB LLVM function.
Evidence
Compiling _tsc.js on Perry 0.5.1579 (macOS arm64), the build reports:
perry: `perry_closure_node_modules_typescript_lib__tsc_js__1` has 463716 instructions
after IR optimization, above the optimized machine-pipeline budget 100000; ...
emitting this unit ... through LLVM's O0 machine pipeline
463,716 instructions in one function — the same shape and nearly the same magnitude as the ~439k-instruction cli.js entry quoted in #8595's own summary.
From the linked binary's symbol table (--report-size):
| symbol |
size |
_perry_closure_…__tsc_js__1 |
6.30 MB |
_perry_closure_…__tsc_js__0 |
2.46 MB |
_perry_closure_…__tsc_js__4801 |
2.44 MB |
_…__tsc_js__init_body — what #8595 targets |
0.88 MB |
__perry_entry_chunk_* symbols |
0 |
Zero outlined chunks were produced. The module entry that the outliner does inspect is 0.88 MB; the body it needs to reach is the 6.30 MB closure.
Mechanism
crates/perry-codegen/src/codegen/entry_outline.rs operates only on hir.init:
// analyze_entry_outlining_with_target
let stmts = &hir.init;
// outline_entry, line ~646
let original = std::mem::take(&mut hir.init);
And automatic admission requires a large top-level statement stream:
const DEFAULT_AUTO_MIN_STMTS: usize = 1_000;
const DEFAULT_AUTO_MIN_SAFEPOINTS: usize = 4_000;
For a CJS module, hir.init holds roughly one statement (invoke the factory, publish module.exports); the tens of thousands of real top-level statements live inside the factory closure. The confirming string is visible in the binary's own retained function source, which begins:
function() {
function __perry_cjs_factory() {
So the admission test can never fire on CJS, no matter how large the module is.
Correction to the mechanism above
I described the location wrong, and it matters. I wrote that the CJS body "lives in hir.functions". It does not.
cjs_wrap::wrap_commonjs_for_target wraps the body as text inside function __perry_cjs_factory() {...}, nested in an anonymous IIFE. Lowering represents that as a Stmt::Let naming an Expr::Closure, nested inside hir.init's own expression tree — not a hir.functions entry, because it is lexically nested rather than a top-level declaration. hir.init itself keeps only the handful of wrapper statements, which is why admission never fires.
The practical consequence: an implementation that goes looking in hir.functions is a silent no-op — it compiles, runs, and changes nothing. Verified by exactly that happening in a first attempt at #10603, caught only by compiling a real fixture and checking nm, not by unit tests. Anyone working this should confirm with nm for __perry_entry_chunk_* symbols rather than trusting a green suite.
Why it matters beyond compile time
#8583 / #8883 / #8228 all bounded compile time for this shape. This issue is about what the surviving giant function costs in the shipped artifact:
- it blows the machine-pipeline budget, so the whole codegen unit is emitted through LLVM's O0 pipeline — no machine-level optimisation, in both size and speed;
- the resulting
tsc binary is 86.4 MB, against 20 MB for Static Hermes on the same input;
./tsc-perry --noEmit demo.ts on a two-line file takes 11 m 33 s (663.29s user, 96% cpu, quiesced box), against 0.78 s for node tsc.js — a ~890x gap, while producing byte-identical, correct output and exit code 2.
CJS is not a corner case for this: it is the shape of most large published bundles.
Suggested direction
Admit oversized function bodies, not just hir.init — or, narrower and probably enough in practice, recognise the CJS factory closure as the logical module entry and route it through the existing chunking path. The globalization machinery #8595 reuses (emit_module_globals) is keyed on module-level lets, so a CJS factory's vars would need the equivalent treatment.
Reproduction
npm install typescript@5.9.3
echo 'import "typescript/lib/_tsc.js";' > tsc-entry.ts
# package.json: {"perry":{"compilePackages":["typescript"],"allow":{"compilePackages":["typescript"]}}}
perry compile tsc-entry.ts -o tsc-perry
Perry 0.5.1579, macOS arm64. Related: #8595 (closed, completed), #8583, #8883, #8228, #10574.
Summary
#8595 ("structured intra-function outlining of oversized generated functions (module-entry IIFE)") landed and works — but it only splits
hir.init, the module top level. A CommonJS module's body is not inhir.init: Perry lowers it into a factory closure. So for a large CJS bundle the outliner sees an essentially empty entry, does nothing, and the pathology #8595 was filed to remove is still there in full.Concretely:
typescript@5.9.3'slib/_tsc.js(5.9 MB, CJS) compiles into one 6.30 MB LLVM function.Evidence
Compiling
_tsc.json Perry 0.5.1579 (macOS arm64), the build reports:463,716 instructions in one function — the same shape and nearly the same magnitude as the ~439k-instruction
cli.jsentry quoted in #8595's own summary.From the linked binary's symbol table (
--report-size):_perry_closure_…__tsc_js__1_perry_closure_…__tsc_js__0_perry_closure_…__tsc_js__4801_…__tsc_js__init_body— what #8595 targets__perry_entry_chunk_*symbolsZero outlined chunks were produced. The module entry that the outliner does inspect is 0.88 MB; the body it needs to reach is the 6.30 MB closure.
Mechanism
crates/perry-codegen/src/codegen/entry_outline.rsoperates only onhir.init:And automatic admission requires a large top-level statement stream:
For a CJS module,
hir.initholds roughly one statement (invoke the factory, publishmodule.exports); the tens of thousands of real top-level statements live inside the factory closure. The confirming string is visible in the binary's own retained function source, which begins:So the admission test can never fire on CJS, no matter how large the module is.
Correction to the mechanism above
I described the location wrong, and it matters. I wrote that the CJS body "lives in
hir.functions". It does not.cjs_wrap::wrap_commonjs_for_targetwraps the body as text insidefunction __perry_cjs_factory() {...}, nested in an anonymous IIFE. Lowering represents that as aStmt::Letnaming anExpr::Closure, nested insidehir.init's own expression tree — not ahir.functionsentry, because it is lexically nested rather than a top-level declaration.hir.inititself keeps only the handful of wrapper statements, which is why admission never fires.The practical consequence: an implementation that goes looking in
hir.functionsis a silent no-op — it compiles, runs, and changes nothing. Verified by exactly that happening in a first attempt at #10603, caught only by compiling a real fixture and checkingnm, not by unit tests. Anyone working this should confirm withnmfor__perry_entry_chunk_*symbols rather than trusting a green suite.Why it matters beyond compile time
#8583 / #8883 / #8228 all bounded compile time for this shape. This issue is about what the surviving giant function costs in the shipped artifact:
tscbinary is 86.4 MB, against 20 MB for Static Hermes on the same input;./tsc-perry --noEmit demo.tson a two-line file takes 11 m 33 s (663.29s user, 96% cpu, quiesced box), against 0.78 s fornode tsc.js— a ~890x gap, while producing byte-identical, correct output and exit code 2.CJS is not a corner case for this: it is the shape of most large published bundles.
Suggested direction
Admit oversized function bodies, not just
hir.init— or, narrower and probably enough in practice, recognise the CJS factory closure as the logical module entry and route it through the existing chunking path. The globalization machinery #8595 reuses (emit_module_globals) is keyed on module-levellets, so a CJS factory'svars would need the equivalent treatment.Reproduction
Perry 0.5.1579, macOS arm64. Related: #8595 (closed, completed), #8583, #8883, #8228, #10574.