| Status | v0 (playbook + Model A xo test / benches) |
| Related | fixtures.md, implementation.md, pipeline.md, stdlib.md, runtime-abi.md, ../AGENTS.md |
How to add a test, and which proof a change needs. Fixture file layout lives in
fixtures.md. The vertical layer list lives in
implementation.md.
Proof is feature-and-vertical. Each language form is driven through the
deepest stage we claim (lex, parse, check, or run). Crate tests cover the crate
that owns the rule. Hosts (xo, LSP, REPL, wasm, www) present that result.
A language, runtime, std, or CLI behavior change updates the applicable proofs
in the same change (or stacked PR). Policy: ../AGENTS.md.
crate tests pure logic of one crate
echo26 / e26 black-box language contract (candidate binary)
examples/ human-runnable demos stay accurate
xo test + std/test Echo-written suites (std + user libs)
| Proof | Lives in | Run with | Owns |
|---|---|---|---|
| Crate tests | crates/<name>/ #[test] (and crates/xo/tests/) |
cargo test -p <crate> or scripts/gate <layer> |
Decode, unify, resolve edge, pretty-print, LSP protocol, cache keys |
| Echo 2026 | echo26/<area>/<feature>/<NNN>_<slug>.echo + sibling goldens |
scripts/gate echo26 / just e26 |
User-visible lex / ast / check / run against xo (or any candidate) |
| Examples | examples/misc/, examples/app/, examples/algos/ |
scripts/gate examples or xo run / xo check of the touched demo |
Copy-paste programs stay current |
xo test |
std/**/*.echo test.it / *_test.echo / tests/ |
scripts/gate std-test / just std-test / xo test std |
Std and userland assertions (Model A registration) |
Benches (xo test --bench, just std-bench) measure. The language contract is
echo26.
Each proof covers a different surface. Ship every row that applies. Crate tests
stay in the crate. Echo 2026 fixtures stay in echo26/. Demos stay under
examples/. xo test stays for Echo-written suites.
| What changed | Write |
|---|---|
| Pure Rust helper / table / algorithm in one crate | Crate unit test in that crate. Stop if the language surface is unchanged. |
| Token, parse form, diagnostic, check rule, runtime meaning, CLI stage flag | Crate test for the new logic. echo26 fixture (happy path, plus a reject fixture when the rule is a hard error). Example if a human would run or copy it. |
Multi-file import / % @ merge |
echo26/multi or echo26/run/multi (unnumbered support files). Resolver crate tests for graph edges. |
std/**/*.echo API |
Co-located test.it (xo test). echo26/run/<area> smoke when the API is language-visible. Crate test in echo_runtime / echo_codegen only when a new native symbol landed. |
| Host only (LSP method, REPL buffer, wasm playground, www page) | Crate or npm test for that host. If the host change depends on new language meaning, also do crate + echo26 + examples. |
| Docs only | www scripts when public Spec / Reference text moved. |
Red-green: write the failing crate test or echo26 fixture first. Confirm it fails for the intended reason. Then implement.
Test the earliest crate that owns the rule. Runtime print belongs in
echo_runtime and echo26 run fixtures. Prefer table-driven cases for kinds,
codes, and small graphs.
echo_pipeline tests the shared analyze / lower contract.
e26, echo_codegen_abi, and echo_reflection are thin today. Add tests when
those crates gain logic.
Put #[cfg(test)] next to the logic (echo_lexer leaders, echo_semantics
infer, echo_resolver graph, echo_runtime scope, echo_lsp session).
#[test]
fn dual_use_star_is_not_a_leader() {
let lexed = lex_str("$ x = 2 * 3\n");
assert!(lexed.diagnostics.is_empty());
// assert token kinds…
}Prove: cargo test -p <crate> or scripts/gate lexer, semantics, and so on.
One small numbered file is one behavior. Layout and candidate protocol:
fixtures.md.
echo26/<area>/<feature>/<NNN>_<slug>.echo source (required)
echo26/<area>/<feature>/<NNN>_<slug>.lex token kinds (required)
echo26/<area>/<feature>/<NNN>_<slug>.ast kind tree (required)
echo26/<area>/<feature>/<NNN>_<slug>.diag lex codes (omit ⇒ none)
echo26/<area>/<feature>/<NNN>_<slug>.check sem-* / res-* (omit ⇒ none)
echo26/<area>/<feature>/<NNN>_<slug>.run xo run stdout (opt-in execute)
echo26/<area>/<feature>/<NNN>_<slug>.runexit process exit (opt-in)
echo26/<area>/<feature>/<NNN>_<slug>.stdin optional bytes piped to `xo run` (absent ⇒ /dev/null)
Only NNN_*.echo files are suite roots. Sibling files such as user.echo are
imports.
Happy path (run): echo26/run/bind/001_multi.echo plus .run with expected
prints. Reject path (check): echo26/check/bind/001_shadow.echo plus .check
containing sem-shadow.
- Write the tiny
.echo(citedocs/syntax.md/semantics.md/ public Spec). cargo build -p xo -p e26e26 --binary target/debug/xo --filter <area>/<feature>/<NNN> --update- Review the goldens. Unexpected tokens or extra
sem-*codes mean the implementation or the fixture is wrong. scripts/gate echo26(orjust e26).
e26 always runs lex + ast + check. run only if .run / .runexit
exists. That execute path is xo run (AOT + clang + libecho_runtime).
Prefer extending an existing area (lex/, lits/, leaders/, parse/,
check/, infer/, effect/, multi/, run/…) over a new top-level folder.
If a user would xo run the program, keep a demo under examples/misc/ (tiny),
examples/app/ (kitchen sink / HTTP), or examples/algos/ (classic algorithms).
Examples are not goldens. They must stay runnable.
scripts/gate examples checks and runs the finite entries. Skip
examples/app/server.echo in that gate: that process listens until you stop
it. Dirty support modules (user.echo, multi/lib.echo, …) map to their
package entry.
./target/debug/xo run --no-cache examples/misc/hello.echo
./target/debug/xo run --no-cache examples/app/surface.echo
scripts/gate examplesUse implementation.md as the layer list. For a new
language form:
| Layer | Proof |
|---|---|
| Spec | Rule in www Spec / docs/syntax.md (or the layer doc). |
echo_syntax / lexer |
Crate test for the token / leader / dual-use case, and echo26 .lex / .diag. |
| parser / ast | Crate test for the tree shape, and the required .ast. |
| index / resolver | Crate tests for graph / merge / export, and echo26/multi/** or run/multi. |
| semantics | Crate tests for the rule, and .check (omit the file when no sem-*). Add a reject fixture when the feature is a hard error. |
| HIR / MIR / codegen / runtime | Crate tests for lowering / ABI / values, and opt-in .run / .runexit when we claim Run. |
| CLI | xo flags used by e26 (lex / ast / check / run --diag-codes). |
| fmt | echo_ast pretty unit tests (idempotence). No echo26 fmt stage yet. |
| LSP | echo_lsp crate tests over echo_pipeline::analyze. |
| REPL | crates/xo/tests/repl_forms.rs (JIT). Language meaning stays in e26 AOT. |
| wasm / www | cargo test -p echo_wasm; scripts/gate web. Playground run is a host demo. |
| std | test.it plus echo26/run/<pkg> when the API is user-visible. |
| examples | Touched demo still runs. |
Parse-only work still needs .ast. Check-only still needs e26 check. Claiming
Run without a .run fixture is incomplete.
A feature is suite-complete when
implementation.md §7 holds: spec matches code, the
shared pipeline implements meaning through the deepest claimed stage,
diagnostics have stable codes (and a reject fixture for hard errors),
e26 --binary xo is green, touched crate tests are green, touched examples
and xo test suites pass, and hosts only present the rule.
Roadmap §7 is the feature × layer honesty matrix. Keep it current when a vertical lands.
scripts/gate is the focused dispatcher. See
development-speed.md.
| Command | What it proves |
|---|---|
gate changed |
Dirty files → the smallest useful checks (crate layer, echo26, std-test, examples, web, docs, tools) |
gate echo26 |
Build xo + e26, run the Echo 2026 suite. PR hard gate. |
gate std-test |
xo test std (AOT). Dirty std/**/*.echo routes here. |
gate examples |
xo check / xo run of finite example entries. Dirty examples/** routes here. |
gate <crate-layer> |
cargo test -p echo_* (lexer, semantics, std = echo_std crate, …) |
gate workspace |
rustfmt + cargo check + nextest/workspace Rust tests |
gate web |
www lint / format / docs+prose+std-ref scripts / build |
PR CI (docs/ci.md) runs a subset of cargo test -p xo (non-JIT),
xo run examples/misc/hello.echo, and gate echo26. REPL / JIT tests stay
off in CI while run_jit_ir SIGSEGV under those LLVM loads is open.
e26 execute is AOT (xo run). xo run --jit, REPL, and wasm playground-run
share echo_runtime_* symbols and use different hosts.
# 1. crate that owns the new logic
cargo test -p echo_semantics
# 2. fixture + goldens
e26 --binary target/debug/xo --filter check/bind --update # review the diff
scripts/gate echo26
# 3. std / examples if those surfaces moved
xo test std/math.echo
./target/debug/xo run --no-cache examples/misc/hello.echo
# 4. dirty-file dispatcher
scripts/gate changed --explain
scripts/gate changedDo not --update the whole suite unless the change is suite-wide on purpose.
User and std suites written in Echo. e26 goldens stay in echo26/.
- Suite files call
std/testhelpers at top level (e.g.test.it,test.bench). - Those helpers register cases via
runtime.test_register/runtime.test_bench_register(std only). - Registration is active only when the host sets
XO_TEST(done byxo test). - After the entry top-level body,
runtime.test_finishruns the selected kind of cases and becomes the process exit status (failure count). Underxo run, finish returns “suite off” so normal programs are unchanged. xo testruns onlytest.itcases.xo test --benchsetsXO_BENCHand runs onlytest.benchcases (harness-looped auto-N).
/ std/test
test.it("name", () {
test.eq(1, 1)
test.true(|)
})
test.bench("hot_path", () {
$ n = abs_i(-3)
test.eq(n, 3)
})
Bodies may also be named binds ($ case = () { … }; test.it("n", case)).
| Export | Role |
|---|---|
it(name, body) |
Register a zero-arg function value as a test case |
bench(name, body) |
Register a zero-arg body; harness calls it N times |
eq / ne |
Assert deep equality |
true / false |
Assert bool |
fail(msg) |
Mark current case failed |
Custom libraries may wrap these; only privileged std talks to / runtime.
- Body is zero-arg; the runtime scales N until one measured run lasts
about 1s (capped), then reports
Nand ns/op. - Assertions (
eq/fail, …) still work inside a bench body; a failed assert fails the bench. - Co-located next to production or tests (same file as
test.itis fine). - Registration is a no-op without
XO_TEST; benches run only withXO_BENCH. - Prefer no asserts in the hot body so
ns/opmeasures the work, nottest.eq.
Benches call real functions (same bodies as demos / std), with args built in the bench body. No separate “canary wrapper” module.
| Location | Examples |
|---|---|
std/list |
sum_ints_1k, sort_ints_1k |
std/bytes |
checksum_1k, checksum_64k |
std/crypto/hash/sip |
sip_empty … sip_64k |
std/crypto/hash/sha256 |
sha256_empty, sha256_1k |
std/collections/map |
seed_1k_get |
examples/algos/* |
fib, gcd, sum_to, sorts, primes, call overhead |
just bench-host
just std-bench # all std co-located benches
just algo-bench # algorithms + selected std hot paths
xo test --bench -O2 std --bench-out .xo/bench/last.jsonl
xo test --bench -O2 std --bench-out .xo/bench/last.jsonl \
--bench-baseline .xo/bench/baseline.jsonl --bench-threshold 20Example output:
bench abs_i N=25000000 40ns/op (1000000000ns)
xo test --bench: 1 passed, 0 failed, 1 total (…)
While benches run, each finished case can be appended as JSONL (one object
per line) when --bench-out PATH is set. The file is truncated at the start of
the run, then grown as cases complete (safe to tail -f).
| Flag | Role |
|---|---|
-O / --opt-level LEVEL |
LLVM opt for suite compile: 0/1/2/3/z (same as xo run). Default 0. Prefer -O2 for benches. |
--bench-out PATH |
Stream JSONL results (requires --bench) |
--bench-baseline PATH |
Prior JSONL to compare after the run (needs --bench-out) |
--bench-threshold PCT |
Exit 1 if any case is worse than baseline by more than PCT% ns/op |
JSONL fields: v, file, name, opt (O0…Oz), status (ok/fail),
and on success n, ns_per_op, total_ns.
Compare keys are file::name@opt so O0 and O2 baselines do not mix.
Compare lines look like:
REG std/math.echo::abs_i@O2 1000 → 1300 ns/op (+30.0%)
IMP std/str.echo::cat@O2 2000 → 1800 ns/op (-10.0%)
NEW std/list.echo::sum@O2 500ns/op
summary: 1 regression(s), 1 improvement(s), 1 new, 0 gone
Save a local baseline after a good run:
xo test --bench -O2 std --bench-out .xo/bench/last.jsonl
cp .xo/bench/last.jsonl .xo/bench/baseline.jsonl
# later:
xo test --bench -O2 std --bench-out .xo/bench/last.jsonl \
--bench-baseline .xo/bench/baseline.jsonl --bench-threshold 20Keep the opt level fixed when comparing. Absolute ns/op is not portable
across machines; use relative deltas on the same recipe.
- Host: run a prebuilt
xo(target/debug/xoor release). Do not foldcargo build -p xointo every bench iteration. That measures the Rust toolchain, not Echo. - Suite files: each
.echois still compiled to an AOT child, but IR and AOT artifact caches (under.xo/cache/) make the second run mostly load + exec. Use--cache-statusto confirmhit; use--no-cacheonly for cold pipeline experiments. just std-benchuses a prebuiltXObinary and passes--cache-status.
xo test # ./ *_test.echo and tests/** (+ std/ co-located suites)
xo test path/to/file.echo
xo test path/to/dir
xo test '**/*_test.echo'
xo test a_test.echo tests/
xo test std # co-located std suites (`test.it`)
xo test --bench # only files with test.bench (default discovery)
xo test --bench std # all co-located std benchmarks
xo test --bench std/math.echo- Each matched file is compiled and executed as a suite entry with
XO_TEST=1. - With
--bench, alsoXO_BENCH=1. Only benchmarks run;test.itis skipped. Discovery skips files that do not containtest.bench(. - Without
--bench, onlytest.itruns; registered benches are ignored. - Paths: files, directories (walk), or simple globs (
*,**,?). - Directory convention:
*_test.echoanywhere- all
.echoundertests/ - under a
std/path: co-located suites that calltest.it(/test.bench(
Either is allowed:
- Separate
map_test.echonext to production (recommended for libs) - Same file with top-level
test.it(...)/test.bench(...). Cases run only underxo test/xo test --bench, not underxo run(registration no-op withoutXO_TEST)
Production APIs still use normal \ exports for importers.
- e26 goldens (use
echo26/+e26) - Magic test keywords
- Userland
/ runtimetest hooks - Fuzzing, allocation counters, or statistical (Criterion-style) benches (later)