From 647d3ee05e12fa80d7de33f0c442595b79730966 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 17:38:25 +0900 Subject: [PATCH] examples: replace snake with mandelbrot (f64 + libc dyn-link demo) --- .gitignore | 2 +- examples/mandelbrot/Makefile | 15 ++ examples/mandelbrot/mandelbrot.ww | 82 +++++++++ examples/snake/Makefile | 16 -- examples/snake/snake.ww | 289 ------------------------------ 5 files changed, 98 insertions(+), 306 deletions(-) create mode 100644 examples/mandelbrot/Makefile create mode 100644 examples/mandelbrot/mandelbrot.ww delete mode 100644 examples/snake/Makefile delete mode 100644 examples/snake/snake.ww diff --git a/.gitignore b/.gitignore index d810e1ce..65685524 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ selfhost/test/*.combined.ww examples/**/*.o examples/**/*.s examples/**/*.combined.ww -examples/snake/snake +examples/mandelbrot/mandelbrot # Stage-0 binaries under bootstrap//. Untracked by default — # committing them is the v1.0 lock per PLAN.md (the new trust diff --git a/examples/mandelbrot/Makefile b/examples/mandelbrot/Makefile new file mode 100644 index 00000000..cd9e181c --- /dev/null +++ b/examples/mandelbrot/Makefile @@ -0,0 +1,15 @@ +# examples/mandelbrot — built with the ww toolchain only (w6c → w6a → w6l). +# +# `ww build` does the whole pipeline. -l c routes putchar through w6l's +# dynamic linker (PT_INTERP + PT_DYNAMIC + PLT/GOT), so the resulting +# binary maps libc.so.6 at runtime via the system loader. No cc. + +WW := $(shell cd ../..; pwd)/out/bin/ww + +mandelbrot: mandelbrot.ww + $(WW) build mandelbrot.ww -L /usr/lib -l c + +clean: + rm -f mandelbrot mandelbrot.o mandelbrot.s mandelbrot.combined.ww + +.PHONY: clean diff --git a/examples/mandelbrot/mandelbrot.ww b/examples/mandelbrot/mandelbrot.ww new file mode 100644 index 00000000..da6ba1a2 --- /dev/null +++ b/examples/mandelbrot/mandelbrot.ww @@ -0,0 +1,82 @@ +// mandelbrot — ASCII Mandelbrot set, printed via libc. +// +// What this proves: +// 1. ww does f64 arithmetic (mul, add, sub, div) end to end. +// 2. The @symbol FFI mechanism binds write() from libc.so.6. +// 3. w6l's dynamic linker (PT_INTERP + PT_DYNAMIC + PLT/GOT) reaches +// a real .so at runtime — `ldd mandelbrot` shows libc.so.6 as a +// NEEDED entry. +// +// We use libc's `write` (a thin syscall wrapper) rather than `putchar` +// because the runtime here doesn't call __libc_start_main, so stdio +// state stays uninitialised and putchar's buffer is never flushed. +// +// Build: see ./Makefile. No cc — pure ww toolchain. + +@symbol("write") fn c_write(fd: i32, buf: *void, n: u64) i64; + +def W: i32 = 78; +def H: i32 = 30; +def MAXI: i32 = 80; + +// Number of iterations before |z| escapes |z|>2, capped at MAXI. +fn iterate(cr: f64, ci: f64) i32 = { + let zr: f64 = 0.0; + let zi: f64 = 0.0; + let i: i32 = 0; + for (i < MAXI) { + let zr2: f64 = zr * zr; + let zi2: f64 = zi * zi; + if (zr2 + zi2 > 4.0) { return i; }; + let nz: f64 = zr2 - zi2 + cr; + zi = (2.0 * zr) * zi + ci; + zr = nz; + i += 1; + }; + return MAXI; +}; + +// Pick an ASCII glyph by escape iteration count. Inside the set +// (n == MAXI) renders as space — the classic look. +fn shade(n: i32) i32 = { + if (n >= MAXI) { return ' ': i32; }; + let chars: str = " .:-=+*#%@"; + let idx: i32 = (n * chars.len) / MAXI; + if (idx >= chars.len) { idx = chars.len - 1; }; + return chars[idx]: i32; +}; + +export fn main() i32 = { + // Classic Mandelbrot window. The y range is squashed to W/H so + // the picture looks roughly proportional in a terminal cell. + // + // XXX ww's compiler currently emits positive bit patterns for + // negative f64 literals (the unary minus is dropped during + // codegen). As a workaround we build negatives via 0.0 - x. + let z: f64 = 0.0; + let xmin: f64 = z - 2.5; + let xmax: f64 = 1.0; + let ymin: f64 = z - 1.1; + let ymax: f64 = 1.1; + let dx: f64 = (xmax - xmin) / (W: f64); + let dy: f64 = (ymax - ymin) / (H: f64); + + // Compose the picture into a row buffer, then flush per row to + // avoid one libc call per character. + let row: [128]u8; + let y: i32 = 0; + for (y < H) { + let ci: f64 = ymin + (y: f64) * dy; + let x: i32 = 0; + for (x < W) { + let cr: f64 = xmin + (x: f64) * dx; + let n: i32 = iterate(cr, ci); + row[x] = (shade(n)): u8; + x += 1; + }; + row[W] = 10u8; // '\n' + c_write(1, row.ptr: *void, (W + 1): u64); + y += 1; + }; + return 0; +}; diff --git a/examples/snake/Makefile b/examples/snake/Makefile deleted file mode 100644 index bc431fdb..00000000 --- a/examples/snake/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -# examples/snake — built with the ww toolchain only (w6c → w6a → w6l). -# -# `ww build` does the whole pipeline. -l ncurses -l c routes through -# w6l's dynamic linker (PT_INTERP + PT_DYNAMIC + PLT/GOT), so the -# resulting binary maps libncurses.so.6 + libc.so.6 at runtime via -# the system loader. No cc. - -WW := $(shell cd ../..; pwd)/out/bin/ww - -snake: snake.ww - $(WW) build snake.ww -L /usr/lib -l ncurses -l c - -clean: - rm -f snake snake.o snake.s snake.combined.ww - -.PHONY: clean diff --git a/examples/snake/snake.ww b/examples/snake/snake.ww deleted file mode 100644 index a3215bc4..00000000 --- a/examples/snake/snake.ww +++ /dev/null @@ -1,289 +0,0 @@ -// snake — ncurses snake game in ww. -// -// What this proves: -// 1. ww the language can express an interactive program. -// 2. The @symbol FFI mechanism binds to libncurses and libc. -// 3. w6l's new dynamic linker (PT_INTERP + PT_DYNAMIC + PLT/GOT) -// reaches a real .so at runtime — `ldd snake` shows -// libncurses.so.6 and libc.so.6 as NEEDED entries. -// -// Build: see ./Makefile. Pipeline is the ww toolchain (w6c → w6a → w6l) -// with -l ncurses -l c. No cc. -// -// Controls: w/a/s/d to steer, q to quit. - -// ---- ncurses bindings ------------------------------------------------- -// -// mvaddstr is preferred over mvaddch because chtype's width depends on -// whether ncurses was built with --enable-widec. A NUL-terminated -// 1-byte string sidesteps the question and works on either build. - -@symbol("initscr") fn nc_initscr() *void; -@symbol("endwin") fn nc_endwin() i32; -@symbol("cbreak") fn nc_cbreak() i32; -@symbol("noecho") fn nc_noecho() i32; -@symbol("curs_set") fn nc_curs_set(v: i32) i32; -@symbol("timeout") fn nc_timeout(d: i32) void; -@symbol("getch") fn nc_getch() i32; -@symbol("clear") fn nc_clear() i32; -@symbol("refresh") fn nc_refresh() i32; -@symbol("mvaddstr") fn nc_mvaddstr(y: i32, x: i32, s: *u8) i32; - -// ---- libc bindings ---------------------------------------------------- - -@symbol("rand") fn c_rand() i32; -@symbol("srand") fn c_srand(s: u32) void; -@symbol("time") fn c_time(t: *i64) i64; - -// ---- board sizing ----------------------------------------------------- - -def W: i32 = 40; -def H: i32 = 20; -def CAP: i32 = 200; // max snake length, plenty for 40x20 - -def DIR_UP: i32 = 0; -def DIR_RIGHT: i32 = 1; -def DIR_DOWN: i32 = 2; -def DIR_LEFT: i32 = 3; - -def K_W: i32 = 119; -def K_A: i32 = 97; -def K_S: i32 = 115; -def K_D: i32 = 100; -def K_Q: i32 = 113; - -// ---- game state ------------------------------------------------------- -// -// Snake body lives in a circular buffer of (x, y) pairs addressed by -// (tail, len). Head index is cached as `head`. Arrays live in main's -// frame and are passed in as *i32; struct-with-array codegen still -// segfaults today (probe11.ww). - -type state = struct { - head: i32, - tail: i32, - len: i32, - dx: i32, - dy: i32, - dir: i32, - fx: i32, - fy: i32, - score: i32, - dead: bool, -}; - -fn next_idx(i: i32) i32 = { - let n: i32 = i + 1; - if (n >= CAP) { n = 0; }; - return n; -}; - -fn occupied(s: *state, xs: *i32, ys: *i32, x: i32, y: i32) bool = { - let i: i32 = s.tail; - let k: i32 = 0; - for (k < s.len) { - if (xs[i] == x) { - if (ys[i] == y) { return true; }; - }; - i = next_idx(i); - k += 1; - }; - return false; -}; - -fn place_food(s: *state, xs: *i32, ys: *i32) void = { - let tries: i32 = 0; - for (tries < 1000) { - let r1: i32 = c_rand(); - let r2: i32 = c_rand(); - if (r1 < 0) { r1 = -r1; }; - if (r2 < 0) { r2 = -r2; }; - let x: i32 = r1 % W; - let y: i32 = r2 % H; - if (!occupied(s, xs, ys, x, y)) { - s.fx = x; - s.fy = y; - return; - }; - tries += 1; - }; -}; - -fn init_state(s: *state, xs: *i32, ys: *i32) void = { - s.dx = 1; - s.dy = 0; - s.dir = DIR_RIGHT; - s.dead = false; - s.score = 0; - - let cx: i32 = W / 2; - let cy: i32 = H / 2; - xs[0] = cx - 2; ys[0] = cy; - xs[1] = cx - 1; ys[1] = cy; - xs[2] = cx; ys[2] = cy; - s.tail = 0; - s.head = 2; - s.len = 3; - - place_food(s, xs, ys); -}; - -fn handle_key(s: *state, k: i32) void = { - if (k == K_W) { - if (s.dir != DIR_DOWN) { s.dx = 0; s.dy = -1; s.dir = DIR_UP; }; - }; - if (k == K_S) { - if (s.dir != DIR_UP) { s.dx = 0; s.dy = 1; s.dir = DIR_DOWN; }; - }; - if (k == K_A) { - if (s.dir != DIR_RIGHT) { s.dx = -1; s.dy = 0; s.dir = DIR_LEFT; }; - }; - if (k == K_D) { - if (s.dir != DIR_LEFT) { s.dx = 1; s.dy = 0; s.dir = DIR_RIGHT; }; - }; -}; - -fn step(s: *state, xs: *i32, ys: *i32) void = { - let nx: i32 = xs[s.head] + s.dx; - let ny: i32 = ys[s.head] + s.dy; - - if (nx < 0) { s.dead = true; return; }; - if (ny < 0) { s.dead = true; return; }; - if (nx >= W) { s.dead = true; return; }; - if (ny >= H) { s.dead = true; return; }; - - let grow: bool = false; - if (nx == s.fx) { - if (ny == s.fy) { grow = true; }; - }; - let scan: i32 = s.len; - if (!grow) { scan = s.len - 1; }; - let i: i32 = s.tail; - let k: i32 = 0; - for (k < scan) { - if (xs[i] == nx) { - if (ys[i] == ny) { s.dead = true; return; }; - }; - i = next_idx(i); - k += 1; - }; - - let h: i32 = next_idx(s.head); - xs[h] = nx; - ys[h] = ny; - s.head = h; - s.len += 1; - - if (grow) { - s.score += 1; - place_food(s, xs, ys); - } else { - s.tail = next_idx(s.tail); - s.len -= 1; - }; -}; - -// itoa: write decimal of n into NUL-terminated buf (room >= 12). Returns -// number of digits written, not counting the NUL. -fn itoa(buf: *u8, n: i32) i32 = { - if (n == 0) { - buf[0] = '0': u8; - buf[1] = 0u8; - return 1; - }; - let tmp: [16]u8; - let i: i32 = 0; - let v: i32 = n; - for (v > 0) { - tmp[i] = ((v % 10) + 48): u8; - v = v / 10; - i += 1; - }; - let j: i32 = 0; - let cnt: i32 = i; - for (j < cnt) { - i -= 1; - buf[j] = tmp[i]; - j += 1; - }; - buf[cnt] = 0u8; - return cnt; -}; - -// ---- rendering -------------------------------------------------------- - -fn draw_walls() void = { - let x: i32 = 0; - for (x < W + 2) { - nc_mvaddstr(0, x, "#".ptr); - nc_mvaddstr(H + 1, x, "#".ptr); - x += 1; - }; - let y: i32 = 0; - for (y < H + 2) { - nc_mvaddstr(y, 0, "#".ptr); - nc_mvaddstr(y, W + 1, "#".ptr); - y += 1; - }; -}; - -fn render(s: *state, xs: *i32, ys: *i32) void = { - nc_clear(); - draw_walls(); - - nc_mvaddstr(s.fy + 1, s.fx + 1, "*".ptr); - - let i: i32 = s.tail; - let k: i32 = 0; - for (k < s.len - 1) { - nc_mvaddstr(ys[i] + 1, xs[i] + 1, "o".ptr); - i = next_idx(i); - k += 1; - }; - nc_mvaddstr(ys[s.head] + 1, xs[s.head] + 1, "@".ptr); - - nc_mvaddstr(H + 2, 0, "score: ".ptr); - let buf: [16]u8; - itoa(buf.ptr, s.score); - nc_mvaddstr(H + 2, 7, buf.ptr); - nc_mvaddstr(H + 2, 14, " (wasd to move, q to quit)".ptr); - - nc_refresh(); -}; - -// ---- main ------------------------------------------------------------- - -export fn main() i32 = { - c_srand((c_time(nil)): u32); - - nc_initscr(); - nc_cbreak(); - nc_noecho(); - nc_curs_set(0); - nc_timeout(120); - - let s: state; - let xs: [200]i32; - let ys: [200]i32; - init_state(&s, xs.ptr, ys.ptr); - - let quit: bool = false; - for (!s.dead) { - render(&s, xs.ptr, ys.ptr); - let k: i32 = nc_getch(); - if (k == K_Q) { quit = true; break; }; - if (k != -1) { handle_key(&s, k); }; - step(&s, xs.ptr, ys.ptr); - }; - - if (!quit) { - render(&s, xs.ptr, ys.ptr); - nc_mvaddstr(H + 3, 0, "game over - press any key".ptr); - nc_refresh(); - nc_timeout(-1); - nc_getch(); - }; - - nc_endwin(); - return 0; -};