Language: English | 简体中文
xbintsc compiles a practical subset of TypeScript directly to native binaries.
It parses TypeScript itself, binds names, lowers the program to LLVM IR text,
and hands the IR to clang, which produces a standalone executable linked
against a small C runtime.
The design goals are:
- Incremental compilation — builds are content-addressed and skipped when nothing changed.
- Well-factored modules — lexer, parser, binder, codegen, driver, extensions each live in their own directory with their own tests.
- Extensive tests — per-module unit tests plus end-to-end tests that compile and run real binaries.
- Pluggable extensions — Node
fs, theguiHTML/CSS renderer , … are optional compile modules, not core code. - Self-hosting — the compiler compiles itself:
src/cli/main.tsbuilds into a nativexbintscbinary whose emitted LLVM IR is a fixpoint across generations. CI builds these binaries for every platform and a release attaches them. - Multi-platform — macOS, Linux and Windows on x64 and arm64 (Apple Silicon / AArch64), validated by GitHub Actions on native runners.
source.ts
│ lexer src/lexer
▼
tokens
│ parser src/parser → AST (src/ast)
▼
AST
│ binder src/binder → scopes, symbols, closures
▼
bound AST
│ codegen src/codegen → LLVM IR text (src/codegen/llvm.ts)
▼
module.ll ──clang──► module.o ──link──► executable
▲
runtime/ (C runtime, NaN-boxed values)
JavaScript values are a single 64-bit word (xt_value). Doubles are stored
unboxed; everything else is a tagged pointer: a 16-bit tag in the high bits
plus a 48-bit payload. The representation is defined once in
src/codegen/values.ts and runtime/rt.h.
Every compiled function uses the same ABI:
xt_value fn(xt_value env, int32_t argc, xt_value *argv);env threads captured variables (by reference, through boxes), so direct calls
and closure calls share one code path. JavaScript semantics that are awkward to
inline (+ coercion, property access, inspection) are delegated to @xt_*
runtime calls. Numeric arithmetic and comparison are the exception: when both
operands are doubles, codegen emits the hardware operation inline and only falls
back to the runtime for the coercing cases.
runtime/ implements strings, objects, arrays, closures, arithmetic,
comparison, exceptions and Node-like console.log inspection. The C code is
split by function across translation units (xt_alloc.c, xt_values.c,
xt_containers.c, xt_stdlib.c, xt_builtins.c, xt_io.c) sharing the
private runtime/rt_internal.h. Heap objects are allocated by xt_alloc and
reclaimed by a non-moving mark-sweep collector: roots are explicit value slots
(module globals, prototype caches), runtime root providers (the event loop,
the promise microtask queue) and a conservative scan of the active C stack.
Collection is armed automatically once the live heap crosses a threshold
(XT_GC_THRESHOLD pins it) and can also run on demand. After every collection the
threshold becomes twice the surviving heap (floor 1 MiB), so a program with a
small live set collects often and keeps a small resident set;
XT_GC_TRACE=1 prints one line per collection (heap and live-allocation counts,
the next trigger and the live set by object kind) to stderr.
Errors capture a JavaScript-level stack (function name, source file and a
function-level line) from a shadow call stack maintained by generated code; the
same frames are dumped on a fatal native signal. Function symbols keep their
source names (xt_fn_3_add), so native debuggers and crash reports are readable
too.
Before you start: check the per-platform prerequisites in Requirements. xbintsc does not bundle a compiler, so every platform needs a clang-compatible toolchain installed (macOS: Xcode Command Line Tools; Linux: clang+lld; Windows: Visual Studio C++ build tools with their bundled LLVM/MSVC).
Every GitHub Release attaches a
release archive per platform, plus a .sha256 checksum next to it:
| Platform | Archive |
|---|---|
| Windows x64 / arm64 | xbintsc-<version>-win32-x64.tar.zst / xbintsc-<version>-win32-arm64.tar.zst |
| Linux x64 / arm64 | xbintsc-<version>-linux-x64.tar.zst / xbintsc-<version>-linux-arm64.tar.zst |
| macOS x64 / arm64 (Apple Silicon) | xbintsc-<version>-darwin-x64.tar.zst / xbintsc-<version>-darwin-arm64.tar.zst |
(.tar.gz is used only when zstd is unavailable on the build machine.)
<version> is the release version (for example 0.3.8), embedded in the asset
name so downloads from different releases do not collide.
Each archive unpacks to a xbintsc-<os>-<arch>/ folder containing the compiler
(bin/xbintsc[.exe]) and the C runtime. It needs no Node.js, but you must
have the platform toolchain installed (see Requirements).
Unpack it and put bin/ on PATH (or call bin/xbintsc directly) — there is no
install step for xbintsc itself.
macOS / Linux example — here using xbintsc-<version>-darwin-arm64.tar.zst (swap in
-linux-x64, etc. for your platform):
# 1. Verify the checksum (optional but recommended)
shasum -a 256 -c xbintsc-<version>-darwin-arm64.tar.zst.sha256 # Linux: sha256sum -c
# 2. Unpack
tar -xf xbintsc-<version>-darwin-arm64.tar.zst
# 3. Compile and run hello.ts (below)
./xbintsc-darwin-arm64/bin/xbintsc run ./hello.ts
# 4. ...or emit a standalone binary
./xbintsc-darwin-arm64/bin/xbintsc build ./hello.ts --out ./build
./build/helloWindows example — install the Visual Studio C++ build tools (MSVC, MSBuild
and its bundled LLVM; see Requirements), then download
an archive from the
Releases page (here
xbintsc-<version>-win32-x64.tar.zst; use -arm64 on Windows on ARM) and run the
commands below in an x64 Native Tools Command Prompt for VS 2022 (Windows on
ARM: ARM64 Native Tools) — Windows 10+ ships tar:
# 1. Verify the checksum (optional but recommended)
(Get-FileHash .\xbintsc-<version>-win32-x64.tar.zst -Algorithm SHA256).Hash
Get-Content .\xbintsc-<version>-win32-x64.tar.zst.sha256
# 2. Unpack
tar -xf .\xbintsc-<version>-win32-x64.tar.zst
# 3. Compile and run hello.ts (below)
.\xbintsc-win32-x64\bin\xbintsc.exe run .\hello.ts
# 4. ...or emit a standalone hello.exe
.\xbintsc-win32-x64\bin\xbintsc.exe build .\hello.ts --out .\build
.\build\hello.exe// hello.ts
console.log("Hello from xbintsc!");Tip: add xbintsc-win32-x64\bin to PATH to call xbintsc instead of the full path.
The compiler is also published as the xbintsc npm package. This is the
shortest path for a Node project: install it as a dev dependency, then run it
through npx or an npm script.
npm install --save-dev --save-exact xbintsc
npx xbintsc doctor
npx xbintsc build src/main.ts --out buildIt needs Node.js 22+ and a clang 16+ toolchain. The install also brings the
matching @xbintsc/<os>-<arch> package with prebuilt runtime archives (core,
ext_node, and gui/media where the release builds them), so the first build
links them instead of compiling C. If npm skips that optional package, the
runtime is compiled from the shipped sources and cached in .xbintsc/. See
Using xbintsc from npm for a complete guide, CI setup
and caveats.
In a source checkout, run the TypeScript sources directly through tsx (or use
npm run xbintsc). The bin launcher also works here: without a dist/ build it
falls back to tsx automatically.
# Install dependencies
npm install
# Compile and run a program
npm run xbintsc -- run examples/hello.ts
# equivalently
npx tsx src/cli/main.ts run examples/hello.ts
# Produce a native binary
npx tsx src/cli/main.ts build examples/hello.ts --out build/examples
./build/examples/hello
# Inspect the generated LLVM IR
npx tsx src/cli/main.ts emit examples/hello.ts | head
# Use an optional extension (here, Node's fs via import)
npx tsx src/cli/main.ts run examples/node/read.ts --ext nodeCLI options:
-o, --output <path> Output path
--out <dir> Output directory (default: build/)
--emit <kind> exe | obj | ir (default: exe)
-O0..-O3 Optimization level (default: -O2)
--ext <names> Comma separated extensions (e.g. node)
--ext-native <m> Register a C++/Rust extension from a JSON manifest
--config <path> Use a project config (default: xbintsc.config.json)
--no-config Do not read any project config
--icon <path> Embed an application icon (PNG/ICO/ICNS)
--bundle macOS: also produce a .app bundle
--app-name <name> Bundle / display name
--app-id <id> macOS bundle identifier (e.g. com.example.demo)
--force Ignore the incremental cache
--verbose Print progress information
--time Print per-phase build timings
--debug Emit debug info (DWARF/CodeView) and keep frame pointers
--check Run the semantic checker (default)
--no-check Skip the semantic checker; only erase type syntax
Build options can be checked in as xbintsc.config.json, so xbintsc build
needs no arguments. All paths resolve against the config file's directory, and
any CLI flag overrides the matching config field:
{
"entry": "src/app.ts",
"outDir": "build",
"extensions": ["gui"],
"app": {
"name": "Demo",
"icon": "assets/app.png",
"bundle": true,
"bundleId": "com.example.demo"
}
}The icon is embedded into the binary at compile time (--icon / app.icon).
On Windows it becomes a PE resource (Explorer/taskbar icon); on macOS --bundle
lays out Demo.app with an AppIcon.icns; on every platform a gui program
sets its window/Dock icon from the embedded image at startup. See
doc/icon.md.
import { build, compileString } from "xbintsc";
const { ir } = compileString("console.log(1 + 1);");
const result = build("program.ts", { emit: "exe", outDir: "build" });xbintsc can also build native desktop applications whose UI is HTML/CSS. The
optional gui extension links a self-hosted, GPU-accelerated HTML/CSS renderer
(its own HTML parser, CSS cascade, layout and SDL_GPU compositor — no system
WebView, no bundled browser) into the executable and exposes it as an importable
module:
// app.ts
import { createWindow, run } from "gui";
// The .html asset loader AOT-compiles the document's scripts (see below)
import page from "./page.html";
const win = createWindow({ title: "Demo", width: 900, height: 600 });
win.on("click", (e: any) => console.log(e.x, e.y, e.target));
win.loadHTML(page);
run(); // main loop, until every window closes# Source checkout: build the engine archive once (fetches and builds SDL3,
# FreeType/HarfBuzz and glslang; default text uses the bundled stb_truetype), then run the bundled example
npm run gui
npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
# A release archive or an npm install already ships the engine:
xbintsc run app.ts --ext gui
# <video>/<audio> decoding is an optional link feature: the compiler sees the
# elements in the page and links the FFmpeg archive for that build only, so a
# GUI-only binary stays FFmpeg-free.
npm run media
xbintsc run examples/gui/media.ts --ext guiA document's <script> bodies are AOT-compiled by xbintsc into the binary —
inline scripts (a bare <script> is already treated as TypeScript) and external
<script src="./app.ts"> files alike — so there is still no JavaScript engine at
runtime. Element handles, DOM mutation, events and the rest of the scripting
surface are documented in doc/gui-scripts.md.
In a project, list the extension in xbintsc.config.json
("extensions": ["gui"]) and the flag becomes unnecessary, exactly as for
node. XT_GUI_AUTOCLOSE_MS=<ms> closes all windows after a delay, which is how
the end-to-end tests run the examples headlessly.
examples/gui/pelican-bike is a playable 2D game
whose document also opens unchanged in a browser.
Platforms and full details: doc/gui.md is the engine overview
(architecture and roadmap); the per-function documents live under
doc/gui/ — API, DOM/CSS, layout, rendering, media, input, animation,
runtime, build and known issues. Linux and macOS are built and exercised in CI;
their release archives and the optional @xbintsc/<os>-<arch> npm packages ship
the engine (gui.a) plus the optional media archive (media.a). Windows x64
gets the same pair (gui.lib through SDL_GPU's
Vulkan backend, and media.lib from FFmpeg compiled with clang-cl for the MSVC
ABI), built in CI with the Visual Studio MSVC + LLVM toolchain a local build
uses; Windows arm64 still skips them. Running the examples in CI stays
provisional because hosted runners have no GPU/Vulkan driver. Building
media.lib on Windows needs a POSIX shell and GNU make for FFmpeg's configure
step: Git for Windows supplies the shell, and make is a native GNU make
(Chocolatey). See doc/gui/build.md. Without the media archive
<video> and <audio> fire error.
The driver keys each build on the entry source hash, compiler version, options,
platform and the active extension set (src/driver/cache.ts). If every recorded
output still exists, the build returns immediately. The C runtime and extension
sources are compiled once and cached on the same principle.
An extension is a plain object (src/extensions/registry.ts). Three are bundled:
gui, a GPU HTML/CSS renderer
(see doc/gui.md); media, the optional FFmpeg-backed
<video>/<audio> decoder that requires gui; and node, itself split into
one folder per Node module, each pairing its exports with the C sources that
implement them:
src/extensions/node/ runtime/ext_node/
index.ts # nodeExtension fs/read_file.c fs/write_file.c fs/fs_ops.c
fs/index.ts path/path.c os/os.c process/process.c
fs/read-file.ts
path/index.ts
os/index.ts
process/index.ts
// src/extensions/node/index.ts
const modules: readonly NodeModule[] = [fsModule, pathModule, osModule, processModule];
export const nodeExtension: Extension = {
name: "node",
runtimeSources: () => [...new Set(modules.flatMap((m) => m.runtimeSources()))],
modules: () => Object.fromEntries(
modules.flatMap((m) => {
const entry = { namespace: m.namespace, exports: m.exports?.() ?? m.builtins() };
return [[m.name, entry], [`node:${m.name}`, entry]];
}),
),
};Registering it links the extra C sources and makes the Node APIs available to
import: import { readFileSync } from "fs" resolves to the C symbol with the
uniform (argc, argv) calling convention. path, os and process
additionally hook into namespace dispatch, so import path from "path" (or
import * as path from "path") makes path.join(...) and process.cwd() lower
to their runtime entries. Adding a module means dropping a folder under
src/extensions/node/ and its C counterpart under runtime/ext_node/; the core
compiler never changes.
Node module coverage:
Extensions do not have to be written in C or TypeScript. Any library that
exposes extern "C" entry points with the runtime ABI
(xt_value fn(int32_t argc, xt_value *argv)) can be linked in. Build the C++ or
Rust code into an object file or static archive, describe it with a small JSON
manifest, and pass the manifest to --ext-native:
./examples/extensions/cpp/build.sh
xbintsc run examples/extensions/cpp/demo.ts \
--ext-native examples/extensions/cpp/xbintsc.manifest.jsonimport { add } from "mathx" then lowers to the C++/Rust symbol exactly like a
C runtime binding. See examples/extensions/ for
working C++ and Rust projects, and runtime/xt_ext.h / runtime/xt_ext.rs for
the authoring helpers.
npm run typecheck # tsc --noEmit
npm test # unit + end-to-end (runs real binaries when clang is present)
npm run test:e2e # only the compile-and-run tests
npm run coverage # V8 coverage of the TypeScript compiler (src/)
npm run coverage:runtime # LLVM coverage of the C runtime (runtime/) via clangTests are organised by module under tests/ (lexer, parser, binder,
codegen, driver, extensions, cli, e2e).
V8 can only see the compiler's own sources, so npm run coverage:runtime
covers the C runtime with clang's source instrumentation instead: it exports
CCC_OVERRIDE_OPTIONS (every compile/link the driver performs gets
-fprofile-instr-generate -fcoverage-mapping), runs the suite, and reports
the collected profiles against runtime/ with llvm-cov. Pass
-- --report-only to only re-report profiles from an earlier instrumented run,
or -- --threshold 60 to fail below a coverage bar. CI runs the report with
--threshold 80, so a runtime-coverage regression fails the job. The instrumented objects
live in xbintsc_CACHE_DIR, a separate cache from .xbintsc, because the
object cache key does not include compiler flags: sharing one cache would
silently reuse un-instrumented objects. It also sets xbintsc_PREFER_PREBUILT=0
so the runtime is compiled from source rather than linked from an
un-instrumented runtime/lib archive. Requires llvm-profdata/llvm-cov
matching the resolved clang; the script
skips with a warning when they are missing.
See Requirements for the full, per-platform list.
- Node.js 22+ — to run the compiler from npm or from source; released binaries are standalone
- macOS: the Xcode Command Line Tools (
xcode-select --install) - Windows: the Visual Studio C++ build tools (MSVC, MSBuild and the bundled
LLVM;
scripts/setup-vs-toolchain.ps1imports that exact toolchain) - Linux:
clangandlldfrom your distribution - To build a binary yourself: a
clang-compatible compiler onPATH(override withxbintsc_CLANG)
Check your environment with xbintsc doctor.
- Implemented features
- Unimplemented features
- Support matrix — generated from the executable test262 corpus (unmodified test262 cases, compiled and run)
- Application icons & project config
The compiler is strict about its language subset — types are structurally
checked by a default checker and then erased (definite mismatches, missing
properties and non-callable calls are rejected, but there is no full inference
or strict-null mode), Node built-ins need --ext node, packages from
node_modules are bundled at build time, strings follow UTF-16
length/indexing over UTF-8 storage, and await only suspends on a pending
promise. An agent that assumes "TypeScript" means "what
Node accepts" will write code that does not compile, so the documentation is
layered for exactly that reader:
- AGENTS.md — the entry point for an agent working in this repository: what xbintsc is, the repository map, the build gate and the rules.
- doc/ai/ — the task-oriented guide for using the compiler, split by task: build recipe, CLI and config, language support, extensions, troubleshooting, contributing. Read the page for the task, not the whole directory. Chinese: doc/ai/zh-CN/.
- llms.txt — a machine-readable index of every document, for a tool that wants one fetch instead of a checkout.
Failures point the way themselves: a compiler error ends with a
hint: … — see <document> line naming the page that explains the fix. Pass
--no-hints for plain output.
{ "name": "mathx-cpp", "objects": ["build/mathx.o"], "linkerFlagsByPlatform": { "linux": ["-lstdc++", "-lm"], "darwin": ["-lc++"], "win32": ["-lmsvcprt"] }, "builtins": { "cppClamp": { "symbol": "mathx_clamp" } }, "modules": { "mathx": { "exports": { "add": { "symbol": "mathx_add" } } } } }