Pinned post
Perry Update: Compiling JavaScript/TypeScript is complicated!
How Perry compiles JavaScript and TypeScript to a native binary, and why the hard part isn't the code generation.
The pipeline (image):
1. SWC parses your .js/.ts and its dependencies from node_modules.
2. The AST becomes perry's HIR, a high-level intermediate representation.
3. HIR transforms: closure conversion, async/await lowering, inlining.
4. LLVM lowers the HIR to machine code for the target.
5. The system linker links the object files with perry's runtime (including a generational GC) and the native libraries the program needs.
The result is one self-contained executable. No JIT, no VM warm-up.
The hard part sits between steps 2 and 4. At every o.x and every f(a), the compiler must decide before the program runs: one load at a fixed offset, or the general property lookup (find the layout, search it, walk the prototype chain). A JIT watches the program and guesses; an AOT compiler has to know.
TypeScript types can't decide it. They can be wrong (as, any, stale .d.ts); they say what a value may be rather than where its fields live, and most npm code ships without them. Trust a wrong type, and you read the wrong slot. So the rule is: types pick the lowering, shapes prove the semantics. This is also something no other JS/TS AOT compiler does yet.
That's what perry's whole-program facts do (the lower half of the image):
- Per-unit facts: every module is cut into units, cycles of declarations, and each unit is analysed for object layouts and for what its functions read, keep, call, and change.
- Contracts: each unit publishes what others may rely on, e.g., "Circle has r at slot 0, and nothing adds keys to it".
- Census: one program-wide check of which layouts anything can break (delete, computed-key writes, stores through unknown values).
- Rounds: affected units re-solve until nothing changes.
Proven: p.x is one load at a fixed offset, and c.area() is a direct call behind a single class check. Not proven: the general lookup, which is always correct. Missing facts cost speed, never correctness.
Where it stands (2026-10-30), on the TypeScript compiler's own source: 20,028 sites proven per unit, up from 390 before 2026-10-25. tsc's checker serves as an independent oracle, never as input: its types pin down 42,313 sites, leaving a 22,865-site worklist. Still open: unit edges (Richards: 37 sites per unit vs 1,673 whole-program), stores into built-in prototypes, compile cost.
What compiles and matches node: the AWFY benchmarks, the TypeScript compiler, commander, fastify, qs, yargs, marked, effect and the 22-program js-compiled suite.
Small programs, like benchmarks, etc., work out exactly the way we need them to. The difficult piece is compiling entire programs, including their NPM dependencies. This is still ongoing, but progress is going well. :)
How Perry compiles JavaScript and TypeScript to a native binary, and why the hard part isn't the code generation.
The pipeline (image):
1. SWC parses your .js/.ts and its dependencies from node_modules.
2. The AST becomes perry's HIR, a high-level intermediate representation.
3. HIR transforms: closure conversion, async/await lowering, inlining.
4. LLVM lowers the HIR to machine code for the target.
5. The system linker links the object files with perry's runtime (including a generational GC) and the native libraries the program needs.
The result is one self-contained executable. No JIT, no VM warm-up.
The hard part sits between steps 2 and 4. At every o.x and every f(a), the compiler must decide before the program runs: one load at a fixed offset, or the general property lookup (find the layout, search it, walk the prototype chain). A JIT watches the program and guesses; an AOT compiler has to know.
TypeScript types can't decide it. They can be wrong (as, any, stale .d.ts); they say what a value may be rather than where its fields live, and most npm code ships without them. Trust a wrong type, and you read the wrong slot. So the rule is: types pick the lowering, shapes prove the semantics. This is also something no other JS/TS AOT compiler does yet.
That's what perry's whole-program facts do (the lower half of the image):
- Per-unit facts: every module is cut into units, cycles of declarations, and each unit is analysed for object layouts and for what its functions read, keep, call, and change.
- Contracts: each unit publishes what others may rely on, e.g., "Circle has r at slot 0, and nothing adds keys to it".
- Census: one program-wide check of which layouts anything can break (delete, computed-key writes, stores through unknown values).
- Rounds: affected units re-solve until nothing changes.
Proven: p.x is one load at a fixed offset, and c.area() is a direct call behind a single class check. Not proven: the general lookup, which is always correct. Missing facts cost speed, never correctness.
Where it stands (2026-10-30), on the TypeScript compiler's own source: 20,028 sites proven per unit, up from 390 before 2026-10-25. tsc's checker serves as an independent oracle, never as input: its types pin down 42,313 sites, leaving a 22,865-site worklist. Still open: unit edges (Richards: 37 sites per unit vs 1,673 whole-program), stores into built-in prototypes, compile cost.
What compiles and matches node: the AWFY benchmarks, the TypeScript compiler, commander, fastify, qs, yargs, marked, effect and the 22-program js-compiled suite.
Small programs, like benchmarks, etc., work out exactly the way we need them to. The difficult piece is compiling entire programs, including their NPM dependencies. This is still ongoing, but progress is going well. :)