# Xo 0.32 release notes


**Downloads:** [Xo 0.32.0 on GitHub](https://github.com/xo-lang/xo/releases/tag/v0.32.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. 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](https://github.com/xo-lang/xo/blob/main/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 `UInt8` uses 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=false` keeps the old layout.
- **`List.sort_in_place` and `List.sort_in_place_by`** sort 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_lower` and `to_upper` work 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_chars` and 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`, `cut` and the rest), and Rune gains the remaining classes (`is_upper` to `is_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_rfc3339` and 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`).


