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

Xo 0.41 release notes

released 2026-11-06 · tag v0.41.0 · all releases

Downloads: Xo 0.41.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.40.0.

Xo 0.41.0 fixes a native HTTP server that could hang on SIGTERM (0.36.0 to 0.40.0), checks a dependency’s function bodies only as a build reaches them, so builds of large module graphs take less memory, and makes the edit loop faster: a one-line compiler edit rebuilds xo in 10.7 to 13.7 s on the Mac instead of 49 to 65 s. std gains CBOR with derive Cbor, and /debug/sched reports open fds and pending timers for long-running services.

Upgrade if you run a native HTTP server with keep-alive clients. One change can stop a program from compiling: cbor.CborErr has a new variant, so an exhaustive match on it needs an arm for Type.

Breaking changes

  • std/cbor: CborErr has a new variant, Type(msg); a match on CborErr that lists every variant fails to compile until it handles Type.

Other changes you may notice:

  • xo build and xo run check a dependency’s function bodies only as they reach them (decision 0517): a type error in a dependency function the program never calls no longer fails the build. xo check, xo test and xo vet of the dependency still report it, and XO_DEP_LAZY=0 checks every body. s3 + dynamodb on linux/arm64: peak RSS 932 to 631 MB, cold 4.44 to 4.01 s.
  • xo get checks each newly required module version once and prints a warning with its first errors; the requirement is still added. XO_GET_CHECK=0 skips it.
  • xo run DIR/main.xo runs the program of DIR instead of stopping with a usage error.
  • A native HTTP server’s responses written after a stop request carry Connection: close, as net/http’s after Shutdown.
  • xo check DIR runs the native support check by default (decision 0523): every function of the named directories’ modules, a library’s too, so what a native build would refuse (XO1201) shows at check time. A walk (DIR/...) runs it only with --support; --support=false skips it. A use go program now fails plain xo check, as its native build does; --backend=go checks it by the Go backend’s rules.

Fixes

  • A native HTTP server no longer hangs on SIGTERM or a stop request when a kept-alive connection answered a request in the 20 ms before the stop (arm64, amd64 and LLVM backends; 0.36.0 to 0.40.0).
  • Native backends run more function values: a function value with var parameters (a named function passed as a value, a stored closure, one that fails with ?), Mutex.with given a function value that is not a closure literal, and a value whose type differs only in effect arguments (Request[env] as Request[fx]) now build and run; each said XO1201 where the Go backend ran it.
  • Effects: a type with an effect parameter implements an interface with one when the arguments agree (ProcProvider[env] as Provider[env]), where it was XO0701.
  • WebAssembly: a function value or closure with a parameter or result of more than 256 leaves is called by memory, so such programs build and run on wasip1 and wasip2.
  • Go backend: a std method with a var receiver in a module the Go backend compiles from its native implementation builds (it said there was no route).
  • xo serve: a cached check no longer keeps answering its old result after an edit to another directory of the module.
  • xo check --support of a large workspace no longer holds every module in one checker (aws’s 438 modules passed 18 GB), and no longer reports a false XO1201 for a shared module’s bodies.
  • A syntax error in a lazily checked dependency body is reported at its file and line and fails the build; it was a compiler fault.

Standard library and runtime

  • std/cbor (decision 0527): derive Cbor, cbor.marshal[T], cbor.unmarshal[T] and cbor.unmarshal_prefix[T] with fxamacker/cbor v2’s rules and tags (key,omitempty,keyasint,toarray); 2,000 typed cases agree with the oracle.
  • std/amqp (decision 0521): message_format on send (Service Bus batches), manual credit (credit: 0, issue_credit, drain), ReceiverSettleMode.Second, annotations on modify, and remote_attach().
  • std/sql’s memory driver indexes UNIQUE INT and TEXT columns and writes tables in place (decision 0528): on linux/arm64 a lookup by UNIQUE column on 10,000 rows takes 0.002 of its time before and INSERT plus DELETE 0.003, and the notes service serves 1.58 times the requests at saturation.
  • For long-running services: /debug/sched reports fds_open and timers_pending, and XO_CRASH_FILE=PATH appends a fault report with its traceback to PATH as well as standard error (Linux and macOS), like Go’s debug.SetCrashOutput.
  • Heap: 2 and 4 KiB buffers take 1.27 times their size instead of 2.01, and sizes from 1 to 8 KiB 1.03 to 1.06 times instead of up to 1.41 (decision 0506).
  • Profiling: a CPU profile holds 262,144 samples instead of 32,768, and one that still fills its ring says PROFILE INCOMPLETE with the share lost and the XO_PROF_HZ that would keep them all.

Speed and memory of the toolchain

  • Edit loop: a one-line compiler edit rebuilds xo through xobuild in 10.7 to 13.7 s on the Mac instead of 49 to 65 s; on linux/arm64 a warm one-line compiler edit went from 17.5 s to 8.2 to 8.3 s wall now that a cc link reuses unchanged objects (121 of 127).
  • Warm builds: the first edit after a cold build is warm (s3 + dynamodb: 0.34 s, was 1.17 s), the second reuses the caches of the one before (0.33 s, was 0.58 s), and a cold build is no longer slowed by the background build of std (4.50 to 4.56 s, was 5.22 to 5.36 s).
  • Build memory: a cold build of s3 and dynamodb peaks at 822 MB instead of 932 (1,108 MB two steps back).
  • Compiler: a cold build of xo takes 0.972 of the wall and 0.956 of the CPU of before, output identical.
  • Editor: xo lsp uses xo check’s module check cache (first diagnostics on compiler/ssa/build.xo 537-758 to 355-399 ms), references take 142 to 148 ms instead of 622 to 733, and xo def, refs, type, symbols and doc are answered by a running xo serve.

Diagnostics

  • An XO0701 that asks for an effect also names the callers that will need it, so one round of edits fixes the chain; XO0701, XO0702, XO0202, XO0401 and XO0502 teach the threaded effect form (guide: knowledgebase/guides/effectful-closures.md); parse_int and friends point to Int.parse; an unknown field or method lists what the type has.
  • A type mismatch (XO0401) between two types that print alike, such as a module’s own struct Conn against the prelude’s Conn, names each by its module path and notes where the module’s type hides the prelude’s; it read “expected Conn, found Conn”.
  • A ${...} whose closure spans lines inside a one-line string is one error at the ${ saying to bind the value with let first (it was five errors ending at the closing brace), and an enum variant field written as a bare type (Create(Str)) says the field needs a name, with a fix that adds value: .

Xo against Go

Measured before the cut (rvg-precut/report.md, Go 1.26.1, main f5c60ea35):

  • macOS arm64 programs: ahead of Go on 15 of 20 run rows by wall (an interpreter 0.32 of Go’s time, collection iteration 0.28, a mutex 0.32, spawn 0.44), level on four, one slower (nbody 1.036); every row at 0.29 to 0.85 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.318 after an edit; base64 0.662 and 0.788. After an edit xo’s test run peaks higher (base64 1.94 times Go’s memory, json 1.30).
  • Builds: cold 1.05 to 1.33 s against Go’s 1.55 to 1.90 s, an edit 40 to 110 ms against 100 to 130 ms.
  • HTTP server (linux/arm64): /hello serves 1.29 times Go’s requests per second at 32 connections and 1.81 times at 256; a JSON response (/alloc) 5.94 and 3.12 times, and at a fixed 20,000 requests/s its p99 is 0.22 of Go’s. Every row is level with 0.40.0.
  • linux/amd64 (micro13, --release): ahead of Go on five of seven programs (JSON 0.39 of Go’s time, string building 0.55, fib 0.62), spawn100k level; JSON’s peak memory is 3.3 times Go’s and map1M’s 1.41 (being worked on). Level with 0.40.0 on every row.
    • Note (2026-11-08): kept as history; skewed by WSL /mnt/c exec cost (about 6 ms a run, more for Go’s larger binary), see the ext4 rerun on the 0.44 page.

Known issues

  • A native HTTP server’s memory grows with the number of requests in flight: a response’s text that grows through the heap’s size classes leaves a free block in each, and Linux is not given those pages back, so the notes service holds more memory at 128 to 256 concurrent connections than at 8 (its Go twin stays flat). A fix to the JSON list encoder and a scavenger proposal (decision 0533) are in progress.
  • The full list is in CHANGELOG.md.