# Xo 0.27 release notes


**Downloads:** [Xo 0.27.0 on GitHub](https://github.com/xo-lang/xo/releases/tag/v0.27.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.

## Xo against Go

Each release is measured against Go on the same Mac
([bench/results/release-vs-go-v0.27.0.md](https://github.com/xo-lang/xo/blob/main/bench/results/release-vs-go-v0.27.0.md),
lines 11 to 24):

- Run time: Xo is faster on 14 of 20 workloads, within noise on 2, and
  slower on 4 (conc rwmutex 3.58 times Go's wall time, llvm nbody 1.57,
  conc spawn 1.20, conc pmap 1.07).
- Peak memory of the run: lower than Go's on all 20 (0.38 to 0.99).
- Binary size: 1.85 to 1.94 MB against Go's 2.39 to 2.43 MB.
- Compile: cold builds 1.8 to 2.3 s against Go's 1.6 to 1.9 s; a warm
  build of an unchanged program, 0.27 to 0.34 s against 0.04 s (0.42 to
  0.64 s in 0.26.0), is still the largest gap, and xo's build now peaks
  at 0.49 to 0.50 GB cold (0.76 to 0.89 GB in 0.26.0).

## Breaking changes

1. **A third-party `use go` needs a pin** (decision 0356). A `use go` of
   a package outside Go's standard library needs `require go <module>
   <version>` in xo.mod; without it the use is error XO1319. Before, the
   build followed the module's latest version and recorded it only in
   the git-ignored `.xo/`, so two machines could build different code
   from the same commit. `xo fix` adds the line with the version the
   binding resolved, and `xo mod tidy` adds missing pins. xo.lock now
   records each pinned Go module's go.sum hashes (`go <module> <version>
   h1:...`): a changed module is XO1304, a missing line XO1305 (run `xo
   mod tidy`). Go's standard library packages need nothing.
2. **std's errors print plain messages** (decision 0358). Display of
   std's error enums is a sentence, as Go's error strings are: `PATH: no
   such file or directory`, `connection refused`, `no rows in result
   set`, instead of nested Debug output such as `Conn(msg:
   "Io(msg: \"tls: ...\")")`. This covers FsErr, NetErr, IoErr, EnvErr,
   ProcErr, ClockErr, SqlErr, HttpErr, ClientErr, UrlErr, CookieErr,
   TlsErr, SmtpErr and TemplateErr. `${e:?}` (Debug) is unchanged. A
   program or test that compares the printed text changes with it.

## New

- **`--release` is a preset** (decision 0354). Each setting it changes
  has its own flag in any build (`xo build`, `run`, `test`), which wins
  over the preset whatever the order: `--debug-contracts`, `--dbg`,
  `--error-args`, `--debug-info=on|off|split`, `--profile-support`,
  `--trace-support` (`on|off`, a bare flag is on) and `--opt=O0..O3`
  (LLVM). `--error-args=off` no longer needs `--release`.
- **`--debug-info=split`** (decision 0355) keeps the debug information
  beside the program: `PROG.dSYM` on macOS, `PROG.debug` on Linux, found
  by tracebacks, lldb, gdb and addr2line.
- **Build information in every binary.** `xo version -m BINARY` prints
  what built it, as `go version -m` does: the xo, backend, target, every
  effective setting, the stamps and the sources' hashes. A native crash
  report names the build on its second line.
- **Deterministic scheduling on the native backends** (decision 0350):
  `xo run --seed=N`, `xo build --seed=N`, and `xo test --seed=N` /
  `--explore=N` run every task on one thread with a seeded choice at
  each scheduling point, so a seed reproduces an interleaving.
  `--explore` prints the command that reproduces a failure.
- `XO_PROF_HEAP_AT=TEXT` (std/debug) writes the heap in use when a
  labeled point is reached.

## Faster

- **`xo check`** is about 2.5 times faster on a warm run (xo itself 1.8 s
  to 0.67 s, a small server 0.59 s to 0.26 s) and peaks a third lower in
  memory: an unchanged module is skipped and its diagnostics replayed,
  and the native runtime's bodies are checked as a build checks them.
  `xo check --support` is 2.3 to 2.6 times faster (3.7 s to 1.6 s on xo
  itself). The output is unchanged; `XO_CHK_CACHE=off` checks
  everything.
- **Warm builds**: the module check cache and a shared cache pack are on
  by default (a warm build of xo itself 0.885 of the wall time; a new
  program's first build 0.831), front end cache keys that are never
  stored are no longer looked up, and std and runtime function bodies
  are parsed only when needed (a small program's warm build peaks about
  40% lower in memory). `XO_CHK_CACHE=off`, `XO_SHARED_PACK=off` and
  `XO_LAZY_PARSE=off` turn them off.
- **Runtime**: channels of one-word values (integers, Float, Bool, enums
  without payloads) send the word instead of a one-element list (ping
  pong 0.835 of the wall time); `List.sort` sorts runs of 16 and merges
  between two buffers (1M Ints 0.82 of the wall time, now level with
  Go); derived JSON decoders find every field in one pass; the macOS
  monotonic clock is read as Go reads it.
- **Code generation** on x86_64: a better register ranking for long hot
  ranges, cold-path saves, and a larger inlining budget for a function
  called once.
- **std/http and std/url**: a URL is parsed once, a plain query is read
  in place, and router path parameters skip a range check.

## Fixes

Most of these come from field testing of 0.26.0 on an Intel Mac and on
Windows. Thank you to the testers: every report below was reproduced,
fixed with a test, and checked again.

- **x86_64 miscompile**: a function whose entry block runs without its
  frame (shrink wrapping) read parameters passed on the stack from the
  wrong offset, giving wrong results on linux/amd64.
- **macOS on Intel**: programs with `use c` or `--ld=cc` link again;
  `--race` binaries load ThreadSanitizer's runtime beside them, and
  `--backend=llvm --race` uses xo's own runtime instead of Apple clang's,
  which stopped at SIGILL.
- **Cross builds** link or fail with a message naming the target, instead
  of exiting 0 with only the assembly written; `--targets=all` skips a
  target the program cannot support (a network program and wasip1/wasm)
  with a note; `--targets=wasip1/wasm` alone builds, and wasm code sees
  `target.os` as Wasi.
- **Windows paths**: `xo init` and `xo test` no longer hang at a drive
  root, and `C:/...` paths are absolute everywhere in the CLI;
  `XO_TRACEBACK=locals` says once that locals are not available on
  Windows.
- **`xo check`** checks by the native backends' rules, as a build does
  (`use c` passes; `--backend=go` for the Go backend's), and the native
  support check is `--support`.
- **Language and diagnostics**: `r ?? e => body` builds with a plain
  variable; a generic struct's field default that mentions its type
  parameter builds natively; `"${r?:?}"` parses; a parse error in a
  parameter list no longer cascades into unrelated errors; `xo fix`
  removes a `?` after an expression that cannot fail; a hole inside a
  string has type `Str`.
- **std/sql**: Postgres numeric, json and date columns read as the spec
  says, and a wrong parameter count is `Param`.
- **Tools**: `xo dump` lists live tasks only; `XO_TRACEBACK=locals` shows
  a list's elements and leaves out locals not set yet; `xo query type
  --dir` and the MCP query tool find a file under the directory; `xo
  serve` explains a socket path that is too long; `xo fmt ./...` no
  longer rewrites files under `testdata/`, and every `DIR/...` walk
  skips `testdata`, `vendor` and names starting with `.` or `_`; a test's
  `opt?` failure names its location.
- **Very large programs**: three quadratic compile costs in very long
  functions are gone, and a `match` of tens of thousands of arms
  assembles on arm64.

## Known gaps

- Spawning many short tasks is slower than Go: 1.20 times Go's wall
  time in this release's spawn workload, and the field report measured
  100,000 spawns at 1.25 times Go.
- Sorting strings: `List.sort` of Strs takes 1.18 times Go's
  `slices.Sort` (Ints and Floats are now level with Go).
- Floating point: the n-body workload takes 1.57 times Go's wall time.
- A read-write lock contended by many tasks is several times slower than
  Go's (3.58 times in this release's comparison): Xo spreads lock-bound tasks
  over all its workers, where Go keeps them near one.

The full Xo against Go comparison for this release is in
[bench/results/release-vs-go-v0.27.0.md](https://github.com/xo-lang/xo/blob/main/bench/results/release-vs-go-v0.27.0.md).

