Chris Tate 057ac2cea2 Pin corpus coverage for the already-static destructuring surface
- 2530-2535 and 2541 lock in the destructuring forms the static tier already compiled but the corpus never pinned: object/array/tuple declarations (renames, defaults, holes, nesting, fresh rest tails), the options-object parameter pattern (element and whole-pattern defaults, tuple/rest parameter patterns), for-of heads (containers, defaults, rest, existing bindings), object rest freshness and key order, assignment basics (swaps, chains, expression value), and evaluation order (RHS once, elements left to right, lazy defaults that see earlier bindings).
- All byte-identical against Node under both backends, and each reports 100% static coverage.
2026-07-23 17:27:33 -05:00
2026-07-22 18:08:04 -05:00
2026-07-23 16:06:34 -05:00
2026-07-22 18:08:04 -05:00
2026-07-22 18:08:04 -05:00
2026-07-22 18:08:04 -05:00
2026-07-22 18:08:04 -05:00
2026-07-22 18:08:04 -05:00
2026-07-22 18:08:04 -05:00

scriptc

Zero-runtime TypeScript. scriptc compiles ordinary TypeScript into small, fast native executables — no Node, no V8, no JavaScript engine in the binary.

$ cat fib.ts
function fib(n: number): number {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(30));

$ scriptc run fib.ts
832040

$ scriptc build fib.ts && ls -la fib
-rwxr-xr-x  178K  fib        # a self-contained native binary, ~2ms startup

No changes to your code. No annotations, no dialect — the same TypeScript you run on Node, type-checked by the real TypeScript compiler and compiled to native. What compiles behaves byte-for-byte like Node.

Install

$ npm install -g scriptc

Requires clang (preinstalled with Xcode Command Line Tools). macOS arm64 is the primary platform; Linux and Windows binaries build by cross-compilation, each verified by its own differential test lane.

The idea: staticness you can see

Most TypeScript is far more static than the ecosystem assumes. scriptc decides, construct by construct, what can compile to native code — and tells you:

$ scriptc coverage app.ts

  statements analyzed   4481
  compile statically    4451  (99%)

  blockers:
      ×2  functions with optional parameters as values   SC1090
      ×1  Promise.reject                                 SC2020

Three tiers, always explicit:

  1. Compiled statically — native code, no engine. The default, and the only mode unless you opt out.
  2. Runs dynamically (--dynamic) — an embedded JavaScript engine (quickjs-ng, ~620KB) executes what can't be static: npm dependencies' shipped JS, any-typed code. Every value crossing back into static code is validated at runtime — a lying type throws a catchable TypeError instead of corrupting memory.
  3. Rejected — everything else fails with a specific error code, a code frame, and usually a rewrite hint. Nothing is ever silently miscompiled.

What compiles

The static surface covers the language and the standard library real programs use:

  • The language — classes with single inheritance and true dynamic dispatch (devirtualized when provably safe), closures with JS capture semantics, generics (monomorphized), discriminated unions as tagged values driven by TypeScript's own narrowing, async/await on stackful fibers with JS-exact scheduling, exceptions with finally, destructuring, spread, optional/default/rest parameters, getters/setters, iterators over strings/arrays/Maps/Sets, template literals, regular expressions (the engine is the same ECMAScript-exact bytecode interpreter QuickJS uses, linked only into regex-using binaries).
  • The standard library — strings with UTF-16-exact semantics, arrays/Maps/Sets with JS-exact ordering and identity, JSON with runtime-validated casts, Math, typed arrays and Buffer, Error hierarchies with typed catch.
  • Node's API surface — fs (sync and promises), path (byte-exact port), process, child_process with piped streams, os, crypto, url/URL, zlib, timers and signal handlers on a dependency-free event loop — and the server stack: net, http, https, tls (vendored mbedTLS), dgram, dns, fs.watch, readline. Real proxy servers compile.
  • fetch and the WHATWG web subset (streams, Headers, AbortSignal) over the same native net/TLS stack — redirects, gzip, AbortSignal.timeout, Node-shaped error causes; no libcurl, no system HTTP dependency.
  • npm dependencies (with --dynamic) — packages resolve with Node's own algorithm, typecheck against their shipped .d.ts, and their JS is embedded into the binary at build time. Binaries never read node_modules at runtime.

Programs typecheck against TypeScript's real es2025 lib (plus @types/node when your project has it), and your tsconfig.json governs checker strictness. Anything reached that has no lowering is a precise diagnostic, never a surprise.

Correctness

Two enforcement mechanisms run on every change:

  • Differential testing — every corpus program (800+ tests) runs under Node and as a native binary; stdout, stderr, and exit codes must match byte-for-byte. Number formatting is JS-exact (shortest-roundtrip, fuzz-verified against Node on a million doubles). Servers are tested with live client drivers against both implementations.
  • Memory-safety lane — the entire corpus re-runs under AddressSanitizer with a reference-count audit; leaks and use-after-free are build failures.

The deliberate divergences from Node (there are a few dozen, mostly around timing internals and error-object properties) are documented and numbered; nothing diverges silently.

Performance

Measured on Apple M-series against the same workloads in Node, Go, Rust, and Zig (all byte-identical output, verified):

dimension scriptc context
startup ~2.4ms Node: ~47ms; on par with Zig, ahead of Go/Rust
binary size 170–200KB static, ~3MB with --dynamic + embedded deps Go: ~2MB; Node SEA: 60–100MB
memory (RSS) 1–4MB typical Node: 67–116MB
runtime JS-faithful f64 semantics; competitive with the systems languages on most workloads integer inference and ownership analysis are on the roadmap

Escape hatches, honestly priced

  • comptime(() => ...) runs TypeScript at build time (in an isolated VM inside the compiler) and bakes the result into the binary as a literal.
  • --dynamic embeds the engine for npm deps and any code. scriptc coverage --dynamic reports exactly which statements run where and what the remaining blockers are. Static stays the default: a binary never silently grows an engine.
  • Checked casts — JSON.parse(...) as Config inserts a runtime validation that throws a catchable error naming the offending path (expected number at $.port, got string). TypeScript's as is a promise; scriptc verifies it.

Architecture

flowchart LR
    TS[TypeScript] -->|tsc: parse + typecheck| L[lowering]
    L --> IR[typed IR]
    IR --> C[C]
    C -->|clang| BIN[native executable]
  • packages/compiler — frontend (tsc API → IR), the IR with validator/serializer, the LLVM and C backends. The IR is the only interface between the ends; LLVM is the default code generator (with a transparent fallback for programs outside its tier), and C is the reference backend forever (readable, source-line-annotated output via --backend c).
  • packages/runtime — the C runtime: refcounted values with a cycle collector, stackful fibers and the event loop (kqueue), the server stack, JS-exact number formatting. Feature units are link-gated: binaries pay only for what they use.
  • packages/cli — scriptc build | run | coverage.

Development

$ pnpm install && pnpm build
$ pnpm test                      # differential corpus + diagnostics snapshots
$ SCRIPTC_SAN=1 pnpm test        # the same corpus under ASan + RC audit
$ pnpm scriptc build x.ts --emit-ir   # keep .scriptc/x.c and x.ir.json

Every feature lands with differential tests; both lanes green is the merge bar.

S
Description
GitHub Trending: vercel-labs/scriptc
Readme Apache-2.0
167 MiB
Languages
TypeScript 67.9%
C 24%
JavaScript 7.7%
C++ 0.3%