Execution backends

Four ways to run XS: the bytecode VM (default), the tree-walker interpreter, the JIT, and transpilation to JS or C.

Summary

The VM is the default and the production target; it is a few times faster than the interpreter on every workload. The interpreter (--interp) is reserved for the REPL and for plugins that need AST-level runtime hooks. Both backends are run against the same test suite on every commit and their outputs are diff'd; a divergence fails the test even when each backend passes on its own. The JIT (--jit) is a single register-allocating tier targeting x86-64 and aarch64; protos with unsupported opcodes fall back to the VM. xs build compiles to.xsc bytecode for distribution without the source. WASM: build with make wasm; the binary is ~650KB and runs in any WASI runtime or the browser playground. Transpilers: --emit js targets browsers and Node.js; --emit c produces compilable C.

Canonical

xs script.xs                     -- bytecode VM (default)
xs --interp script.xs            -- tree-walker (use for plugin debugging)
xs --jit script.xs               -- JIT (x86-64 and aarch64)
xs build script.xs               -- compile to bytecode (.xsc)
xs run script.xsc                -- run compiled bytecode

The VM and interpreter produce identical results for correct programs. The VM is the default because it's a few times faster on every workload the interpreter handles; the interpreter exists for AST-level plugin hooks and for debugging the interpreter itself.

The JIT is a single register-allocating tier. Bytecode is lowered to a small linear IR, per-block liveness is computed, three callee-saved registers hold vregs via linear-scan allocation, and a per-arch code generator emits native code with SMI fast paths for integer arithmetic/compares, an XMM fast path for boxed floats, an inlined monomorphic LOAD_GLOBAL cache, inline closure upvalue access, and a refcount-pair elimination peephole. Protos that step outside the supported opcode set drop to the bytecode VM -- there is no template-JIT middle tier. See STATUS.md for the supported opcodes and measured numbers.

The build command compiles to a .xsc file that can be distributed and run without the source.

WebAssembly (WASM)

XS can be compiled to WebAssembly using wasi-sdk, allowing the full interpreter to run in the browser or any WASM runtime.

make wasm    # produces xs.wasm

The WASM build includes the interpreter, VM, semantic analysis, type checking, effects, pattern matching, generators, closures, enums, structs, classes, duration literals, temporal primitives, and the JS/C transpiler. Output matches the native binary on the suites that don't require networking, real filesystem access, signals, or native plugins.

Not available in WASM:

  • Networking (HTTP, sockets, TLS) - no raw sockets in browser
  • File system - uses a custom virtual FS in the browser, not real disk
  • Native plugins (.so/.dll) - no dlopen in WASM
  • JIT compilation - can't map executable memory
  • REPL - needs terminal stdin
  • LSP/DAP - needs stdio/socket communication
  • Profiler sampling (SIGPROF) - no signals in WASM
  • input() - no stdin in browser context

The WASM binary is ~650KB and loads in under a second. It powers the playground at xslang.org/playground.

Transpilation

xs --emit js script.xs     # transpile to JavaScript
xs --emit c script.xs      # transpile to C

The JS transpiler maps XS constructs to idiomatic JavaScript: classes become JS classes, pattern matching becomes if/else chains, channels become array-backed queues, and builtins like str(), len(), type() map to their JS equivalents. The output runs in any browser or Node.js.