On this page
Xo Conventions
Status: approved (2026-11-05)
How to write Xo that reads like the rest of Xo and runs fast. spec/core.md defines the language; this file
collects the habits that the compiler, std, the gotchas and the measurements agree on. Every rule names its
source. Enforced rules are checked by the compiler, xo fmt or xo vet (the code is given); advisory
rules are not checked, and recurring ones may become vet checks later.
Sources are paths in this repository: core is spec/core.md, std is spec/std.md, gotcha X is
knowledgebase/gotchas/X.md, perf guide is knowledgebase/std-performance-guide.md.
1. Naming
1.1 Types, variants and interfaces are UpperCamel; functions, methods, variables, fields and modules are snake_case; constants are UPPER_SNAKE. Enforced (XO0101). Source: core 1.5.
1.2 Do not reuse predeclared names (Str, Time, Err, Result, ok, err) for your own types or
variables; name what the thing is (TimeRes, LevelErr). Enforced at top level (XO0603). Source: gotcha
predeclared-names-collide.
1.3 An error enum is <Thing>Err, implements Display, and a blocking API’s has a Canceled variant.
Advisory (std’s convention). Source: std section 0; core 2.3.
enum BufErr {
Net(err: NetErr)
TooLong
UnexpectedEof
}1.4 Do not let a local shadow a parameter of the same function. Only same-block shadowing is an error, so
the checker allows it. Advisory. Source: gotcha xo-local-shadows-parameter (a var us hid the parameter us).
1.5 Name the arguments when a call takes two or more of the same type. Advisory. Source: core 3.2.
transfer(from: a, to: b, amt: 5)1.6 Convert numbers by calling the type: Float(n). Enforced (XO0601, with an xo fix rewrite). Source:
gotcha numeric-conversion-is-call.
2. Effects and capabilities
2.1 A pub function declares its effects in uses, and only the ones it performs. Enforced: a missing
effect is XO0701 (error), an unused one XO0703 (vet warning). Source: core 6.1, 6.2.
pub fn fetch_user(db: Db, id: Int) -> User ! LoadErr
uses net
requires id > 02.2 Library code takes the capabilities it needs (Fs, Net, Clock) as parameters and narrows them
with sub, read_only or only; only main creates them. Advisory (xo audit reports the reach). Source:
core 6.3.
3. Errors, Option and Result
3.1 Propagate with ?; convert errors with from variants or .map_err. Enforced (XO0202 when the error
does not convert). Source: core 2.6. For Go habits, see knowledgebase/guides/option-result-for-go.md.
3.2 Never drop a Result silently: .ignore_err("reason"), never let _ =. Enforced (XO0201, XO0206; vet
note XO0208 lists every reason for review). Source: core 2.6.
Err(_) => os.fs.remove(tmp).ignore_err("another process stored it")(compiler/cli/core/check.xo:528)
3.3 Do not turn an I/O error into a default (.ok() ?? []): match it, keep the default for NotFound
only, or mark a deliberate case // xo:allow XO0903 <reason>. Enforced (vet warning XO0903). Source: gotcha
io-error-defaulted-away.
3.4 Prefer the short forms of ?? to a four-line match. Enforced (vet note XO0906). Source: core 2.5, 2.6,
decision 0321.
let n = fields.get(k) ?? continue
let v = fetch(id) ?? e => return Err(LoadErr.Fetch(e))3.5 Turn an Option into a Result with .or_err(E)?; a bare opt? in a Result function is an error.
Enforced (XO0204, XO0205). Source: core 2.5.
3.6 Check inputs with requires ... else E; for a value that cannot be missing, use or_fault or
get_or_fault with a reason. Advisory. Source: core 9.1, 3.6; std.
3.7 Read a map with get, get_or_fault or update, never m[k]. Enforced (XO0410). Source: core 3.6.
3.8 Inside Mutex.with, return a Result as the closure’s value and apply ? outside: an Err returned
from inside undoes the write. Enforced (checker warning XO0540). Source: core 5.4; gotcha
mutex-with-err-undoes-state (a WebSocket flake).
let outcome = q.state.with(fn(var st) -> Result[Job, QueueErr] { ...; Ok(st.ack(id)) })
outcome?3.9 Keep a pure method’s result: xs = xs.reverse(), or use the in-place form (sort_in_place).
Enforced (XO0209). Source: core 2.6; gotchas list-reverse-returns-copy, list-sort-returns-a-copy; std
(sort_in_place, decisions 0374, 0508).
4. Values, collections and strings
Values have value semantics: a copy is independent, and a write to a shared value copies it first. Most rules here avoid copies the program never needed.
4.1 Change a map value with Map.update, not get, change and put. A forgotten put loses the update, and
the copy costs time. Enforced in part (XO0520 error for a mutation never stored; vet note XO0532 for the long
form). Source: core 5.2; gotcha value-semantics-lost-update. A closure that captures still allocates its
environment per call (gotcha map-update-copied-value).
us.update(a, [], fn(var l) { l.push(x) })(compiler/arm64/byref.xo:532)
4.2 Change list elements in place (xs[i].push(v)), not by copying out and storing back. Enforced (vet
note XO0536). Source: core 5.1; gotcha element-copy-store-back-quadratic (a PE link ran past 24 minutes and
did not finish, then took 276 s). Measured: g.blocks[g.cur].insts.push(x) 63,580 µs before in-place updates,
104 µs after (bench/results/nested-update-2026-10-11.md:19).
4.3 Clone a Str or Bytes slice before keeping it: a slice is a view of its parent (decision 0330) and keeps all of it alive. Enforced in part (vet –cost note XO0911). Source: core 3.6; gotchas slice-view-of-reused-buffer, slice-facts-pin-parent. Measured: 2,000 kept views held 2.1 GB, cloned 4.9 MB (bench/results/views-retention-2026-10-22.md:14-15).
out.push(NlMsg{ty: ty, body: buf[off + 16..off + len].clone()})(std/native/llrt/ifaces.xo:165)
4.4 Size a List when the size is known: List.with_capacity(n) (also BytesBuilder and Buf). Advisory.
Source: std; bench/results/capacity-hints-2026-11-05.md:55-56 (0.85 and 0.862 of the time);
bench/results/buf-with-capacity-2026-11-05.md:16 (0.938). Map, Set and StrBuilder take no hint.
4.5 Build text by appending to a value nothing else holds (s += piece, "${s}${n}"); do not build a piece
only to append it. Advisory. Source: perf guide 3 (template render 0.934, 37% fewer allocations);
bench/results/nested-update-2026-10-11.md (rows[0].text += "x" 2,455 µs to 172 µs). A StrBuilder did not pay
for small results (perf guide 8); there is no current A/B of s = s + x in a loop against a builder, so this
rule’s builder half is open.
4.6 Append unchanged runs in one call, not one element at a time. Advisory. Source: gotcha std-port-costly-go-idioms (map_runes 1.20 to 1.38x Go before, 0.86 to 0.90 after).
4.7 Prefer a List indexed by a dense id to a Map; do not memoize cheap work in a Map. Advisory. Source: gotcha memo-map-slower-than-recompute (memos took cancel’s CPU from 3.8 s to 7.6 s).
4.8 Sort by an Int key, not a tuple holding a Str. Advisory. Source: gotcha sort-by-tuple-key-and-map-list-append (7.2 s of a 174.7 s compile).
4.9 Use Bytes for byte data. A List of a sized integer takes 8 bytes per element on native targets
today (10M UInt32 and 10M Int both used 82,706,432 bytes). Advisory. Source: gotcha
sized-int-lists-take-eight-bytes (may change with decision 0369, Proposed).
4.10 Write () for an empty arm body: {} in expression position is an empty Map. Advisory. Source:
core 1.6; gotcha fmt-empty-block-is-map.
4.11 In a pattern a bare name is a variant or a new binding, never a constant; compare constants with
==. Enforced (XO0302, XO0430). Source: gotcha const-in-match-pattern.
4.12 Parenthesize ?? inside a slice bound: digits[0..(Int.parse(a) ?? 9)]. Advisory. Source: gotcha
coalesce-binds-looser-than-range.
5. Performance
Measured costs from std’s ports against Go; each claim carries an A/B result (decision 0340).
5.1 Compute tables and parsed layouts once in a top-level const (computed lazily, once per process,
decision 0332), not per call; global mutable state stays forbidden (XO0416). Enforced for time layouts (vet
warning XO0916), advisory otherwise. Measured: parsing a layout per call 5,344 M instructions, a Mutex map
cache 1,995 M, a lazy const 1,710 M, Go 983 M (bench/results/time-format-cache-variants-2026-10-22.md:12-15).
This replaces the older advice of no package-level caches.
const HTTP_DATE_LAYOUT: time.Layout = time.layout("{weekday:short}, {day} ...")(std/native/http.xo:2045)
5.2 No small List per loop iteration: use locals, or one list made before the loop. Enforced for std (vet –cost note XO0914, checked by cmd/xo TestVetCostStd), advisory elsewhere. Source: perf guide 3 (path parts 0.927 with eight locals).
5.3 In hot loops, read by offset instead of slicing to compare. Enforced for std (vet –cost note XO0913). Source: perf guide 4 (path is_match 1.70x Go to 1.00x).
5.4 Keep values that travel inside other values to a word or two; avoid wide structs passed by value. A copy costs one count per heap field. Advisory (vet –cost note XO0910 above 8 words). Source: core 5.1; gotcha wide-struct-by-value-copies (a 28-word Zone: zone_load 1.744x to 1.001x).
5.5 Before spawning a task, take only the fields it needs (let part = c.field). Enforced at 32 or more
heap fields (vet note XO0534). Source: core 5.1.
5.6 Do not write an unchanged value into a collection that may be shared; test first. Advisory. Source: gotcha shared-list-write-same-value (5.75 MB of copies).
5.7 Hoist string compares and mode dispatch out of hot loops; dispatch on an Int. Advisory. Source: bench/results/strcompares-2026-10-24.md:22-27 (43% of the run in the dispatch).
5.8 In word arithmetic, use the wrapping and logical forms where overflow is wanted or impossible
(wrapping_add, wrapping_shl, shr_logical; mask instead of % 4): + - * << on Int are checked.
Advisory. Source: gotchas std-port-costly-go-idioms (RSA sign4096 7.8%), xo-checked-word-arithmetic,
int-no-wrapping-shr.
5.9 Function values borrow their arguments, so any, filter and fold with a closure are cheap. A
measured fact, not a rule to avoid them. Source: bench/results/callbacks-0337-2026-10-22.md:19-21 (31 to 34%
less after decision 0337).
6. Memory
6.1 Break cycles through callbacks: native memory is reference counted with no cycle collector, so a callback stored in the object it captures is never freed. Drop such hooks on close or on a final error. Advisory (the arm64 goldens’ leak check catches it). Source: gotcha callback-capturing-its-owner-leaks; decision 0395. See core 5.5.
6.2 std and runtime code: keep a value alive while a raw address into it is in use (raw.alive(b) after
the last raw access when the address goes into a field, a block parameter or another function). Enforced in
part (vet –cost warning XO0915 within one block; the compiler keeps it alive within one statement since
decision 0460). Source: gotcha raw-address-outlives-bytes.
7. Concurrency
7.1 Share mutable state only through Mutex or Atomic; closures capture by value. Enforced (XO0502).
Source: core 3.3, 5.1.
7.2 Check and act in one with. Enforced (checker warning XO0804, a split critical section). Source: core
5.4.
7.3 Start every task in a scope or a task.Group that does not escape it; use os.tasks for work that
lives as long as the program. Enforced (XO0801, XO0802, XO0803). Source: core 7.1.
7.4 defer belongs to its block, not the function, and runs shielded from cancellation: put cleanup
there. Advisory (semantics that differ from Go). Source: core 3.5; gotchas defer-differs-from-go,
canceled-sleep-unwinds.
7.5 Use clock.sleep_or_cancel when you must return your own Canceled error; a !is_canceled() loop around
sleep never sees the cancel. Advisory. Source: core 7.2.
7.6 A busy loop must reach a scheduling point (a channel operation, a sleep, an effect call). Advisory. Source: gotcha busy-loop-starves-deterministic-scheduler.
7.7 Under contention use one Atomic.add, not your own compare-and-swap loop. Advisory. Source: gotcha
atomic-cas-loop-contention (1.5 to 2.1x Go’s CPU).
8. Tests
8.1 Where tests go (the owner’s rule, 2026-10-13, decision 0209): inline test blocks next to the code
for small self-contained functions, where the test reads as an example; _test.xo files for substantial
suites and shared helpers. A helper whose signature takes testing types belongs in a _test.xo file: there
testing is predeclared (core 9.2), while in an ordinary file it would need use std/testing, which every
build would then load. Advisory for programs; in this repository std/ and compiler/ use _test.xo only
(decision 0262, a repotest check). Sources: decisions 0209, 0262; core 9.2.
fn check_files(os: testing.TestOs, dir: Str, files: List[(Str, Str)]) -> Str ! FsErr {(compiler/check/check_test.xo:41)
8.2 Builds skip test code (_test.xo files, decision 0348; inline test blocks, decision 0389), so
production code cannot call a test helper and a broken test fails only xo check and xo test. Enforced.
Source: core 9.2.
8.3 opt? fails a test directly in a test body; in helpers write .or_err(...)?. Enforced (XO0204,
XO0205 in helpers). Source: core 9.2; decision 0347.
8.4 Test against the fakes on os: TestOs: FakeNet (handle, fail_next, listen(":0")), MemFs
(fail_next, set_disk_usage) and the virtual clock, which advances when every task is blocked. Real
capabilities need uses net and --real, and such a test must check something only the real one shows
(os.fs is a MemFs). Advisory. Source: core 9.2; std 8; gotcha testos-fs-is-memfs.
os.fs.fail_next(FsOp.Rename, "dst/a.txt", FsErr.Io(path: "dst/a.txt", msg: "disk full"))8.5 Give functions contracts where they state the truth: xo test fuzzes functions with requires and
ensures and shrinks failures; property tests (test "..." for x: T) do the same. Contract clauses must be
pure. Advisory. Source: core 9.1, 9.2.
8.6 In timing tests, prepare every input first and time only the operation. Advisory. Source: gotcha timing-test-inline-inputs.
9. Layout, comments and formatting
9.1 Run xo fmt: two-space indentation, one statement, member or arm per line, runs of blank lines
collapsed, a list broken right after its opening bracket laid out one element per line with a trailing comma.
Enforced (xo build --ci fails on unformatted files). Source: core 10.5; toolchain.md. To keep a comment next
to a list element, break after the bracket (gotcha fmt-comments-in-joined-lists-move); fmt does not align
tables (gotcha xo-fmt-const-table-one-per-line).
9.2 Break lines after a binary operator; start a continuation with . to split a method chain; one
statement per line. Enforced (XO0178 with a fix, XO0161, XO0162, XO0175). Source: core 1.3.
9.3 use lines come before the first declaration and name absolute paths. Enforced (XO0173, XO0158). No
order is required among them. Source: core 4.1.
9.4 Comment with //; document every pub item with /// (Markdown). House style: a file begins with a
// header naming what it implements and its spec section or decision. Advisory. Source: core 1.2; for
example compiler/cli/core/format/format.xo:1-12, std/bufio.xo:1-6.
9.5 A module is one directory; a one-file module is NAME.xo with NAME_test.xo beside it. Advisory.
Source: core 1.1; decision 0262.
9.6 Run xo vet (and xo vet --cost for hot code); its warnings and notes are listed in core 10.6.
dbg left in code fails --ci (XO0901).
Open points for the owner
- 4.5: whether to recommend StrBuilder at all; the evidence on current main favors in-place
+=, and an A/B would settle it. - 5.2 and 5.3 are enforced for std only; whether
xo vetshould report them for every program by default. - 8.1: core 9.2 calls test placement “a note, not a rule”; this file states it as a convention. Whether core 9.2 should point here.
- Rules left out as std or runtime internal (cold-path splitting and the inline budget; bounded
str_find): they belong in the perf guide, not here.