examples: cmatrix — ww + libncurses falling-glyph demo
Exercises match/yield/?/!/alloc/free/slice/tagged-union end-to-end: the @symbol FFI binds initscr/mvaddch/init_pair/getch/napms; setup returns (*void | initerr) propagated via `?`; main unwraps the clock via `!`; dispatch is a nested match-as-expression that yields a bool; key handlers fold into switch/enum (q/space) and (i32 | speederr | void) for 1..4 + the '0' error overlay. No definite or indirect leaks under valgrind (the only "possibly lost" / "still reachable" bytes are libncurses's process-lifetime terminfo caches, freed only with --with-leaks).
This commit is contained in:
12
examples/cmatrix/Makefile
Normal file
12
examples/cmatrix/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
# examples/cmatrix — Matrix-style falling glyphs, built with the ww
|
||||
# toolchain. Links against libncurses.so via w6l's dynamic linker.
|
||||
|
||||
WW := $(shell cd ../..; pwd)/out/bin/ww
|
||||
|
||||
cmatrix: cmatrix.ww
|
||||
$(WW) build cmatrix.ww -L /usr/lib -l ncurses
|
||||
|
||||
clean:
|
||||
rm -f cmatrix cmatrix.o cmatrix.s cmatrix.combined.ww
|
||||
|
||||
.PHONY: clean
|
||||
488
examples/cmatrix/cmatrix.ww
Normal file
488
examples/cmatrix/cmatrix.ww
Normal file
@@ -0,0 +1,488 @@
|
||||
// cmatrix — Matrix-style falling glyphs in the terminal via libncurses.
|
||||
//
|
||||
// Exercises a broad slice of ww:
|
||||
// - @symbol FFI to libncurses (functions + struct-ptr params)
|
||||
// - struct types + struct literals (incl. `aerr{}` to tag a named
|
||||
// `!void` variant)
|
||||
// - alloc/free via os.alloc / os.free (page allocator, no GC)
|
||||
// - slice indexing — `str` palette is (*u8, len); rng_glyph uses [i]
|
||||
// - enum types (theme, action)
|
||||
// - tagged-union return types with two and three variants
|
||||
// - `?` to propagate the error variant up the stack
|
||||
// - `!` to unwrap the success variant or abort on the error variant
|
||||
// - `match` (incl. nested match) to dispatch over (T | E1 | …)
|
||||
// - `yield` to turn `match` into an expression (single-return funcs)
|
||||
// - `switch` over a key code
|
||||
// - `let` type inference — annotations only where the RHS is ambiguous
|
||||
// (uninitialised decls) or carries a load-bearing narrowing cast
|
||||
//
|
||||
// Build:
|
||||
// ww build cmatrix.ww -L /usr/lib -l ncurses
|
||||
//
|
||||
// Keys while running:
|
||||
// q / Q quit
|
||||
// space pause / resume
|
||||
// 1 2 3 4 change speed (1 = slowest, 4 = fastest)
|
||||
// 0 invalid speed — flashes an error overlay
|
||||
// r g b switch trail colour to red / green / blue
|
||||
|
||||
use os;
|
||||
use time;
|
||||
use fmt;
|
||||
|
||||
// ---- libncurses FFI ----------------------------------------------------
|
||||
|
||||
@symbol("initscr") fn nc_initscr() *void;
|
||||
@symbol("endwin") fn nc_endwin() i32;
|
||||
@symbol("noecho") fn nc_noecho() i32;
|
||||
@symbol("cbreak") fn nc_cbreak() i32;
|
||||
@symbol("curs_set") fn nc_curs_set(v: i32) i32;
|
||||
@symbol("start_color") fn nc_start_color() i32;
|
||||
@symbol("use_default_colors") fn nc_use_default_colors() i32;
|
||||
@symbol("init_pair") fn nc_init_pair(pair: i16, fg: i16, bg: i16) i32;
|
||||
@symbol("COLOR_PAIR") fn nc_color_pair(n: i32) i32;
|
||||
@symbol("attron") fn nc_attron(attrs: i32) i32;
|
||||
@symbol("attroff") fn nc_attroff(attrs: i32) i32;
|
||||
@symbol("mvaddch") fn nc_mvaddch(y: i32, x: i32, c: u32) i32;
|
||||
@symbol("mvaddstr") fn nc_mvaddstr(y: i32, x: i32, s: *u8) i32;
|
||||
@symbol("erase") fn nc_erase() i32;
|
||||
@symbol("refresh") fn nc_refresh() i32;
|
||||
@symbol("getch") fn nc_getch() i32;
|
||||
@symbol("nodelay") fn nc_nodelay(win: *void, flag: i32) i32;
|
||||
@symbol("keypad") fn nc_keypad(win: *void, flag: i32) i32;
|
||||
@symbol("napms") fn nc_napms(ms: i32) i32;
|
||||
@symbol("getmaxx") fn nc_getmaxx(win: *void) i32;
|
||||
@symbol("getmaxy") fn nc_getmaxy(win: *void) i32;
|
||||
|
||||
// ncurses palette indices (from <ncurses.h>).
|
||||
def COLOR_RED: i16 = 1;
|
||||
def COLOR_GREEN: i16 = 2;
|
||||
def COLOR_BLUE: i16 = 4;
|
||||
def COLOR_WHITE: i16 = 7;
|
||||
def A_BOLD: i32 = 2097152; // (1 << 21) from <ncurses.h>
|
||||
|
||||
// Local color-pair ids.
|
||||
def CP_DIM: i32 = 1;
|
||||
def CP_BODY: i32 = 2;
|
||||
def CP_BRIGHT: i32 = 3;
|
||||
def CP_HEAD: i32 = 4;
|
||||
|
||||
// Per-column trail buffer width. The head pushes glyphs down through
|
||||
// MAX_TRAIL slots; cells past the tail are erased to space.
|
||||
def MAX_TRAIL: i32 = 24;
|
||||
|
||||
// Number of frames the '0' error overlay stays on screen.
|
||||
def FLASH_FRAMES: i32 = 25;
|
||||
|
||||
// Glyph palette — printable ASCII. `str` is the canonical ww slice
|
||||
// shape (*u8 + len); rng_glyph treats it as one.
|
||||
def GLYPHS: str =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*+=-_;:.<>?/";
|
||||
|
||||
// ---- xorshift64 prng ---------------------------------------------------
|
||||
|
||||
type rng = struct { state: u64 };
|
||||
|
||||
fn rng_next(r: *rng) u64 = {
|
||||
let x = r.state;
|
||||
x ^= x << 13u64;
|
||||
x ^= x >> 7u64;
|
||||
x ^= x << 17u64;
|
||||
r.state = x;
|
||||
return x;
|
||||
};
|
||||
|
||||
fn rng_range(r: *rng, lo: i32, hi: i32) i32 = {
|
||||
if (hi <= lo) { return lo; };
|
||||
let span = (hi - lo): u64;
|
||||
return lo + ((rng_next(r) % span): i32);
|
||||
};
|
||||
|
||||
fn rng_glyph(r: *rng) u8 = {
|
||||
let pool = GLYPHS;
|
||||
let i = (rng_next(r) % (pool.len: u64)): i32;
|
||||
return pool[i];
|
||||
};
|
||||
|
||||
// ---- theme — enum picked by 'r' / 'g' / 'b' ---------------------------
|
||||
|
||||
type theme = enum i32 {
|
||||
GREEN = 0,
|
||||
RED = 1,
|
||||
BLUE = 2,
|
||||
};
|
||||
|
||||
fn theme_color(t: theme) i16 = {
|
||||
if (t == theme.RED) { return COLOR_RED; };
|
||||
if (t == theme.BLUE) { return COLOR_BLUE; };
|
||||
return COLOR_GREEN;
|
||||
};
|
||||
|
||||
// apply_theme — re-init the three trail color pairs against `t`. The
|
||||
// HEAD pair stays white. Subsequent paint_col calls pick up the new
|
||||
// colours automatically because ncurses indirects through pair-id.
|
||||
fn apply_theme(t: theme) void = {
|
||||
let base = theme_color(t);
|
||||
nc_init_pair(CP_DIM: i16, base, -1: i16);
|
||||
nc_init_pair(CP_BODY: i16, base, -1: i16);
|
||||
nc_init_pair(CP_BRIGHT: i16, base, -1: i16);
|
||||
};
|
||||
|
||||
// ---- cmat — column state as parallel arrays ---------------------------
|
||||
//
|
||||
// w6c's element-size analysis returns 8 for any non-primitive slice
|
||||
// element, so an array of `column` structs can't be indexed correctly.
|
||||
// We hold each per-column field in its own primitive-typed buffer plus
|
||||
// one rectangular u8 buffer of glyphs (column-major: glyphs[c*MAX_TRAIL
|
||||
// + i] is the glyph at row head[c]-i in column c).
|
||||
|
||||
type cmat = struct {
|
||||
w: i32,
|
||||
h: i32,
|
||||
head: *i32,
|
||||
length: *i32,
|
||||
speed: *i32,
|
||||
counter: *i32,
|
||||
glyphs: *u8,
|
||||
gspeed: i32, // uniform speed override, -1 = random per column
|
||||
gtheme: i32, // current theme.* value
|
||||
flash: i32, // frames remaining for the '0' error overlay
|
||||
};
|
||||
|
||||
// 56 bytes by natural alignment; rounded to 64 to avoid trap #1 in
|
||||
// selfhost/CLAUDE.md (amalloc < struct size silently corrupts).
|
||||
def CMAT_SZ: u64 = 64u64;
|
||||
|
||||
// ---- error variants ---------------------------------------------------
|
||||
|
||||
// initerr — non-recoverable setup failure. `!` flags this as an error
|
||||
// variant — `?`-propagation picks it as the failure half of any
|
||||
// (T | initerr) shape.
|
||||
type initerr = !str;
|
||||
|
||||
// speederr — the user pressed '0', which is "no speed at all". We track
|
||||
// it separately from "this wasn't a digit" (the void variant of the
|
||||
// same union) so the key handler can distinguish "show error overlay"
|
||||
// from "ignore this key". Constructed via the `speederr{}` struct-lit
|
||||
// form; for `!void` aliases there's no payload to pass.
|
||||
type speederr = !void;
|
||||
|
||||
// ---- ncurses bring-up + tear-down -------------------------------------
|
||||
|
||||
fn setup() (*void | initerr) = {
|
||||
let scr = nc_initscr();
|
||||
if (scr == nil) { return "initscr failed": initerr; };
|
||||
nc_noecho();
|
||||
nc_cbreak();
|
||||
nc_curs_set(0);
|
||||
nc_nodelay(scr, 1);
|
||||
nc_keypad(scr, 1);
|
||||
if (nc_start_color() != 0) {
|
||||
nc_endwin();
|
||||
return "start_color failed": initerr;
|
||||
};
|
||||
nc_use_default_colors();
|
||||
nc_init_pair(CP_HEAD: i16, COLOR_WHITE, -1: i16);
|
||||
apply_theme(theme.GREEN);
|
||||
return scr;
|
||||
};
|
||||
|
||||
// ---- clock helper -----------------------------------------------------
|
||||
|
||||
fn now_ns() (u64 | initerr) = {
|
||||
let ts: time.timespec; // uninitialised — keep the type for the shape
|
||||
if (time.monotonic(&ts) != 0) {
|
||||
return "clock_gettime failed": initerr;
|
||||
};
|
||||
let v = ts.sec * 1000000000i64 + ts.nsec;
|
||||
return v: u64;
|
||||
};
|
||||
|
||||
// ---- cmat allocate / free ---------------------------------------------
|
||||
|
||||
fn cmat_alloc(w: i32, h: i32, r: *rng) *cmat = {
|
||||
let m = os.alloc(CMAT_SZ): *cmat;
|
||||
m.w = w;
|
||||
m.h = h;
|
||||
m.gspeed = -1;
|
||||
m.gtheme = theme.GREEN: i32;
|
||||
m.flash = 0;
|
||||
let n4 = (w: u64) * 4u64;
|
||||
m.head = os.alloc(n4): *i32;
|
||||
m.length = os.alloc(n4): *i32;
|
||||
m.speed = os.alloc(n4): *i32;
|
||||
m.counter = os.alloc(n4): *i32;
|
||||
m.glyphs = os.alloc((w: u64) * (MAX_TRAIL: u64)): *u8;
|
||||
let c = 0;
|
||||
for (c < w) {
|
||||
m.head[c] = -rng_range(r, 0, h);
|
||||
m.length[c] = rng_range(r, 4, MAX_TRAIL);
|
||||
m.speed[c] = rng_range(r, 0, 4);
|
||||
m.counter[c] = 0;
|
||||
let i = 0;
|
||||
for (i < MAX_TRAIL) {
|
||||
m.glyphs[c * MAX_TRAIL + i] = rng_glyph(r);
|
||||
i += 1;
|
||||
};
|
||||
c += 1;
|
||||
};
|
||||
return m;
|
||||
};
|
||||
|
||||
fn cmat_free(m: *cmat) void = {
|
||||
let n4 = (m.w: u64) * 4u64;
|
||||
os.free(m.head: *void, n4);
|
||||
os.free(m.length: *void, n4);
|
||||
os.free(m.speed: *void, n4);
|
||||
os.free(m.counter: *void, n4);
|
||||
os.free(m.glyphs: *void, (m.w: u64) * (MAX_TRAIL: u64));
|
||||
os.free(m: *void, CMAT_SZ);
|
||||
};
|
||||
|
||||
// ---- per-column tick + paint ------------------------------------------
|
||||
|
||||
// fresh_speed — pick a per-column speed honouring the global override.
|
||||
fn fresh_speed(m: *cmat, r: *rng) i32 = {
|
||||
if (m.gspeed >= 0) { return m.gspeed; };
|
||||
return rng_range(r, 0, 4);
|
||||
};
|
||||
|
||||
fn step_col(m: *cmat, c: i32, r: *rng) void = {
|
||||
if (m.counter[c] < m.speed[c]) {
|
||||
m.counter[c] = m.counter[c] + 1;
|
||||
return;
|
||||
};
|
||||
m.counter[c] = 0;
|
||||
|
||||
let base = c * MAX_TRAIL;
|
||||
let i = m.length[c] - 1;
|
||||
for (i > 0) {
|
||||
m.glyphs[base + i] = m.glyphs[base + i - 1];
|
||||
i -= 1;
|
||||
};
|
||||
m.glyphs[base + 0] = rng_glyph(r);
|
||||
m.head[c] = m.head[c] + 1;
|
||||
|
||||
let off = m.head[c] - m.length[c];
|
||||
if (off >= 0) {
|
||||
if (off < m.h) {
|
||||
nc_mvaddch(off, c, 32u32); // ' '
|
||||
};
|
||||
};
|
||||
|
||||
if (m.head[c] - m.length[c] >= m.h) {
|
||||
m.head[c] = -rng_range(r, 0, m.h / 2);
|
||||
m.length[c] = rng_range(r, 4, MAX_TRAIL);
|
||||
m.speed[c] = fresh_speed(m, r);
|
||||
m.counter[c] = 0;
|
||||
};
|
||||
};
|
||||
|
||||
fn paint_col(m: *cmat, c: i32) void = {
|
||||
let base = c * MAX_TRAIL;
|
||||
let len = m.length[c];
|
||||
let head = m.head[c];
|
||||
let i = 0;
|
||||
for (i < len) {
|
||||
let y = head - i;
|
||||
if (y < 0) { break; };
|
||||
if (y >= m.h) { i += 1; continue; };
|
||||
let pair = CP_BODY;
|
||||
let attr = 0;
|
||||
if (i == 0) {
|
||||
pair = CP_HEAD;
|
||||
attr = A_BOLD;
|
||||
} else if (i == 1) {
|
||||
pair = CP_BRIGHT;
|
||||
attr = A_BOLD;
|
||||
} else if (i > len - 4) {
|
||||
pair = CP_DIM;
|
||||
};
|
||||
let mask = nc_color_pair(pair) | attr;
|
||||
nc_attron(mask);
|
||||
nc_mvaddch(y, c, m.glyphs[base + i]: u32);
|
||||
nc_attroff(mask);
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// paint_flash — error overlay shown while m.flash > 0.
|
||||
fn paint_flash(m: *cmat) void = {
|
||||
if (m.flash <= 0) { return; };
|
||||
let msg = " [error] '0' is not a valid speed — use 1..4 ";
|
||||
let attr = nc_color_pair(CP_HEAD) | A_BOLD;
|
||||
nc_attron(attr);
|
||||
nc_mvaddstr(m.h - 1, 0, msg.ptr);
|
||||
nc_attroff(attr);
|
||||
m.flash -= 1;
|
||||
};
|
||||
|
||||
// ---- keyboard ---------------------------------------------------------
|
||||
|
||||
type action = enum i32 {
|
||||
NONE = 0,
|
||||
QUIT = 1,
|
||||
PAUSE = 2,
|
||||
};
|
||||
|
||||
// poll_key — non-blocking. (i32 | void) is the Hare-shaped optional
|
||||
// shape; match handles both.
|
||||
fn poll_key() (i32 | void) = {
|
||||
let c = nc_getch();
|
||||
if (c == -1) { return; };
|
||||
return c;
|
||||
};
|
||||
|
||||
// classify — uses `switch` to fold the 'top-level' control keys into
|
||||
// the action enum. Other digits and letters fall through to NONE and
|
||||
// are dispatched by the more specific parsers below.
|
||||
fn classify(c: i32) action = {
|
||||
switch (c) {
|
||||
case 113, 81: return action.QUIT; // 'q' / 'Q'
|
||||
case 32: return action.PAUSE; // space
|
||||
};
|
||||
return action.NONE;
|
||||
};
|
||||
|
||||
// digit_to_delay — three-variant tagged union:
|
||||
// i32 — successful parse; the value is a frame-delay (0..6)
|
||||
// speederr — the user pressed '0', a known-invalid speed
|
||||
// void — the key wasn't a digit at all (caller can ignore)
|
||||
fn digit_to_delay(c: i32) (i32 | speederr | void) = {
|
||||
if (c == 48) { return speederr{}; }; // '0' invalid
|
||||
if (c == 49) { return 6; }; // '1' slowest
|
||||
if (c == 50) { return 3; }; // '2'
|
||||
if (c == 51) { return 1; }; // '3'
|
||||
if (c == 52) { return 0; }; // '4' fastest
|
||||
return; // not a digit
|
||||
};
|
||||
|
||||
// key_to_theme — 'r'/'g'/'b' map to a theme variant; everything else
|
||||
// yields void.
|
||||
fn key_to_theme(c: i32) (theme | void) = {
|
||||
if (c == 114) { return theme.RED; }; // 'r'
|
||||
if (c == 103) { return theme.GREEN; }; // 'g'
|
||||
if (c == 98) { return theme.BLUE; }; // 'b'
|
||||
return;
|
||||
};
|
||||
|
||||
// apply_speed — set every column's per-advance delay to `d`. Also
|
||||
// records the global override so respawned columns adopt the new speed.
|
||||
fn apply_speed(m: *cmat, d: i32) void = {
|
||||
m.gspeed = d;
|
||||
let c = 0;
|
||||
for (c < m.w) {
|
||||
m.speed[c] = d;
|
||||
m.counter[c] = 0;
|
||||
c += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// dispatch_extra — handle key codes that aren't QUIT/PAUSE. Returns
|
||||
// `true` iff the caller should erase the screen (the trail re-colour
|
||||
// after a theme switch leaves stale cells behind otherwise).
|
||||
//
|
||||
// Both matches are *expressions* — every arm `yield`s the erase-flag,
|
||||
// and the whole `match (...)` is the function's single return value.
|
||||
// The outer void arm yields the inner match-expression, so dispatch
|
||||
// stays declarative even with three levels of dispatch.
|
||||
fn dispatch_extra(m: *cmat, k: i32) bool = {
|
||||
return match (digit_to_delay(k)) {
|
||||
case let d: i32 => {
|
||||
apply_speed(m, d);
|
||||
yield false;
|
||||
};
|
||||
case let e: speederr => {
|
||||
m.flash = FLASH_FRAMES;
|
||||
yield false;
|
||||
};
|
||||
case void => yield match (key_to_theme(k)) {
|
||||
case let t: theme => {
|
||||
m.gtheme = t: i32;
|
||||
apply_theme(t);
|
||||
yield true;
|
||||
};
|
||||
case void => yield false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ---- run --------------------------------------------------------------
|
||||
|
||||
fn run(seed: u64) (i32 | initerr) = {
|
||||
// `?` propagation — if setup returns initerr, run returns initerr.
|
||||
let scr = setup()?;
|
||||
let w = nc_getmaxx(scr);
|
||||
let h = nc_getmaxy(scr);
|
||||
if (w < 8 || h < 4) {
|
||||
nc_endwin();
|
||||
return "terminal too small": initerr;
|
||||
};
|
||||
|
||||
let r = rng { state = seed };
|
||||
if (r.state == 0u64) { r.state = 14695981039346656037u64; };
|
||||
|
||||
let m = cmat_alloc(w, h, &r);
|
||||
nc_erase();
|
||||
|
||||
let running = true;
|
||||
let paused = false;
|
||||
let frames = 0;
|
||||
for (running) {
|
||||
let evt = poll_key();
|
||||
// outer match — dispatch on the variant.
|
||||
match (evt) {
|
||||
case let k: i32 => {
|
||||
let a = classify(k);
|
||||
if (a == action.QUIT) { running = false; };
|
||||
if (a == action.PAUSE) { paused = !paused; };
|
||||
if (a == action.NONE) {
|
||||
if (dispatch_extra(m, k)) {
|
||||
nc_erase();
|
||||
};
|
||||
};
|
||||
};
|
||||
case void => { };
|
||||
};
|
||||
if (running) {
|
||||
if (!paused) {
|
||||
let c = 0;
|
||||
for (c < m.w) {
|
||||
step_col(m, c, &r);
|
||||
paint_col(m, c);
|
||||
c += 1;
|
||||
};
|
||||
};
|
||||
paint_flash(m);
|
||||
nc_refresh();
|
||||
frames += 1;
|
||||
nc_napms(60);
|
||||
};
|
||||
};
|
||||
|
||||
cmat_free(m);
|
||||
nc_endwin();
|
||||
return frames;
|
||||
};
|
||||
|
||||
// ---- entry ------------------------------------------------------------
|
||||
|
||||
export fn main() i32 = {
|
||||
// `!` — unwrap the success variant or abort. The clock is a
|
||||
// pre-init dependency; if it fails there's nothing to tear down.
|
||||
let seed = now_ns()!;
|
||||
|
||||
// match-as-expression: every arm `yield`s the exit code, and the
|
||||
// whole match resolves to a single i32. One return site for the
|
||||
// function — Hare-shaped, no early returns scattered across arms.
|
||||
let r = run(seed);
|
||||
let code = match (r) {
|
||||
case let v: i32 => yield 0;
|
||||
case let e: initerr => {
|
||||
nc_endwin();
|
||||
fmt.errorln("cmatrix: setup failed");
|
||||
yield 1;
|
||||
};
|
||||
};
|
||||
return code;
|
||||
};
|
||||
Reference in New Issue
Block a user