Xo 0.41 release notes
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:
CborErrhas a new variant,Type(msg); amatchonCborErrthat lists every variant fails to compile until it handlesType.
Other changes you may notice:
xo buildandxo runcheck 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 testandxo vetof the dependency still report it, andXO_DEP_LAZY=0checks every body. s3 + dynamodb on linux/arm64: peak RSS 932 to 631 MB, cold 4.44 to 4.01 s.xo getchecks each newly required module version once and prints a warning with its first errors; the requirement is still added.XO_GET_CHECK=0skips it.xo run DIR/main.xoruns 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 DIRruns 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=falseskips it. Ause goprogram now fails plainxo check, as its native build does;--backend=gochecks 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
varparameters (a named function passed as a value, a stored closure, one that fails with?),Mutex.withgiven a function value that is not a closure literal, and a value whose type differs only in effect arguments (Request[env]asRequest[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]asProvider[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
varreceiver 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 --supportof 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]andcbor.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_formaton send (Service Bus batches), manual credit (credit: 0,issue_credit,drain),ReceiverSettleMode.Second, annotations onmodify, andremote_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/schedreportsfds_openandtimers_pending, andXO_CRASH_FILE=PATHappends 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 lspusesxo 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, andxo def,refs,type,symbolsanddocare answered by a runningxo 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_intand friends point toInt.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 Connagainst the prelude’sConn, names each by its module path and notes where the module’s type hides the prelude’s; it read “expectedConn, foundConn”. - A
${...}whose closure spans lines inside a one-line string is one error at the${saying to bind the value withletfirst (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 addsvalue:.
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-moduleagainstgo 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):
/helloserves 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/cexec cost (about 6 ms a run, more for Go’s larger binary), see the ext4 rerun on the 0.44 page.
- Note (2026-11-08): kept as history; skewed by WSL
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.