Write classes and generics the way you would in Kotlin or Swift. The compiler quietly decides whether each value lives on the stack, in an arena, on the heap, or behind a refcount — and frees it for you. No garbage collector. No lifetime annotations. No ceremony.
Every value lands on the stack, in an arena, on the heap, or behind a refcount — the compiler picks the tier for you, with no lifetimes to annotate.
Double-free and use-after-free are proven impossible at compile time — the safety a borrow checker buys you, with no lifetime annotations to write.
A function’s failure set is part of its type. Catch it, or pass it up the stack — and nothing throws behind your back.
Real vector types — f64x4, f32x8 — with fused multiply-add and horizontal sums. One binary probes the CPU at startup and runs the widest instructions it has, with a safe fallback on older chips.
Copy loops become memcpy, math loops auto-vectorise, tail calls become plain loops — Axle does this before LLVM even runs, handing the backend code that already arrives optimised.
Lists, maps, sets, an HTTP client, sockets, JSON — the standard library ships what C and C++ make you go find.
Pick a task, then pick the language you would otherwise reach for. Same outcome — Axle just asks less of you to get there.
use std::net::HttpClient;
fn fetch(url : string) : string ! IOException {
return HttpClient::get(url);
}long fetch(const char *url) {
CURL *c = curl_easy_init();
if (!c) return -1;
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_perform(c);
long code = 0;
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code);
curl_easy_cleanup(c);
return code;
}Safe code is supposed to cost you something. A bounds check on every access. An optional wrapped around every get. A length compared a million times inside a loop you already know is fine. Axle reads the guards you wrote — a loop bound, an if (i < len), a capacity that never moves — follows them through loops and across function calls, and removes what they make redundant. You annotate nothing. You give up nothing.
list.get(i) ?? 0 looks like one comparison. It is three: the optional the callee wraps its answer in, your unwrap, and the check inside. All of it exists for a case the caller usually rules out — and when it does, all three go. What is left is the load, the same one arr[i] would have emitted.
A program driving the standard library’s ArrayList compiles to zero bounds traps — its internal checks fall out of the invariants its own methods already maintain. The RingBuffer you wrote this morning gets the same treatment on the same terms. No blessed list of types, nothing to annotate.
An index read out of another array cannot be proven — nothing in the program says what that value holds. Those keep their check, and your program stays correct. The optimiser never trades a guarantee for a benchmark.
Pre-1.0, and conservative by construction: doubt always resolves to keeping the check. Where the proof isn’t there, your program runs exactly as it did.
Compile- and run-time peak RAM, CPU time, wall-clock and binary size across -O0…-O3 — straight from axle bench.
Benchmarks for v0.8.6
StringBuilder append throughput — building one large string.
| Variant | Run | Compile | Output | |||
|---|---|---|---|---|---|---|
| Run time | CPU time | RAM | Compile time | RAM | Binary | |
| Axle -O0 | 5 ms | 5 ms | 29.7 MB | 84 ms | 117.4 MB | 330 KB |
| Axle -O1 | 5 ms | 5 ms | 29.7 MB | 81 ms | 119.9 MB | 330 KB |
| Axle -O2 | 5 ms | 5 ms | 29.7 MB | 91 ms | 117.8 MB | 329 KB |
| Axle -O3 | 5 ms | 4 ms | 29.7 MB | 91 ms | 119.1 MB | 329 KB |
| Rust -O3 | 5 ms | 5 ms | 29.7 MB | 124 ms | 145.8 MB | 3788 KB |
| Rust -O2 | 6 ms | 6 ms | 29.7 MB | 126 ms | 144.4 MB | 3788 KB |
| Rust -O1 | 8 ms | 8 ms | 29.7 MB | 107 ms | 140.3 MB | 3788 KB |
| C++ -O2 | 13 ms | 13 ms | 34.9 MB | 499 ms | 120.8 MB | 17 KB |
| Rust -O0 | 13 ms | 13 ms | 29.7 MB | 101 ms | 141.7 MB | 3791 KB |
| C++ -O3 | 14 ms | 14 ms | 34.7 MB | 519 ms | 120.9 MB | 17 KB |
| C++ -O1 | 15 ms | 15 ms | 35.0 MB | 482 ms | 120.3 MB | 17 KB |
| C++ -O0 | 51 ms | 51 ms | 34.9 MB | 470 ms | 117.0 MB | 30 KB |
// EXPECTED_EXIT: 0
// StringBuilder benchmark #1 — raw append throughput.
// Shared logic with append.cpp / append.rs
// (run via `axle bench --compare cpp,rust`).
//
// Workload: append a fixed 16-byte ASCII chunk 1,000,000 times,
// building a 16 MB buffer (~20 capacity-doubling reallocations).
// Output: total length + 3 sampled bytes (start / middle / end) so
// the three languages cross-check byte-identically without an
// O(n^2) full-string walk.
use std::text::StringBuilder;
fn main() : i32 {
let n : i64 = 1000000;
let chunk : string = "abcdefghijklmnop"; // 16 ASCII bytes
let sb : StringBuilder = new StringBuilder();
let i : i64 = 0;
while (i < n) {
sb.append(chunk);
i = i + 1;
}
// O(1) byte length as the cross-impl correctness signal (the
// workload is deterministic, so identical length ⇒ identical
// bytes). Reading individual bytes back via `byteAt` would be
// O(n) per call (AxleString carries no length → `strlen`), which
// would measure the string ABI, not the builder.
Console::println(sb.byteLength());
sb.dispose();
return 0;
}Axle is pre-1.0 and the core is solid. These land after the first stable release — each one an extension of the same idea, not a rewrite of it.
The same native treatment SIMD gets today — kernels the compiler checks, not a bolted-on API.
Compile-time metaprogramming in the spirit of Rust’s, for the boilerplate no optimiser should have to see.
Load and run .axle modules at runtime — plugins and scripting on top of the same toolchain.
A limited --target wasm mode, so the same source runs in the browser.
Declarative metadata in the Java / TypeScript style, read by tooling and codegen alike.
Drop hand-written LLVM IR into a function — read and injected straight by codegen when you need the last word.
Tests as a first-class language construct, not a convention bolted on the side.
Pre-1.0 and moving fast. Install the toolchain and write your first program in a couple of minutes.