Axiom 0.7.9 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/stable/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 117 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 over 136,000 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

Axiom’s memory model

RECAP

Automatic ownership for everyday values. Checked regions when you need a bounded allocation lifetime. Reference counting and arenas work together, with no tracing collector or borrow checker.

Region-scoped
Choose an allocation lifetime with region. The compiler checks visible reference escapes.
Effect-checked
Alloc, Mut and Unsafe describe memory operations and participate in effect checks and restrictions.
Counted
When the final share ends, owned fields are released and eligible small blocks become reusable.
Arena-hybrid
Region resets reclaim their allocation extent, including cycles within it. Counting also works inside regions.
Predictable reclamation
Ownership events and reset boundaries define reclamation points, without a constant-time guarantee.

Know the boundaries: cycles and larger dead blocks wait for an arena reset. Some ownership paths still retain shares. Raw-address validity remains an unsafe obligation; effect checks do not prove it.

Read the guarantees and limits

The tour

Get a feel for the language.

Working programs, from your first data type to calling Rust. Explore the code, follow the annotations, and see exactly what each program prints.

01/17 · 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.
  • L36optUnwrapOr supplies the default where there is no value.
  • L29One row function aligns the header and data. {id:<9} pads to nine columns; the status column leaves two spaces after its longest value.
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)))

(:: printRow (-> String String String ()))
;@axiom:effect(io)
(fn (printRow id s a)
  (println "{id:<9}{s:<17}{a}"))

(:: row (-> String Parcel ()))
;@axiom:effect(io)
(fn (row id p)
  {
    (let s (status p))
    (let a (optUnwrapOr (alert p) "-"))
    (printRow id s a)
  })

(:: main ())
;@axiom:effect(io)
(fn (main)
  {
    (printRow "parcel" "status" "action")
    (row "AX-1041" Ordered)
    (row "AX-1042" (InTransit "DHL" 2))
    (row "AX-1043" (Held "customs"))
    (row "AX-1044" Delivered)
  })
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 3 ms3,000,000 Collatz sequences · hyperfine best of 20, interleaved
Axiom0.449 s
Rust0.452 s
C0.452 s

All three finish within three milliseconds on this arithmetic loop. This workload does not measure allocation-heavy code.

Compile to a native binaryAxiom is the slowest, by 183 msone file, cold · hyperfine best of 15, interleaved
Axiom0.301 s
Rust0.118 s
C0.162 s

Axiom takes 0.301 seconds, against Rust’s 0.118 seconds (2.55×) and C’s 0.162 seconds (1.86×). These are whole-process builds of the same small program.

Binary size13× smaller than Rust, 7% over Cthe executable on disk
Axiom35,912 B
Rust470,984 B
C33,432 B

About one thirteenth of the Rust binary’s size, and seven percent larger than C, with no C runtime inside it.

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.9 (build f75536bfbee8 2582b9ae2212) · rustc 1.99.1 · clang 23.1.3 · 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 because no toolchain for either was on the machine, and this project publishes only numbers it has 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.449 s0.452 s0.452 s
Compile to a native binaryone file, cold · hyperfine best of 15, interleaved0.301 s0.118 s0.162 s
Binary sizethe executable on disk35,912 B470,984 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
MemoryRECAP: reference counting and checked regions over arenas. 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/stable/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 ())
    ;@axiom:effect(io)
    (fn (main)
      (println "Hello from Axiom! 🚀"))
    
    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 calls, checked generics, aliases, fixed-width integers and explicit as conversions.
  • ADTs / data typesCompleteSums with positional or named fields.
  • Pattern matching (match)CompleteNested, exhaustive, checked in every function.
  • StructsCompleteNamed construction, per-field mut, generic parameters, automatic rendering.
  • Lambda / function valuesCompleteClosures, partial application, _ holes.
  • LoopsCompletefor over ranges and containers, while.
  • SyscallsCompleteNo libc between your program and the kernel on any of the eight hosted targets.
  • Module visibilityCompleteOnly pub leaves a module.
  • Self-hostingDoneThe Rust compiler it replaced has been deleted.

Working, with stated limits

  • EffectsEnforced; unresolved calls markedConstructor allocation contributes Alloc; calls the compiler cannot resolve are marked incomplete.
  • 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 lexical scopes and inferred escape checksScalar results may leave a region; references stay within its lifetime.
  • Resource ownershipFunctionalFiles, sockets, terminal modes, channels and mutexes close or free themselves when their last owner lets go.
  • 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.
  • ContractsFunctionalChecked preconditions and postconditions, shown in editor tooltips and the library reference.
  • Error handlingFunctional; adopted at the syscall seamResult, the postfix ?, and handlers that skip a bad record without unwinding.
  • Editor supportFunctionalLanguage server, formatter, semantic highlighting, tree-sitter grammar and VS Code extension.

Removed

  • 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
Do numbers convert automatically?

No. Numeric operands must have the same type, and integer literals start as Int. Write (as value Type) to widen, narrow or convert to Float. Integer addition, subtraction and multiplication wrap at their declared width; narrowing retains the low destination bits.

type gives an existing type another name. An alias does not create a distinct type or permit mixed arithmetic. A pipeline such as (|> order total dollars) passes each result through ordinary, checked calls.

Numbers, conversions and aliases
Where do imports and local bindings go?

Imports form a module header before every other declaration. Inside { ... }, (let name value) binds a value for the rest of the block. Add mut when assigning that binding with set.

Modules and imports
Does it have a garbage collector?

Axiom calls its hybrid memory model RECAP: regions, effect checks, reference counting and arenas. There is no tracing collector. Releasing a counted value’s final share releases its owned fields and makes eligible small blocks reusable. Files, sockets, channels and mutexes have owners that close or free them when their final share ends.

Reference counting does not collect unreachable cycles. Larger dead allocations and some retained shares wait for an arena reset. Use (region r body) to bound an allocation lifetime; the compiler checks that references stay within it.

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 way out, 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.

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 an extern 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.