Xo 0.40 release notes
Downloads: Xo 0.40.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.39.0.
Xo 0.40.0 fixes a heap corruption in the native HTTP server that 0.36.0 to 0.39.0 shipped, and adds xo debug: a debugger built into the xo command, for macOS arm64, Linux and Windows amd64, in a terminal, as JSON for agents, and in VS Code. std gains YAML (a port of gopkg.in/yaml.v3) and JPEG encoding, zip archives past 65,535 entries, and large values now build on native arm64.
Upgrade if you run a native HTTP server that sends trailers or HTTP/2 settings. One change can stop a program from compiling: jpeg.JpegErr has a new variant, so an exhaustive match on it needs an arm for Encode.
Security
- A native HTTP server no longer corrupts its heap when it sends response trailers or HTTP/2 settings (0.36.0 to 0.39.0, native and LLVM backends; the Go backend was not affected). The server freed those runtime blocks 8 bytes off, which silently corrupted a free list in a normal build. A freed address that is not a block’s start now ends the program with status 70 and “not a block’s prefix” instead of corrupting memory. Found by the 2026-11-05 race stress; the same mistake in an h2c server installed by two threads at once (the losing table) is fixed too.
Breaking changes
- std/jpeg:
JpegErrhas a new variant,Encode(msg: Str), forjpeg.encode’s errors; amatchonJpegErrthat lists every variant fails to compile (missing Encode(_)) until it handlesEncode.
Other changes you may notice:
Duration.seconds()is Go’sDuration.Secondsto the bit on every backend (decision 0503): about 2 in a million durations past one second print a different last digit at full precision.xo check DIRin a workspace checks DIR’s module and the workspace modules it imports, not every module of the workspace; check the workspace’s own directory (or walk it) to check them all (decision 0511).xo def,xo docandxo query deflist several declarations in file and position order.- darwin debug builds carry the get-task-allow entitlement in their ad hoc signature (559 bytes more), so
xo debugcan control them;--releaseand-sbuilds do not. - The VS Code extension has its own repository, xo-lang/vscode (Marketplace
xo-lang.xo, also on Open VSX);editors/vscodeis gone from this one.
xo debug
- A debugger in the
xocommand (decision 0253):xo debug PROG [ARGS]runs a native program with break, run, continue, step, next, finish, locals, print, backtrace, frame, threads and quit, stepping by source line through inlined calls, recursion and tail calls. Written in Xo, with its own DWARF reader and its own process control on each OS: Mach calls on macOS, ptrace on Linux, the system’s debugging functions on Windows. - Xo’s tasks:
taskslists the program’s task tree from the runtime’s own records (what each task is blocked in, and where), andtask Nselects a parked task’s frames solocalsandprintread them. - Attach to a running program:
xo debug -p PID PROGstops every thread, andquitdetaches and leaves it running, on macOS, Linux and Windows. Under Linux’s Yama ptrace_scope 1 (WSL and many distributions) only a descendant can be attached; the error says so. - For agents and editors:
--batchtakes commands from-cflags,--jsonanswers one JSON object a line,--timeout=DURends a wait for a stop that never comes, andxo debug --dapserves the Debug Adapter Protocol. The VS Code extension adds a debug typexowith launch and attach. - Platforms: macOS arm64, Linux arm64 and x86_64, Windows amd64.
Standard library
- std/yaml (decision 0512):
derive Yaml,yaml.decode[T]andyaml.encode_value[T], a port of gopkg.in/yaml.v3 v3.0.1’s decoder and emitter with the same values, error texts andMarshalbytes (442 corpus documents, 8,000 fuzz documents against yaml.v3); strict decoding (strict: true, yaml.v3’s KnownFields), streams of documents (decode_all,encode_all),,inlineand skipped fields, Duration and Bytes fields; ayaml.Nodetree with comments (parse,parse_all,encode_node, Node fields inderive Yamlstructs; 442 documents equal to yaml.v3’s) andyaml.to_json. Against yaml.v3 on linux/arm64: decode 0.62, encode 0.604 of its time. - std/jpeg encodes (decision 0510):
jpeg.encode(img, quality = 75)is Go’s image/jpeg Encode byte for byte (Gray, YCbCr in any ratio, RGBA, CMYK), at 0.83 of Go’s time (encrgba 0.90) on linux/arm64. - std/archive writes zip64 (decision 0499), as Go’s archive/zip does, past 65,534 entries or 4 GB, byte for byte the Go backend’s output, instead of refusing the 65,536th entry (the 0.39.0 limit).
- std/metrics:
child(values)on Counter, Gauge, Histogram and Summary gives a held series with no lookup per call (decision 0501). debug.mem_trim()gives memory back on Linux and from every thread (decision 0505): 160 MB of freed 16 KiB blocks left 324 MB resident after the trim before, 4 MB now (Go 8).
Fixes
- Large values build on native arm64 (0.39.0’s known issue, found by the AWS SDK: wafv2 and mediaconvert): a box of 520 words or more, a value of about 2,050 words,
==on a struct of 254 words or more, a call with a value of 3,600 words or more, and a list of elements over 4,095 words no longer fail to build. - Compiler: a program type named
Select(AWS DynamoDB’senum Select) no longer takes the runtime’s place in a program that selects; two modules that each declare a struct of one name and a function type over it no longer share one type (XO1201); a program’s first build after std’s variants were cached no longer fails to link with a duplicatezero_valsymbol (aws issue 12). All three failed the build; none miscompiled. - A miscompile: two packages’ types of one name (
a.Provider,b.Provider) boxed into interfaces no longer share a type number: the native backend refused such a program (XO1510) and LLVM ran it wrong, reading one type’s value as the other’s (found by the AWS SDK; 0.39.0 too). - std/http: a client reaches a server on an IPv6 literal (
http://[::1]:PORT/, it was refused as an invalid URL), andClient.openwith anhttp.pipebody is full duplex on the test network. - Debug information: a variable passed to a call in a caller saved register is no longer described after the call returns, so lldb’s and
xo debug’sfinishno longer show the call’s result as the caller’s variable. - Profiling:
XO_PROF=cpuno longer dies in its signal handler on spawn heavy programs (8 of 30 runs at 1,000 Hz on linux/arm64; 0.39.0 too, 1 of 30), and a sample in a linux/arm64 atomic helper has its caller. --race: runtime code inlined into a program is no longer reported as the program’s race (LLVM backend; http_h2_config on macOS up to 13 runs in 24), nor two tasks that fault one after the other on one worker.xo run --watchno longer restarts the program on a comment-only edit (0.39.0’s known issue).- WebAssembly: a program using
Atomic.addbuilds again, and atask.isolatethat caught a foreground child’s fault no longer ends main early. - The full list is in CHANGELOG.md.
- Warm builds: a program that calls another module’s non-
pubDisplay,DebugorErrormethod no longer checks those modules again on every warm build (0.39.0 too).
Tools for large workspaces
xo check,xo query,xo def,xo docandxo testcheck only what their question needs (decisions 0511, 0513, 0515). In the AWS SDK (474 modules, linux/arm64): a one-service check went from past 18 GB to about 1 GB,xo check ./...takes 49.5 s at 3.7 GB where one checker stopped at 30 GB,query typeon an s3 file takes 282 MB, andxo def payload_hash35 MB in 0.53 s (was 6.47 GB, 9 s).
Speed and memory
- Edit builds after a cold cache reach separate compilation after one background build instead of about 15: an AWS SDK program’s third one-line edit 0.30 s and 396 MB, where it took 0.79 to 1.09 s and 655 MB, on linux/arm64.
- Lists:
List.mapto the same element type,List.sort,List.sort_byandList.reverseof a list nothing else holds work in place (decision 0508): reverse takes 0.457 of the time and 0.609 of the memory it did, a 1M Int map 0.663 and 0.609, on linux/arm64. Results of map, indexed, group_by and sort_by are allocated at their size (1M Ints mapped to pairs 0.85 on linux/arm64, 0.80 on darwin/arm64). - Tasks:
Atomic[Int].addis one fetch and add; spawn100k on arm64 is now 0.77 to 0.81 of Go’s time, and darwin/amd64’s 29 to 26 ms (Go 25 to 27) with fewer worker wakes on macOS (a worker alone still wakes one more, so scopes of tiny tasks keep 0.39.0’s speed). A request and reply between two fibers no longer costs a wake per turn: tcpping’s handshake 0.93 of Go’s (from 1.54), smtp send 0.53 of its time before on linux/arm64. - Sorting:
List[Int].sort_in_placeruns pdqsort in place (0.681 of the old time for 1M Ints on linux/amd64), andBuf[T].with_capacityreserves its room: micro13 sort1M 65 to 61 ms on linux/amd64, at or ahead of Go 1.26.1. Str.repeatandBytes.repeatfill by doubling, as Go does: 160 MB of a one byte pattern 354 to 4.0 ms (Go 3.0) on linux/arm64.
Xo against Go
Measured before the cut (bench/results/release-vs-go-v0.40.0-precut.md, Go 1.26.1):
- HTTP server (linux/arm64):
/helloserves 1.28 times Go’s requests per second at 32 connections and 1.80 times at 256; a JSON response (/alloc) 5.98 and 3.13 times. Every row is level with 0.39.0. - linux/amd64 (micro13,
--release): ahead of Go on six of seven programs (JSON 0.40 of Go’s time, string building 0.57, fib 0.61), spawn100k level; against 0.39.0, sort1M 0.65 of its time and map1M 0.90.- 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
- macOS arm64: tests of std’s json run in 0.37 of
go test’s time cold (0.50 in 0.39.0’s pre-cut) and 0.34 after an edit; collections 0.03 to 0.69 of Go’s time.
Known issues
- Shutdown hang (native backends, 0.36.0 to 0.40.0; found 2026-11-05): a native HTTP server with idle keep-alive connections can hang on shutdown (SIGTERM, or a stop request) when a connection answered a request in the 20 ms before the stop;
servethen waits until the grace period ends, or for ever without one. Fixed in 0.41.0.