diff --git a/examples/cmatrix/Makefile b/examples/cmatrix/Makefile new file mode 100644 index 00000000..048a2994 --- /dev/null +++ b/examples/cmatrix/Makefile @@ -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 diff --git a/examples/cmatrix/cmatrix.ww b/examples/cmatrix/cmatrix.ww new file mode 100644 index 00000000..85b484b2 --- /dev/null +++ b/examples/cmatrix/cmatrix.ww @@ -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 ). +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 + +// 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; +};