XS: A 2.9 MB Binary That Is an Entire Programming Toolchain
Every time I pick up a new language, there’s a setup phase where I’m not writing programs - I’m installing things. A build system here, a formatter over there, a package manager that somehow requires a specific version of itself to bootstrap. By the time I’ve typed println("hello"), an hour has evaporated into configuration files I’ll never think about again.
XS has a different idea about all of that.
The story in one sentence
XS is a general-purpose language where one 2.9 MB binary contains the compiler, language server, debugger, formatter, linter, test runner, profiler, and package manager - and the same source compiles to native machine code, JavaScript, or WebAssembly, running unchanged on Linux, macOS, Windows, iOS, Android, ESP32, and Raspberry Pi.
That is not a feature roadmap. It is what ships at v1.2.24.
curl -fsSL xslang.org/install | sh
xs --version
xs fmt # formatter
xs test # test runner
xs debug # debugger
xs upgrade # self-updater
No separate npm install -g eslint. No pip install black. No apt install gdb. One binary, no version conflicts, everything already there.
Why the design is interesting
The language itself is deliberately unpretentious. Types are optional, and optional really means optional - the type checker only activates on annotated code:
{- classic recursive fib, no types needed -}
fn fib(n) {
if n <= 1 { return n }
return fib(n - 1) + fib(n - 2)
}
println(fib(10))
Add annotations when complexity demands enforcement:
fn fib(n: int) -> int {
if n <= 1 { return n }
return fib(n - 1) + fib(n - 2)
}
Untyped code passes through without complaint. There’s also a @memoize decorator for pure functions, pattern matching with exhaustiveness checking, and algebraic effects - a less common feature where you declare a side requirement as a named effect and let the caller decide how to handle it:
effect Ask {
fn prompt(msg) -> str
}
fn greet() {
let name = perform Ask.prompt("name?")
return "Hello, {name}!"
}
let result = handle greet() {
Ask.prompt(msg) => resume("World")
}
println(result)
Algebraic effects in a general-purpose language is genuinely uncommon. Most languages either avoid them entirely or bolt them on as a library.
One detail worth flagging early: the bytecode VM holds a global interpreter lock during dispatch, releasing it around sleep, I/O, and channel receives. Same model as CPython. Two pure-compute threads take turns rather than running in parallel. It’s a known tradeoff and at least it’s documented honestly.
Six backends, one source file
The backend count is where XS gets unusual:
| Backend | How to invoke | Notes |
|---|---|---|
| Bytecode VM | xs (default) | Normal runs |
| JIT | xs --jit | x86-64 + aarch64 only |
| Tree-walk interpreter | xs --interp | REPL, AST-level debugging |
| C transpiler | xs --emit c | Self-contained C, any compiler |
| JS transpiler | xs --emit js | Node or browser, smaller than WASM |
| WASM | xs.wasm | Browser runtime |
Benchmark for fib(30) on Linux x86-64 (best of three, cold from disk):
| JIT | VM | Interpreter |
|---|---|---|
| 31 ms | 62 ms | 138 ms |
The correctness discipline is worth noting: the tree-walk interpreter and the bytecode VM are diffed against each other on every commit. If they produce different results, the build fails - even if each backend separately passes all tests. For a project built and maintained by one person, that is a surprisingly tight quality bar.
The whole thing builds from 132 KLOC of C source with gcc or clang and GNU make. No other build or runtime dependencies. The HTTPS client is handled by an embedded BearSSL tree rather than linking against the system OpenSSL.
What HN is actually arguing about
This submission reached 11 points and five comments. Not a front-page hit. But the two substantive comments go straight to the questions any serious evaluator would ask.
One commenter noted that @memoize is an interesting design choice, then asked: does it imply immutability? The reasoning is sound - if a function can be memoized safely, is that because its inputs are immutable, or because the language guarantees referential transparency in some other way? The decorator ships but the docs don’t spell out the invariant.
The sharper question came from a second commenter: “Does it have destructors? Is it memory-safe?”
The documentation is silent on this. There is no explicit memory model, no ownership system, no garbage collector description - not in the introduction, not in the reference. For a language that targets ESP32 (a microcontroller with 520 KB of RAM and no virtual memory), that silence is significant. Either the answer is tucked somewhere in the reference I haven’t found, or it is a question that a future version will have to answer directly. The C source is there to read, but relying on source archaeology to understand memory behavior is not a good sign for the docs.
Should you look at it?
| Read it if… | Skip it if… |
|---|---|
| You want a batteries-included scripting language that deploys to any platform with a CPU | You need memory safety guarantees before you ship |
| You enjoy reading opinionated language implementations | You need an established package ecosystem today |
| You target embedded hardware and want one tool that does everything | Your organization needs a language with a ten-year track record |
XS is a serious solo project that knows what it wants to be. The toolchain compactness is genuine. The language design has real ideas - algebraic effects, optional typing, six coherent backends from one source. Whether the depth is there for large codebases, and whether the memory model is sound for constrained hardware, are questions that 5 HN comments cannot answer.
The playground runs in a browser at xslang.org/playground. Five minutes of trying it will tell you more than this post can.
Discussion on Hacker News · Submitted by xs-lang
Hoang Yell
A software developer and technical storyteller. I read Hacker News every day and retell the best stories here — in English and Vietnamese — for curious people who don't have time to scroll.