Xo 0.32 release notes
Downloads: Xo 0.32.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. Versions 0.28 to 0.31 were internal bootstrap compilers with no
public archives, so these notes cover everything since 0.27.0.
Xo against Go
Each release is measured against Go on the same Mac (bench/results/release-vs-go-v0.32.0.md, lines 9 to 295):
- Run time: Xo is faster on 14 of 20 workloads, within noise on 4, and slower on 2 (llvm nbody 1.42 times Go’s wall time, conc pmap 1.08). conc rwmutex, 3.58 times Go’s in 0.27.0, is now within noise, and conc spawn went from 1.20 to 0.47.
- Peak memory of the run: lower than Go’s on all 20 (0.38 to 0.97).
- Binary size: 1.72 to 1.79 MB against Go’s 2.39 to 2.43 MB (1.85 to 1.94 MB in 0.27.0).
- Compile: cold builds 1.09 to 1.29 s against Go’s 1.45 to 1.50 s; a warm build of an unchanged program is 0.03 s, the same as Go’s (0.27 to 0.34 s in 0.27.0); an edit of one function rebuilds in 0.33 to 0.36 s against Go’s 0.10 to 0.11 s.
There are no breaking changes. A release build’s binary is much smaller, lists of small numbers use far less memory, and many std operations are faster.
Smaller binaries
- A release build of hello world on darwin/arm64 is about 213 KB, from 705 KB before this cycle’s size work, and its code is a quarter of the size (bench/results/size-2026-10-24.md). The pieces:
- a program that cannot use concurrency leaves out the scheduler, the poller and the signal handlers (a release hello’s code -31%);
- a function nothing reaches is emptied to a single trap instruction (decision 0371), on darwin/arm64, linux/arm64 and linux/amd64;
- one map implementation per key type instead of three per element type (decision 0372);
- a build without debug information does not link the DWARF readers;
- a release link keeps only the function names a traceback reads (hello -1.6%, a CRUD server -1.4%).
- Fault messages are built when a fault happens, from the file, line and column, instead of being stored as strings.
- A stripped build’s traceback says it has no symbols instead of printing wrong function names.
Lists and sorting
- Lists of sized integers and Float32 store each element at its own width (decision 0369): a list of
UInt8uses one byte per element, not eight. Building and summing 10 million UInt32 and 10 million UInt8 takes 0.53 of the time and 0.27 of the memory.--pack-lists=falsekeeps the old layout. List.sort_in_placeandList.sort_in_place_bysort the list you hold, stable, using a scratch area smaller than the list (decision 0374): sorting 2 million strings needs 16 MB of extra memory instead of 80 MB.
Strings
xo build --inline-str(opt-in, decision 0370) stores a string of 15 bytes or fewer inside its value: 2 million short keys use 0.38 to 0.62 of the memory, at a cost of 6 to 11% more instructions on string-heavy code and about 8% more code. Use it for memory-bound programs.- ASCII
to_lowerandto_upperwork 16 bytes at a time (0.64 of the instructions on arm64). - New Str methods from Go’s strings package, with Go’s tests:
cut,cut_prefix,cut_suffix,replace_n,trim_charsand its start and end forms,split_n,split_after,find_any,contains_any,equal_fold,count,fields, and the predicate family (find_where,rfind_where,contains_where,trim_where,split_where,map_runes). - Bytes gains the same families (
contains,split,fields,trim,replace,to_lower,to_upper,equal_fold,find_any,cutand the rest), and Rune gains the remaining classes (is_uppertois_number,to_title,utf8_len).
Faster
- Builds: an unchanged program’s rebuild is a cache hit, about 0.05 s (decision 0360), and the compiler’s cache is about 40% smaller on disk. Windows builds of xo programs take 7 to 10 seconds, down from minutes.
- Concurrency: a contended RWMutex takes 0.26 of the time, a contended Mutex 0.65.
- JSON: decoding a list of Times takes 0.68 of the instructions; a Duration, Time, base64 Bytes or Float is parsed once.
- Time: RFC 3339 parsing 0.75 of the time; formatting 0.96 and Duration parsing 0.96 in this release alone.
- Strings and Unicode:
- sorting a list of strings 0.89;
- case conversion 0.88 to 0.93;
- the Rune class searches 0.77 of the instructions;
- quoting 0.90.
- Crypto: RSA signing on the native backend matches LLVM.
- Codegen:
- Float32 arithmetic in single precision registers;
- a modulo index in a loop 0.71;
- Int32 division by a constant 0.88;
- bounds and overflow checks removed where a called function’s result range proves them (decision 0376).
Fixes
- A TLS server could hang when the client’s Finished message and its first data arrived in one read (about 1 in 300 HTTP/2 requests with a streamed body).
- A rare hang where the main task slept in the poller with work pending.
- The native JSON decoder stops at 10,000 levels of nesting, and refuses a number outside Float’s range instead of returning 0.
Time.parse_rfc3339and a JSON Time refuse a time outside Time’s range instead of wrapping (decision 0364).- tar, zip and unzip refuse a NUL byte in an entry path.
- Concurrent builds of one program no longer overwrite each other’s objects.
- The LLVM backend builds the concurrency runtime on Linux.
- Windows release binaries name their PDB by file name only, so no build path is left in any executable.
Testing
Every public std function has a corpus or golden call. New corpora are ported from Go’s tests (strconv, time, crypto vectors, JSON, tar and zip, strings and bytes), and a cross-backend fuzzer covers 20 std areas.
Known gaps
- Big-number arithmetic is about 1.15 times LLVM’s instructions on arm64 (RSA, P-521).
- Time formatting is about 1.08 of LLVM’s instructions, and Duration parsing about 1.04.
- On darwin/amd64, unreachable-function removal is opt-in (
XO_PASSES=+hollow).