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
- Frontend: TypeScript parses and checks the program against
es2025, scriptc's ambient declarations, and available project declarations.tsconfig.jsoncontrols checker strictness. The compiler uses the checked types and narrowing information to lower supported operations; unsupported operations produce diagnostics. - 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=irwrites this representation as JSON. - LLVM backend:
--emit=llvmwrites 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. - 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:
Select an output stage with --emit:
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 withSCRIPTC_EXIT_FREE=1(for external leak checkers), free everything at exit. - Concurrency:
async/awaituses 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, anddnsAPIs 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
| Path | Purpose |
|---|---|
packages/compiler | Frontend, typed IR, validation, serialization, LLVM backend, native-helper integration, and coverage analysis. |
native/llvm-codegen | LLVM 22 helper for verification, optimization, assembly, and object output. |
packages/runtime | C 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/cli | CLI commands, including build, run, and coverage. |
See the repository README for the development workflow.