SXN is a standalone QuickJS-based runtime for .sx systems code and ordinary
JavaScript. SxfeScript adds explicit mutation, affine values, borrows, and
erasable TypeScript-style annotations without a Vite or AOT build step.
This repository is intentionally independent from Rayact. Its QuickJS source
was a direct snapshot of Rayact's customized fork at commit 66f4965, and has
since diverged under its own name, ArcSX (see
third_party/QUICKJS-PROVENANCE.md for the full lineage).
A pitch-and-explainer site for both -- SxfeScript against TypeScript, what
ArcSX actually runs, and what's open for debate versus fixed -- lives at
sxfescript.github.io, built from
docs/ and published from a separate repo,
SxfeScript/sxfescript.github.io
(scripts/publish-docs.sh). Contributions, including disagreement with the
current design, are welcome -- see CONTRIBUTING.md for
what's genuinely open and the one constraint that isn't (no JIT, for mobile).
sxfescript.github.io/docs is every
one of these markdown files rendered as a browsable site, generated from this
repo by scripts/publish-docs.sh so a spec edit is a docs edit. Start with
the quick start, or the
examples if you'd rather read
code first. There is an llms.txt
index and a single-file
llms-full.txt for tooling.
Everything past what's here -- the language, the ABI, the runtime and Node
surfaces, native calling, bytecode, and the full performance write-up behind
the two tables below -- lives in spec/; see that directory's own
files for each topic.
Complete programs that run as-is are in examples/, all of them
.sx:
| File | What it shows |
|---|---|
hello.sx |
Erasable types, let mut, and an &mut borrow |
velocity.sx |
A primitive-only interface as a fixed-layout struct |
server.sx |
Sxn.serve with Request/Response routing and a JSON body |
fetch.sx |
fetch, then the same response read as a stream |
files.sx |
Sxn.file/Sxn.write, and node:fs over the same file |
ffi.sx |
Calling a C function through Sxn.ffi |
macOS/Linux (arm64 or x64):
curl -fsSL https://sxfescript.github.io/latest/install.sh | bashWindows (arm64 or x64):
irm https://sxfescript.github.io/latest/install.ps1 | iexBoth install to ~/.sxn/bin (%USERPROFILE%\.sxn\bin on Windows) and add it
to your PATH. Swap latest for a version tag (v0.0.2) in either URL to pin
a specific release instead of always getting the newest one.
The 0.0.2 pre-release includes binaries for macOS arm64/x64, Linux arm64/x64,
and Windows arm64/x64. The release builds are documented in llm.txt
and the reproducible packaging entry point is scripts/release.sh.
Needs OpenSSL, libcurl, libuv, zlib, and libffi on the system (brew install openssl curl libuv zlib libffi on macOS; apt install libssl-dev libcurl4-openssl-dev libuv1-dev zlib1g-dev libffi-dev on Debian/Ubuntu). CMake finds all five and
fails clearly, naming the missing one, if any aren't there.
cmake --preset debug
cmake --build --preset debug
ctest --preset debugRun the example:
./build/debug/sxn examples/velocity.sxRun the tests against a Debug build. QuickJS gates its leak tracking on
#ifndef NDEBUG (ENABLE_DUMPS in third_party/quickjs/quickjs.c), so in a
Release build the sxn-leak-check test still runs but has nothing to detect
and always passes. A Debug build is what actually catches a leaked atom,
object or string -- an atom leak in the node:* layer sat unnoticed behind a
green Release run until it aborted the first Debug one.
The repository contains a working QuickJS-backed CLI, an in-memory .sx
frontend, fixed-layout arena primitives, package workflow commands, an LSP
transport, VS Code language packaging, specifications, and tests. The native
opcode lowering, full control-flow ownership pass, native npm registry backend,
and semantic LSP features are tracked in spec/IMPLEMENTATION.md and are not
yet represented as complete production implementations.
Two things the annotations do today rather than being stripped. &mut requires
a let mut owner, so borrowing an immutable binding is a compile error naming
SX2003 -- the one ownership rule enforced ahead of that control-flow pass. And
the declared type reaches codegen: safe let mut n: i32 wraps at the 32-bit
boundary where every other annotation keeps exact JavaScript arithmetic, and a
small function with a fully declared scalar signature is inlined into its
caller, worth 3.1x on a two-argument add. spec/PERFORMANCE.md has the
measurements.
Two documents cover what actually runs, and split the same way the codebase does:
spec/RUNTIME.md-- the WinterTC web APIs and theSxnhost namespace:fetch,Sxn.serve(HTTP, SSE, WebSocket upgrade), Web Streams,URLPattern, Web Crypto,structuredClone,Sxn.memoryUsage()andSxn.gc(), andSxn.ffifor calling a C function directly. Every name in the Minimum Common API is there except WebAssembly's. This is the half that travels when the engine is embedded elsewhere, and the only half a mobile build needs.spec/NODE.md-- what makessxnusable as a Node alternative: CommonJS, a superset of thenode:builtins, and.nodenative-addon loading over 120 Node-API entry points. This half exists to emulate Node and nothing else, so a build with no Node surface drops it and loses nothing on the runtime side.
spec/NATIVE.md is the design note behind that split, written against a
concrete question: when this engine is folded into Rayact, which of Sxn.ffi
and .node-addon loading goes with it. (Answer: Sxn.ffi, because Rayact
already loads native code in its engine core on every platform including
mobile, and has no Node layer to put an addon loader in.)
A third document, spec/BYTECODE.md, covers .sxbc: sxn compile app.sx produces bytecode for distribution (--strip drops the compiling
machine's own paths from it), sxn --compile-cache app.sx compiles once and
reuses the result on later launches, and sxn app.sxbc runs either one
directly. Real, measured gains -- see that document for the numbers -- and
proportional to how much there is to parse: noticeable on a large file,
negligible on a one-liner.
benchmarks/wintertc/run.sh runs matched WinterTC-style workloads against
sxn, Node and Bun side by side. No category is
hidden -- the others win the ones you'd expect them to. Each runtime runs the
same workload with the same iteration counts, written in that runtime's
idiomatic form (Bun.serve/Bun.env for Bun, Sxn.serve for sxn); Buffer,
TextEncoder and EventEmitter are the APIs under test and are the same in all
three. Bun is optional -- its rows are skipped with a note if it isn't
installed.
sh benchmarks/wintertc/run.shFor performance measurements, use the optimized binary explicitly; the script accepts any SXN path. For example:
RUNS=1000 SXN=build/release/sxn sh benchmarks/wintertc/run.shKeep Debug for leak and correctness checks; Release is the appropriate binary for throughput, startup, and pause timing.
Everything below was measured on both, because a single machine can flatter a runtime and neither of these is neutral: the Mac is the faster chip but a working laptop under load, and the Linux box is slower per core but idle.
| Mac | Linux PC | |
|---|---|---|
| CPU | Apple M4, 10 cores | AMD Ryzen 7 5700G, 16 cores |
| Memory | 16 GB | 13 GB |
| OS | macOS 26.6.2 (arm64) | Ubuntu 23.10, kernel 6.5.0-44 (x86_64) |
| Compiler | Apple clang | gcc 13.2 |
| Node | v25.2.1 | v23.11.1 |
| Bun | 1.2.17 | 1.2.21 |
| Load while measuring | 2-5 | 0.4-1.2 |
Read each machine's table against itself, never across the two. Both now run
the same major Node; the Linux box's Bun is a few patches ahead. What still
differs is the kernel: performance.now costs far more per call there, which
is why its pause totals read in seconds for all three runtimes. Same tree,
same tests, same 95 fixtures passing on both.
How each row is measured: throughput rows are the harness's own 1,000-run medians. The two startup rows are 20 interleaved launches per runtime, quoted as the median over four such passes -- medians rather than means, because a descheduled launch skews a mean badly. Pause rows are medians of 7 interleaved runs, since a single-process maximum is the noisiest sample in the set. Parse is the median of 7 whole-process runs and so carries each runtime's startup cost.
| Category | sxn | Node | Bun | Winner |
|---|---|---|---|---|
| Real-world end-to-end task | 8.4 ms | 76.6 ms | 15.6 ms | sxn |
| Cold start | 7.5 ms | 42.5 ms | 9.4 ms | sxn |
| Sustained throughput: Buffer ops | 19.4 ms | 24.5 ms | 27.1 ms | sxn |
| Sustained throughput: TextEncoder | 4.7 ms | 39.8 ms | 6.2 ms | sxn |
| Sustained throughput: EventEmitter | 6.7 ms | 5.4 ms | 9.2 ms | Node |
| Sustained throughput: JSON round trip | 48.0 ms | 29.3 ms | 24.8 ms | Bun |
| Pause consistency: total time | 146.6 ms | 241.7 ms | 277.0 ms | sxn |
| Pause consistency: worst single pause | 0.01 ms | 0.28 ms | 3.13 ms | sxn |
| Parse 32k-line generated file | 20.1 ms | 49.9 ms | 25.6 ms | sxn |
Seven of nine. The two that are not sxn's are the two worth reading: a JIT
inlines an EventEmitter call to nothing, and an ablation that skips this
interpreter's fused-call guards entirely still only reaches 4.7 ms, because
roughly a third of that row is loop dispatch. JSON is a megabyte parsed and
written back forty times, and the gap there is the same story with more code
in it -- JSON.parse is C in all three, but what surrounds it is not.
| Category | sxn | Node | Bun | Winner |
|---|---|---|---|---|
| Real-world end-to-end task | 7.5 ms | 56.7 ms | 22.4 ms | sxn |
| Cold start | 7.4 ms | 23.1 ms | 13.3 ms | sxn |
| Sustained throughput: Buffer ops | 38.0 ms | 39.2 ms | 83.2 ms | sxn |
| Sustained throughput: TextEncoder | 9.1 ms | 80.6 ms | 18.0 ms | sxn |
| Sustained throughput: EventEmitter | 15.9 ms | 10.1 ms | 25.2 ms | Node |
| Sustained throughput: JSON round trip | 82.9 ms | 113.5 ms | 60.3 ms | Bun |
| Pause consistency: total time | 2855.9 ms | 3295.3 ms | 3252.4 ms | sxn |
| Pause consistency: worst single pause | 0.25 ms | 1.72 ms | 6.48 ms | sxn |
| Parse 32k-line generated file | 36.6 ms | 51.5 ms | 54.2 ms | sxn |
Seven of nine again, and the same two are not sxn's, which is the useful part: two machines, two chips, two operating systems, and the shape of the result does not move. Buffer is the one row where Node is close here rather than behind, and JSON is closer than it is on the Mac -- against this Node, sxn takes JSON while Bun keeps it.
The full write-up -- pause-row detail, the no-JIT tradeoff, every
optimization behind these numbers in the order it landed, and what's still
open -- is in spec/PERFORMANCE.md.