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

Xo 0.44 release notes

released 2026-11-08 · tag v0.44.0 · all releases

Downloads: Xo 0.44.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. These notes cover everything since 0.41.0.

Xo 0.44.0 fixes a native HTTP server on Linux that stalled on pipelined requests whose sender closed its side right after writing them, brings std/sqlite to windows/amd64 with no C compiler needed, adds destructuring assignment ((a, b) = (b, a), struct and list targets), and makes many byte and string paths faster with 32 byte vector scans on arm64 and amd64. std/cli gains Go flag forms for ports of Go tools, and std/sql’s drivers carry their effects.

Upgrade if you run a native HTTP server on Linux behind clients or proxies that pipeline requests. No change in this release stops a program that compiled with 0.41.0 from compiling, with one possible exception: code that keeps a sql.Db in a closure whose type fixes its effects now names the effect argument (see below).

Breaking changes

None marked breaking. Changes you may notice:

  • std/sql drivers carry their effects (decision 0534): Driver, DriverConn, DriverRows, DriverCursor, Db, Tx and Rows take an effect parameter fx, and open_driver returns Db[fx], so std/sqlite’s Db performs fs rather than claiming net.dial. sql.Db without the argument stays valid; code that keeps a Db in a closure with fixed effects names it, sql.Db[()] for Postgres and the in memory driver, and xo fix writes that for you.
  • xo check on a directory inside std (xo check std/...) exits 2 with a usage message; xo test --std-module=M checks a std module.
  • List targets of a destructuring assignment ([a, b] = xs) report XO0303, as let’s list patterns do (they were XO0502).

Language

  • Destructuring assignment (decisions 0536 and 0538): (a, b) = (b, a), (q, r) = divmod(x, y) and (p.x, xs[i], _) = t store a tuple’s elements into existing places, and P{x, y} = p or P{x: st.lo, ..} = p store a struct’s fields, the value first, then each place left to right; XO0539 for a place named twice.
  • Effects and interfaces (decision 0540): a method whose parameter type leaves its effect arguments out (next: Next) satisfies an interface method that writes them (next: Next[fx]); it was XO0701.

Fixes

  • A native HTTP server on Linux answers pipelined requests whose sender closed its side right after writing them; each such connection stalled until its sender gave up. A known issue of the 0.43.0 seed, never in a public release.

  • A type with a method named debug or display that takes parameters (a generated API client’s debug(project, zone, ...)) builds; formatting such a value called that method with only its receiver, which stopped native builds with XO1510, LLVM with a clang type error and Go with a compile error.

  • A stack overflow on Linux and macOS is reported as “fatal error: stack overflow”, as on Windows; on macOS it read as a bus error.

  • Native backends: a closure that captures more than 32,760 bytes of words builds (a generated GCP client’s service); a method of a struct with an effect parameter called through an interface value compiles; == on a value holding an http.Response or http.Request compiles; each was refused before.

  • Go backend: a struct with no fields and derive Json, two types’ methods of one name with default arguments, return of an if or match mixing T values and Err(e), a std module’s constant read from user code, and xs.contains(x) on a list of a user type all build and run again.

  • Test worlds: a FakeNet handler’s streamed body reaches Client.send and Client.open natively, and a read or write that waits past its deadline ends with Timeout on the Go backend (it reported a deadlock).

  • Cross builds of a program with a use c source to darwin/amd64 from Linux, or to windows/amd64 with no sysroot, stop with a message that says what is needed instead of a C compiler error.

  • Debugging: lldb on macOS shows a string literal held in a variable correctly in a separately compiled build, and xo debug’s task N on a task std spawned selects the first frame outside the runtime.

  • std/amqp: an unsettled send with the reader task running no longer waits forever.

  • arm64 programs with more than 128 MB of code no longer crash in reference counting: a retain or release whose object sat in x16 went through a linker branch island that sets x16 (a generated GCP client’s tests stopped with SIGSEGV).

  • Native backends: a recursive value (a Box) of more than 32,760 bytes of words builds, and structs, tuples and enums of that size compare.

  • Linux with 64 KiB pages: freeing a 32 KiB heap block no longer discards the pages of the block beside it.

Standard library

  • std/sqlite on windows/amd64 (decision 0534 stage 2): linked by xo’s own PE linker, with prebuilt SQLite objects in the release, so a program using std/sqlite builds with no C compiler; use c calls on windows/amd64 use the Win64 convention.
  • std/cli for ports of Go tools: single_dash_long: true reads -name, -name=value and -name value as the long forms (decision 0547), stop_at_positional: true ends the flags at the first positional argument as Go’s flag package does (decision 0556), and bool_flag(..., default:) sets a flag’s value when it is absent.
  • BytesBuilder.write_le(v, n) and write_be(v, n) append the n (0 to 8) low bytes of a word in one step, on every backend (decision 0561).
  • std/json derive Json(match: fold) decodes object keys as Go’s encoding/json does (an exact key first, else the first field whose key folds equal) (decision 0550).
  • std/sql’s memory driver takes CREATE INDEX and DROP INDEX (PostgreSQL’s syntax, one column), so a WHERE column = value on an indexed column visits only the matching rows.
  • use c: a C function returning a struct of a const byte pointer and a length binds as a Bytes result, NUL bytes kept; a std module’s binding can name its handle types in the module’s own types.
  • std/amqp and std/amqptest: desired capabilities on links (Event Hubs v2 geo-replication), an event hub’s georeplication_factor, and Broker.attaches().
  • For long-running services: /debug/sched reports heap_chunks and heap_cache_bytes; heap profiles carry Go’s bytes label, so go tool pprof -tagfocus=192B keeps one size.

Tools

  • Windows version information, icons and a manifest (decision 0560): xo build --versioninfo=FILE.json reads goversioninfo’s JSON, --icon=FILE.ico and --manifest=FILE set the icons and manifest, so a project moving from goversioninfo keeps its versioninfo.json.
  • LLVM builds of a function with more than 8,000 blocks on arm64 compile in about a minute instead of more than 10 (LLVM’s AArch64 machine combiner is turned off for that module).

Speed and memory

  • 32 byte vector scans (arm64 NEON, x86_64 SSE2) in std’s byte paths, on linux/arm64 against the release before each step: a one byte find 0.68, Str and Bytes == over 16 bytes 0.73, Str.count of a newline 0.741, find of a common first byte 0.347, Str.quote 0.542, equal_fold on 4 KiB 0.185 (0.165 of Go’s).
  • Crypto on arm64 and amd64 vector units: SHA-256 0.884 of before on linux/arm64 (0.79 on linux/amd64), SHA-1 0.884 (0.86), SHA-512 0.965.
  • Encoders: std/cbor’s deterministic marshal of a derived struct 0.442 of before, and cbor.marshal of a derived type writes its encoding directly, 0.46 of that (2.8 ms against Go’s 4.3 ms), JsonWriter’s strings 0.75, JSON decode of 10 KB documents 0.827 wall and 0.785 max RSS on linux/arm64, std/sql’s memory driver ORDER BY 0.574, labelled std/metrics counters 0.760 (a small List literal argument no longer allocates).
  • Memory: a List or Str growing past 1 MiB reuses its last mapping (JSON decode on linux/amd64: max RSS 45,352 to 12,092 KB, Go 13.9 MB); a List of word-sized elements starts with room for 2, so xo’s own build peaks 4.4% lower.
  • Toolchain: a warm build on Linux no longer copies its part objects (an unchanged hello wrote 3.7 MB to the cache every build); the native backends’ self build peaks at 6.2 to 6.4 GB mapped instead of 6.8 to 6.9 on linux/amd64; std’s parses come from the build cache.
  • More std paths: std/path’s dir, base, ext and split 0.918 of before (clean 0.934, join 0.942), an AES key schedule 0.717, std/netip’s IPv4 parse 0.816, on linux/arm64.
  • Long-running services on Linux give the system the pages of free heap blocks of 16 KiB and over once they stay free 30 to 60 s (decision 0533).
  • Go backend builds of large generated packages take less memory: azure armnetwork compiles in 8.1 GB and 61 s instead of 9.1 GB and 68 s.

Known issues

  • On macOS, once std’s prebuilt variants are in the cache, a program’s default build compiles separately and its debug map names the object files lldb reads in the build cache (~/Library/Caches/xo/backend/dbgobj/), so the binary carries the builder’s home directory despite --trimpath. Debugging works, but a binary you hand to someone else names your home directory. Build with XO_SEPCOMP=0 (or --release) for a binary to share. Decision 0563 settles it. The release’s own binaries are built whole and hold no build path.

Xo against Go

Measured before the cut on the release candidate (b0ee4622c, Go 1.26.1):

  • macOS arm64 programs: ahead of Go on 15 of 20 run rows by wall (an interpreter 0.31 of Go’s time, collection iteration 0.27, a mutex 0.30, spawn 0.46), level on four, one slower (nbody 1.026); every row at 0.32 to 0.84 of Go’s peak memory.
  • Tests of std (xo test --std-module against go test): json cold 0.395 of Go’s time and 0.271 after an edit; base64 0.671 and 0.705. After an edit xo’s test run peaks higher (base64 1.68 times Go’s memory, json 1.22).
  • Builds: cold 1.04 to 1.41 s against Go’s 1.44 to 1.51 s, an edit 40 to 100 ms against 100 to 110 ms; binaries 1.45 to 1.63 MB against 2.39 to 2.43.
  • HTTP server (linux/arm64): /hello serves 1.29 times Go’s requests per second at 32 connections and 1.83 times at 256; a JSON response (/alloc) 5.99 and 3.18 times, and at a fixed 20,000 requests/s its p99 is 0.22 of Go’s. Every row is level with 0.41.0.
  • linux/amd64 (micro13, --release, 9 rounds): ahead of Go on six of seven programs (JSON 0.29 of Go’s time, string building 0.40, map1M 0.69, fib 0.92, both sorts 0.94), spawn100k level; JSON’s peak memory is now 0.70 of Go’s (0.21 of 0.41.0’s), map1M’s 1.42 times. Level with 0.41.0 on every row but JSON (0.725), within 2%.