Axiom 0.7.8:用 Axiom 自身编写的函数式系统编程语言
Show HN: Axiom: The Functional Systems Langauge
Axiom 0.7.8 发布,这是一门开源函数式系统编程语言,编译器本身由超过 119,000 行 Axiom 代码写成,可从提交的 LLVM IR 经 llc 与 C 链接器重建并校验两代字节一致。它通过类型与效果系统在编译期捕获遗漏分支和 I/O 副作用,直接编译为含分配器与系统调用的原生二进制,无 VM 和 GC,已为 macOS 与 Linux arm64 提供预编译版本。
Axiom 0.7.8 Open source. Built in Axiom.
The functional systems language
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.
01 / Expressive by designTypes that model your ideas.02 / Explicit where it countsEffects the compiler checks.03 / Native from the startLLVM. Straight to 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
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.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 over 119,000 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.
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.
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.
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.
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.
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 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
| 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.
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.
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 runhello/Main.ax
(import IO) (:: main Int) ;@axiom:effect(io) (fn (main) { (println "Hello from Axiom! 🚀") 0 })axiom run
Hello 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. The compiler doesn't run on Windows: cross-compile to it from Linux or macOS. 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. - SyscallsCompleteNo libc between your program and the kernel on any of the eight hosted targets.
- 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.
- Resource ownershipFunctionalFiles, sockets, terminal modes, channels and mutexes close or free themselves when their last owner lets go.
- 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
- 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. Files, sockets, channels and mutexes are owners too: each closes or frees itself when its last reference goes. When you want reclamation at a point of your choosing, (region r body) rolls the allocator back in one step.
The memory modelWhat 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 gateWhy 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 obfuscationHow 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 compilerDo 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 referenceAre 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.
Packages in the referenceWhy 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.
来源:Hacker News · axiomlang.software