# Xo 0.40 release notes


**Downloads:** [Xo 0.40.0 on GitHub](https://github.com/xo-lang/xo/releases/tag/v0.40.0),
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](/docs/getting-started/#install) 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: `JpegErr` has a new variant, `Encode(msg: Str)`**, for `jpeg.encode`'s errors; a `match` on `JpegErr` that lists every variant fails to compile (`missing Encode(_)`) until it handles `Encode`.

Other changes you may notice:

- **`Duration.seconds()` is Go's `Duration.Seconds` to 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 DIR` in 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 doc` and `xo query def` list 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 debug` can control them; `--release` and `-s` builds do not.
- **The VS Code extension has its own repository**, xo-lang/vscode (Marketplace `xo-lang.xo`, also on Open VSX); `editors/vscode` is gone from this one.

## `xo debug`

- **A debugger in the `xo` command** (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:** `tasks` lists the program's task tree from the runtime's own records (what each task is blocked in, and where), and `task N` selects a parked task's frames so `locals` and `print` read them.
- **Attach to a running program:** `xo debug -p PID PROG` stops every thread, and `quit` detaches 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:** `--batch` takes commands from `-c` flags, `--json` answers one JSON object a line, `--timeout=DUR` ends a wait for a stop that never comes, and `xo debug --dap` serves the Debug Adapter Protocol. The VS Code extension adds a debug type `xo` with launch and attach.
- Platforms: macOS arm64, Linux arm64 and x86_64, Windows amd64.

## Standard library

- **std/yaml** (decision 0512): `derive Yaml`, `yaml.decode[T]` and `yaml.encode_value[T]`, a port of gopkg.in/yaml.v3 v3.0.1's decoder and emitter with the same values, error texts and `Marshal` bytes (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`), `,inline` and skipped fields, Duration and Bytes fields; a `yaml.Node` tree with comments (`parse`, `parse_all`, `encode_node`, Node fields in `derive Yaml` structs; 442 documents equal to yaml.v3's) and `yaml.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's `enum 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 duplicate `zero_val` symbol (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), and `Client.open` with an `http.pipe` body 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`'s `finish` no longer show the call's result as the caller's variable.
- **Profiling:** `XO_PROF=cpu` no 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 --watch`** no longer restarts the program on a comment-only edit (0.39.0's known issue).
- **WebAssembly:** a program using `Atomic.add` builds again, and a `task.isolate` that 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-`pub` `Display`, `Debug` or `Error` method 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 doc` and `xo test` check 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 type` on an s3 file takes 282 MB, and `xo def payload_hash` 35 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.map` to the same element type, `List.sort`, `List.sort_by` and `List.reverse` of 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].add` is 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_place` runs pdqsort in place (0.681 of the old time for 1M Ints on linux/amd64), and `Buf[T].with_capacity` reserves its room: micro13 sort1M 65 to 61 ms on linux/amd64, at or ahead of Go 1.26.1.
- **`Str.repeat` and `Bytes.repeat`** fill 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): `/hello` serves 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/c` exec cost (about 6 ms a run, more for Go's larger binary), see the [ext4 rerun on the 0.44 page](/releases/0.44/#xo-against-go).
- **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; `serve` then waits until the grace period ends, or for ever without one. Fixed in 0.41.0.

