The standard library
std/ is written in Metaxu, modeled on Ante's stdlib (the closest relative language: algebraic effects plus ownership), and it doubles as the compiler's toughest test suite; several silent-degradation bugs were found only by writing these modules. import std.foo resolves to std/foo.mx; from std.foo import x brings names into scope, and import std.foo as f gives you qualified calls. This chapter tours each module with running code.
std.prelude
The everyday names in one import. Only unambiguous names are re-exported; map, filter and sum exist in several modules and stay in them.
metaxufrom std.prelude import unwrap_or, clamp, parse_int;
fn main() -> int {
print(clamp(99, 0, 10));
print(unwrap_or(parse_int("41"), 0) + 1);
0
}
std.option and std.result
Combinators over the builtin Some/None and Ok/Err: plain matches, no effects. Chapter 9 shows them chained; the basics:
metaxuimport std.option as opt;
import std.result as res;
fn main() -> int {
let a = opt.map(Some(20), fn(x: int) -> x * 2);
print(opt.unwrap_or(a, 0));
print(opt.unwrap_or(None, 7));
let r = res.map_err(Err("no"), fn(m: string) -> "error: " + m);
match r { Ok(v) => print(v), Err(e) => print(e) };
0
}
std.fail, std.throw, std.early_return
Failure as effects: a computation performs fail() or throw(e), a boundary handler (try_opt, catch_, with_early_return) decides what that means, and the abort semantics skip everything after the failing perform. Chapter 9 has the worked examples.
std.stream
A stream is a thunk performing the Emit effect once per element; consumers are handlers, transformers re-emit, so sum(filter(map(iota(n), f), p)) chains lazily with no intermediate collections. Chapter 8 builds this module up from the effect system.
std.iter
The adapters std.stream defers because they emit tuples: enumerate, zip, take_while, windows, chunks and friends. They are ordinary stream transformers and compose with std.stream's consumers.
metaxufrom std.stream import iota, emit_vec, collect;
from std.iter import zip;
from std.vec import of3;
fn main() -> int {
let pairs = collect(zip(iota(10), emit_vec(of3("a", "b", "c"))));
for (n, s) in pairs {
print(s, n)
}
0
}
One documented caveat: zip realizes its second stream into a Vec before pulling the first, because single-shot delimited continuations cannot step two producers in lockstep. Pass the finite stream second.
std.vec
Eager combinators over the builtin Vec: Vec in, fresh Vec out, the argument never mutated. The lazy equivalents live in std.stream.
metaxufrom std.vec import range_vec, map, filter, sum;
fn main() -> int {
let squares = map(range_vec(1, 6), fn(x: int) -> x * x);
print(sum(squares));
let odds = filter(squares, fn(x: int) -> x % 2 == 1);
print(len(odds));
0
}
std.sort
Stable merge sort, following std.vec's contract. Comparators are a strict less-than predicate, not a three-way cmp; stability is what lets chained sort_by_key calls order by several keys.
metaxufrom std.sort import sort, binary_search;
from std.vec import of3;
fn main() -> int {
let s = sort(of3(31, 4, 15));
print(s[0], s[1], s[2]);
match binary_search(s, 15) {
Some(i) => print("found at", i),
None => print("absent")
};
0
}
std.map
An association-list Map. Every operation is O(n), stated up front, and the API is shaped so a real hash map can replace the representation without changing callers. put overwrites, get answers an Option, and the map mutates in place.
metaxuimport std.map as map;
fn main() -> int {
let @mut m = map.empty();
map.put(m, "ada", 1815);
map.put(m, "grace", 1906);
map.put(m, "ada", 1816);
print(map.size(m));
match map.get(m, "ada") { Some(y) => print(y), None => print("absent") };
print(map.get_or(m, "alan", 0));
0
}
std.string
Helpers over builtin strings. Metaxu has no char type; s[i] yields a one-character string, and this module's API is built on that.
metaxufrom std.string import repeat, reverse, join;
from std.vec import of3;
fn main() -> int {
print(repeat("ab", 3));
print(reverse("stressed"));
print(join(of3("a", "b", "c"), ", "));
0
}
outputababab
desserts
a, b, c
std.parse
Text into values, with the library's two standard answer shapes: parse_int answers an Option, parse_int_or_fail performs Fail so a multi-field parse aborts at the first bad field (chapter 9 shows that composition). Floats, radix prefixes and overflow detection are documented gaps, not silent misbehavior.
metaxufrom std.parse import parse_int, trim, split_on;
fn main() -> int {
match parse_int(trim(" 42 ")) { Some(n) => print(n), None => print("bad") };
match parse_int("4x2") { Some(n) => print(n), None => print("bad") };
print(len(split_on("3,14,15", ",")));
0
}
std.semver
The first piece of the package manager written in Metaxu: versions, version requirements in the Cargo dialect, and ranges closed under intersection, union and complement, which is what a version solver needs. It is a line-for-line port of glade's Python module, and a test runs both over the same few hundred cases and compares every answer (docs/glade_in_metaxu.md).
metaxufrom std.semver import parse_version, parse_requirement, range_contains,
range_intersect, range_to_string;
fn main() -> int {
match parse_requirement("^1.2") {
None => print("bad requirement"),
Some(r) => {
print(range_to_string(r));
match parse_version("1.5.0") {
None => print("bad version"),
Some(v) => print(range_contains(r, v))
};
match parse_version("2.0.0-rc.1") {
None => print("bad version"),
Some(v) => print(range_contains(r, v))
};
match parse_requirement(">=1.5, <3") {
None => print("bad requirement"),
Some(other) => print(range_to_string(range_intersect(r, other)))
}
}
};
0
}
output>=1.2.0, <2.0.0
1
0
>=1.5.0, <2.0.0
1.5.0 is inside ^1.2; 2.0.0-rc.1 is not, because a prerelease only satisfies a requirement that names a prerelease of the same version. Booleans print as 1 and 0.
std.solve
The version solver, PubGrub, ported from glade's Python. It takes the dependency graph as data: every version of every package with its requirements, which is what a registry index lists. Here the newest foo wants a bar the project rules out, so the solver settles on the older foo; had nothing fit, out.explanation would hold the reason as sentences.
metaxufrom std.semver import version, parse_requirement, range_any, version_to_string;
from std.solve import graph_new, graph_add, dep, solve;
fn req(name: string, text: string) -> Dep {
match parse_requirement(text) { None => dep(name, range_any()), Some(r) => dep(name, r) }
}
fn main() -> int {
let @mut g = graph_new();
let @mut foo10 = Vec.new();
foo10.push(req("bar", "^1"));
graph_add(g, "foo", version(1, 0, 0), foo10);
let @mut foo11 = Vec.new();
foo11.push(req("bar", "^2"));
graph_add(g, "foo", version(1, 1, 0), foo11);
graph_add(g, "bar", version(1, 0, 0), Vec.new());
graph_add(g, "bar", version(2, 0, 0), Vec.new());
let @mut root = Vec.new();
root.push(req("foo", "^1"));
root.push(req("bar", "^1"));
graph_add(g, "app", version(0, 1, 0), root);
let out = solve(g, "app", version(0, 1, 0), Vec.new(), Vec.new());
print(out.ok);
let @mut i = 0;
while i < len(out.names) {
print(out.names[i] + " " + version_to_string(out.versions[i]));
i = i + 1
}
0
}
output1
bar 1.0.0
foo 1.0.0
std.hex and std.sha256
Bytes are a Vec of ints from 0 to 255 (chapter 5). std.hex renders them as lowercase hex and parses hex back, and std.sha256 is SHA-256 written in Metaxu, tested against Python's hashlib on both engines. The package manager hashes every vendored tree with it, and a native binary that does so needs no crypto library.
metaxufrom std.hex import to_hex, from_hex;
from std.sha256 import sha256_hex, sha256_string;
fn main() -> int {
print(sha256_string(""));
match from_hex("616263") {
None => print("bad hex"),
Some(bytes) => {
print(bytes.from_bytes());
print(sha256_hex(bytes) == sha256_string("abc"));
print(to_hex(bytes))
}
};
match from_hex("6x") { None => print("bad hex"), Some(b) => print(len(b)) };
0
}
outpute3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
abc
1
616263
bad hex
std.path, std.json and std.toml
Three text formats the package manager lives on, each written in Metaxu and each tested against the Python module that defines it: std.path follows posixpath (join, dirname, basename, normalize, relpath), std.json writes what json.dumps writes, and std.toml reads the subset of TOML that manifests and lockfiles use (tables, arrays of tables, dotted and quoted keys, inline tables, strings, integers, booleans, arrays) and writes it back in the same layout. The values are ordinary enums, Json and Toml, so a document is something you pattern match on.
metaxufrom std.path import join, normalize, relpath;
from std.json import Json, member, to_json;
from std.toml import Toml, parse, to_toml, get_table, get_str;
fn main() -> int {
print(normalize(join("pkgs/geom", "../util/./src")));
print(relpath("/work/app/vendor/geom", "/work/app"));
let @mut fields = Vec.new();
fields.push(member("name", JStr("app")));
fields.push(member("deps", JArr(Vec.new())));
print(to_json(JObj(fields)));
match parse("[package]\nname = \"app\"\n\n[dependencies]\ngeom = { git = \"https://x/geom\", rev = \"v1\" }\n") {
Err(e) => print(e),
Ok(doc) => {
match get_table(doc, "package") {
None => print("no package table"),
Some(pkg) => match get_str(pkg, "name") {
None => print("no name"),
Some(name) => print(name)
}
};
print(to_toml(doc))
}
};
match parse("version = 1.5\n") { Err(e) => print(e), Ok(doc) => print("parsed") };
0
}
outputpkgs/util/src
vendor/geom
{"name":"app","deps":[]}
app
[package]
name = "app"
[dependencies]
geom = { git = "https://x/geom", rev = "v1" }
line 1: floats are not supported
std.fs, std.process, std.env and std.io
Files, other programs, the environment and the error stream are effects. Each operation has a runtime mapping on both engines (POSIX calls natively, Python's os and subprocess on the interpreter), and a failure is an ordinary catchable error whose text is the same on both: read: mx.toml: No such file or directory. Because they are effects, a handler in scope replaces the machine. This program writes and reads a manifest and runs git, and nothing touches the disk or starts a process: two handlers answer everything.
metaxufrom std.fs import Fs, read_text, write_text, exists;
from std.process import Process, output_of;
fn index_of(names: Vec, name: string) -> int {
let @mut i = 0;
let @mut found = 0 - 1;
while i < len(names) && found < 0 {
if names[i] == name { found = i } else { () };
i = i + 1
}
found
}
fn main() -> int {
let @mut names = Vec.new();
let @mut bodies = Vec.new();
handle Fs with {
write_bytes(path, bytes) -> {
let k = index_of(names, path);
if k < 0 { names.push(path); bodies.push(bytes) } else { bodies[k] = bytes };
resume(())
},
read_bytes(path) -> {
let k = index_of(names, path);
if k < 0 { raise("virtual read: " + path + " was never written") } else { resume(bodies[k]) }
},
exists(path) -> resume(index_of(names, path) >= 0)
} in {
write_text("mx.toml", "[package]\nname = \"app\"");
print(read_text("mx.toml"));
print(exists("mx.lock"));
if exists("mx.lock") { print(read_text("mx.lock")) } else { print("no lock yet") }
};
let @mut git = Vec.new();
git.push("git");
git.push("rev-parse");
git.push("HEAD");
handle Process with {
run(argv, cwd) -> resume(1),
status(h) -> resume(0),
stdout(h) -> resume("0123abcd\n"),
stderr(h) -> resume("")
} in {
match output_of(git, "") { Ok(t) => print("fake git said " + t.trim()), Err(e) => print(e) }
};
0
}
output[package]
name = "app"
0
no lock yet
fake git said 0123abcd
Without the handlers the same calls reach the real filesystem and the real git. One thing to know when writing such a handler: a failure raised inside a handler arm surfaces at the handle expression, not inside a try that sits in the handled body (the arm runs in the handler's frame), so a virtual filesystem reports a missing file through exists or a Result rather than by raising. std.env.args() gives the command line (metaxuc run prog.mx -- a b, or a native binary's own arguments), std.env.lookup an environment variable as an Option, and std.io.eprintln writes to stderr. docs/io_runtime.md is the contract for all four.
std.math
Constants are real module-level bindings, read as plain names. sqrt/sin/cos wrap the builtin methods; abs/min/max/clamp take unannotated parameters and work on ints and floats.
metaxufrom std.math import pi, abs, sqrt, powi;
fn main() -> int {
print(pi);
print(abs(-7));
print(sqrt(9));
print(powi(2, 10));
0
}
output3.141592653589793
7
3.0
1024
std.random
Randomness as an effect: next() has no default handler, so the source is always an explicit choice. with_seed installs a xorshift64 generator (same sequence on every backend); with_sequence scripts the draws outright. There is deliberately no real-entropy fallback: performing Random.next unhandled is a loud error, never a silently constant "random" number.
metaxufrom std.random import Random, with_seed, next_below;
fn main() -> int {
with_seed(7, fn() -> () {
print(next_below(100), next_below(100), next_below(100));
()
});
0
}
std.state
The classic State effect with the classic three runners: eval_state answers the result, exec_state the final state, run_state both. The state lives in a cell shared by the handler's arms, which is how the runners read it back after the block completes.
metaxufrom std.state import State, run_state, modify, increment;
fn step() performs State -> () {
increment(5);
modify(fn(s: int) -> s * 2);
()
}
fn main() -> int {
let r = run_state(10, fn() -> () { step() });
print(r.state);
0
}
std.log
The sink is a handler choice, not a global logger. Unhandled, the effect's declared defaults print with level prefixes; quietly silences, with_collected_logs hands you the lines as a Vec, with_min_level filters by re-performing into the next enclosing handler, so filters and collectors compose.
metaxufrom std.log import Log, log_info, log_warn, with_collected_logs;
fn work() -> () {
log_info("starting");
log_warn("low disk");
()
}
fn main() -> int {
work(); # unhandled: the defaults print
let lines = with_collected_logs(fn() -> () { work() });
print("collected", len(lines));
0
}
output[info] starting
[warn] low disk
collected 2
std.test
Assertions report through a Report effect instead of aborting, so a suite runs to the end and a runner tallies. Importing the module's assert_eq shadows the aborting builtin of the same name on purpose (chapter 7's plain-call precedence rule). The report's failure count doubles as a process exit code.
metaxufrom std.test import assert_eq, run_tests;
fn main() -> int {
let report = run_tests("suite", fn() -> () {
assert_eq(1 + 1, 2, "arithmetic");
assert_eq(len("abc"), 3, "len");
()
});
print(report.total, "run,", report.failed, "failed");
report.failed
}
std.sync and std.gpu
std.sync is thread-safe sharing: protect(v) builds a Protected value the spawn checker recognizes as separate by construction, and with_lock/read/write/update are the access idioms. Chapter 11 covers it with the Thread and Mutex effects. std.gpu backs the Tile kernels and the Gpu effect; chapter 13 covers tiles, kernels, and the Metal path.
A last practical note: a std.* name with no file under std/ (std.simd, std.matrix) still imports; nothing is rewritten, and calls fall through to interpreter builtins or fail loudly at run time.