#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.