The guide
README.md is a thin pointer. This is where the depth lives: what the
language is, why it’s built this way, what exists, and an index of
every design document. History — what shipped when, and what each
slice found, including corrections — is ROADMAP.md.
Neither belongs in the index below, which says what each document
settles, not how it got there.
Design lands here before the code that implements it, which is the
cheap place for it to be wrong. When it turns out wrong anyway, the
document that made the claim is corrected in place rather than quietly
edited — ROADMAP.md says which.
Where this came from
cancho began as the lower-level sibling of Lex, a high-level, interpreted
language, and took its worldview from it: ownership and authority as one
system, defined behaviour, a canonical content-addressable syntax tree. That
is history. cancho stands on its own now: it has its own compiler, its own
standard library, its own packages and its own tools (cancho authority,
cancho-vcs, the products listed in the repositories of this organisation),
and it depends on nothing from Lex or from lex-os, the agent runtime Lex had.
If a runtime that confines a program to what its authority report says is
needed, it is built in cancho, from the report the compiler already writes.
The documents that compared the two (vcs.md,
under-a-grant.md, related-work.md)
are kept as the record of what was measured and decided; they are not a plan
to integrate with Lex.
Why
Three things this philosophy buys that no systems language currently combines:
- Capabilities all the way down. Ownership and effects are the same
idea — both are resource tracking. Allocation is an effect, a heap
value is a linear resource, FFI is a capability you must be granted.
Owning a capability discharges its effects and borrowing declares
them, so
main’s row is[]however much it does. - Determinism as a language property. No UB, defined evaluation
order, deterministic layout — which is what makes replay, attestation
and content-addressing mean anything, and exactly what C throws away.
Written out operation by operation in
defined-behaviour.md. - A checker that is fast and total, because the guarantee is only worth what it costs to verify.
What exists
| Settled by | ||
|---|---|---|
| Capabilities | World, Io, Fs(prefix), Ffi(lib), Heap, Args, File, Net(bound) — linear values, split once, released by name | linearity-and-effects.md |
| Effect rows | A canonically ordered set, exact in both directions, every label tracing to a builtin | linearity-and-effects.md |
| Narrowing | Prefix extension, one way, and it consumes what it attenuates | filesystem.md, reach.md |
| Authority report | cancho authority, computed from reachability; --output json for a supervisor, and it fails closed on foreign code | authority.md |
| Borrowing | Lexical regions, no borrow checker; &! is a lock on the binding | aliasing.md |
| Memory | Arenas, a general heap with recursive types, boxed slices, growable buffers | heap.md, boxed-slices.md |
| Types | int byte bool float (and f32 from edition 6), structs, enums with exhaustive match, tuples, generics with [T: val] bounds | floating-point.md, tuples.md |
| Defined behaviour | Checked arithmetic that traps, left-to-right evaluation, every C hole named and closed | defined-behaviour.md |
| Threads | Compiler-provided spawn/join, never crossing the C ABI; one pointer-width payload today; atomics designed, not built | threads.md, atomics.md |
| Program identity | cancho ids — per-declaration content hashes, checked against golden fixtures | canonical-ast.md, hash-stability.md |
| A content-addressed op log | cancho vcs publish/log — every declaration as a typed, gated operation | vcs.md, vcs-publish.md |
| Compile time | Pure calls on constant arguments folded; static items whose bodies run during compilation | compile-time-data.md |
| I/O | The console in three directions, file handles as linear resources, bulk reads and writes, a Net capability for sockets | file-handles.md, bulk-io.md, net.md |
| Refusals | Every rule carries a stable tag; check --output json reports every independent one as data | agent-errors.md |
| Standard library | Written in cancho, including shortest round-trip float printing and a UTF-8 decoder | standard-library.md |
| Two backends | Cranelift (dev) and LLVM (release, default) — an opt-in second backend became the default once nothing it refused had an asker left | llvm-backend.md |
What is not there yet, and why, is ROADMAP.md.
The language in one page
A value is res or val. A res value is consumed exactly once on
every path. Mode is structural — a res member makes the whole aggregate
res — so Held[File] is res where Held[int] is val.
res struct Ticket { serial: int }
fn redeem(t: Ticket) -> [] int {
let Ticket { serial } = t; // the whole is spent, the parts produced
return serial; // `int` is `val`, so nothing is owed now
}
There is no drop and no destructor: a resource is destroyed by naming the
function that knows how, which is what keeps an effect row honest once there
are effect rows.
Looking without spending is a borrow. borrow freezes for a read,
borrow mut locks for a write — nothing else may touch the value at all,
not even a read.
fn serial_of[&r](t: &r Ticket) -> [] int { return t.serial; }
borrow held as &r in {
putchar(i, 48 + serial_of(r)); // `held` is frozen; `r` reads it
} // owned again here
There is no borrow checker. A region is a block, so a reference’s
validity is lexical rather than inferred: no non-lexical lifetimes, no
variance, no dataflow. A binding is Owned, Frozen or Locked, set at
block entry and restored at block exit; r_inner <= r_outer holds exactly
when the outer block encloses the inner one, which is a walk up a stack; and
escape is an occurs-check over one type.
An effect is a borrowed capability.
fn triple(n: int) -> [] int { return n * 3; } // pure, and says so
fn show[&i](io: &!i Io, n: int) -> [io_write] int { ... } // borrows the console
fn main(world: World) -> [] int {
let Split { io, ffi, fs, heap, args } = split(world); // the one source of authority
release(args); release(heap); release(fs); release(ffi); // unused authority is still a resource
borrow mut io as &!i in { show(i, 7); }
release(io); // destroyed exactly once
return 0;
}
[io_write] on a signature means the function was handed an &!i Io it did
not create — so reading the row and reading the parameter list are the same
act, and a function that was given nothing cannot print however much it
wants to. There is no ambient constructor: Io { } is refused, main’s
World is the only authority in the program, and it is linear, so forgetting
to release it does not compile.
main’s own row is [] even though it prints, because it owns the
capability rather than borrowing one — and ownership is already visible in
the parameter list.
A row is a canonically ordered set, so it hashes, which is what per-unit
identity is made of. It is exact in both directions: performing an effect you
did not declare and declaring one you never perform are both errors, because
an inexact row means [] stops meaning pure. And there is no list of legal
labels — every label traces back to a builtin that performs one, so a label
with nothing underneath it is refused the moment it is written.
A foreign call is the same idea pointed at C.
extern fn labs[&f](ffi: &f Ffi("libc"), n: int) -> [ffi("libc")] int;
let libc = narrow(ffi, "libc"); // `Ffi("")` -> `Ffi("libc")`, one way only
borrow libc as &f in { n = labs(f, 0 - 7); }
Narrowing is prefix extension and goes one way. Ffi("") names no library,
so it authorises nothing until narrowed; an Ffi("libcrypto") can never
become an Ffi("libc"). It also consumes what it attenuates, so there is
no way back to the wider capability. The capability is checked and then
erased: what libc receives is the integer and nothing else.
Memory has three shapes, and the same region machinery checks all of them.
region a { // an arena: released in one call
let xs = alloc_slice[a](5, 0); // xs : &!a [int]
xs[0] = 3; // bounds-checked; out of range traps
}
let node = box(h, Node { value: 1 }); // the heap: `Box[T]` is `res`
let tail = unbox(h, node); // the only thing that ends one
An arena is a region — same block, same parent chain, same occurs-check.
Nothing whose type mentions a leaves the block, and release is one free
whatever was allocated. Box[T] is res, so the exactly-once rule written
for capabilities and file handles turns out to be a memory safety rule
for free: no leaks, no double frees, no use-after-free, none of them checked
by anything new.
A string is a run of bytes and claims no encoding. str is not a type;
a string is &r [byte], an ordinary slice and therefore an ordinary
reference, so regions, the escape check and the unique-to-shared coercion
all came for free.
Arithmetic is checked. +, -, * and unary - produce the right
answer or trap — they never wrap. Wrapping is expressible but has to be
asked for by name. Division traps on a zero divisor and on int::MIN / -1
rather than being undefined. Evaluation order is left to right everywhere,
including a struct literal’s fields, and that is enforced rather than merely
intended.
Real programs run on this, byte-for-byte checked against the originals:
GNU coreutils base64 and sort (porting.md), a REST
endpoint answered over a real socket (net.md), and
examples/seek/, not a port — a search tool shaped for an agent calling it
rather than a person typing at a shell (agent-tools.md).
Related work
cancho builds on other people’s ideas, and the closest of them got there first.
- Austral — the nearest relative: linear types, capabilities as linear
values with a root capability handed to the entry point, lexical
borrowing, no borrow checker. Most of this core is Austral’s first.
cancho adds the same authority stated as an exact effect row, which
is what
cancho authorityreads. - Koka — effects as a row of labels. Kept the row; no handlers, no row polymorphism.
- Effekt — effects as capabilities, from the effect-handler side: the closest statement of “an effect is a borrowed capability”.
- Cyclone — lexical regions; here without Rust’s inference.
- Rust — ownership as move; the borrow checker declined.
- Vale — generational references, which
Genis. - Zig —
defer. - Pony, Hylo — other answers to aliasing and ownership. Pony’s
valmeans something different from this one. - The object-capability model (E, Robust Composition) and Capsicum — no ambient authority.
- Lex — where the worldview came from (history; cancho no longer depends on it).
The competitor is WASI, not Rust. For running code you did not write,
WebAssembly with WASI enforces authority at run time, by trying. cancho
knows it before execution, from the program’s text, with no runtime
cost. What each project contributed, traced to the document that used it:
related-work.md.
Design commitments
| Area | Commitment | Why |
|---|---|---|
| Memory | Linear/affine types + regions/arenas — not an NLL borrow checker | Local, cheap, total to check |
| Effects | Capability-typed effects in the type system, unified with linearity | One resource system, not two |
| Behaviour | No UB, defined evaluation order, deterministic layout | Reproducibility is load-bearing |
| AST | Canonical, content-addressable, stable per-unit identity | Designed in, never retrofitted |
| Types | Fast, total, decidable inference | The guarantee must be cheap |
| Metaprogramming | Hygienic deterministic comptime — no textual or proc macros | Macros break stable content-addressing |
| Generics | Monomorphised | Zero-cost, matches Rust’s codegen |
| FFI | Explicit, capability-gated | C’s effects must not be invisible |
| Backend | LLVM by default, Cranelift for fast dev builds | Reuse; own backend only if zero-C becomes a goal |
Taken from Lex’s design, long ago: examples-as-tests, [budget], effect declarations as
the function’s contract.
Explicit non-goals
- Not a Rust clone. No trait-system maximalism, no GATs, no specialisation, no borrow checker. Chasing Rust’s power means inheriting Rust’s implementation cost and abandoning totality — the failure mode this design exists to avoid.
- Not self-hosting-first. The compiler started in Rust, as a starting
point, not an end-state; it is now being written in cancho itself, stage by
stage, with the Rust compiler as the check at each one
(
self-hosting.md).
Performance
Linearity and effects are erased at compile time; generics monomorphise.
The gap to C is a backend question, not a price of ownership: Rust sits
within 4% of C on the same kernels
(against-c-and-rust.md), and once the overflow
trap and a vectorising backend (--backend llvm, now the default) are
both in place, this language measures statistically indistinguishable
from C on the one kernel checked directly against it.
The derivation — what the trap costs, what each backend can and cannot
express, and every kernel measured — lives where it was measured rather
than restated here: overflow-cost.md,
check-cost.md, backend-limits.md,
llvm-backend.md,
benchmarks-game.md.
Layout
crates/cancho-syntax lexer, canonical-shaped AST, parser
crates/cancho-types the type vocabulary: representation and unification
crates/cancho-ir resolution, type checking, monomorphisation; the IR
crates/cancho-codegen Cranelift lowering, native object emission
crates/cancho-codegen-llvm the LLVM backend, --backend llvm (default)
crates/cancho-id canonical encoding and content hashes
crates/cancho-vcs content-addressed operation log (vcs.md)
crates/cancho the CLI
std/ the standard library, as Lex source
examples/ programs meant to be read
packages/ published cancho-vcs packages (package-system.md)
tests/accept fixtures that must compile and run
tests/reject fixtures that must be refused, each stating why
benches/ checked/wrapping pairs; what the overflow trap costs
scripts/bench.py runs them and prints the table
docs/ this directory
Everything a program can be refused for is refused in cancho-ir, so the
backend has no error path for a program — only for the environment. Each
fixture declares its own expectation in its header (//~ STDOUT, //~ EXIT, //~ ERROR), so adding a rule to the language means adding a
fixture.
Every design document
One line of purpose, one short phrase of status. What each slice found —
the measurements, the bugs, the corrections — is ROADMAP.md,
not repeated here; a doc’s own header carries its own detail.
| Doc | Purpose | Status |
|---|---|---|
linearity-and-effects.md | Core type-system rules: linear/affine ownership, capability-typed effects, how they unify | settled and built (#2) |
bootstrap.md | Where self-hosting stands and how to use the compiler in cancho; below it, what M0 settled (host language, file extension, the M0 surface) | current; history below |
canonical-ast.md | AST shape, canonicalisation, per-unit identity (cancho ids) | written and built |
memory-model.md | Regions/arenas, escape, the escape hatches | not written — settled piecemeal by heap.md/sharing.md; nothing left to write on its own |
strings.md | A string is bytes, not an encoding; byte as storage | settled and built |
boxed-slices.md | Box[[T]]: a pointer and a length | settled and built |
many-files.md | A program in more than one file; identity by content | settled and built |
arguments.md | The Args capability | settled and built |
reading-references.md | Reading through a reference; a reference gives references | settled and built |
heap.md | Heap and Box[T]: one value, one allocation | settled and built |
filesystem.md | Fs(prefix), the path check | settled and built |
sharing.md | Why Rc is not expressible, and Gen | settled and built — corrects linearity-and-effects.md §9 |
tuples.md | (A, B) as a structural anonymous struct | settled and built |
shadowing.md | Shadowing: allowed exactly when the binding is dead | settled and built |
standard-input.md | getchar: a second label on Io, not a new capability; read_bytes (edition 9): the bulk read behind the same label, 73x getchar on macOS | settled and built |
modules.md | module/import/pub; a module is a namespace, not a trust boundary | settled and built |
standard-library.md | What belongs in std, and how a function earns its way in | written and built |
mode-polymorphism.md | [T: val] bounds | settled and built — found a leak and a double free |
collections.md | Which collections hold a resource, and why it’s about shape | settled and built |
json.md | std.json: a strict, zero-copy tape parser and a Writer that cannot write bad JSON | built; 13,000 numbers bit-exact against Rust, 1,500 mutated documents against serde_json, 355 MB/s to the tape; parse_with (§3.1): no region per call, 5.8x on macOS and 8.7x more at 16 threads, identical to parse on 58,733 documents and the 318 files of JSONTestSuite |
map.md | std.map: a hash map from byte strings to copyable values, iterating in insertion order | built; tested against a model over 30,000 random operations; found the LLVM backend growing the stack inside loops |
http.md | std.http and std.route: a strict request parser (refuses what request smuggling lives on) and a router that answers with an id you match on | built; 1,089 requests identical to httparse field for field, none accepted that it refuses; 1.0-1.4 µs a request, flat to 5,000 routes |
server.md | examples/api: a JSON API server over std.http, std.route, std.json and native sockets – one thread, a Poller, keep-alive, pipelining, no Ffi | built; 17 tests over real sockets; migrated off poll(2)/libc (§8): authority now exact, throughput 134,000 → 73,000 a second – the kernel’s epoll in this VM, not the program (a C server falls the same way) – still ~25× FastAPI |
http-server.md | http.server: the server loop as a package – wait/next/respond, the application owns its loop; why a callback does not type-check; hold/answer for a request whose answer is not ready, and the server’s poller shared with the application’s own handles (§10); the byte-fed mode (§11): open_bytes/attach/input/output/detach for a server with no sockets, so it can sit behind the TLS engine, and examples/https_hello that does; streaming request bodies and Expect: 100-continue (§12): limits, then body_part/body_take/body_state/proceed on a held request’s ticket, in-place chunked decoding, rule-tagged refusals, a head deadline and a body stall timeout | built; first package that imports std; examples/api migrated, 21 + 2 tests, ~7% faster than the loop inside the example; §10 tested against 7 mutations; §11: 15 tests without a socket, 79 of 90 mutants, HTTPS example against curl, OpenSSL and Python, cost measured; §12: 22 tests without a socket and 4 with, uploads to 1 GiB hashed, mutants (§12.13) |
http-client.md | http.client: a non-blocking HTTP/1.1 client for a program with one poller (cancho-gateway’s upstream side, http-server.md §11.8 item 4): a table of connections fed bytes and returning events, no I/O and no authority in the package; streamed request bodies with back-pressure, status and headers as soon as complete, bodies in pieces by length / chunked / until close, Expect: 100-continue, 1xx skipped, early responses; pooling with the strict reuse rules and the one retry of cancho-hooks §53; timeouts on the caller’s clock; TLS to upstreams through packages/tls in the example’s driver; DNS the caller’s | built; 118 unit tests on both backends, 517 mutants, a million damaged responses, live against Python, nginx, examples/https_hello and a hostile server over plain TCP and TLS 1.3, measured cost (§10) |
parallelism.md | threads and vectorization: what exists, what was measured (LLVM vectorizes what cannot trap; a multi-field payload crosses by reference; DuckDB on one core), two ways of keeping the overflow guarantee that did not work, and the order to do it in, each step with a gate that can fail | strategy, with its measurements (benches/parallel/) |
atomics.md | atomics and a bounded channel: hard constraint, or nobody asked? – nobody asked (the compiler already emits an atomic, both backends have all five operations, &Atomic already crosses); one res Atomic of n words in heap memory because a borrow copies, sequentially consistent only, row conc, edition 8; and the finding that the checker does not stop two threads writing one value (threads.md §3 corrected). A gate fixed first: litmus tests with a power check, stress, nine mutants, a model check; stages A0-A3 | design, with measurements (benches/atomics/); not built |
zeroed-slices.md | box_slice(h, n, 0) (and byte_of(0), false, 0.0) is calloc with no fill loop; untouched pages are never resident. jsonq: 0.46 s to 0.15 s, 427 MB to 89 MB peak | built on both backends; freed-memory and residency checks; seven of eight mutants killed, the eighth equivalent |
checked-output.md | flush_out(io) -> [io_write] Done, edition 5: flush standard output and learn whether what was written arrived – fflush and then ferror, because fflush answers 0 after an earlier failure emptied the buffer (measured); gap L1 of agent-toolbox.md | built on both backends; /dev/full, a closed descriptor and an earlier failure each observed; seven mutants killed |
memory-moves.md | copy_within(buf, dst, src, n): a bounds-checked memmove inside one slice, edition 5; why a byte loop cannot do it (overlap, direction, 0.5 GB/s: 114.7 ms to compact a 64 MiB arena) and what the two backends emit | built, with a fixture, nine trapping shapes on both backends and three mutants checked |
byte-search.md | index_of_byte(text, b): the first b in a slice as one memchr, edition 5, and std.bytes’ find and count_byte on it; four to five times faster when the byte is rare, and the measured LLVM regression when a needle’s first byte is common | built, with fixtures on both backends and mutants checked |
word-scan.md | load_le64, byte_mask64, trailing_zeros, leading_zeros, popcount (edition 9): scanning memory a word or 64 bytes at a time with a checked unaligned load, a 64-byte compare mask and the bit counts, on both backends and wasm32-wasip1; measured against the byte loop and memchr on 1 GiB, on arm64 and x86-64 | built, with fixtures on both backends, a differential test against Rust over random buffers and every offset and length, and the trapping edges |
bulk-copy.md | copy_into(dst, src): all of one slice onto the front of another as one memmove, edition 5; why overlap is possible, and std.buffer’s copies rewritten on it (640 MiB of appends: 2.00 s to 0.026 s on Cranelift, 0.47 s to 0.026 s on LLVM) | built, with a fixture on both backends, six trapping and edge shapes, and mutants checked |
native-sockets.md | Servers without Ffi("libc"): typed Listener/Conn/Poller handles, Clock, and the four-stage road to a toolchain with no C | slice 1 built (edition 5): tcp_listen/tcp_accept/conn_read/conn_write over typed handles on both backends, no Ffi; fixed-signature fd builtins rejected because an int descriptor is forgeable (§2); Poller, Clock, tcp_connect next; tcp_listen_on (edition 9): listen on one IPv4 literal, with the host in the authority row (§12) |
udp.md | Datagram sockets without Ffi: a connected Udp for one upstream (net_out), a bound one for a server (net_in), replies to a sender only through a ticket the runtime issued; no sendto with a free destination | built, both halves (udp_connect/udp_send/udp_recv/Poller; udp_bind/udp_recv_from/udp_send_to with peer tickets); both backends; round-trip measurement not done (#355, #356; asker cancho-dns#18) |
signals.md | Signals/SignalWatch (edition 6): knowing you were asked to stop without Ffi("libc") – a capability narrowed to the signals it claims, so the authority report names them and stays bounded; a bitmask signals_pending, a handle the Poller waits on, and signals_close for “a second signal kills at once” | built on both backends: signalfd on Linux, measured; kqueue EVFILT_SIGNAL on macOS, written and not run |
conn-peer.md | The other end of a connection: conn_peer(&Conn, &![byte]) (getpeername, 19 bytes, no effect row, not authority), std.addr (Peer, canonical text, parse, IPv4-mapped normalised, a /32 and /64 key) and std.conns.peer; asked at call time, not stored at accept; the socket peer, not an identity (PROXY protocol out of scope) | built on both backends; examples/tls_echo logs it and bounds by it (#367); Linux and macOS arm64 run, x86-64 is CI’s; WASI refuses it |
directory-handles.md | Dir (edition 6): a path opened beneath a directory one component at a time, following no link – the fix for a symlink inside a narrowed prefix or a tool’s --root reading outside it (#227, gap L6). open_dir, dir_enter, dir_open_read, dir_close, and std.dirs for a whole path; measured against openat2 | slices 1 and 2 built on both backends (read beneath; create, append, rename, remove, sync beneath); cancho-tools on top is slice 3; slice 4 (edition 7) is dir_rename_new, the rename that never replaces a name, with the filesystems measured |
processes.md | Exec (edition 7): starting a program without Ffi("libc") – a capability narrowed to a path prefix of the programs it may start, a linear Child that must be reaped, Pipe ends the parent cannot leak, an exact environment and descriptor set, and the Poller waiting on both (#237, cancho-tools#10) | slices 0 to 6 built: close-on-exec everywhere, the capability with children and channels, the Poller watching both, std.process.capture (#271, #274, #275, #276), and the MCP server that uses them (cancho-tools#17); slice 5, the two prerequisites of a run tool: a child’s working directory (exec_spawn_in, §4.10) and capture_both (§7.2); letting go of a child for a job-starting server – exec_spawn_detached, child_pid, child_release and the exec_detached label (edition 9, §4.11, #524); stopping what a detached worker started – child_kill_group, which capture uses on its kill paths (edition 9, §4.12, #533). Slices 0 to 6 are built, and the status line says which is which (#536) |
directory-listing.md | Listing a directory and a file’s status on a Dir handle rather than a path (#222, gaps L2 and L3): dir_list, dir_next, dir_list_close, dir_stat, and std.dirs.list sorted bytewise; the dirent/stat layout table for three targets; measured cost per entry | slices 1 and 2 built on both backends (dir_list, dir_next, dir_list_close, std.dirs.list; dir_stat); the list tool is slice 3 |
foreign-authority.md | What a program that calls C can reach, symbol by symbol: Ffi("libc,libssl") is a set of libraries (one capability, lent narrower), the report lists every reachable foreign symbol as scope:symbol (unbounded_by, one a line, so a CI pin diffs an added symbol), and a foreign function must borrow exactly one Ffi | built on both backends; closes a hole: an extern fn with no capability was accepted, ran a shell and reported bounded: true. Symbol-level rows and capabilities weighed and declined; the statx argument shape and \x found, pinned, not fixed |
narrowing-into-several.md | Why a program that reads two unrelated files (/dev/urandom and a CA bundle) holds Fs("") and reports fs_read(""): narrowing consumes, so one Fs makes one chain. Options weighed (a set of paths, a remainder, a fork from a borrow, lending a root narrower, directory handles); recommended: narrow(fs, "a", "b") answers a tuple of children, for Fs and Exec, with no report change. Found a latent row bug: Label::covers compared path labels by byte prefix; fixed in PR #359 | built (PR #364; the covers fix first, PR #359); all six questions decided as proposed 2026-10-07; found by cancho-pg#14; examples/tls_echo_fixed is the payoff |
websocket-spike.md | A WebSocket/OCPP connection layer on cancho against the lex runtime: 10,000 connections, measured, plus the gaps found and the cancho-cache-for-Redis question | measured: 80 MB and 0% idle CPU at 10,000 connections (the runtime’s thread-per-connection stand-in saturates a core near 1,500); the service-level gaps are listed in §10 |
formatting.md | cancho fmt: the canonical layout with comments, blank lines and literal spellings kept | built; refuses, rather than risks, a file it cannot reproduce |
testing.md | trap() and std.test’s assert — what a program needed to state “this must be true” at all | trap(), std.test, and cancho test are built; no per-test timeout, no message on a failed assertion |
satisfy.md | The spec-to-implementation loop: a contract is a .cho file whose tests call the candidate; satisfaction is check plus test composed, with the verdict and the recomputed SigId answered (#406) | built; the first contract (tests/satisfy/) is CI-checked |
slicing.md | s[a..b], half-open and trapping | settled and built |
defer.md | defer E; as sugar, expanded during lowering | settled and built |
authority.md | cancho authority; release at main is the declaration | settled and built |
budget.md | Whether [budget] is a type-system feature | settled — no |
reach.md | What a program can reach, via a real REST endpoint | settled and built; corrected by opaque-pointers.md and foreign-linking.md |
opaque-pointers.md | c_ptr: one opaque foreign-pointer shape | settled and built (edition 3) |
foreign-linking.md | -l/-L: linking beyond libc | settled and built — examples/tls_client/, a real handshake |
tls-nonblocking.md | https for a service with one thread: a non-blocking OpenSSL client on the Poller (memory BIOs), certificate verification with every failure told apart, the authority it costs, a resolver that does not stop the loop and a pinned address, what cancho-hooks would change, and 12 gaps with reproducers | spiked and measured (examples/tls_nb/): 64 handshakes on one thread, ~0.6 ms of CPU each (0.3 resumed), 26-48 KiB a connection; not built into any service; OpenSSL through FFI by decision, a pure cancho TLS is a research project |
overflow-cost.md | What the overflow trap costs, measured | measured — corrects the README and defined-behaviour.md §2.1 |
bitwise.md | & | ^ ~ << >>, hex literals | settled and built |
porting.md | Real programs ported and checked byte-for-byte: base64, sort | done twice |
against-c-and-rust.md | cancho against C and Rust on the same algorithm | measured |
purity.md | The checked purity proof C can only promise and Rust can’t state | measured and unspent |
floating-point.md | float, IEEE-754 binary64 | settled and built |
float-printing.md | Shortest round-trip decimal printing | settled and built |
f32.md | f32, IEEE-754 binary32, asked by cancho-gpu: the type, the f32 suffix, f32_of/float_of32/bits_of32/f32_of_bits, the bit-for-bit gate against binary64, sqrt32 and the int conversions, and std.fmt32 (Rust’s {:?} and {:.N} and a decimal read straight to the nearest f32, every 2³² pattern checked) | F1, F2 and F3 built (edition 6); F4 cancho-gpu not |
compile-time.md | Constant folding and pure-call evaluation | settled and built |
compile-time-data.md | static items evaluated during compilation | settled and built |
layout.md | What every leaf costs; packing and transposing | measured, deferred with a trigger (cancho layout) |
benchmarks-game.md | Five kernels from the Computer Language Benchmarks Game | measured |
bulk-io.md | write_bytes: a whole slice in one call | settled and built |
file-handles.md | An open file as a linear resource | settled and built |
file-writes.md | The write side of a file handle: append, positional read/write, fsync, truncate, rename, lock — what a durable log needs; fopen-based opens to avoid variadic open | slice 1 built (edition 5): open_append/open_write/open_new/open_rw, file_write/file_pwrite/file_pread/file_sync/file_truncate/file_size on both backends; the cost of sync measured (0.4 µs written, ~190 µs durable, 4.2 µs batched by 100); slice 2 adds fs_rename, fs_remove and file_lock (a lock the kernel drops when the holder dies) |
utf8.md | Decoding &r [byte] into code points | settled and built |
standard-error.md | err_write: a third label on Io, not an eighth capability | settled and built |
../AGENTS.md | How to write cancho in one page | written and enforced |
float-math.md | sqrt as a builtin, then exp/log/pow, then floor/ceil/round/fabs/fmin/fmax/sin/cos, then exp/log/pow fixed to 1–4 ulp and expm1/log1p/log2/log10/the hyperbolics added, then tan/atan/atan2/asin/acos and a Payne–Hanek reduction for every finite argument | settled and built; every function measured against libm in ulps |
gpu.md | Whether cancho can run on a GPU, and whether lex-gpu should exist | measured; decided |
line-reading.md | Whether std needs a line reader | measured — no; found and fixed a silent truncation bug |
agent-errors.md | Refusals a machine can read: stable rule tags, check --output json | settled and built |
structured-ingest.md | The AST as JSON and back: cancho print --output json and cancho ingest, the ingest path an agent writes | built |
tty.md | Tty, the serial-port capability (#422 T1): both measured termios ABIs, the surface, the labels — design | |
crc.md | std.crc: CRC-16/XMODEM, CRC-32/IEEE and two CRC-64s (XZ, and Redis’s Jones), and the two-halves design | built |
net-bounds.md | Separate ways in and out, set-valued bounds, exact host matching (#362) | design; §2.4 is a report fix recommended first |
shared-store.md | Several threads, one store: the answer is process-per-core, with the two missing primitives named (#354) | design; answered, topic closed |
agent-tools.md | A tool genuinely shaped for an agent’s own loop | built (examples/seek/) |
agent-toolbox.md | Whether a set of unix-like tools in cancho, with a JSON contract, rule-tagged errors with repair hints and a compiler-derived authority, is worth building — and the protocol that would say so | design; the probes it rests on are run and recorded, nothing is built. Found: Fs extent cannot be static (narrow takes a literal), no directory listing or regex, sha256 traps past 64 KiB, a failed stdout write is invisible, and lex-os reads a cancho net_out label as no network; corrects bulk-io.md §3.3 and agent-tools.md §1 |
aliasing.md | Whether &! should mean Rust’s &mut | measured — no |
check-cost.md | The price of every check this language emits | measured — corrects overflow-cost.md and gpu.md |
poison.md | A per-lane flag instead of a trap, on a vector ISA | measured — depends on the check |
backend-limits.md | What Cranelift can and cannot be asked for | an audit, checked against source |
llvm-backend.md | A second backend, --backend llvm | complete, and the default |
effect-polymorphism.md | Whether an effect row can be polymorphic | a documented no, counted by reading |
hash-stability.md | How often a content hash actually moves | measured — the plateau vcs.md needed |
stability-exemptions.md | The plateau rule as a checked property: every historical .cho revision that fails today’s compiler, by rule, with a reason and a count; scripts/history.py --explain and the stability-plateau CI job fail on any increase | built; measured baseline 210 revisions, 140 failing (67%) |
vcs.md | How much of lex-vcs cancho can reuse | plateau answered yes; foundation, gate, op log and attestation built |
vcs-publish.md | The first real caller of cancho-vcs: publish, no diffing needed | built (cancho vcs publish/log) |
package-system.md | What a package would be, built from what already exists rather than invented fresh | cancho vcs resolve/vcs lock/vcs fetch are the first three real slices; net.sockets, net.connect, agent.wire, http.request, and http.response (packages/) are five real packages, examples/fetch/fetch.cho composes two of them at once with no new tooling, and http.request/net.connect/http.response each depend on net.sockets – a true closure of stores, resolved recursively by vcs publish --requires/vcs resolve/vcs fetch (§4.6), the one thing this row used to call missing. §7 (packages across repositories) is built for its first two steps: a lock can carry an origin (a git URL, a full commit hash and a path) and vcs fetch/resolve/lock --git fetch it into a cache of checkouts keyed by commit, with every pin re-checked as for a local store; vcs publish --dir publishes a library of several files in dependency order, and static declarations now publish. §8 (the project file) is built, cancho test in a project included: cancho.toml, cancho install, cancho add, cancho build with no files, and a compiler revision in --version that a project checks. §9 (prebuilt compilers) is built, unreleased: scripts/package-release.sh, scripts/install.sh (checksum and reported commit both checked) and a release workflow whose tag is the commit. Still missing: the first published release, and a human-readable version string |
character-literals.md | 'a': a third spelling of an integer | settled and built |
emitted-checks.md | The price list, read out of the binary | an audit; found a real compiler bug |
flags.md | std.flags: a cursor, not a getopt_long table | settled and built |
first-page.md | What a reader of README.md actually learns | measured, and acted on — this guide is the result |
under-a-grant.md | Whether a lex-os grant can decide a cancho authority report | measured — filesystem is enforceable; network and exec are not |
net.md | Net: sockets, taken out of libc | settled and built |
related-work.md | What cancho took from Cyclone, Koka, Rust, Vale, Zig, Lex, Austral | written |
differential.md | The constant folder against the backend | a test; found and fixed one disagreement |
connect.md | examples/fetch/: an HTTP client, and what connect needed | a probe |
listen.md | examples/collect/: the inbound counterpart | a probe; cleared Net’s last bar |
internal-errors.md | A backend failure as a located refusal, not an environment error | settled and built |
fuzzing.md | A mutation fuzzer over the whole corpus | a test; found four printer bugs |
function-values.md | Whether closures exist, and what mode a capturing one has | settled and built |
threads.md | Compiler-provided spawn/join, never crossing the C ABI | single-leaf slice and owned-capability case built; multi-field payload decided not yet |
editions.md | An edition marker, a path for renames | design, measured |
defined-behaviour.md | Every place C/Rust leave behaviour open, defined instead | written and enforced |
crypto.md | std.crypto’s first slice: SHA-256 | built |
sha512.md | std.crypto’s second slice: SHA-512 | built |
ed25519.md | std.ed25519: sign and verify | built |
chacha20.md | std.chacha20: ChaCha20, Poly1305 and the AEAD (RFC 8439), for the pure TLS client | built; not independently reviewed |
hkdf.md | SHA-384, std.hmac and std.hkdf for the TLS 1.3 key schedule; the 64 KiB trap every std hash had | built; not independently reviewed |
tls-pure.md | The TLS 1.3 client with no C library: where the code lives, the sans-io API both backends share, cipher policy, threat model, trust, refusal tags, open questions | design; the protocol built in packages/tls (#205, tls-core.md), chain validation not yet (#206) |
x25519.md | std.x25519, and std.field25519: one constant-time field for X25519 and Ed25519 | built; checked with Valgrind (ctgrind); not independently reviewed |
x509.md | packages/x509: a strict DER reader and an X.509 v3 parser, the pure TLS client’s certificates | built; not independently reviewed |
rsa.md | std.bigmod (Montgomery arithmetic to 4,096 bits) and std.rsa (PKCS#1 v1.5 and PSS verification) | built; not independently reviewed |
ecdsa.md | std.ecdsa (P-256 and P-384 verification, DER and raw signatures) on std.bigmod’s new register API | built; not independently reviewed |
tls-core.md | packages/tls: the TLS 1.3 client handshake and record layer – the PR split, a slot, the state machine, pinned certificates until #206, and why the byte-for-byte gate uses a recorded ChaCha20 handshake instead of RFC 8448’s AES trace | built: the protocol, the 64-slot engine, live servers, a lying server, 22 mutants killed |
x509-verify.md | packages/x509’s chain verification for #206: the API over a sent chain’s ranges, path building with backtracking and a budget, what leaf, intermediate and root are each checked for, name constraints and the types it cannot read, and the four gates (x509-limbo, an OpenSSL matrix, saved real chains against the system roots, mutants); §10, a chain verified with no name for a purpose (verify_chain, for verify-ca and client certificates), the name as its own step, and the hazard | built: 9,743 of limbo’s 9,802 cases pass (every BetterTLS one), 39 disagreements each explained against OpenSSL, 22 mutants killed; packages/tls verifies with it. §10: a 56-case no-name matrix against openssl verify -purpose sslclient/sslserver, one known disagreement listed; 33 mutants killed |
tls-parity.md | #207: parity with the OpenSSL backend (“AEAD parity”): what OpenSSL 3.0.13’s ClientHello offers, measured; constant-time AES-GCM, P-256/P-384 key exchange and HelloRetryRequest, TLS 1.2 with ECDHE and AEAD suites only; what stays different and why; client certificates and ALPN, and the revocation and post-quantum decisions (§6) | built: AES-GCM (§3.1.1), P-256/P-384 (ecdh.md), TLS 1.3’s AES-GCM and HelloRetryRequest (§3.3.1), TLS 1.2 (§3.4.1); §6 (#386): client certificates and ALPN built, revocation and X25519MLKEM768 decided (proposed) |
ecdh.md | std.ecdh: P-256 and P-384 key exchange with a secret scalar – complete formulas, a masked table, std.bigmod’s reductions made constant time, and the timing test that found LLVM turning the masks into branches | built; not independently reviewed |
value-barrier.md | value_barrier, edition 6: a value clang -O2 cannot reason about, so a constant-time mask stays an and | built |
crypto-builtins.md | Hardware AES and carry-less multiply: aes_encrypt_block, ghash_update (since #382 ghash_powers, gcm_tag, gcm_tag_diff; aes_ctr32) and hw_aes_gcm, block-level over byte slices so no 128-bit type is needed; measured so far: Cranelift has no such instruction, and LLVM compiles the intrinsics only with the target feature on | built: DIT, the builtins, per-key caching and the std hardware path |
wide-multiply.md | mul_wide, add_carry and sub_borrow: a 64x64->128 multiply and add/subtract with carry as builtins over int pairs, lowered without a branch or a libcall on both backends and on wasm32; prototyped on X25519 (5 limbs of 51 bits); then P-256’s kernels (five 56-bit limbs inside them), std.bigmod (64-bit limbs) and Poly1305 (44-bit limbs) moved onto them (#381, §12) | built; measured on x86-64 and arm64; dudect, mutants and differentials in §12 |
gcm-wide.md | #382: multi-block AES-GCM and aggregated GHASH: where a record’s time goes (the cipher is 17% of 16 KiB, a region’s malloc is 88% of 64 B on Darwin), aes_ctr32 eight blocks at a time, GHASH reflected with one reduction for eight blocks and the powers of H in the prepared key, a tag in one call with no scratch | built: 5.9 to 11.2 times at 16 KiB, 89% (x86-64) to 68% (M4) of OpenSSL’s speed; the record layer is what dominates now |
sha256-builtin.md | #445 and #446: hw_sha256, sha256_blocks and sha256_block_words (SHA-256’s compression on SHA-NI and ARMv8 SHA-2, a state of eight ints and N blocks of bytes, or one block of words where std.crypto keeps its partial block), std.crypto on them, and HMAC with a key’s pads hashed once (hkdf.md §9); Valgrind hides SHA-NI on x86-64, so the guard counts the software path there | built |
tls-hooks.md | The pure TLS backend in cancho-hooks (#210): the two interfaces are not the same (an adapter is needed), one source cannot carry both capability rows (no effect polymorphism), the pure client has no resumption, and “no Ffi at all” cannot hold because hooks also holds libc; the build, the tests on both backends, the measurements | design |
tls-resumption.md | TLS 1.3 resumption for packages/tls: the full handshake’s 2.8 ms is two X25519 and two ECDSA verifications (measured); PSK with (EC)DHE removes the verifications and keeps forward secrecy, about 1.2 ms; eight rules bound the hazard that a resumed session skips verification. Built: resumes against OpenSSL, Go, rustls, wolfSSL, nginx and GnuTLS; a resumed connection measured at half a full one’s CPU | built |
tls-server.md | A TLS 1.3 server for packages/tls, for cancho-mqtt and cancho-gateway: constant-time ECDSA P-256 signing first (ecdsa-sign.md, built), then the server handshake with SNI, ALPN and HelloRetryRequest (built: interop with OpenSSL, curl, Go, wolfSSL and mosquitto, a lying client of 110 cases, a differential against openssl s_server, fuzzing and mutants; a full handshake measured at 3.5 to 4 ms of CPU on X25519 or P-256 (6 ms on P-384), 17 to 20 times OpenSSL’s); then examples/tls_echo, the server the broker and the gateway copy (§11: handshakes bounded in progress and a second, delayed not refused; reload on SIGHUP; close_notify on stop; 3.0 ms a handshake on an M4 and 5.5 ms on x86-64; no foreign code); session tickets (§12, #394); client certificates (§13): modes off, optional and required, a trust store of its own that is never the system bundle and is replaced at run time, the CertificateRequest, the client’s chain verified for clientAuth and its CertificateVerify (P-256, P-384, RSA-PSS, Ed25519), the verified identity given to the program per slot, no slot memory; interop with OpenSSL, curl, Go, wolfSSL and mosquitto, a lying client of 78 connections, a differential against openssl s_server, a fuzz harness, 202 mutants; a client certificate costs 1.7 to 4.2 ms of the server’s CPU on an M4; TLS 1.2 as a later step | steps 1 to 5 built; not independently reviewed |
tls-memory.md | Memory per TLS connection (#383, of #378): measured at 50 to 10,000 connections on arm64 and x86-64 beside OpenSSL’s server (an established idle tls_echo connection is 119 KiB, OpenSSL’s 14 to 49); where the slot’s 263 KiB and the 119 KiB are (the work area std.ecdh needs is 75 KiB of integers every handshake writes, and nothing is released); six ways out, and the one chosen: a small core per slot, and the handshake’s scratch and the record buffers leased from a pool of work areas | built, measured: an established idle tls_echo connection 17 KiB (from 119), 4.5 KiB of it the engine’s; §11: one whose client presented a verified certificate 19 KiB (from 126: #398 kept the leaf in the work area), the leaf in a 2 KiB room a slot |
ecdsa-sign.md | std.ecdsa_sign: ECDSA P-256 signing in constant time on std.ecdh’s ladder, RFC 6979’s nonce hedged with the caller’s randomness, a signature verified before it is given out; and packages/x509’s x509_key, P-256 private keys from PKCS#8 and SEC 1 PEM. Step 1 of tls-server.md | built; not independently reviewed |
tls-performance.md | TLS performance (#378), first deliverable: where the time of a full server and client handshake goes on x86-64 and arm64, measured beside OpenSSL. Three operations (two X25519, the signature, the verifications) are 77 to 91% of a handshake and each costs inside it what it costs alone; the engine’s copies, zeroing, hashing, HKDF and event loop are about 5%; every region is a malloc and free of 64 KiB, which is 6 to 10% of a handshake and 45 to 53% of a request on a kept connection (glibc told not to trim made a request 1.9 to 2.5 times cheaper on x86-64). Targets and gates stated first; a ranked lever list with gains computed from the measured parts (the 5-times end of the target is reached without a new builtin, by 1% (arithmetic); wide multiply is the one thing the language cannot do); a regression guard counting instructions under valgrind, in CI; open questions. §10, as built after #392 to #398: the server’s full handshake 2.86 times OpenSSL’s cycles on x86-64 (2.12 times its CPU on arm64), the client’s 2.77 (2.26), a resumed one 2.34 (2.17), a kept request 1.11 to 1.15; the allocator now the largest cost of every handshake (telling glibc not to trim removed a third of a full one and half a resumed one); each lever against its projection, T1 to T4 met and T5 not, the next levers ranked | measured; design; the levers built in #392 to #397, the guard’s baselines complete |
record-in-place.md | L1 and R1 of #378 (#447, #448): a kept https_hello request spent 24% copying a connection’s core into a work area and back and clearing it, and that was the 296 words copied one at a time, not the bytes; a TLS record went through a region and byte loops around the cipher. Block copies; an established connection that rests in its area until another needs one (at most 32, bounded memory); seal_in_place/open_in_place for AES-GCM (both paths) and ChaCha20-Poly1305, the record sealed where it is sent from and opened straight into the received data, no region | built: a kept request 1.20 to 0.94 times OpenSSL’s cycles (x86-64); a record through both engines 5.7 (64 B) to 1.5 (16 KiB) times cheaper; 2.1 MiB more per engine at most, idle connections unchanged |
region-limit.md | #514: a region is one chunk, 65,536 bytes, and an allocation past it was a bare SIGILL learned from a crash in production. Measured exactly (8,192 ints or 65,536 bytes, the whole chunk, on both backends); a constant size is refused at check with region-chunk-exceeded, the message saying the limit and box_slice; a size the compiler cannot see still traps but writes a named line to stderr, on both backends. A heap-backed chunk is argued and not built (a language decision) | built on both backends; the boundary measured; mutants listed in §6 |
region-chunk-cache.md | A1 of #378 and #375, one mechanism: every region was a 64 KiB malloc and free, and glibc trimmed and regrew its heap for nearly each (112 brk calls a full TLS server handshake, 136 a resumed one, measured; 0.7 µs a pair on macOS and serialised across threads). Each thread keeps the chunks its regions give back, at most 16 (1 MiB), last in first out, which is the region stack (regions nest strictly); the bytes a region used are cleared when it ends; a thread’s chunks are freed when it ends; the same runtime on both backends, a global on WASI. Options measured or argued (mallopt is glibc-only and changes every allocation), hazards (secrets, threads, memory), gates stated first. As built: 9 brk calls in a whole server run; a full TLS server handshake 34.6% cheaper (1.86 times OpenSSL’s cycles on x86-64), a resumed one 55.8% (1.04 times OpenSSL’s resumed: T5 met for the server; the client’s 1.21, not met); 16 threads on macOS 0.04 s where 8.8 | built on both backends and WASI; counted under strace and an LD_PRELOAD shim; 14 of 14 mutants killed |
p256-fast.md | #380 (of #378): faster P-256 – where the time goes (field operations are 93-98% of every operation, a bigmod.mul is 115 ns of which 55% is checked arithmetic), then a ten-limb 28-bit field with straight-line generated kernels and the NIST prime’s form, a fixed-base table for k·G as a static, wNAF verification; §11, a table for a key that verifies many signatures (the server’s own, B2; a client’s P-256 trust anchors, B3): four runs of 65 wNAF digits sharing 64 doublings, 10,305 bytes a key, once per engine and key; a verification 2.1 times cheaper (2.26 times fewer instructions), a full server handshake 24% fewer instructions (1.46 times s_server), a client’s 18% (1.41 times tconn); §12, P-384 measured (8 to 17 times OpenSSL) and decided: its verification is worth it, on #381’s field; §13 its multiplications on mul_wide (#381), four 64-bit limbs measured and not chosen | built; gates in §8, §11.6 and §13.3; not independently reviewed |
self-hosting.md | Whether cancho could host its own toolchain | run; decided not yet — no asker |
agent-cli.md | cancho introspect/skill: the CLI surface as data, imported from lex-lang’s ACLI integration | built; found a false-familiarity bug in the process (-o/-l/-L would have rendered as --o/--l/--L) |
next-phase.md | Replacing episodic duplication/staleness hunts with a mechanical check, argued from MANIFESTO.md and three of this session’s own mistakes | §3’s real instance (10 examples duplicating std.io) migrated, 7 of them (§3.1 has the three exceptions); §4’s own standing check, the actual proposal, not built yet |
linearity-and-effects.md was the gating artifact: the decision set that
determined whether this is a three-month prototype or a three-year project.
It was settled before M2 started, its must-reject list was read as what it is
— the M2 conformance suite, stated in advance — and M2 was then built against
it slice by slice.