Axiom 0.7.8 Open source. Built in Axiom.

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.

shapes.ax — from source to executable
A recorded terminal session: A sum type: three shapes, each with named fields. area promises it performs no I/O. The compiler holds it to that. report prints, so it says so: effect(io). Compile and run in one step. No build file. $ axiom run shapes.ax Add a fourth shape, and nothing else. The match in area no longer covers every shape. The compiler names it, and the missing arm. $ axiom check shapes.ax Handle the hexagon, and report one. $ axiom run shapes.ax A native executable, with its allocator and syscalls inside it. $ axiom build shapes.ax -o shapes $ ./shapes
Your next native binary starts here.

Prebuilt for macOS & Linux arm64. Setup & other targets

curl -fsSL https://raw.githubusercontent.com/chrispaig3/axiom/trunk/scripts/install.sh | bash

Early-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.sh
  • Make 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 explain has a page for each one of the 108 codes.

    axiom explain AX3005
  • Built 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=ai
  • Read the compiler. In Axiom.

    The compiler is 118,834 lines of Axiom. A clean checkout rebuilds it from committed LLVM IR with llc and a C linker, and stops unless two generations are byte-identical.

    scripts/bootstrap-from-seed.sh
  • Bring Rust along.

    Keep useful Rust code within reach. Declare functions in an extern block; --crate builds 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.
  • L18alert returns Option String, because some parcels need no action.
  • L31optUnwrapOr supplies the default where there is no value.
  • L33{id:<9} pads to nine columns. Format strings are compiled, not interpreted at run time.
parcels.ax
(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
  })
axiom run parcels.ax
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.

Run timeAll three within 5 ms3,000,000 Collatz sequences · hyperfine best of 20, interleaved
Axiom0.443 s
Rust0.448 s
C0.444 s

All three finish within five milliseconds of one another. Axiom emits LLVM IR, so a loop that is only arithmetic and branches gets the machine code the other two get.

Compile to a native binaryAxiom is the slowest, by 49 msone file, cold · hyperfine best of 15, interleaved
Axiom0.166 s
Rust0.117 s
C0.151 s

Axiom is the slowest of the three, by forty-nine milliseconds against rustc at 1.42x, and fifteen behind clang at 1.10x. Published because it is what was measured, by the script beside the sources.

Binary size13× smaller than Rust, 7% over Cthe executable on disk
Axiom35,784 B
Rust469,608 B
C33,432 B

Thirteen times smaller than the Rust binary, and about seven percent larger than C, with no C runtime inside it at all.

Undefined symbols0 for Axiom, 70 for Rust, 1 for Cnm -u <binary> | wc -l
Axiom0
Rust70
C1

The whole program is in the file. Nothing is resolved at load time, because there is nothing left to resolve.

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
Run time, compile time, binary size and undefined symbol count for the same Collatz workload in Axiom, Rust and C.
MeasurementAxiomRustC
Run time3,000,000 Collatz sequences · hyperfine best of 20, interleaved0.443 s0.448 s0.444 s
Compile to a native binaryone file, cold · hyperfine best of 15, interleaved0.166 s0.117 s0.151 s
Binary sizethe executable on disk35,784 B469,608 B33,432 B
Undefined symbolsnm -u <binary> | wc -l0701

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.

AxiomRustGoHaskell
MemoryBump allocation, reference counting, and regions when you choose the moment. No tracing collector, no borrow checker.Ownership and borrowingTracing GCTracing GC
Inside the binaryYour code, its allocator and raw syscalls. No C library function is called.std links the C libraryGC and scheduler runtimeThe GHC runtime system
Side effectsInferred per function; declared or restricted with a tag the compiler checks.Not trackedNot trackedTracked in types, by hand
MacrosRewrite the program tree before type checking; hygienic.Token streamsNoneTemplate Haskell
Build and runaxiom run f.ax: one step, no build file needed.cargogo buildcabal or stack

Get 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.

  1. Install LLVM and a C compiler

    xcode-select --install
    brew install llvm
    export 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.

  2. 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 | bash
    export 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.

  3. Start a project and run it

    axiom new writes a program and an axiom.pkg manifest. Inside a project, run and build need no file name.

    axiom new hello && cd hello && axiom run
    hello/Main.ax
    (import IO)
    
    (:: main Int)
    ;@axiom:effect(io)
    (fn (main)
      {
        (println "Hello from Axiom! 🚀")
        0
      })
    
    axiom run
    Hello from Axiom! 🚀
    

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.
  • LoopsCompletefor over ranges and containers, while.
  • SyscallsCompleteEight targets, no libc between you and the kernel.
  • Module visibilityCompleteOnly pub leaves 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 import or a nested macro, or test two binders for sameness.
  • ConcurrencyLanguage form, two loweringsparallel, channels, a mutex and task pools; taskFold reclaims 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 extern blocks, 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 seamResult, the try form, 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 are data.
  • 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 status
Does 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.

The memory model
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.

The freestanding gate
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.

Binary and asset obfuscation
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.

Follow a program through the compiler
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.

Effects in the reference
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 records
Is 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.

Packages in the reference
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.

Diagnostics and AXSYM

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.