How memory is managed
A bump allocator over mmap, with explicit arena reclamation where peak memory matters. No tracing collector, and no borrow checker to satisfy.
- Rustownership and borrowing
- Gotracing GC
- Haskelltracing GC
v0.7.5MITsix targetsno runtime
Algebraic data types, exhaustive matching and an effect system the compiler checks, lowered through LLVM to a native executable with no VM, no collector and no libc inside it.
curl -fsSL https://raw.githubusercontent.com/chrispaig3/axiom/trunk/scripts/install.sh | bash(import IO)
(data Shape
(Circle Int)
(Square Int))
(:: area (-> Shape Int))
(fn (area s)
(match s
((Circle r) (* 3 (* r r)))
((Square w) (* w w))
)
)
(:: main Int)
;@axiom:effect(io)
(fn (main)
(let (
(c (area (Circle 4)))
(q (area (Square 6)))
)
{
(println "circle = {c}")
(println "square = {q}")
0
}
)
)
compiling…Not a rewrite in progress — the Rust implementation it replaced has been deleted. A clean checkout rebuilds the whole thing with nothing but llc and a C linker, and checks the fixpoint on the way, every time — scripts/bootstrap-from-seed.sh.
Six complete programs, each one compiled and run to produce the output shown under it. They walk from the thing every language has — types and matching — to the things only this one does: checked effects, a region per row, and a join across processes.
(import IO)
(import Err)
(data Parcel
(Ordered)
(InTransit { carrier : String, days : Int })
(Held { why : String })
(Delivered))
(:: status (-> Parcel String))
(fn (status p)
(match p
((Ordered) "packing")
((InTransit carrier days) (format "{carrier}, {days} days out"))
((Held why) (format "held at {why}"))
((Delivered) "delivered")
)
)
(:: alert (-> Parcel (Option String)))
(fn (alert p)
(match p
((Ordered) (Some "not shipped yet"))
((InTransit _ _) None)
((Held why) (Some (format "call about {why}")))
((Delivered) None)
)
)
(:: row (-> String Parcel Int))
;@axiom:effect(io)
(fn (row id p)
(let (
(s (status p))
(a (optUnwrapOr (alert p) "-"))
)
(println "{id:<9}{s:<22}{a}")
)
)
(:: main Int)
;@axiom:effect(io)
(fn (main)
{
(println "parcel status action")
(row "AX-1041" Ordered)
(row "AX-1042" (InTransit "DHL" 2))
(row "AX-1043" (Held "customs"))
(row "AX-1044" Delivered)
0
}
)
compiling…Axiom lowers to LLVM IR and hands it to llc, which is the backend Rust and clang use. On work that is just arithmetic and branches, that means the machine code is the machine code — and the measurement says so.
| Measurement | Axiom | Rust | C |
|---|---|---|---|
| Run time3,000,000 Collatz sequences · best of 20, interleaved | 0.446 s | 0.447 s | 0.446 s |
| Compile to a native binaryone file, cold · best of 15, interleaved | 0.140 s | 0.111 s | 0.132 s |
| Binary sizethe executable on disk | 35,560 B | 469,592 B | 33,432 B |
| Undefined symbolsnm -u <binary> | wc -l | 0 | 70 | 1 |
Collatz step counts for 1..3,000,000, summed and printed. Signed 64-bit integers, no allocation, no library call in the hot loop. All three binaries print 428343467. Each figure is the best of its runs, not the mean: interference only ever makes a run slower, so the minimum is the closest estimate of the cost itself — the methodology the repository uses for its own benchmarks.
The runs are also interleaved — one repetition of each binary, in turn — and that correction changed the answer. A first pass that ran each binary in a block put Axiom 1.6× behind Rust, which would have been a finding if it were real. It was an artefact: a background build starting midway through taxes whichever block it lands on. Alternating gives every binary the same interference, and the three collapse onto each other.
The three programs are in the repository — web/bench/ — and run-bench.sh beside them produces every cell of this table, so it can be re-run rather than taken on trust.
Apple M1, macOS 26.6.2, darwin-aarch64 · Axiom 0.7.5 · rustc 1.98.1 · clang 23.1.0. Go and Haskell are absent on purpose: no toolchain for either was on the machine, and this project does not publish a number it has not measured. One micro-benchmark is one micro-benchmark — it says nothing about allocation-heavy work, which scripts/bench-datastructures.sh measures separately, and less flatteringly.
axiom build --input collatz.ax --output out-axiom
rustc -O collatz.rs -o out-rust
clang -O2 collatz.c -o out-cDesign, not benchmarks — the numbers are one section up. Every Axiom cell here is held by something in the repository; the other columns are restricted to facts nobody disputes.
A bump allocator over mmap, with explicit arena reclamation where peak memory matters. No tracing collector, and no borrow checker to satisfy.
Inferred per function, and checkable from source. ;@axiom:effect(io) above a declaration is a claim the compiler tests against what the body actually reaches — and a false one is an error.
The program tree itself, because the syntax is already a tree. Expansion runs before the type checker, so everything a macro generates is checked like anything else.
axiom run f.ax. One step, one binary, no build file. A dependency is a path on your machine — there is no registry to configure and no lockfile to resolve.
Axiom is 0.x. The feature-by-feature status table — what is complete, what is partial, what was removed, each with the test that holds it — is in docs/status.md, and nothing on this page is a promise that table does not make.
Axiom is small on purpose, and the places that pays off are the ones where a runtime you did not write is a liability.
Your program is the whole artifact. The allocator is emitted into your executable, I/O is the syscall, and nm -u on the result lists zero undefined symbols — nothing to initialise at start-up, because there is nothing left to resolve.
--target picks the syscall ABI and emits code for any of the six from any host; only the final link needs that target's linker.extern block names symbols in a static archive; one flag builds the crate on the far side.;@axiom:restrict(no-io,no-alloc,no-foreign) is not a comment. It is a claim the compiler tests against the effect row and the call graph — and when it fails, the message names the exact path of calls to where the effect enters.
nm -u is empty, so there is no symbol for a dynamic linker to bind, and nothing to redirect.Most toolchains publish their failures and keep their successes to themselves. Axiom publishes both — one line per diagnostic and one line per symbol — so an agent can ask what a file already provides without paying to read it again.
One thing Axiom is not built for, said here rather than discovered later: concurrency is one narrow form and no runtime. parallel runs its bindings beside the caller and joins them in the order written — as child processes by default, whose isolation is true by construction, or as threads under --threads. Only a machine word crosses a join, and a binding that captures a reference the parent holds is refused under either lowering. There is no async, no scheduler and no task system; stdlib/Par.ax is a bounded pool over the same primitives, and it is the rest of the story.
One structured diagnostic is built at every stage that can refuse, and the renderers never see anything the compiler did not already know. So the report you read and the line your tooling parses can never disagree about what went wrong.
(:: main Int)
(fn (main)
(let ((x 0))
{
(set x 1)
x
}))error[AX3012]: cannot assign to immutable binding `x`
--> count.ax:5:12
|
3 | (let ((x 0))
| - `x` is bound here
...
5 | (set x 1)
| ^ `x` cannot be assigned
|
= help: declare it mutable: `(mut x ...)` ~> mut x
= help: only a binding introduced by `(let ((mut x ...)) ...)` may be the target of `set`
= help: run `axiom explain AX3012` for a full explanation
compilation failed due to 1 previous error
The other question a tool asks is not about failure at all. axiom symbols runs the same pipeline as check and prints one line per symbol — kind, name, location, exact type. An agent greps ^D Maybe for the constructor set instead of re-parsing the file.
Fn add (Int -> (Int -> Int)) [main.ax:9:5-8]
Data Option data Option [builtin]
Ctor Some (a -> Option a) [builtin]
Ctor None Option a [builtin]
Data Vec data Vec [builtin]
Data Maybe data Maybe [main.ax:1:7-12]
Ctor Nothing Maybe a [main.ax:2:4-11]
Ctor Just (a -> Maybe a) [main.ax:3:4-8]
Struct Point struct Point [main.ax:5:9-14]
@27bcb2… is a content-derived id that does not move when the declaration is reordered, reformatted, or read from another path — identity is the id, and the rest of the row is the contract.The exact span, the kind of error, the primary label, the message, and a replacement a tool applies as a byte-range substitution instead of parsing English.
Both blocks are real compiler output. This line is the golden of one diagnostics fixture, re-rendered against the live compiler on every run — output nobody could reproduce would fail the build. diagnostics.md has the grammar; axiom explain --list names all 80 codes.
(:: helper (-> Int Int))
(fn (helper x) (+ x 1))
(:: main Int)
(fn (main)
(helpr 5))E AX3001 main.ax:6:4-9 undefined-variable "undefined variable `helpr`" #"no binding named `helpr` in scope" ?6:4-9:"a similarly named binding `helper` is in scope; did you mean this?"~>"helper"
No build system to configure, no formatter to choose, no test runner to add, no language server to install separately. Eleven subcommands, one download, and the same compiler behind all of them.
axiom explainaxiom lsptree-sitter-axiomtree-sitter CLI — and it is parsed against all 642 .ax files in the repository on every change.docs/lsp.md has a configuration for Neovim, Helix, Emacs and VS Code; Zed needs only the grammar. The server and the grammar are independent: the server colours nothing, so a buffer with it attached and no grammar installed is plain text.
You need llc from LLVM and a C compiler for the final link. That is the whole list — Axiom's compiler is written in Axiom, so there is no other toolchain to install first. The installer is at the top of this page; if you would rather build from source, that is what a contributor does.
bootstrap/ holds the compiler's own LLVM IR, one file per host it can build on, committed — so this needs nothing but the two prerequisites above.
git clone https://github.com/chrispaig3/Axiom && cd Axiom./scripts/bootstrap-from-seed.sh --install .axiom-binIO is Axiom's own standard library, so the compiled binary calls no C function at all — and the ;@axiom:effect(io) line is checked, not decorative.
(import IO)
(:: main Int)
;@axiom:effect(io)
(fn (main)
{
(println "Hello, Axiom!")
0
}
)axiom run f.axaxiom build --input f.ax --output faxiom check f.axaxiom test tests/axiom explain AX3001Everything else — the FFI, macros, the language server, the gate battery — is in the repository.
llc and a C compiler are the only prerequisites. The installer verifies the archive, builds and runs a program that imports the standard library, and only then reports success.
curl -fsSL https://raw.githubusercontent.com/chrispaig3/axiom/trunk/scripts/install.sh | bash