Xo is experimental. The source is now public on GitHub, or try Xo in your browser.

Xo 0.27 release notes

released 2026-10-23 · tag v0.27.0 · all releases

Downloads: Xo 0.27.0 on GitHub, with archives for macOS (Apple silicon and Intel, signed and notarized), Linux (arm64 and x86-64), and Windows (x86-64, Authenticode signed), and a SHA256SUMS file. Getting started shows how to install one.

Xo against Go

Each release is measured against Go on the same Mac (bench/results/release-vs-go-v0.27.0.md, lines 11 to 24):

  • Run time: Xo is faster on 14 of 20 workloads, within noise on 2, and slower on 4 (conc rwmutex 3.58 times Go’s wall time, llvm nbody 1.57, conc spawn 1.20, conc pmap 1.07).
  • Peak memory of the run: lower than Go’s on all 20 (0.38 to 0.99).
  • Binary size: 1.85 to 1.94 MB against Go’s 2.39 to 2.43 MB.
  • Compile: cold builds 1.8 to 2.3 s against Go’s 1.6 to 1.9 s; a warm build of an unchanged program, 0.27 to 0.34 s against 0.04 s (0.42 to 0.64 s in 0.26.0), is still the largest gap, and xo’s build now peaks at 0.49 to 0.50 GB cold (0.76 to 0.89 GB in 0.26.0).

Breaking changes

  1. A third-party use go needs a pin (decision 0356). A use go of a package outside Go’s standard library needs require go <module> <version> in xo.mod; without it the use is error XO1319. Before, the build followed the module’s latest version and recorded it only in the git-ignored .xo/, so two machines could build different code from the same commit. xo fix adds the line with the version the binding resolved, and xo mod tidy adds missing pins. xo.lock now records each pinned Go module’s go.sum hashes (go <module> <version> h1:...): a changed module is XO1304, a missing line XO1305 (run xo mod tidy). Go’s standard library packages need nothing.
  2. std’s errors print plain messages (decision 0358). Display of std’s error enums is a sentence, as Go’s error strings are: PATH: no such file or directory, connection refused, no rows in result set, instead of nested Debug output such as Conn(msg: "Io(msg: \"tls: ...\")"). This covers FsErr, NetErr, IoErr, EnvErr, ProcErr, ClockErr, SqlErr, HttpErr, ClientErr, UrlErr, CookieErr, TlsErr, SmtpErr and TemplateErr. ${e:?} (Debug) is unchanged. A program or test that compares the printed text changes with it.

New

  • --release is a preset (decision 0354). Each setting it changes has its own flag in any build (xo build, run, test), which wins over the preset whatever the order: --debug-contracts, --dbg, --error-args, --debug-info=on|off|split, --profile-support, --trace-support (on|off, a bare flag is on) and --opt=O0..O3 (LLVM). --error-args=off no longer needs --release.
  • --debug-info=split (decision 0355) keeps the debug information beside the program: PROG.dSYM on macOS, PROG.debug on Linux, found by tracebacks, lldb, gdb and addr2line.
  • Build information in every binary. xo version -m BINARY prints what built it, as go version -m does: the xo, backend, target, every effective setting, the stamps and the sources’ hashes. A native crash report names the build on its second line.
  • Deterministic scheduling on the native backends (decision 0350): xo run --seed=N, xo build --seed=N, and xo test --seed=N / --explore=N run every task on one thread with a seeded choice at each scheduling point, so a seed reproduces an interleaving. --explore prints the command that reproduces a failure.
  • XO_PROF_HEAP_AT=TEXT (std/debug) writes the heap in use when a labeled point is reached.

Faster

  • xo check is about 2.5 times faster on a warm run (xo itself 1.8 s to 0.67 s, a small server 0.59 s to 0.26 s) and peaks a third lower in memory: an unchanged module is skipped and its diagnostics replayed, and the native runtime’s bodies are checked as a build checks them. xo check --support is 2.3 to 2.6 times faster (3.7 s to 1.6 s on xo itself). The output is unchanged; XO_CHK_CACHE=off checks everything.
  • Warm builds: the module check cache and a shared cache pack are on by default (a warm build of xo itself 0.885 of the wall time; a new program’s first build 0.831), front end cache keys that are never stored are no longer looked up, and std and runtime function bodies are parsed only when needed (a small program’s warm build peaks about 40% lower in memory). XO_CHK_CACHE=off, XO_SHARED_PACK=off and XO_LAZY_PARSE=off turn them off.
  • Runtime: channels of one-word values (integers, Float, Bool, enums without payloads) send the word instead of a one-element list (ping pong 0.835 of the wall time); List.sort sorts runs of 16 and merges between two buffers (1M Ints 0.82 of the wall time, now level with Go); derived JSON decoders find every field in one pass; the macOS monotonic clock is read as Go reads it.
  • Code generation on x86_64: a better register ranking for long hot ranges, cold-path saves, and a larger inlining budget for a function called once.
  • std/http and std/url: a URL is parsed once, a plain query is read in place, and router path parameters skip a range check.

Fixes

Most of these come from field testing of 0.26.0 on an Intel Mac and on Windows. Thank you to the testers: every report below was reproduced, fixed with a test, and checked again.

  • x86_64 miscompile: a function whose entry block runs without its frame (shrink wrapping) read parameters passed on the stack from the wrong offset, giving wrong results on linux/amd64.
  • macOS on Intel: programs with use c or --ld=cc link again; --race binaries load ThreadSanitizer’s runtime beside them, and --backend=llvm --race uses xo’s own runtime instead of Apple clang’s, which stopped at SIGILL.
  • Cross builds link or fail with a message naming the target, instead of exiting 0 with only the assembly written; --targets=all skips a target the program cannot support (a network program and wasip1/wasm) with a note; --targets=wasip1/wasm alone builds, and wasm code sees target.os as Wasi.
  • Windows paths: xo init and xo test no longer hang at a drive root, and C:/... paths are absolute everywhere in the CLI; XO_TRACEBACK=locals says once that locals are not available on Windows.
  • xo check checks by the native backends’ rules, as a build does (use c passes; --backend=go for the Go backend’s), and the native support check is --support.
  • Language and diagnostics: r ?? e => body builds with a plain variable; a generic struct’s field default that mentions its type parameter builds natively; "${r?:?}" parses; a parse error in a parameter list no longer cascades into unrelated errors; xo fix removes a ? after an expression that cannot fail; a hole inside a string has type Str.
  • std/sql: Postgres numeric, json and date columns read as the spec says, and a wrong parameter count is Param.
  • Tools: xo dump lists live tasks only; XO_TRACEBACK=locals shows a list’s elements and leaves out locals not set yet; xo query type --dir and the MCP query tool find a file under the directory; xo serve explains a socket path that is too long; xo fmt ./... no longer rewrites files under testdata/, and every DIR/... walk skips testdata, vendor and names starting with . or _; a test’s opt? failure names its location.
  • Very large programs: three quadratic compile costs in very long functions are gone, and a match of tens of thousands of arms assembles on arm64.

Known gaps

  • Spawning many short tasks is slower than Go: 1.20 times Go’s wall time in this release’s spawn workload, and the field report measured 100,000 spawns at 1.25 times Go.
  • Sorting strings: List.sort of Strs takes 1.18 times Go’s slices.Sort (Ints and Floats are now level with Go).
  • Floating point: the n-body workload takes 1.57 times Go’s wall time.
  • A read-write lock contended by many tasks is several times slower than Go’s (3.58 times in this release’s comparison): Xo spreads lock-bound tasks over all its workers, where Go keeps them near one.

The full Xo against Go comparison for this release is in bench/results/release-vs-go-v0.27.0.md.