The functional systems language
High-level thinking.
Native-level control.
Express your ideas with powerful types. Catch missing cases and check effects at compile time. Ship a native binary with no VM or garbage collector.
For people who care how their code reaches the machine.
See it in action
A small program. The whole picture.
Write it. Run it. Break it.
Watch the compiler catch what changed.
Prebuilt for macOS & Linux arm64. Setup & other targets
curl -fsSL https://raw.githubusercontent.com/chrispaig3/axiom/trunk/scripts/install.sh | bashEarly-stage language. Real compiler. See what’s ready today.
Why Axiom
A small language. Serious capabilities.
The expressiveness of functional programming, with a direct path to native code. Every design choice gives you something concrete to work with.
Ship the binary. That’s it.
Your program compiles to a native executable, with its allocator included and direct calls to the kernel. No VM to provision. No garbage collector to tune.
scripts/check-freestanding.shMake side effects explicit.
Keep computation separate from I/O. The compiler infers effects, checks
restrict(no-io), and requires functions that perform I/O to declare it.;@axiom:restrict(no-io)Find the problem. Keep moving.
Get a precise location, a stable error code, and often a fix your tools can apply.
axiom explainhas a page for each one of the 108 codes.axiom explain AX3005Built for you. And your agents.
Uniform syntax keeps code generation predictable. Structured diagnostics, exact spans, and stable symbol IDs give your tools the same facts you use to review their work.
--diagnostic-format=aiRead the compiler. In Axiom.
The compiler is 118,834 lines of Axiom. A clean checkout rebuilds it from committed LLVM IR with
llcand a C linker, and stops unless two generations are byte-identical.scripts/bootstrap-from-seed.shBring Rust along.
Keep useful Rust code within reach. Declare functions in an
externblock;--cratebuilds and links the crate. Rust values are dropped when their last Axiom reference goes.axiom build --crate DIR
The tour
Get a feel for the language.
Ten working programs, from your first data type to calling Rust. Explore the code, follow the annotations, and see exactly what each program prints.
01/14 · parcels.ax
Shipment status report
Sum types with named fields, matched exhaustively. Option is built in, so a missing value is a type rather than a null.
- L6A constructor can carry named fields.
- L18
alertreturnsOption String, because some parcels need no action. - L31
optUnwrapOrsupplies the default where there is no value. - L33
{id:<9}pads to nine columns. Format strings are compiled, not interpreted at run time.
(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
})
parcel status action AX-1041 packing not shipped yet AX-1042 DHL, 2 days out - AX-1043 held at customs call about customs AX-1044 delivered -
Performance
Small binaries. Measured performance.
In this Collatz benchmark, Axiom runs within milliseconds of C and Rust, with a binary close to C’s size. One workload, one machine, with the source and method available for you to reproduce.
Apple M1, macOS 27.0.1, darwin-aarch64 · Axiom 0.7.8 · rustc 1.98.1 · clang 23.1.2 · timed with hyperfine 1.20.0. All three print 428343467.
How it was measured, and the table behind the charts
Collatz step counts for 1..3,000,000, summed and printed. Signed 64-bit integers, no allocation, no library call in the hot loop. 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 runs are 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. It was an artefact: a background build starting midway 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, in 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. 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 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-c
| Measurement | Axiom | Rust | C |
|---|---|---|---|
| Run time3,000,000 Collatz sequences · hyperfine best of 20, interleaved | 0.443 s | 0.448 s | 0.444 s |
| Compile to a native binaryone file, cold · hyperfine best of 15, interleaved | 0.166 s | 0.117 s | 0.151 s |
| Binary sizethe executable on disk | 35,784 B | 469,608 B | 33,432 B |
| Undefined symbolsnm -u <binary> | wc -l | 0 | 70 | 1 |
How it compares
A different set of tradeoffs.
Functional types, explicit effects, and native output in one language. See how Axiom’s choices sit alongside Rust, Go, and Haskell.
axiom run f.ax: one step, no build file needed.cargogo buildcabal or stackGet started
Your first native binary starts here.
Axiom needs llc from LLVM and a C compiler for the final link. The compiler itself is written in Axiom, so there is no other toolchain to install first.
Install LLVM and a C compiler
xcode-select --installbrew install llvmexport PATH="$(brew --prefix llvm)/bin:$PATH"The Command Line Tools supply the C compiler for the final link; Homebrew's LLVM supplies
llc, which is not on the default path.Install Axiom
A prebuilt archive where one exists; otherwise, build from the committed seed.
curl -fsSL https://raw.githubusercontent.com/chrispaig3/axiom/trunk/scripts/install.sh | bashexport PATH="$HOME/.axiom/bin:$PATH"For macOS and Linux on arm64. It verifies the archive's SHA-256, then builds and runs a program that imports the standard library with the new compiler before it replaces anything, and it only ever replaces an installation it made itself.
Start a project and run it
axiom newwrites a program and anaxiom.pkgmanifest. Inside a project,runandbuildneed no file name.axiom new hello && cd hello && axiom run(import IO) (:: main Int) ;@axiom:effect(io) (fn (main) { (println "Hello from Axiom! 🚀") 0 })axiom runHello from Axiom! 🚀
The commands you will use most
axiom check- type-check and verify every effect claim, no code generation
axiom build- a native executable, named after the project
axiom test- every function whose name starts with test
axiom fmt- the one canonical layout
axiom explain AX3005- the full explanation behind any diagnostic code
axiom symbols Main.ax --calls- declarations, effects, stable IDs and call edges in AXSYM
axiom emit-llvm Main.ax -o Main.ll- inspect the LLVM IR before assembly
axiom build Main.ax --obfuscate -o app- optional literal masking and internal symbol scrambling
axiom repl- an interactive session, compiled to native code line by line
Set up your editor for highlighting, go-to-definition, hover and fixes.
Imported modules come from the entry directory, package dependencies and explicit search paths. Set AXIOM_PATH for extra module roots and AXIOM_STDLIB to override the installed library. Module search and compiler inspection
Where it runs
| Target | Prebuilt | Notes |
|---|---|---|
darwin-aarch64 | yes | Apple silicon. Prebuilt archive. |
linux-aarch64 | yes | Prebuilt archive. |
--target emits for any of them from any host; only the final link needs that target's linker. Source-only targets: baremetal-aarch64, darwin-x86_64, freebsd-aarch64, freebsd-x86_64, linux-x86_64, windows-aarch64, windows-x86_64. --target emits for them, and CI doesn't run the test battery there. What "supported" means here.
Bare-metal AArch64 uses a board-defined platform seam and an optional QEMU smoke run. Windows and FreeBSD have narrower bootstrap coverage. Embedded targets
Status
Know what’s ready. See what’s next.
Axiom is 0.x: ready to explore, still evolving. Start with a tool, an experiment, or a contribution. Use this status map to decide where it fits; these rows reflect the project’s tested implementation status.
Complete
- Functions & typesCompleteCurried signatures, rigid type variables, exact return types.
- ADTs / data typesCompleteSums with positional or named fields.
- Pattern matching (
match)CompleteNested, exhaustive, checked in every function. - StructsCompletePer-field
mut, generic parameters, automatic rendering. - Lambda / function valuesCompleteClosures, partial application,
_holes. - LoopsComplete
forover ranges and containers,while. - SyscallsCompleteEight targets, no libc between you and the kernel.
- Module visibilityCompleteOnly
publeaves a module. - Self-hostingDoneThe Rust compiler it replaced has been deleted.
Working, with stated limits
- EffectsEnforced; two limits statedTwo inference gaps, both stated: unresolvable calls are marked incomplete, and constructor allocation is not counted.
- MacrosPartialA template cannot generate
importor a nestedmacro, or test two binders for sameness. - ConcurrencyLanguage form, two lowerings
parallel, channels, a mutex and task pools;taskFoldreclaims each delivered answer under an explicit scalar-only callback contract. - Region syntaxChecked scope, and annotated signatures with the escape ruleScalars leave a region; typed promotion is planned.
- FFIFunctionalRust through
externblocks, sealed owners and callbacks borrowed for the call. - Standard libraryFunctionalCollections, cryptography, optional binary obfuscation, dates, JSON, networking and an embedded database.
- Error handlingFunctional; adopted at the syscall seam
Result, thetryform, and handlers that skip a bad record without unwinding. - Editor supportFunctionalLanguage server plus a tree-sitter grammar.
Removed on purpose
- Type classesReplacedBy capability records: an interface is a struct of functions.
- ListsRemovedSequences are
(Vec T). - TuplesRemovedProducts are
struct, sums aredata. - Linear typesRemovedDeterministic reclamation is reference counting's job.
Not here yet: a package index and version pinning, async and a scheduler, a compiler that runs on Windows (it builds Windows executables from elsewhere), and prebuilt archives beyond arm64. The full table
FAQ
A few things worth knowing.
Is Axiom ready for production?
Not for most teams yet. It is 0.x: the core language (types, matching, structs, loops, modules) is complete, the FFI, standard library and editor support are functional, and macros are partial. Every row of the status table names the test behind it.
There is no package index and no green threads. What is here is a small language whose compiler you can read and whose claims are tested.
Implementation statusDoes it have a garbage collector?
No tracing collector. Memory comes from a bump allocator over mmap, and every heap block carries a reference count, so a value is freed the moment its last reference dies, with nothing written in the source. When you want reclamation at a point of your choosing, (region r body) rolls the allocator back in one step.
What does "no libc" actually mean?
The code Axiom generates, and its standard library, reach the kernel through raw syscalls: printing, allocation, files, processes and sockets included. scripts/check-freestanding.sh fails the build if the generated IR calls a C library function or the executable imports one.
The final link is done by your system C compiler, which on Linux adds the C runtime's startup code; on macOS arm64, nm -u on a compiled program is empty. An extern block to Rust is the one deliberate door, and a no_std crate adds no C library function through it.
Why S-expressions?
Because the program is already a tree. There is no operator precedence to memorise, no ambiguous parse, and macros operate on the same structure the compiler checks. That uniformity is also what makes the language easy for a tool or an agent to generate correctly.
The editor side is covered: a tree-sitter grammar colours by syntactic role with rainbow brackets, and axiom fmt settles layout.
Can I make a distributed binary harder to inspect?
Choose axiom build Main.ax --obfuscate -o app to mask string literals, scramble internal function names and strip local symbols. Fresh build seeds make each build differ. The mode retains opaque function entries through optimisation and omits source-level backtrace tables.
Crypto.Obfuscate also packs authenticated encrypted assets with context binding, split key shares and explicit key erasure. These features make static inspection harder; someone inspecting the running process can recover embedded keys and decoded data. Static archives do not accept the compiler flag.
How can tools inspect my program?
axiom --diagnostic-format=ai symbols Main.ax --calls emits AXSYM declarations, stable IDs, author tags, derived effects and call edges. Agent.Tags reads that stream, including effect and macro declarations. Use AI format for symbols; JSON format is refused.
symbols --mir --axir exposes analysis and inspection records. Native builds compile the checked, expanded syntax tree to LLVM IR. Use emit-llvm to see that output; the legacy axiom FILE form is deprecated.
Do I have to annotate every function's effects?
No. Four effects must be declared through the call chain: effect(io), effect(entropy), effect(spawn) and effect(block). Allocation and mutation are inferred and reported. A function that performs a raw operation, calls a precondition interface or casts a value into an unrelated reference type declares effect(unsafe). A trusted wrapper contains that obligation for its callers. restrict(...) and pure add stronger checks where you need them.
Are there traits or type classes?
They were replaced by capability records. An interface is a parameterised struct holding functions, and an instance is an ordinary value of it, passed where it is needed. Dispatch is application: no table, no resolution rules, and a function generic over an interface can call its methods.
Capability recordsIs there a package manager?
A project is an axiom.pkg file: axiom new writes one. A depend line names a directory of modules or a git URL, axiom fetch clones the URLs, and two dependencies may never provide the same module. There is no central index and no version pinning yet, and the compiler never fetches or runs another project's build on its own.
Why does the compiler have an "ai" output format?
For agents and other tools. --diagnostic-format=ai prints one line per diagnostic with exact spans and any fix that applies, and axiom symbols prints one line per declaration with an id that survives reformatting, so a tool can learn what a file provides without reading it again. The human report is rendered from the same diagnostic, so the two cannot disagree.
From idea to executable
Think in types.
Build something native.
Your first program is a few commands away. Try the language, inspect the compiler, and help shape what comes next.