Documentation content

How It Works

This page describes how scriptc turns a TypeScript program into an executable: the compiler stages, the outputs you can inspect at each stage, the native runtime that executables link, and how the compiler is tested.

Compiler pipeline

TypeScript / JavaScript
         |
         v
TypeScript parser and type checker
         |
         v
Lowering and typed IR ----> serialized IR (--emit=ir)
         |
         v
LLVM IR -----------------> LLVM source (--emit=llvm)
         |
         v
LLVM helper -------------> assembly / object (--emit=asm|obj)
         |
         v
Runtime pack and linker -> executable
  1. Frontend: TypeScript parses and checks the program against es2025, scriptc's ambient declarations, and available project declarations. tsconfig.json controls checker strictness. The compiler uses the checked types and narrowing information to lower supported operations; unsupported operations produce diagnostics.
  2. Typed IR: A validated, serializable intermediate representation connects the frontend and backend. Types are concrete: generic functions are specialized, unions use tags, and closures have explicit captures. --emit=ir writes this representation as JSON.
  3. LLVM backend: --emit=llvm writes textual LLVM IR. The matching LLVM 22 helper generates assembly or objects on supported macOS, Linux, and Windows hosts, including output targeting WASI. Executable and library builds use this backend. Large executables, native and WASI alike, are divided into partitions that are optimized and compiled in parallel; optimized builds summarize every partition first so small functions can still be inlined across partitions. The partition count depends only on the program, so output is the same on every host, and a rebuild reuses the compiled partitions that an edit does not affect.
  4. Linking: Runtime packs contain the precompiled C runtime and vendored libraries. A hashed manifest selects the artifacts the program needs for its features, optimization mode, and (for libraries) state model. The target linker and SDK or sysroot produce the executable. Sanitizer builds use an instrumented C runtime.

comptime calls run during the frontend stage in an isolated evaluator inside the compiler, based on QuickJS. Their results are embedded in the program as constants.

Inspect compiler output

The following examples use this program:

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

Select an output stage with --emit:

$ scriptc build fib.ts --emit=ir >/dev/null
$ ls .scriptc/
fib.ir.json
$ scriptc build fib.ts --emit=llvm >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
$ scriptc build fib.ts --emit=asm >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
fib.s
$ scriptc build fib.ts --emit=obj >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
fib.o
fib.s

Without -o, artifacts are written to .scriptc/ next to the input. Different output kinds accumulate in that directory; rebuilding a kind updates its file.

External linking

Program objects define main and leave selected scr_* runtime functions undefined. A strong reference to scr_runtime_abi_v21 checks runtime compatibility at link time.

--print=native-link-info emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See Native Program Objects for examples.

Native runtime

  • Memory: Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with --sanitize, and processes run with SCRIPTC_EXIT_FREE=1 (for external leak checkers), free everything at exit.
  • Concurrency: async/await uses stackful fibers. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux.
  • Networking: Supported net, http, https, tls, dgram, and dns APIs use native implementations. TLS uses vendored mbedTLS.
  • Numbers: Values use JavaScript's double-precision representation. Number-to-string formatting uses shortest-roundtrip output and is tested against Node.js.
  • Regular expressions and Unicode: The runtime uses QuickJS's regular-expression and Unicode libraries, linked into programs that use them.

See Limitations for runtime differences and the Node.js compatibility reference for API support.

Testing

The test suite runs each test program under Node.js and as a compiled executable, and compares stdout, stderr, and exit codes. Server tests drive both implementations with the same clients. Number formatting is also fuzz-tested against Node.js's String(x).

The same tests also run with AddressSanitizer and a reference-count audit at exit, to catch leaks and memory errors. You can enable these checks in your own executable builds with --sanitize.

Test coverage provides evidence for the cases exercised. It does not establish complete compatibility with Node.js.

Repository layout

PathPurpose
packages/compilerFrontend, typed IR, validation, serialization, LLVM backend, native-helper integration, and coverage analysis.
native/llvm-codegenLLVM 22 helper for verification, optimization, assembly, and object output.
packages/runtimeC runtime, reference counting and cycle collection, fibers, event loop, networking, and the Node.js API implementations.
packages/runtime-*Runtime object packs and manifests for individual targets.
packages/cliCLI commands, including build, run, and coverage.

See the repository README for the development workflow.