METAXU

Metaxu

A systems language with algebraic effects, mode-based memory safety, and tile-level GPU kernels compiled to Metal.

You write what should happen; handlers decide where it runs: a test, a thread pool, an Apple GPU. Memory safety comes from inferred modes, with no lifetime annotations. And the compiler is readable Python that emits text, built so you can learn from it.

uv tool install git+https://github.com/anoojpatel/metaxu@v0.1.0

one semantics, three backends: interpreter · LLVM native · Metal · reaching for C-like performance*

#why

A wall can carry a message

The name comes from Simone Weil. Her metaxu is the wall between two prison cells: it keeps the prisoners apart, and it is also what they tap on to talk. A programming language sits in the same position, between what you mean and what the hardware does. Metaxu is designed to be that kind of wall. You write at the level of your intent, and what runs is native code that does what you meant.

The second goal is access. Systems languages tend to gatekeep, and compilers tend to be black boxes. Metaxu’s compiler is plain Python that emits text at every step, written to be read, so that learning how a language works is part of using one.

#why build it

Built with simple tools, so that one day it can build itself

A powerful language does not need a large organization behind it. Metaxu’s compiler is plain Python. The parser is a PLY grammar, the checker is ordinary code over an immutable syntax tree, and the native backend writes LLVM IR as text and hands it to clang. Anyone can read each of those, and each stage writes out something you can look at. Those tools were chosen because they are enough. With the mechanics this small, the quality of the language comes down to its design, and a design is something a few people can argue over and get right.

It should be easy to learn. Effects, modes and inference each carry a lot of the language, but you meet them one at a time, and unannotated code stays ordinary code. The book covers the whole language in eighteen chapters, and every example in it runs as a test, so the text and the compiler cannot drift apart. The compiler is the other teaching tool. It is a Python library, and one call runs a program through it and hands back every stage for inspection: the parse tree, the typed tree, the HIR, the MIR and the LLVM IR it lowers to (chapter 17 walks through them).

The end of that road is a compiler written in Metaxu. When the language can express its own parser, checker and lowering, the Python compiler becomes stage zero: it builds the Metaxu compiler once, and from then on Metaxu lowers itself to LLVM IR. A systems language should be able to write its own compiler, and that test already shapes the design: every phase that exists in Python today is written with its rewrite in Metaxu in mind.

#design

Three driving decisions

1. Effects are the machine model. Threads, exceptions and async are not separate features. Each is an effect: a function performs an operation, such as spawn or read, and a handler somewhere up the call stack decides how that operation runs. That is why one kernel function runs as a sequential CPU loop under a test and on a Metal GPU in production, with no change to the kernel.

2. Modes replace lifetimes. There is no garbage collector and there are no lifetime annotations. The checker tracks three properties of every value: whether it may leave its region, whether it may be mutated, and how many times it may be consumed. It infers them from use; you write an annotation only to demand a guarantee. Thread safety follows the same rule. Nothing is marked Send. Capture a shared value in a spawned closure and write to it without a lock, and the compiler rejects the program where it can prove the race; the runtime rejects it where it cannot, with the same message.

3. Algebraic subtyping and a functional surface bridge high level and low level. Inference is SimpleSub-style algebraic subtyping: you annotate function signatures and almost nothing else, and the checker infers the rest, generics included, from how values flow. The language reads like a functional one: expressions, immutable bindings by default, pattern matching, functions as values. The same programs compile to native code with stack allocation by default, SIMD for fixed-size vectors and a small C runtime, because modes give the compiler the ownership facts a functional surface usually hides.

#what it looks like

Streams are effects; consumers are handlers

One mechanism carries exceptions, generators, threads, and GPU launches. Here it carries a stream: each element flows from iota through two handlers into sum by suspending and resuming, one element at a time. No iterator protocol, no intermediate collections. It prints 39999799994.

streams.mxchapter 8 of the book, where this example runs as a test
from std.stream import iota, map, filter, sum;

fn main() -> int {
    let total = sum(filter(map(iota(200000), fn(x: int) -> x * 2),
                           fn(x: int) -> x > 5));
    print(total);
    0
}

A handler that does not resume is an exception. The arm's value becomes the value of the whole handle block, and the rest of work is torn down. Prints before, then 3.

bail.mxchapter 8: handlers that abort
effect Bail {
    bail(code: int) -> Unit
}

fn work() -> int performs Bail {
    print("before");
    perform Bail.bail(3);
    print("after");    # never runs
    99
}

fn main() -> int {
    let r = handle Bail with {
        bail(code) -> code
    } in {
        work()
    };
    print(r);
    0
}

Threads are an effect whose operations map to runtime symbols. Four threads bump one counter behind protect, and the result is 1000 on every run. Nothing is annotated for sharing: the checker recognizes a protected value as safe to capture.

counter.mxchapter 11: std.sync
extern type Thread[T];

effect Thread = {
    fn spawn[T](f: fn() -> @global T) -> @global Thread[T] with EFFECT_SPAWN
    fn join[T](thread: @global Thread[T]) -> @global T with EFFECT_JOIN
}

from std.sync import protect, update, read;

fn main() -> int {
    let p = protect(0);
    let @mut handles = Vec.new();
    let @mut i = 0;
    while i < 4 {
        let t = perform Thread.spawn(|| {
            let @mut j = 0;
            while j < 250 {
                update(p, fn(n: int) -> n + 1);
                j = j + 1
            };
            0
        });
        handles.push(t);
        i = i + 1
    };
    let @mut k = 0;
    while k < 4 {
        perform Thread.join(handles[k]);
        k = k + 1
    };
    print(read(p));
    0
}

Drop the lock and write to a plain Vec from the spawned closure, and the program does not compile. A Vec is one heap object that every alias sees, so the checker names the race at the spawn site and lists the three fixes.

race.mxchapter 11: what the checker rejects
extern type Thread[T];

effect Thread = {
    fn spawn[T](f: fn() -> @global T) -> @global Thread[T] with EFFECT_SPAWN
    fn join[T](thread: @global Thread[T]) -> @global T with EFFECT_JOIN
}

fn main() -> int {
    let mut v = Vec.new();
    v.push(0);
    let t = perform Thread.spawn(|| { v[0] = v[0] + 1; 0 });
    0
}
<mem>:11:34: borrow check failed: closure passed to spawn-mapped
operation 'Thread.spawn' (with EFFECT_SPAWN) captures AND WRITES 'v',
which has shared mutable identity (Vec): two threads mutating through
one handle race (reads are free — contention weakens access, it does
not revoke it); protect it (std.sync.protect), move it into exactly
one thread (move(v)), or take responsibility with `unsafe { .. }`
  11 |     let t = perform Thread.spawn(|| { v[0] = v[0] + 1; 0 });
     |                                  ^~~~~~~~~~~~~~~~~~~~~~~~~

GPU kernels are written at the tile level, the way Triton kernels are. A Tile is an immutable value with its shape in its type, so a mismatched dot is a compile error, and the interpreter, the native runtime and the Metal path agree on every float, bit for bit.

tiles.mxchapter 13: GPU tiles
fn main() -> int {
    let a = Tile.arange(2, 3);
    print(a);
    let b = Tile.transpose(a);
    let c = Tile.dot(a, b);
    print(c);
    print(Tile.sum(c));
    let z = Tile.filled(2, 2, 1.5);
    print(Tile.sum(Tile.scale(z, 2.0)));
    0
}
tile[2x3](0, 1, 2; 3, 4, 5)
tile[2x2](5, 14; 14, 50)
83
12.0

# The Metaxu Book documents the whole language, and every example in it, these five included, runs as a test on every commit. Nothing on this page can quietly rot.

Microbenchmarks against C

Five small kernels, each with a hand-written C twin. A Metaxu build asserts output equality with its twin before any timing runs. Both sides use the same clang at -O2 with function alignment pinned; runs alternate order; the ratio is a median of 11 rounds. What a table like this can and can't tell you is spelled out below it.

benchmarkwhat it stresses metaxucratio
fib(35)recursion, calls29.7 ms29.7 ms1.00x
mandelbrot 800²float ALU, branches50.1 ms49.0 ms1.02x
orbit 20M stepsstruct-per-step rebuild171.0 ms176.2 ms0.97x
nsieve 3×2MVec indexing, bounds-checked83.2 ms73.3 ms1.13x
par_sum 40M × (1+4 thr)OS threads, join values62.6 ms60.8 ms1.03x
pipeline 200k3-stage effects pipeline (metaxu-only)8.5 ms—~42 ns/elem

# every number on this page is from a pinned benchmark or test gate in the repository (benchmarks/suite, 2026-09-29, clang -O2, 11 rounds). Absolute times vary on a shared container; the paired, interleaved ratios are the stable signal. The pipeline row was 166 ms (about 830 ns per element) before handlers that only tail-resume began running on the current stack; each element still crosses three handlers, now at about 14 ns a crossing.

# the asterisks. These are microbenchmarks, and microbenchmarks reward whoever wrote them. The kernels are tiny and fit in cache. The C twins are straightforward ports, not code a C expert spent a week tuning, and a determined person could tilt either column. Nothing here exercises heavy allocation, large working sets, or a real program's shape. We keep the table because it pins regressions and keeps performance in view while the language grows, not as a scoreboard against C. Read every ratio as "in the same neighborhood," nothing finer. This is where performance work starts, not where it ends.