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

Xo 0.25 release notes

released 2026-10-22 · tag v0.25.0 · all releases

0.25.2: the first binary release

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

0.25.2 is the first Xo release with prebuilt binaries. It is 0.25.0 plus the fixes below. 0.25.1 was tagged but never got binaries: its Windows xo.exe crashed on every build, because Xo’s COFF writer overflowed the 16-bit relocation count of a section with 65,535 or more relocations. 0.25.2 writes the extended count, as LLVM’s COFF writer does.

Fixed since 0.25.0:

  • Windows: sections with 65,535 or more relocations are written correctly, so the Windows xo.exe builds programs again.
  • json.decode and parse_value reject a byte after the value, as RFC 8259 and Go do.
  • Map helpers for Map[K, Bytes] no longer share a name with the Map[K, Str] ones, which let a cached function call one with the other’s result type.
  • The arm64 inliner no longer inlines a function into itself after a callback brings its own call in.
  • RSA: the secret exponent loop no longer allocates, so the constant-time checks at clang -O2 find no select in it. No secret-dependent select was found, and RSA’s output is unchanged.
  • Diagnostics: a Set suggests add and remove; Time and Duration methods suggest the operators or clock.since(t); m[k]? and m[k] ?? d suggest m.get(k); m[k] += v suggests m.update(k, 0, ...).

Known issue: on macOS, --race reports show their stack frames without file and line (fixed on main for the next release).

The notes below are 0.25.0’s.

Introduction

Xo 0.25 makes everyday code cheaper without changing what it means: callbacks no longer copy what they only read, slicing a string no longer allocates, and a Mutex release hands off to the writer it woke. It also adds r ?? e => ... for early exits from a Result, lazily computed constants, any integer type as an index, --race with call stacks on every native target, and xo vet --cost, which reports costs the source does not show.

Changes to the language

  • Any integer type indexes a List or Bytes (reads, assignments, get, slice bounds; Str slice bounds too) (decision 0331).
  • x ?? return v, ?? break, ?? continue, and the Result form r ?? e => body, which replaces the four line match of an early error exit (decision 0321).
  • A top-level const may call effect-free functions and is computed once, on first use (const ISO: Layout = time.layout(...)) (decision 0332).
  • Slices of a Str or Bytes are views of the original, with no copy; clone() makes an independent copy when a small slice of a large input is kept (decision 0330).
  • Evaluation order: a by-value receiver that is a variable or a field path is read after the call’s arguments, as a var receiver already was: t.size(t.add(v)) sees the item add pushed (decision 0326).
  • A function value borrows its arguments: a callback that only reads its element no longer counts it, with no new syntax (decision 0337).

Standard library

  • time layouts are named fields ("{year}-{month}-{day}"), checked at compile time when constant; time.layout(text) gives a Layout parsed once. With a Layout, native formatting takes 2.6 times fewer instructions than with a layout string, and parsing 2.1 times fewer.
  • Str.clone and Bytes.clone. url.parse_query and mime.parse_media_type copy their tokens when the input is over 4 KiB, so a kept token no longer keeps a large input alive: 318 MB to 6 MB resident in a probe that keeps one token from each of 300 1 MiB inputs.
  • A Mutex release yields to the writer it wakes when readers are queued behind it. The CRUD server on linux/arm64: 88.6k to 190.8k requests per second on 3 cores (2.15x) and 70.5k to 136.0k on 6 (1.93x), p99 53.8 ms to 15.6 ms and 65.7 ms to 27.0 ms; unchanged on 1 core.
  • std/debug: a server’s CPU profile, heap profile, and scheduler state over HTTP behind an auth function (decision 0325).
  • std/metrics and std/trace.
  • An idle worker polls the network itself (epoll, kqueue) and runs the fibers its events ready, as Go’s netpoller does (decision 0324), and a with_read share of a Mutex no longer counts the value’s reference (decision 0334).
  • ChaCha20 with NEON and the ML-KEM NTT with SSE2.

Tools

  • xo vet --cost: notes on costs the source does not show: a loop that copies a map’s entries first (XO0907), an element counted up and down each iteration (XO0908), a list update that copies the list each iteration (XO0909), a value over 8 words copied into a closure or an Option in a loop (XO0910), and a stored view of a parameter’s text that keeps the whole input alive (XO0911) (decision 0327).
  • xo bench --ab: interleaved A/B rounds of two programs, or of one under two settings, with each metric’s median, spread, and a verdict (proposed design, built; decision 0340).
  • A code generator failure names the Xo source (file:line:col: in function).
  • xo prof URL fetches a running server’s /debug/profile?seconds=N or /debug/heap and summarizes it like a file.
  • XO0540, a checker warning: a Mutex.with closure that writes its value and then returns Err on another path (the write is undone).
  • --race reports carry the call stack on every native target, and --race runs on amd64 (darwin, linux); windows/amd64 builds with it, but is not called supported until a Windows run with threads passes (decision 0273).

Performance

  • Callbacks: xs.any(fn(u) { u.name == want }) over users with two string fields takes 34% fewer instructions natively and 27% fewer with LLVM; two tasks walking one shared list, 43% and 50% fewer (191 ms to 75 ms natively).
  • Network servers against Go (linux/arm64): TLS 1.3 handshakes 1.2 to 1.5 times Go’s rate; HTTP/2 1.22 to 1.25 times on one stream and 1.63 to 1.66 on 100. Bulk TLS is behind (0.61 to 0.76 of Go’s), and so is a single HTTP/2 256 MiB download (0.61 to 0.67).
  • LLVM builds unroll only loops with Float arithmetic, keeping nbody’s gain (2,308 M to 1,160 M instructions) without 0333’s 12.8% more CPU for xo’s own LLVM build (decision 0338).
  • Slices that no longer allocate: mime.parse_media_type 25.6% fewer instructions, multipart.parse 16.1%, and split, lines, and trim 11 to 14%.
  • Fiber stacks no longer take transparent huge pages on Linux: the CRUD server at 1024 connections peaked at 2.1 GB of memory before, 47 to 60 MB after (Go 66 to 78 MB).