How It Works
The pipeline
TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ──▶ LLVM IR ──clang──▶ native executable
└─────▶ C ────┘- Frontend — the real TypeScript compiler parses and type-checks your program against
es2025(plus@types/nodewhen your project has it), honoring yourtsconfig.jsonfor checker strictness. The frontend then lowers the checked AST into a typed intermediate representation, using tsc's own type and narrowing answers to drive every decision. A construct with no lowering is a precise diagnostic at this stage — never a miscompile later. - Typed IR — the only interface between the ends: a validated, serializable representation (
--emit-irwrites it as JSON). Types are concrete here; generics have been monomorphized, unions are tagged values, closures have explicit captures. - Backends — the LLVM backend is the default: it emits textual LLVM IR for most of the supported surface, and a program outside its tier falls back to the C backend transparently (one stderr note;
--backend llvmpins it and fails with a diagnostic instead). The C backend is the reference, forever: deliberately readable, source-line-annotated output (--backend c), kept next to the binary by default (--keep-c). Both are compiled by the same clang, and where they overlap they must produce byte-identical program output. - Link — the runtime is a C library of link-gated feature units: binaries pay only for what they use. A hello-world links nothing but libSystem; a regex-using program links the regex engine; an
httpserver links the net stack.
Inspect any stage yourself:
$ scriptc build fib.ts --emit-ir
$ ls .scriptc/
fib
fib.ir.json
fib.ll
$ scriptc build fib.ts --backend c --emit-ir
$ ls .scriptc/
fib
fib.c
fib.ir.jsonThe runtime
- Memory — values are reference-counted; an acyclic value is freed the moment its last reference drops. Reference cycles are collected at deterministic points by a cycle collector, not a concurrent GC. There are no GC pauses and no tracing heap.
- Concurrency —
async/awaitruns on stackful fibers with JS-exact scheduling: microtasks drain in the same order Node's do, timers fire in the same order, and the event loop (kqueue on macOS, epoll on Linux) has no external dependencies. - The server stack —
net,http,https,tls(vendored mbedTLS),dgram,dnsare native implementations on that same loop. - Numbers — JS-exact f64 semantics, including shortest-roundtrip number-to-string formatting fuzz-verified against Node's output.
- Regular expressions — the same ECMAScript-exact bytecode interpreter QuickJS uses, linked only into regex-using binaries.
The correctness story: differential testing
scriptc's correctness claim is not "we implemented the spec" — it is "we ran your semantics against Node's and they matched." Two lanes enforce it on every change:
- The differential corpus — every corpus program runs under Node and as a compiled binary; stdout, stderr, and exit codes must match byte-for-byte. Servers are exercised by live client drivers against both implementations. Number formatting was additionally fuzz-verified against Node's
String(x)on a million random doubles. - The memory-safety lane — the entire corpus re-runs under AddressSanitizer with a reference-count audit at exit; a leak or use-after-free anywhere is a build failure. The same lane is available for your programs as
scriptc build --sanitize.
Where matching Node byte-for-byte is impossible or deliberately not the goal (timing internals, error-object internals, aliasing at the dynamic boundary), the divergence is documented and numbered — see Limitations. Empty divergence space is load-bearing: the verified-identical list records what has been verified, not merely assumed.
The dynamic island
--dynamic embeds quickjs-ng for npm dependencies and any-typed code. Architecturally, the island is a second world with its own heap and its own microtask queue; the boundary between the worlds copies values and validates every dynamic → static crossing at runtime. Static code never trusts the island — a lying type is a catchable TypeError, not corruption.
Repository layout
| Package | What it is |
|---|---|
packages/compiler | The frontend (tsc API → IR), the typed IR with validator and serializer, the C and LLVM backends, the coverage analyzer. |
packages/runtime | The C runtime: refcounted values with a cycle collector, fibers and the event loop, the server stack, JS-exact number formatting, the island glue. |
packages/cli | scriptc build | run | coverage. |
The repository README covers the development workflow.