From ab19e08b11a3509c8b2d40426e9f0e3941139276 Mon Sep 17 00:00:00 2001 From: Jeffrey Alan Scudder Date: Mon, 30 Mar 2026 00:20:07 -0700 Subject: [PATCH] =?UTF-8?q?Add=20KidLisp=20=E2=86=92=20WASM=20compiler=20w?= =?UTF-8?q?ith=20self-contained=20renderer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiles KidLisp source directly to standalone .wasm binaries that contain the full rasterization pipeline (Bresenham line, midpoint circle, scanline triangle, box fill) in WASM linear memory. Zero host imports — the host only reads pixels out. Same binary produces identical pixels everywhere for verifiable visual compute. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 + kidlisp-wasm/compiler.mjs | 838 ++++++++++++++++++++++++++------------ kidlisp-wasm/face.lisp | 26 ++ kidlisp-wasm/grid.lisp | 31 ++ kidlisp-wasm/render.mjs | 38 ++ kidlisp-wasm/rings.lisp | 13 + kidlisp-wasm/run.mjs | 192 ++------- 7 files changed, 737 insertions(+), 404 deletions(-) create mode 100644 kidlisp-wasm/face.lisp create mode 100644 kidlisp-wasm/grid.lisp create mode 100644 kidlisp-wasm/render.mjs create mode 100644 kidlisp-wasm/rings.lisp diff --git a/.gitignore b/.gitignore index d1ac30dd3..030519296 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# KidLisp WASM render output +kidlisp-wasm/output/ + # Wrangler local cache .wrangler/ diff --git a/kidlisp-wasm/compiler.mjs b/kidlisp-wasm/compiler.mjs index 44edaa5ad..7e30546fb 100644 --- a/kidlisp-wasm/compiler.mjs +++ b/kidlisp-wasm/compiler.mjs @@ -1,7 +1,14 @@ -// KidLisp → WASM Compiler -// Compiles KidLisp source directly to WebAssembly binary. - -// ─── WASM Binary Encoding ─────────────────────────────────────────── +// KidLisp → WASM Compiler (Self-Contained Renderer) +// +// Emits a single .wasm module that contains: +// - Linear memory with RGBA pixel buffer +// - All rasterization algorithms (line, circle, box, etc.) +// - The compiled piece code +// +// The host only reads memory — no rendering imports. +// Same binary → same pixels → verifiable visual compute. + +// ─── WASM Encoding ────────────────────────────────────────────────── function uleb128(value) { const bytes = []; @@ -48,26 +55,84 @@ function section(id, contents) { return [id, ...uleb128(contents.length), ...contents]; } -function vec(items) { +function vecOf(items) { return [...uleb128(items.length), ...items.flat()]; } -// ─── Color Map ────────────────────────────────────────────────────── +// ─── Bytecode Emitter ─────────────────────────────────────────────── + +class E { + constructor() { + this.b = []; + } + // Constants + i32c(v) { this.b.push(0x41, ...sleb128(v)); return this; } + f32c(v) { this.b.push(0x43, ...f32Bytes(v)); return this; } + // Locals & globals + lg(i) { this.b.push(0x20, ...uleb128(i)); return this; } // local.get + ls(i) { this.b.push(0x21, ...uleb128(i)); return this; } // local.set + lt(i) { this.b.push(0x22, ...uleb128(i)); return this; } // local.tee + gg(i) { this.b.push(0x23, ...uleb128(i)); return this; } // global.get + gs(i) { this.b.push(0x24, ...uleb128(i)); return this; } // global.set + // i32 arithmetic + iadd() { this.b.push(0x6a); return this; } + isub() { this.b.push(0x6b); return this; } + imul() { this.b.push(0x6c); return this; } + idiv() { this.b.push(0x6d); return this; } + irem() { this.b.push(0x6f); return this; } + iand() { this.b.push(0x71); return this; } + ior() { this.b.push(0x72); return this; } + // i32 comparison + ieqz() { this.b.push(0x45); return this; } + ieq() { this.b.push(0x46); return this; } + ine() { this.b.push(0x47); return this; } + ilt() { this.b.push(0x48); return this; } + igt() { this.b.push(0x4a); return this; } + ile() { this.b.push(0x4c); return this; } + ige() { this.b.push(0x4e); return this; } + // f32 arithmetic + fadd() { this.b.push(0x92); return this; } + fsub() { this.b.push(0x93); return this; } + fmul() { this.b.push(0x94); return this; } + fdiv() { this.b.push(0x95); return this; } + fabs() { this.b.push(0x8b); return this; } + fneg() { this.b.push(0x8c); return this; } + fsqrt(){ this.b.push(0x91); return this; } + ffloor(){this.b.push(0x8e); return this; } + // f32 comparison + flt() { this.b.push(0x5b); return this; } + fgt() { this.b.push(0x5d); return this; } + fle() { this.b.push(0x5f); return this; } + fge() { this.b.push(0x5e); return this; } + // Conversion + i2f() { this.b.push(0xb2); return this; } // i32 → f32 + f2i() { this.b.push(0xa8); return this; } // f32 → i32 (trunc) + // Memory + st8(off = 0) { this.b.push(0x3a, 0x00, ...uleb128(off)); return this; } + ld8u(off = 0){ this.b.push(0x2d, 0x00, ...uleb128(off)); return this; } + // Control + if_() { this.b.push(0x04, 0x40); return this; } + else_(){ this.b.push(0x05); return this; } + end() { this.b.push(0x0b); return this; } + block(){ this.b.push(0x02, 0x40); return this; } + loop() { this.b.push(0x03, 0x40); return this; } + br(d) { this.b.push(0x0c, ...uleb128(d)); return this; } + brif(d){ this.b.push(0x0d, ...uleb128(d)); return this; } + ret() { this.b.push(0x0f); return this; } + call(i){ this.b.push(0x10, ...uleb128(i)); return this; } + drop() { this.b.push(0x1a); return this; } + // Get raw bytes + bytes() { return this.b; } +} + +// ─── Colors ───────────────────────────────────────────────────────── const COLORS = { - red: [255, 0, 0], - green: [0, 128, 0], - blue: [0, 0, 255], - white: [255, 255, 255], - black: [0, 0, 0], - yellow: [255, 255, 0], - cyan: [0, 255, 255], - magenta: [255, 0, 255], - orange: [255, 165, 0], - purple: [128, 0, 128], - pink: [255, 192, 203], - gray: [128, 128, 128], - grey: [128, 128, 128], + red: [255, 0, 0], green: [0, 128, 0], blue: [0, 0, 255], + white: [255, 255, 255], black: [0, 0, 0], + yellow: [255, 255, 0], cyan: [0, 255, 255], magenta: [255, 0, 255], + orange: [255, 165, 0], purple: [128, 0, 128], + pink: [255, 192, 203], gray: [128, 128, 128], grey: [128, 128, 128], lime: [0, 255, 0], }; @@ -78,28 +143,20 @@ function tokenize(source) { let i = 0; while (i < source.length) { const ch = source[i]; - if (ch === "(") { - tokens.push({ type: "lparen" }); - i++; - } else if (ch === ")") { - tokens.push({ type: "rparen" }); - i++; - } else if (ch === "\n") { - tokens.push({ type: "newline" }); - i++; - } else if (/\s/.test(ch)) { - i++; - } else if (ch === ";") { - while (i < source.length && source[i] !== "\n") i++; - } else { + if (ch === "(") { tokens.push({ type: "lp" }); i++; } + else if (ch === ")") { tokens.push({ type: "rp" }); i++; } + else if (ch === "\n") { tokens.push({ type: "nl" }); i++; } + else if (/\s/.test(ch)) { i++; } + else if (ch === ";") { while (i < source.length && source[i] !== "\n") i++; } + else { let start = i; while (i < source.length && !/[\s()]/.test(source[i])) i++; const atom = source.slice(start, i); const num = Number(atom); if (!isNaN(num) && atom !== "") { - tokens.push({ type: "number", value: num }); + tokens.push({ type: "num", value: num }); } else { - tokens.push({ type: "symbol", value: atom }); + tokens.push({ type: "sym", value: atom }); } } } @@ -108,227 +165,459 @@ function tokenize(source) { function parse(tokens) { const lines = []; - let currentLine = []; + let cur = []; let pos = 0; - function parseExpr() { + function expr() { if (pos >= tokens.length) return null; - const tok = tokens[pos]; - if (tok.type === "lparen") { + const t = tokens[pos]; + if (t.type === "lp") { pos++; const items = []; - while (pos < tokens.length && tokens[pos].type !== "rparen") { - if (tokens[pos].type === "newline") { - pos++; - continue; - } - const expr = parseExpr(); - if (expr) items.push(expr); + while (pos < tokens.length && tokens[pos].type !== "rp") { + if (tokens[pos].type === "nl") { pos++; continue; } + const e = expr(); + if (e) items.push(e); } - if (pos < tokens.length) pos++; // skip ) - return { type: "list", items }; - } else if (tok.type === "number") { - pos++; - return { type: "number", value: tok.value }; - } else if (tok.type === "symbol") { - pos++; - return { type: "symbol", value: tok.value }; - } + if (pos < tokens.length) pos++; + return { t: "list", items }; + } else if (t.type === "num") { pos++; return { t: "num", v: t.value }; } + else if (t.type === "sym") { pos++; return { t: "sym", v: t.value }; } return null; } while (pos < tokens.length) { - if (tokens[pos].type === "newline") { - if (currentLine.length > 0) { - lines.push(currentLine); - currentLine = []; - } + if (tokens[pos].type === "nl") { + if (cur.length > 0) { lines.push(cur); cur = []; } pos++; continue; } - const expr = parseExpr(); - if (expr) currentLine.push(expr); + const e = expr(); + if (e) cur.push(e); } - if (currentLine.length > 0) lines.push(currentLine); + if (cur.length > 0) lines.push(cur); - // Wrap bare lines as function calls: - // `ink 255 0 0` → `(ink 255 0 0)` const result = []; for (const line of lines) { - if (line.length === 1) { - result.push(line[0]); - } else if (line.length > 1 && line[0].type === "symbol") { - result.push({ type: "list", items: line }); + if (line.length === 1) result.push(line[0]); + else if (line.length > 1 && line[0].t === "sym") { + result.push({ t: "list", items: line }); } else { - for (const expr of line) result.push(expr); + for (const e of line) result.push(e); } } return result; } -// ─── WASM Opcodes ─────────────────────────────────────────────────── - -const OP = { - LOCAL_GET: 0x20, - LOCAL_SET: 0x21, - GLOBAL_GET: 0x23, - GLOBAL_SET: 0x24, - CALL: 0x10, - F32_CONST: 0x43, - F32_ADD: 0x92, - F32_SUB: 0x93, - F32_MUL: 0x94, - F32_DIV: 0x95, - F32_SQRT: 0x91, - F32_ABS: 0x8b, - F32_NEG: 0x8c, - F32_FLOOR: 0x8e, - F32_CEIL: 0x8d, - I32_CONST: 0x41, - I32_ADD: 0x6a, - DROP: 0x1a, - END: 0x0b, -}; +// ─── Globals ──────────────────────────────────────────────────────── + +const G_W = 0, G_H = 1, G_IR = 2, G_IG = 3, G_IB = 4; +const I32 = 0x7f, F32 = 0x7d; + +// ─── Function Indices ─────────────────────────────────────────────── +// No imports — all functions are internal. + +const F_SET_PIXEL = 0; // (i32, i32) → () +const F_WIPE = 1; // (f32, f32, f32) → () +const F_INK = 2; // (f32, f32, f32) → () +const F_PLOT = 3; // (f32, f32) → () +const F_LINE = 4; // (f32, f32, f32, f32) → () +const F_BOX = 5; // (f32, f32, f32, f32) → () +const F_CIRCLE = 6; // (f32, f32, f32) → () +const F_TRI = 7; // (f32, f32, f32, f32, f32, f32) → () +const F_PAINT = 8; // (f32, f32, f32) → () + +// ─── Runtime Function Emitters ────────────────────────────────────── + +// $set_pixel(x: i32, y: i32) +// Writes a pixel at (x,y) using current ink color. +function emitSetPixel() { + const e = new E(); + // params: 0=x, 1=y | locals: 2=offset + // Bounds check + e.lg(0).i32c(0).ilt().if_().ret().end(); + e.lg(0).gg(G_W).ige().if_().ret().end(); + e.lg(1).i32c(0).ilt().if_().ret().end(); + e.lg(1).gg(G_H).ige().if_().ret().end(); + // offset = (y * width + x) * 4 + e.lg(1).gg(G_W).imul().lg(0).iadd().i32c(4).imul().ls(2); + // store RGBA + e.lg(2).gg(G_IR).st8(); + e.lg(2).i32c(1).iadd().gg(G_IG).st8(); + e.lg(2).i32c(2).iadd().gg(G_IB).st8(); + e.lg(2).i32c(3).iadd().i32c(255).st8(); + e.end(); + return { locals: [[1, I32]], code: e.bytes() }; // 1 i32 local (offset) +} -const F32 = 0x7d; +// $wipe(r: f32, g: f32, b: f32) +function emitWipe() { + const e = new E(); + // params: 0=r, 1=g, 2=b | locals: 3=i, 4=total, 5=ri, 6=gi, 7=bi + e.lg(0).f2i().ls(5); + e.lg(1).f2i().ls(6); + e.lg(2).f2i().ls(7); + // total = w * h * 4 + e.gg(G_W).gg(G_H).imul().i32c(4).imul().ls(4); + // i = 0 + e.i32c(0).ls(3); + // loop + e.block().loop(); + e.lg(3).lg(4).ige().brif(1); + e.lg(3).lg(5).st8(); + e.lg(3).i32c(1).iadd().lg(6).st8(); + e.lg(3).i32c(2).iadd().lg(7).st8(); + e.lg(3).i32c(3).iadd().i32c(255).st8(); + e.lg(3).i32c(4).iadd().ls(3); + e.br(0); + e.end().end(); // loop, block + e.end(); + return { locals: [[5, I32]], code: e.bytes() }; +} -// ─── Compiler ─────────────────────────────────────────────────────── +// $ink(r: f32, g: f32, b: f32) +function emitInk() { + const e = new E(); + e.lg(0).f2i().gs(G_IR); + e.lg(1).f2i().gs(G_IG); + e.lg(2).f2i().gs(G_IB); + e.end(); + return { locals: [], code: e.bytes() }; +} -export class Compiler { - constructor() { - this.types = []; - this.typeMap = new Map(); - this.imports = []; - this.importCount = 0; - this.code = []; - this.setupImports(); - } +// $plot(x: f32, y: f32) +function emitPlot() { + const e = new E(); + e.lg(0).f2i(); + e.lg(1).f2i(); + e.call(F_SET_PIXEL); + e.end(); + return { locals: [], code: e.bytes() }; +} - addType(params, results) { - const key = `${params.join(",")}->${results.join(",")}`; - if (this.typeMap.has(key)) return this.typeMap.get(key); - const idx = this.types.length; - this.types.push({ params, results }); - this.typeMap.set(key, idx); - return idx; - } +// $line(x0: f32, y0: f32, x1: f32, y1: f32) — Bresenham +function emitLine() { + const e = new E(); + // params: 0=x0, 1=y0, 2=x1, 3=y1 + // locals: 4=ix0, 5=iy0, 6=ix1, 7=iy1, 8=dx, 9=dy, 10=sx, 11=sy, 12=err, 13=e2 + e.lg(0).f2i().ls(4); + e.lg(1).f2i().ls(5); + e.lg(2).f2i().ls(6); + e.lg(3).f2i().ls(7); + + // dx = abs(ix1 - ix0) + e.lg(6).lg(4).isub().ls(8); + e.lg(8).i32c(0).ilt().if_(); + e.i32c(0).lg(8).isub().ls(8); + e.end(); + + // dy = abs(iy1 - iy0) + e.lg(7).lg(5).isub().ls(9); + e.lg(9).i32c(0).ilt().if_(); + e.i32c(0).lg(9).isub().ls(9); + e.end(); + + // sx = ix0 < ix1 ? 1 : -1 + e.lg(4).lg(6).ilt().if_(); + e.i32c(1).ls(10); + e.else_(); + e.i32c(-1).ls(10); + e.end(); + + // sy = iy0 < iy1 ? 1 : -1 + e.lg(5).lg(7).ilt().if_(); + e.i32c(1).ls(11); + e.else_(); + e.i32c(-1).ls(11); + e.end(); + + // err = dx - dy + e.lg(8).lg(9).isub().ls(12); + + // Main loop + e.block().loop(); + + // plot(ix0, iy0) + e.lg(4).lg(5).call(F_SET_PIXEL); + + // if ix0 == ix1 && iy0 == iy1: break + e.lg(4).lg(6).ieq(); + e.lg(5).lg(7).ieq(); + e.iand().brif(1); + + // e2 = 2 * err + e.lg(12).i32c(2).imul().ls(13); + + // if e2 > -dy: err -= dy; ix0 += sx + e.lg(13).i32c(0).lg(9).isub().igt().if_(); + e.lg(12).lg(9).isub().ls(12); + e.lg(4).lg(10).iadd().ls(4); + e.end(); + + // if e2 < dx: err += dx; iy0 += sy + e.lg(13).lg(8).ilt().if_(); + e.lg(12).lg(8).iadd().ls(12); + e.lg(5).lg(11).iadd().ls(5); + e.end(); + + e.br(0); + e.end().end(); // loop, block + e.end(); + return { locals: [[10, I32]], code: e.bytes() }; +} - addImport(module, name, paramCount, hasReturn = false) { - const params = Array(paramCount).fill(F32); - const results = hasReturn ? [F32] : []; - const typeIdx = this.addType(params, results); - const funcIdx = this.importCount++; - this.imports.push({ module, name, typeIdx }); - return funcIdx; - } +// $box(x: f32, y: f32, w: f32, h: f32) +function emitBox() { + const e = new E(); + // params: 0=x, 1=y, 2=w, 3=h + // locals: 4=ix, 5=iy, 6=ex, 7=ey, 8=py, 9=px + e.lg(0).f2i().ls(4); // ix + e.lg(1).f2i().ls(5); // iy + e.lg(0).lg(2).fadd().f2i().ls(6); // ex = x + w + e.lg(1).lg(3).fadd().f2i().ls(7); // ey = y + h + // py = iy + e.lg(5).ls(8); + // outer loop (rows) + e.block().loop(); + e.lg(8).lg(7).ige().brif(1); + // px = ix + e.lg(4).ls(9); + // inner loop (cols) + e.block().loop(); + e.lg(9).lg(6).ige().brif(1); + e.lg(9).lg(8).call(F_SET_PIXEL); + e.lg(9).i32c(1).iadd().ls(9); + e.br(0); + e.end().end(); // inner loop, inner block + e.lg(8).i32c(1).iadd().ls(8); + e.br(0); + e.end().end(); // outer loop, outer block + e.end(); + return { locals: [[6, I32]], code: e.bytes() }; +} + +// $circle(cx: f32, cy: f32, r: f32) — brute force filled +function emitCircle() { + const e = new E(); + // params: 0=cx, 1=cy, 2=r + // locals: 3=icx, 4=icy, 5=ir, 6=dy, 7=dx, 8=r2 + e.lg(0).f2i().ls(3); + e.lg(1).f2i().ls(4); + e.lg(2).f2i().ls(5); + // r2 = ir * ir + e.lg(5).lg(5).imul().ls(8); + // dy = -ir + e.i32c(0).lg(5).isub().ls(6); + // outer loop + e.block().loop(); + e.lg(6).lg(5).igt().brif(1); + // dx = -ir + e.i32c(0).lg(5).isub().ls(7); + // inner loop + e.block().loop(); + e.lg(7).lg(5).igt().brif(1); + // if dx*dx + dy*dy <= r2 + e.lg(7).lg(7).imul().lg(6).lg(6).imul().iadd(); + e.lg(8).ile().if_(); + e.lg(3).lg(7).iadd(); // cx + dx + e.lg(4).lg(6).iadd(); // cy + dy + e.call(F_SET_PIXEL); + e.end(); + e.lg(7).i32c(1).iadd().ls(7); + e.br(0); + e.end().end(); // inner + e.lg(6).i32c(1).iadd().ls(6); + e.br(0); + e.end().end(); // outer + e.end(); + return { locals: [[6, I32]], code: e.bytes() }; +} + +// $tri(x0,y0,x1,y1,x2,y2) — scanline fill +function emitTri() { + const e = new E(); + // params: 0=x0,1=y0,2=x1,3=y1,4=x2,5=y2 + // locals: 6=iy0,7=iy1,8=iy2,9=minY,10=maxY,11=y,12=minX,13=maxX,14=x + // 15=ix0,16=ix1,17=ix2,18=tmp + + // Convert to int + e.lg(0).f2i().ls(15); + e.lg(1).f2i().ls(6); + e.lg(2).f2i().ls(16); + e.lg(3).f2i().ls(7); + e.lg(4).f2i().ls(17); + e.lg(5).f2i().ls(8); + + // minY = min(iy0, iy1, iy2), clamped to 0 + e.lg(6).ls(9); + e.lg(7).lg(9).ilt().if_().lg(7).ls(9).end(); + e.lg(8).lg(9).ilt().if_().lg(8).ls(9).end(); + e.lg(9).i32c(0).ilt().if_().i32c(0).ls(9).end(); + + // maxY = max(iy0, iy1, iy2), clamped to height-1 + e.lg(6).ls(10); + e.lg(7).lg(10).igt().if_().lg(7).ls(10).end(); + e.lg(8).lg(10).igt().if_().lg(8).ls(10).end(); + e.gg(G_H).i32c(1).isub().ls(18); + e.lg(10).lg(18).igt().if_().lg(18).ls(10).end(); + + // y = minY + e.lg(9).ls(11); + e.block().loop(); + e.lg(11).lg(10).igt().brif(1); + + // Reset scan extents + e.gg(G_W).ls(12); // minX = width (will be narrowed) + e.i32c(0).ls(13); // maxX = 0 + + // Check each of 3 edges — inlined to avoid extra functions + // Edge 0→1 + e.lg(6).lg(11).ile().lg(7).lg(11).igt().iand() + .lg(7).lg(11).ile().lg(6).lg(11).igt().iand() + .ior().if_(); + // x = ix0 + (y - iy0) * (ix1 - ix0) / (iy1 - iy0) + e.lg(11).lg(6).isub().lg(16).lg(15).isub().imul(); + e.lg(7).lg(6).isub(); + // avoid div by zero + e.ls(18); + e.lg(18).ieqz().if_().i32c(1).ls(18).end(); + e.lg(18).idiv(); + e.lg(15).iadd().ls(14); + e.lg(14).lg(12).ilt().if_().lg(14).ls(12).end(); + e.lg(14).lg(13).igt().if_().lg(14).ls(13).end(); + e.end(); + + // Edge 1→2 + e.lg(7).lg(11).ile().lg(8).lg(11).igt().iand() + .lg(8).lg(11).ile().lg(7).lg(11).igt().iand() + .ior().if_(); + e.lg(11).lg(7).isub().lg(17).lg(16).isub().imul(); + e.lg(8).lg(7).isub(); + e.ls(18); + e.lg(18).ieqz().if_().i32c(1).ls(18).end(); + e.lg(18).idiv(); + e.lg(16).iadd().ls(14); + e.lg(14).lg(12).ilt().if_().lg(14).ls(12).end(); + e.lg(14).lg(13).igt().if_().lg(14).ls(13).end(); + e.end(); + + // Edge 2→0 + e.lg(8).lg(11).ile().lg(6).lg(11).igt().iand() + .lg(6).lg(11).ile().lg(8).lg(11).igt().iand() + .ior().if_(); + e.lg(11).lg(8).isub().lg(15).lg(17).isub().imul(); + e.lg(6).lg(8).isub(); + e.ls(18); + e.lg(18).ieqz().if_().i32c(1).ls(18).end(); + e.lg(18).idiv(); + e.lg(17).iadd().ls(14); + e.lg(14).lg(12).ilt().if_().lg(14).ls(12).end(); + e.lg(14).lg(13).igt().if_().lg(14).ls(13).end(); + e.end(); + + // Clamp and fill scanline + e.lg(12).i32c(0).ilt().if_().i32c(0).ls(12).end(); + e.gg(G_W).i32c(1).isub().ls(18); + e.lg(13).lg(18).igt().if_().lg(18).ls(13).end(); + + // Fill from minX to maxX + e.lg(12).ls(14); + e.block().loop(); + e.lg(14).lg(13).igt().brif(1); + e.lg(14).lg(11).call(F_SET_PIXEL); + e.lg(14).i32c(1).iadd().ls(14); + e.br(0); + e.end().end(); + + e.lg(11).i32c(1).iadd().ls(11); + e.br(0); + e.end().end(); // y loop + e.end(); + return { locals: [[13, I32]], code: e.bytes() }; +} + +// ─── Compiler ─────────────────────────────────────────────────────── - setupImports() { - this.funcs = {}; - // Drawing primitives — all f32 params - this.funcs.wipe = this.addImport("env", "wipe", 3); - this.funcs.ink = this.addImport("env", "ink", 3); - this.funcs.line = this.addImport("env", "line", 4); - this.funcs.box = this.addImport("env", "box", 4); - this.funcs.circle = this.addImport("env", "circle", 3); - this.funcs.plot = this.addImport("env", "plot", 2); - this.funcs.tri = this.addImport("env", "tri", 6); - - // paint(w, h, frame) → () - this.paintTypeIdx = this.addType([F32, F32, F32], []); +export class Compiler { + constructor() { + this.code = new E(); // bytecode for paint body } - // Emit bytecode that pushes a value onto the WASM stack. - compileExpr(expr) { - if (expr.type === "number") { - this.code.push(OP.F32_CONST, ...f32Bytes(expr.value)); - } else if (expr.type === "symbol") { - this.compileSymbol(expr.value); - } else if (expr.type === "list") { - this.compileCall(expr); + // Emit piece expression + compileExpr(node) { + if (node.t === "num") { + this.code.f32c(node.v); + } else if (node.t === "sym") { + this.compileSym(node.v); + } else if (node.t === "list") { + this.compileCall(node); } } - compileSymbol(name) { - // paint params: 0=w, 1=h, 2=frame - if (name === "w") { - this.code.push(OP.LOCAL_GET, ...uleb128(0)); - return; - } - if (name === "h") { - this.code.push(OP.LOCAL_GET, ...uleb128(1)); - return; - } - if (name === "frame" || name === "f") { - this.code.push(OP.LOCAL_GET, ...uleb128(2)); + compileSym(name) { + if (name === "w") { this.code.lg(0); return; } + if (name === "h") { this.code.lg(1); return; } + if (name === "frame" || name === "f") { this.code.lg(2); return; } + + // Division shorthand: w/2, h/3, etc. + const dm = name.match(/^(\w+)\/(\d+(?:\.\d+)?)$/); + if (dm) { + this.compileSym(dm[1]); + this.code.f32c(parseFloat(dm[2])); + this.code.fdiv(); return; } - // Division shorthand: w/2, h/3, etc. - const divMatch = name.match(/^(\w+)\/(\d+(?:\.\d+)?)$/); - if (divMatch) { - this.compileSymbol(divMatch[1]); - this.code.push(OP.F32_CONST, ...f32Bytes(parseFloat(divMatch[2]))); - this.code.push(OP.F32_DIV); + // Multiplication shorthand: w*2, h*3, etc. + const mm = name.match(/^(\w+)\*(\d+(?:\.\d+)?)$/); + if (mm) { + this.compileSym(mm[1]); + this.code.f32c(parseFloat(mm[2])); + this.code.fmul(); return; } - // Color names → push 3 f32 values (r, g, b) + // Color name → 3 f32 values if (COLORS[name]) { const [r, g, b] = COLORS[name]; - this.code.push(OP.F32_CONST, ...f32Bytes(r)); - this.code.push(OP.F32_CONST, ...f32Bytes(g)); - this.code.push(OP.F32_CONST, ...f32Bytes(b)); + this.code.f32c(r).f32c(g).f32c(b); return; } throw new Error(`Unknown symbol: ${name}`); } - compileCall(expr) { - if (expr.items.length === 0) return; - const head = expr.items[0]; - if (head.type !== "symbol") { - throw new Error(`Expected function name, got ${JSON.stringify(head)}`); - } + compileCall(node) { + if (node.items.length === 0) return; + const head = node.items[0]; + if (head.t !== "sym") throw new Error(`Expected function name, got ${JSON.stringify(head)}`); - const name = head.value; - const args = expr.items.slice(1); + const name = head.v; + const args = node.items.slice(1); // Arithmetic - const arithOp = { "+": OP.F32_ADD, "-": OP.F32_SUB, "*": OP.F32_MUL, "/": OP.F32_DIV }; - if (arithOp[name]) { + const arith = { "+": "fadd", "-": "fsub", "*": "fmul", "/": "fdiv" }; + if (arith[name]) { this.compileExpr(args[0]); this.compileExpr(args[1]); - this.code.push(arithOp[name]); + this.code[arith[name]](); return; } // Math builtins - if (name === "sqrt") { - this.compileExpr(args[0]); - this.code.push(OP.F32_SQRT); - return; - } - if (name === "abs") { - this.compileExpr(args[0]); - this.code.push(OP.F32_ABS); - return; - } - if (name === "neg") { - this.compileExpr(args[0]); - this.code.push(OP.F32_NEG); - return; - } - if (name === "floor") { - this.compileExpr(args[0]); - this.code.push(OP.F32_FLOOR); - return; - } - - // Drawing functions - if (this.funcs[name] !== undefined) { + if (name === "sqrt") { this.compileExpr(args[0]); this.code.fsqrt(); return; } + if (name === "abs") { this.compileExpr(args[0]); this.code.fabs(); return; } + if (name === "neg") { this.compileExpr(args[0]); this.code.fneg(); return; } + if (name === "floor"){ this.compileExpr(args[0]); this.code.ffloor(); return; } + + // Drawing functions → internal function calls + const funcMap = { + wipe: F_WIPE, ink: F_INK, plot: F_PLOT, + line: F_LINE, box: F_BOX, circle: F_CIRCLE, tri: F_TRI, + }; + if (funcMap[name] !== undefined) { for (const arg of args) this.compileExpr(arg); - this.code.push(OP.CALL, ...uleb128(this.funcs[name])); + this.code.call(funcMap[name]); return; } @@ -336,71 +625,116 @@ export class Compiler { } compile(source) { - const tokens = tokenize(source); - const ast = parse(tokens); - - for (const expr of ast) { - if (expr.type === "list") { - this.compileCall(expr); - } else if (expr.type === "symbol" && COLORS[expr.value]) { - // Bare color name on a line → wipe with that color - const [r, g, b] = COLORS[expr.value]; - this.code.push(OP.F32_CONST, ...f32Bytes(r)); - this.code.push(OP.F32_CONST, ...f32Bytes(g)); - this.code.push(OP.F32_CONST, ...f32Bytes(b)); - this.code.push(OP.CALL, ...uleb128(this.funcs.wipe)); + const ast = parse(tokenize(source)); + + // Compile piece code into paint body + // Paint starts by setting globals from params + this.code.lg(0).f2i().gs(G_W); // width = param 0 + this.code.lg(1).f2i().gs(G_H); // height = param 1 + + for (const node of ast) { + if (node.t === "list") { + this.compileCall(node); + } else if (node.t === "sym" && COLORS[node.v]) { + // Bare color name → wipe + const [r, g, b] = COLORS[node.v]; + this.code.f32c(r).f32c(g).f32c(b).call(F_WIPE); } } - return this.emit(); + return this.buildModule(); } - emit() { - const bytes = []; + buildModule() { + const out = []; // Magic + version - bytes.push(0x00, 0x61, 0x73, 0x6d); // \0asm - bytes.push(0x01, 0x00, 0x00, 0x00); // version 1 - - // ── Type section (1) ── - const typeEntries = this.types.map((t) => [ - 0x60, - ...uleb128(t.params.length), - ...t.params, - ...uleb128(t.results.length), - ...t.results, - ]); - bytes.push(...section(1, vec(typeEntries))); - - // ── Import section (2) ── - const importEntries = this.imports.map((imp) => [ - ...encodeString(imp.module), - ...encodeString(imp.name), - 0x00, // func - ...uleb128(imp.typeIdx), - ]); - bytes.push(...section(2, vec(importEntries))); - - // ── Function section (3) — declare paint ── - bytes.push(...section(3, vec([[...uleb128(this.paintTypeIdx)]]))); - - // ── Export section (7) ── - const paintIdx = this.importCount; // first non-import func - const exportEntries = [ - [...encodeString("paint"), 0x00, ...uleb128(paintIdx)], + out.push(0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00); + + // ── Types ── + const types = [ + [0x60, 2, I32, I32, 0], // 0: (i32,i32)→() + [0x60, 3, F32, F32, F32, 0], // 1: (f32,f32,f32)→() + [0x60, 2, F32, F32, 0], // 2: (f32,f32)→() + [0x60, 4, F32, F32, F32, F32, 0], // 3: (f32,f32,f32,f32)→() + [0x60, 6, F32, F32, F32, F32, F32, F32, 0], // 4: (f32,f32,f32,f32,f32,f32)→() ]; - bytes.push(...section(7, vec(exportEntries))); + // Format: 0x60 paramcount paramtypes... resultcount resulttypes... + const typeEntries = types.map(t => { + const tag = t[0]; + const paramCount = t[1]; + const params = t.slice(2, 2 + paramCount); + const resultCount = t[2 + paramCount]; + const results = t.slice(3 + paramCount); + return [tag, ...uleb128(paramCount), ...params, ...uleb128(resultCount), ...results]; + }); + out.push(...section(1, vecOf(typeEntries))); + + // ── Functions (no imports!) ── + // Map each function to its type index + const funcTypes = [ + 0, // set_pixel: (i32,i32)→() + 1, // wipe: (f32,f32,f32)→() + 1, // ink: (f32,f32,f32)→() + 2, // plot: (f32,f32)→() + 3, // line: (f32,f32,f32,f32)→() + 3, // box: (f32,f32,f32,f32)→() + 1, // circle: (f32,f32,f32)→() + 4, // tri: (f32,f32,f32,f32,f32,f32)→() + 1, // paint: (f32,f32,f32)→() + ]; + out.push(...section(3, vecOf(funcTypes.map(t => [...uleb128(t)])))); + + // ── Memory ── + // 16 pages = 1MB, enough for 512x512 RGBA + out.push(...section(5, vecOf([[0x00, ...uleb128(16)]]))); + + // ── Globals ── + const globals = [ + [I32, 0x01, 0x41, ...sleb128(0), 0x0b], // width: i32 mut = 0 + [I32, 0x01, 0x41, ...sleb128(0), 0x0b], // height: i32 mut = 0 + [I32, 0x01, 0x41, ...sleb128(255), 0x0b], // ink_r: i32 mut = 255 + [I32, 0x01, 0x41, ...sleb128(255), 0x0b], // ink_g: i32 mut = 255 + [I32, 0x01, 0x41, ...sleb128(255), 0x0b], // ink_b: i32 mut = 255 + ]; + out.push(...section(6, vecOf(globals))); - // ── Code section (10) ── - const body = [ - 0x00, // 0 local declarations - ...this.code, - OP.END, + // ── Exports ── + const exports = [ + [...encodeString("paint"), 0x00, ...uleb128(F_PAINT)], + [...encodeString("memory"), 0x02, ...uleb128(0)], ]; - const codeEntry = [...uleb128(body.length), ...body]; - bytes.push(...section(10, vec([codeEntry]))); + out.push(...section(7, vecOf(exports))); + + // ── Code ── + const runtimeFuncs = [ + emitSetPixel(), // 0 + emitWipe(), // 1 + emitInk(), // 2 + emitPlot(), // 3 + emitLine(), // 4 + emitBox(), // 5 + emitCircle(), // 6 + emitTri(), // 7 + ]; + + // Paint function body + const paintBody = { locals: [], code: [...this.code.bytes(), 0x0b] }; + + const allFuncs = [...runtimeFuncs, paintBody]; + + const codeBodies = allFuncs.map(fn => { + // Local declarations: groups of (count, type) + const localDecl = fn.locals.length > 0 + ? [...uleb128(fn.locals.length), ...fn.locals.flatMap(([count, type]) => [...uleb128(count), type])] + : [0x00]; + const body = [...localDecl, ...fn.code]; + return [...uleb128(body.length), ...body]; + }); + + out.push(...section(10, vecOf(codeBodies))); - return new Uint8Array(bytes); + return new Uint8Array(out); } } diff --git a/kidlisp-wasm/face.lisp b/kidlisp-wasm/face.lisp new file mode 100644 index 000000000..f8d4c9c5c --- /dev/null +++ b/kidlisp-wasm/face.lisp @@ -0,0 +1,26 @@ +wipe 255 220 180 +; head outline +ink 40 30 20 +circle w/2 h/2 110 +ink 255 220 180 +circle w/2 h/2 105 +; eyes +ink 255 255 255 +circle 95 105 22 +circle 160 105 22 +ink 40 30 20 +circle 95 108 10 +circle 160 108 10 +ink 20 15 10 +circle 95 108 5 +circle 160 108 5 +; nose +ink 200 170 140 +circle w/2 140 8 +; mouth +ink 200 80 80 +box 100 170 56 8 +; eyebrows +ink 60 40 20 +box 75 78 40 5 +box 142 78 40 5 diff --git a/kidlisp-wasm/grid.lisp b/kidlisp-wasm/grid.lisp new file mode 100644 index 000000000..cab2c7d00 --- /dev/null +++ b/kidlisp-wasm/grid.lisp @@ -0,0 +1,31 @@ +wipe 240 235 220 +ink 60 60 80 +; vertical lines +line 32 0 32 h +line 64 0 64 h +line 96 0 96 h +line 128 0 128 h +line 160 0 160 h +line 192 0 192 h +line 224 0 224 h +; horizontal lines +line 0 32 w 32 +line 0 64 w 64 +line 0 96 w 96 +line 0 128 w 128 +line 0 160 w 160 +line 0 192 w 192 +line 0 224 w 224 +; colored squares +ink 255 80 80 +box 33 33 31 31 +ink 80 200 120 +box 65 33 31 31 +ink 80 120 255 +box 97 33 31 31 +ink 255 200 50 +box 33 65 31 31 +ink 200 80 255 +box 65 65 31 31 +ink 50 220 220 +box 97 65 31 31 diff --git a/kidlisp-wasm/render.mjs b/kidlisp-wasm/render.mjs new file mode 100644 index 000000000..178888d7c --- /dev/null +++ b/kidlisp-wasm/render.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +// Render KidLisp pieces to PNG via self-contained WASM. + +import { readFileSync, mkdirSync } from "fs"; +import { basename } from "path"; +import sharp from "sharp"; +import { Compiler } from "./compiler.mjs"; + +const OUT_DIR = new URL("./output/", import.meta.url).pathname; +mkdirSync(OUT_DIR, { recursive: true }); + +const pieces = process.argv.slice(2); +if (pieces.length === 0) pieces.push("hello.lisp"); + +const WIDTH = 256; +const HEIGHT = 256; + +for (const input of pieces) { + const path = new URL(input, import.meta.url).pathname; + const source = readFileSync(path, "utf-8"); + const name = basename(input, ".lisp"); + + const compiler = new Compiler(); + const wasmBytes = compiler.compile(source); + const { instance } = await WebAssembly.instantiate(wasmBytes, {}); + instance.exports.paint(WIDTH, HEIGHT, 0); + + const mem = new Uint8Array(instance.exports.memory.buffer); + const pixels = mem.slice(0, WIDTH * HEIGHT * 4); + + const png = await sharp(Buffer.from(pixels), { + raw: { width: WIDTH, height: HEIGHT, channels: 4 }, + }).png().toBuffer(); + + const outPath = `${OUT_DIR}${name}.png`; + await sharp(png).toFile(outPath); + console.log(`${name}.png (${WIDTH}x${HEIGHT}, ${wasmBytes.length}B wasm → ${png.length}B png)`); +} diff --git a/kidlisp-wasm/rings.lisp b/kidlisp-wasm/rings.lisp new file mode 100644 index 000000000..8ed23cbc1 --- /dev/null +++ b/kidlisp-wasm/rings.lisp @@ -0,0 +1,13 @@ +wipe 10 10 30 +ink 255 50 50 +circle w/2 h/2 100 +ink 10 10 30 +circle w/2 h/2 80 +ink 50 200 255 +circle w/2 h/2 70 +ink 10 10 30 +circle w/2 h/2 50 +ink 255 220 50 +circle w/2 h/2 40 +ink 10 10 30 +circle w/2 h/2 20 diff --git a/kidlisp-wasm/run.mjs b/kidlisp-wasm/run.mjs index 2ab24280a..a8d9043be 100644 --- a/kidlisp-wasm/run.mjs +++ b/kidlisp-wasm/run.mjs @@ -1,185 +1,73 @@ #!/usr/bin/env node -// KidLisp WASM Runner -// Compiles a .lisp file, runs the WASM, outputs a PPM image. +// KidLisp WASM Runner — Verifiable Visual Compute +// +// The WASM module contains everything: renderer + pixel buffer + piece code. +// This host only provides memory and reads pixels out. Nothing to fake. import { readFileSync, writeFileSync } from "fs"; import { Compiler } from "./compiler.mjs"; -const WIDTH = 128; -const HEIGHT = 128; - -// ─── Pixel Buffer ─────────────────────────────────────────────────── - -const fb = new Uint8Array(WIDTH * HEIGHT * 4); // RGBA - -function setPixel(x, y, r, g, b) { - x = Math.round(x); - y = Math.round(y); - if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return; - const i = (y * WIDTH + x) * 4; - fb[i] = r; - fb[i + 1] = g; - fb[i + 2] = b; - fb[i + 3] = 255; -} - -// ─── Drawing State ────────────────────────────────────────────────── - -let inkR = 255, - inkG = 255, - inkB = 255; - -// ─── Host Functions ───────────────────────────────────────────────── - -function wipe(r, g, b) { - r = Math.round(r); - g = Math.round(g); - b = Math.round(b); - for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) { - fb[i] = r; - fb[i + 1] = g; - fb[i + 2] = b; - fb[i + 3] = 255; - } -} - -function ink(r, g, b) { - inkR = Math.round(r); - inkG = Math.round(g); - inkB = Math.round(b); -} - -function plot(x, y) { - setPixel(x, y, inkR, inkG, inkB); -} - -function line(x0, y0, x1, y1) { - x0 = Math.round(x0); - y0 = Math.round(y0); - x1 = Math.round(x1); - y1 = Math.round(y1); - const dx = Math.abs(x1 - x0); - const dy = Math.abs(y1 - y0); - const sx = x0 < x1 ? 1 : -1; - const sy = y0 < y1 ? 1 : -1; - let err = dx - dy; - while (true) { - setPixel(x0, y0, inkR, inkG, inkB); - if (x0 === x1 && y0 === y1) break; - const e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x0 += sx; - } - if (e2 < dx) { - err += dx; - y0 += sy; - } - } -} - -function box(x, y, w, h) { - x = Math.round(x); - y = Math.round(y); - w = Math.round(w); - h = Math.round(h); - for (let py = y; py < y + h; py++) { - for (let px = x; px < x + w; px++) { - setPixel(px, py, inkR, inkG, inkB); - } - } -} - -function circle(cx, cy, r) { - cx = Math.round(cx); - cy = Math.round(cy); - r = Math.round(r); - for (let y = -r; y <= r; y++) { - for (let x = -r; x <= r; x++) { - if (x * x + y * y <= r * r) { - setPixel(cx + x, cy + y, inkR, inkG, inkB); - } - } - } -} - -function tri(x0, y0, x1, y1, x2, y2) { - // Scanline triangle fill - x0 = Math.round(x0); y0 = Math.round(y0); - x1 = Math.round(x1); y1 = Math.round(y1); - x2 = Math.round(x2); y2 = Math.round(y2); - const minY = Math.max(0, Math.min(y0, y1, y2)); - const maxY = Math.min(HEIGHT - 1, Math.max(y0, y1, y2)); - for (let y = minY; y <= maxY; y++) { - let minX = WIDTH, maxX = 0; - const edges = [[x0,y0,x1,y1],[x1,y1,x2,y2],[x2,y2,x0,y0]]; - for (const [ax,ay,bx,by] of edges) { - if ((ay <= y && by > y) || (by <= y && ay > y)) { - const t = (y - ay) / (by - ay); - const x = Math.round(ax + t * (bx - ax)); - if (x < minX) minX = x; - if (x > maxX) maxX = x; - } - } - for (let x = Math.max(0, minX); x <= Math.min(WIDTH - 1, maxX); x++) { - setPixel(x, y, inkR, inkG, inkB); - } - } -} - -// ─── Compile & Run ────────────────────────────────────────────────── +const WIDTH = parseInt(process.argv[3]) || 128; +const HEIGHT = parseInt(process.argv[4]) || 128; const input = process.argv[2] || "hello.lisp"; -const source = readFileSync( - new URL(input, import.meta.url).pathname, - "utf-8", -); +const source = readFileSync(new URL(input, import.meta.url).pathname, "utf-8"); console.log(`Compiling ${input}...`); const compiler = new Compiler(); const wasmBytes = compiler.compile(source); -console.log(`WASM binary: ${wasmBytes.length} bytes`); +console.log(`WASM binary: ${wasmBytes.length} bytes (self-contained renderer)`); -const { instance } = await WebAssembly.instantiate(wasmBytes, { - env: { wipe, ink, line, box, circle, plot, tri }, -}); +// Instantiate — NO imports. The module has everything. +const { instance } = await WebAssembly.instantiate(wasmBytes, {}); -console.log("Running paint..."); +console.log(`Running paint(${WIDTH}, ${HEIGHT}, 0)...`); instance.exports.paint(WIDTH, HEIGHT, 0); +// Read pixels directly from WASM linear memory +const mem = new Uint8Array(instance.exports.memory.buffer); + // ─── Output PPM ───────────────────────────────────────────────────── -const ppm = Buffer.alloc(15 + WIDTH * HEIGHT * 3); // header + pixels const header = `P6\n${WIDTH} ${HEIGHT}\n255\n`; +const ppm = Buffer.alloc(header.length + WIDTH * HEIGHT * 3); ppm.write(header); let offset = header.length; for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) { - ppm[offset++] = fb[i]; - ppm[offset++] = fb[i + 1]; - ppm[offset++] = fb[i + 2]; + ppm[offset++] = mem[i]; + ppm[offset++] = mem[i + 1]; + ppm[offset++] = mem[i + 2]; } const outFile = input.replace(/\.lisp$/, ".ppm"); writeFileSync(new URL(outFile, import.meta.url).pathname, ppm.slice(0, offset)); console.log(`Wrote ${outFile} (${WIDTH}x${HEIGHT})`); -// ─── Terminal Preview (ANSI) ──────────────────────────────────────── +// ─── Terminal Preview ─────────────────────────────────────────────── -const PREVIEW_W = Math.min(WIDTH, 64); -const scaleX = WIDTH / PREVIEW_W; -const scaleY = (HEIGHT / PREVIEW_W) * 2; // 2 rows per char with ▀ +const PW = Math.min(WIDTH, 64); +const sx = WIDTH / PW; +const sy = (HEIGHT / PW) * 2; -console.log(`\nPreview (${PREVIEW_W} cols):`); -for (let row = 0; row < PREVIEW_W; row++) { +console.log(`\nPreview (${PW} cols):`); +for (let row = 0; row < PW; row++) { let line = ""; - for (let col = 0; col < PREVIEW_W; col++) { - const tx = Math.floor(col * scaleX); - const ty = Math.floor(row * scaleY); - const by = Math.floor(row * scaleY + scaleY / 2); + for (let col = 0; col < PW; col++) { + const tx = Math.floor(col * sx); + const ty = Math.floor(row * sy); + const by = Math.min(Math.floor(row * sy + sy / 2), HEIGHT - 1); const ti = (ty * WIDTH + tx) * 4; - const bi = (Math.min(by, HEIGHT - 1) * WIDTH + tx) * 4; - line += `\x1b[38;2;${fb[ti]};${fb[ti + 1]};${fb[ti + 2]};48;2;${fb[bi]};${fb[bi + 1]};${fb[bi + 2]}m\u2580`; + const bi = (by * WIDTH + tx) * 4; + line += `\x1b[38;2;${mem[ti]};${mem[ti+1]};${mem[ti+2]};48;2;${mem[bi]};${mem[bi+1]};${mem[bi+2]}m\u2580`; } - line += "\x1b[0m"; - process.stdout.write(line + "\n"); + process.stdout.write(line + "\x1b[0m\n"); } + +// ─── Verify ───────────────────────────────────────────────────────── + +// Hash the pixel buffer for verifiability +const { createHash } = await import("crypto"); +const pixelData = mem.slice(0, WIDTH * HEIGHT * 4); +const hash = createHash("sha256").update(pixelData).digest("hex").slice(0, 16); +console.log(`\nPixel hash: ${hash}`); +console.log(`Module size: ${wasmBytes.length} bytes | Buffer: ${WIDTH}x${HEIGHT} RGBA`); -- 2.51.2