Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add KidLisp → WASM compiler with self-contained renderer

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) <noreply@anthropic.com>

+728 -395
+3
.gitignore
··· 1 + # KidLisp WASM render output 2 + kidlisp-wasm/output/ 3 + 1 4 # Wrangler local cache 2 5 .wrangler/ 3 6
+577 -243
kidlisp-wasm/compiler.mjs
··· 1 - // KidLisp → WASM Compiler 2 - // Compiles KidLisp source directly to WebAssembly binary. 1 + // KidLisp → WASM Compiler (Self-Contained Renderer) 2 + // 3 + // Emits a single .wasm module that contains: 4 + // - Linear memory with RGBA pixel buffer 5 + // - All rasterization algorithms (line, circle, box, etc.) 6 + // - The compiled piece code 7 + // 8 + // The host only reads memory — no rendering imports. 9 + // Same binary → same pixels → verifiable visual compute. 3 10 4 - // ─── WASM Binary Encoding ─────────────────────────────────────────── 11 + // ─── WASM Encoding ────────────────────────────────────────────────── 5 12 6 13 function uleb128(value) { 7 14 const bytes = []; ··· 48 55 return [id, ...uleb128(contents.length), ...contents]; 49 56 } 50 57 51 - function vec(items) { 58 + function vecOf(items) { 52 59 return [...uleb128(items.length), ...items.flat()]; 53 60 } 54 61 55 - // ─── Color Map ────────────────────────────────────────────────────── 62 + // ─── Bytecode Emitter ─────────────────────────────────────────────── 63 + 64 + class E { 65 + constructor() { 66 + this.b = []; 67 + } 68 + // Constants 69 + i32c(v) { this.b.push(0x41, ...sleb128(v)); return this; } 70 + f32c(v) { this.b.push(0x43, ...f32Bytes(v)); return this; } 71 + // Locals & globals 72 + lg(i) { this.b.push(0x20, ...uleb128(i)); return this; } // local.get 73 + ls(i) { this.b.push(0x21, ...uleb128(i)); return this; } // local.set 74 + lt(i) { this.b.push(0x22, ...uleb128(i)); return this; } // local.tee 75 + gg(i) { this.b.push(0x23, ...uleb128(i)); return this; } // global.get 76 + gs(i) { this.b.push(0x24, ...uleb128(i)); return this; } // global.set 77 + // i32 arithmetic 78 + iadd() { this.b.push(0x6a); return this; } 79 + isub() { this.b.push(0x6b); return this; } 80 + imul() { this.b.push(0x6c); return this; } 81 + idiv() { this.b.push(0x6d); return this; } 82 + irem() { this.b.push(0x6f); return this; } 83 + iand() { this.b.push(0x71); return this; } 84 + ior() { this.b.push(0x72); return this; } 85 + // i32 comparison 86 + ieqz() { this.b.push(0x45); return this; } 87 + ieq() { this.b.push(0x46); return this; } 88 + ine() { this.b.push(0x47); return this; } 89 + ilt() { this.b.push(0x48); return this; } 90 + igt() { this.b.push(0x4a); return this; } 91 + ile() { this.b.push(0x4c); return this; } 92 + ige() { this.b.push(0x4e); return this; } 93 + // f32 arithmetic 94 + fadd() { this.b.push(0x92); return this; } 95 + fsub() { this.b.push(0x93); return this; } 96 + fmul() { this.b.push(0x94); return this; } 97 + fdiv() { this.b.push(0x95); return this; } 98 + fabs() { this.b.push(0x8b); return this; } 99 + fneg() { this.b.push(0x8c); return this; } 100 + fsqrt(){ this.b.push(0x91); return this; } 101 + ffloor(){this.b.push(0x8e); return this; } 102 + // f32 comparison 103 + flt() { this.b.push(0x5b); return this; } 104 + fgt() { this.b.push(0x5d); return this; } 105 + fle() { this.b.push(0x5f); return this; } 106 + fge() { this.b.push(0x5e); return this; } 107 + // Conversion 108 + i2f() { this.b.push(0xb2); return this; } // i32 → f32 109 + f2i() { this.b.push(0xa8); return this; } // f32 → i32 (trunc) 110 + // Memory 111 + st8(off = 0) { this.b.push(0x3a, 0x00, ...uleb128(off)); return this; } 112 + ld8u(off = 0){ this.b.push(0x2d, 0x00, ...uleb128(off)); return this; } 113 + // Control 114 + if_() { this.b.push(0x04, 0x40); return this; } 115 + else_(){ this.b.push(0x05); return this; } 116 + end() { this.b.push(0x0b); return this; } 117 + block(){ this.b.push(0x02, 0x40); return this; } 118 + loop() { this.b.push(0x03, 0x40); return this; } 119 + br(d) { this.b.push(0x0c, ...uleb128(d)); return this; } 120 + brif(d){ this.b.push(0x0d, ...uleb128(d)); return this; } 121 + ret() { this.b.push(0x0f); return this; } 122 + call(i){ this.b.push(0x10, ...uleb128(i)); return this; } 123 + drop() { this.b.push(0x1a); return this; } 124 + // Get raw bytes 125 + bytes() { return this.b; } 126 + } 127 + 128 + // ─── Colors ───────────────────────────────────────────────────────── 56 129 57 130 const COLORS = { 58 - red: [255, 0, 0], 59 - green: [0, 128, 0], 60 - blue: [0, 0, 255], 61 - white: [255, 255, 255], 62 - black: [0, 0, 0], 63 - yellow: [255, 255, 0], 64 - cyan: [0, 255, 255], 65 - magenta: [255, 0, 255], 66 - orange: [255, 165, 0], 67 - purple: [128, 0, 128], 68 - pink: [255, 192, 203], 69 - gray: [128, 128, 128], 70 - grey: [128, 128, 128], 131 + red: [255, 0, 0], green: [0, 128, 0], blue: [0, 0, 255], 132 + white: [255, 255, 255], black: [0, 0, 0], 133 + yellow: [255, 255, 0], cyan: [0, 255, 255], magenta: [255, 0, 255], 134 + orange: [255, 165, 0], purple: [128, 0, 128], 135 + pink: [255, 192, 203], gray: [128, 128, 128], grey: [128, 128, 128], 71 136 lime: [0, 255, 0], 72 137 }; 73 138 ··· 78 143 let i = 0; 79 144 while (i < source.length) { 80 145 const ch = source[i]; 81 - if (ch === "(") { 82 - tokens.push({ type: "lparen" }); 83 - i++; 84 - } else if (ch === ")") { 85 - tokens.push({ type: "rparen" }); 86 - i++; 87 - } else if (ch === "\n") { 88 - tokens.push({ type: "newline" }); 89 - i++; 90 - } else if (/\s/.test(ch)) { 91 - i++; 92 - } else if (ch === ";") { 93 - while (i < source.length && source[i] !== "\n") i++; 94 - } else { 146 + if (ch === "(") { tokens.push({ type: "lp" }); i++; } 147 + else if (ch === ")") { tokens.push({ type: "rp" }); i++; } 148 + else if (ch === "\n") { tokens.push({ type: "nl" }); i++; } 149 + else if (/\s/.test(ch)) { i++; } 150 + else if (ch === ";") { while (i < source.length && source[i] !== "\n") i++; } 151 + else { 95 152 let start = i; 96 153 while (i < source.length && !/[\s()]/.test(source[i])) i++; 97 154 const atom = source.slice(start, i); 98 155 const num = Number(atom); 99 156 if (!isNaN(num) && atom !== "") { 100 - tokens.push({ type: "number", value: num }); 157 + tokens.push({ type: "num", value: num }); 101 158 } else { 102 - tokens.push({ type: "symbol", value: atom }); 159 + tokens.push({ type: "sym", value: atom }); 103 160 } 104 161 } 105 162 } ··· 108 165 109 166 function parse(tokens) { 110 167 const lines = []; 111 - let currentLine = []; 168 + let cur = []; 112 169 let pos = 0; 113 170 114 - function parseExpr() { 171 + function expr() { 115 172 if (pos >= tokens.length) return null; 116 - const tok = tokens[pos]; 117 - if (tok.type === "lparen") { 173 + const t = tokens[pos]; 174 + if (t.type === "lp") { 118 175 pos++; 119 176 const items = []; 120 - while (pos < tokens.length && tokens[pos].type !== "rparen") { 121 - if (tokens[pos].type === "newline") { 122 - pos++; 123 - continue; 124 - } 125 - const expr = parseExpr(); 126 - if (expr) items.push(expr); 177 + while (pos < tokens.length && tokens[pos].type !== "rp") { 178 + if (tokens[pos].type === "nl") { pos++; continue; } 179 + const e = expr(); 180 + if (e) items.push(e); 127 181 } 128 - if (pos < tokens.length) pos++; // skip ) 129 - return { type: "list", items }; 130 - } else if (tok.type === "number") { 131 - pos++; 132 - return { type: "number", value: tok.value }; 133 - } else if (tok.type === "symbol") { 134 - pos++; 135 - return { type: "symbol", value: tok.value }; 136 - } 182 + if (pos < tokens.length) pos++; 183 + return { t: "list", items }; 184 + } else if (t.type === "num") { pos++; return { t: "num", v: t.value }; } 185 + else if (t.type === "sym") { pos++; return { t: "sym", v: t.value }; } 137 186 return null; 138 187 } 139 188 140 189 while (pos < tokens.length) { 141 - if (tokens[pos].type === "newline") { 142 - if (currentLine.length > 0) { 143 - lines.push(currentLine); 144 - currentLine = []; 145 - } 190 + if (tokens[pos].type === "nl") { 191 + if (cur.length > 0) { lines.push(cur); cur = []; } 146 192 pos++; 147 193 continue; 148 194 } 149 - const expr = parseExpr(); 150 - if (expr) currentLine.push(expr); 195 + const e = expr(); 196 + if (e) cur.push(e); 151 197 } 152 - if (currentLine.length > 0) lines.push(currentLine); 198 + if (cur.length > 0) lines.push(cur); 153 199 154 - // Wrap bare lines as function calls: 155 - // `ink 255 0 0` → `(ink 255 0 0)` 156 200 const result = []; 157 201 for (const line of lines) { 158 - if (line.length === 1) { 159 - result.push(line[0]); 160 - } else if (line.length > 1 && line[0].type === "symbol") { 161 - result.push({ type: "list", items: line }); 202 + if (line.length === 1) result.push(line[0]); 203 + else if (line.length > 1 && line[0].t === "sym") { 204 + result.push({ t: "list", items: line }); 162 205 } else { 163 - for (const expr of line) result.push(expr); 206 + for (const e of line) result.push(e); 164 207 } 165 208 } 166 209 return result; 167 210 } 168 211 169 - // ─── WASM Opcodes ─────────────────────────────────────────────────── 212 + // ─── Globals ──────────────────────────────────────────────────────── 170 213 171 - const OP = { 172 - LOCAL_GET: 0x20, 173 - LOCAL_SET: 0x21, 174 - GLOBAL_GET: 0x23, 175 - GLOBAL_SET: 0x24, 176 - CALL: 0x10, 177 - F32_CONST: 0x43, 178 - F32_ADD: 0x92, 179 - F32_SUB: 0x93, 180 - F32_MUL: 0x94, 181 - F32_DIV: 0x95, 182 - F32_SQRT: 0x91, 183 - F32_ABS: 0x8b, 184 - F32_NEG: 0x8c, 185 - F32_FLOOR: 0x8e, 186 - F32_CEIL: 0x8d, 187 - I32_CONST: 0x41, 188 - I32_ADD: 0x6a, 189 - DROP: 0x1a, 190 - END: 0x0b, 191 - }; 214 + const G_W = 0, G_H = 1, G_IR = 2, G_IG = 3, G_IB = 4; 215 + const I32 = 0x7f, F32 = 0x7d; 192 216 193 - const F32 = 0x7d; 217 + // ─── Function Indices ─────────────────────────────────────────────── 218 + // No imports — all functions are internal. 219 + 220 + const F_SET_PIXEL = 0; // (i32, i32) → () 221 + const F_WIPE = 1; // (f32, f32, f32) → () 222 + const F_INK = 2; // (f32, f32, f32) → () 223 + const F_PLOT = 3; // (f32, f32) → () 224 + const F_LINE = 4; // (f32, f32, f32, f32) → () 225 + const F_BOX = 5; // (f32, f32, f32, f32) → () 226 + const F_CIRCLE = 6; // (f32, f32, f32) → () 227 + const F_TRI = 7; // (f32, f32, f32, f32, f32, f32) → () 228 + const F_PAINT = 8; // (f32, f32, f32) → () 229 + 230 + // ─── Runtime Function Emitters ────────────────────────────────────── 231 + 232 + // $set_pixel(x: i32, y: i32) 233 + // Writes a pixel at (x,y) using current ink color. 234 + function emitSetPixel() { 235 + const e = new E(); 236 + // params: 0=x, 1=y | locals: 2=offset 237 + // Bounds check 238 + e.lg(0).i32c(0).ilt().if_().ret().end(); 239 + e.lg(0).gg(G_W).ige().if_().ret().end(); 240 + e.lg(1).i32c(0).ilt().if_().ret().end(); 241 + e.lg(1).gg(G_H).ige().if_().ret().end(); 242 + // offset = (y * width + x) * 4 243 + e.lg(1).gg(G_W).imul().lg(0).iadd().i32c(4).imul().ls(2); 244 + // store RGBA 245 + e.lg(2).gg(G_IR).st8(); 246 + e.lg(2).i32c(1).iadd().gg(G_IG).st8(); 247 + e.lg(2).i32c(2).iadd().gg(G_IB).st8(); 248 + e.lg(2).i32c(3).iadd().i32c(255).st8(); 249 + e.end(); 250 + return { locals: [[1, I32]], code: e.bytes() }; // 1 i32 local (offset) 251 + } 252 + 253 + // $wipe(r: f32, g: f32, b: f32) 254 + function emitWipe() { 255 + const e = new E(); 256 + // params: 0=r, 1=g, 2=b | locals: 3=i, 4=total, 5=ri, 6=gi, 7=bi 257 + e.lg(0).f2i().ls(5); 258 + e.lg(1).f2i().ls(6); 259 + e.lg(2).f2i().ls(7); 260 + // total = w * h * 4 261 + e.gg(G_W).gg(G_H).imul().i32c(4).imul().ls(4); 262 + // i = 0 263 + e.i32c(0).ls(3); 264 + // loop 265 + e.block().loop(); 266 + e.lg(3).lg(4).ige().brif(1); 267 + e.lg(3).lg(5).st8(); 268 + e.lg(3).i32c(1).iadd().lg(6).st8(); 269 + e.lg(3).i32c(2).iadd().lg(7).st8(); 270 + e.lg(3).i32c(3).iadd().i32c(255).st8(); 271 + e.lg(3).i32c(4).iadd().ls(3); 272 + e.br(0); 273 + e.end().end(); // loop, block 274 + e.end(); 275 + return { locals: [[5, I32]], code: e.bytes() }; 276 + } 277 + 278 + // $ink(r: f32, g: f32, b: f32) 279 + function emitInk() { 280 + const e = new E(); 281 + e.lg(0).f2i().gs(G_IR); 282 + e.lg(1).f2i().gs(G_IG); 283 + e.lg(2).f2i().gs(G_IB); 284 + e.end(); 285 + return { locals: [], code: e.bytes() }; 286 + } 287 + 288 + // $plot(x: f32, y: f32) 289 + function emitPlot() { 290 + const e = new E(); 291 + e.lg(0).f2i(); 292 + e.lg(1).f2i(); 293 + e.call(F_SET_PIXEL); 294 + e.end(); 295 + return { locals: [], code: e.bytes() }; 296 + } 297 + 298 + // $line(x0: f32, y0: f32, x1: f32, y1: f32) — Bresenham 299 + function emitLine() { 300 + const e = new E(); 301 + // params: 0=x0, 1=y0, 2=x1, 3=y1 302 + // locals: 4=ix0, 5=iy0, 6=ix1, 7=iy1, 8=dx, 9=dy, 10=sx, 11=sy, 12=err, 13=e2 303 + e.lg(0).f2i().ls(4); 304 + e.lg(1).f2i().ls(5); 305 + e.lg(2).f2i().ls(6); 306 + e.lg(3).f2i().ls(7); 307 + 308 + // dx = abs(ix1 - ix0) 309 + e.lg(6).lg(4).isub().ls(8); 310 + e.lg(8).i32c(0).ilt().if_(); 311 + e.i32c(0).lg(8).isub().ls(8); 312 + e.end(); 313 + 314 + // dy = abs(iy1 - iy0) 315 + e.lg(7).lg(5).isub().ls(9); 316 + e.lg(9).i32c(0).ilt().if_(); 317 + e.i32c(0).lg(9).isub().ls(9); 318 + e.end(); 194 319 195 - // ─── Compiler ─────────────────────────────────────────────────────── 320 + // sx = ix0 < ix1 ? 1 : -1 321 + e.lg(4).lg(6).ilt().if_(); 322 + e.i32c(1).ls(10); 323 + e.else_(); 324 + e.i32c(-1).ls(10); 325 + e.end(); 196 326 197 - export class Compiler { 198 - constructor() { 199 - this.types = []; 200 - this.typeMap = new Map(); 201 - this.imports = []; 202 - this.importCount = 0; 203 - this.code = []; 204 - this.setupImports(); 205 - } 327 + // sy = iy0 < iy1 ? 1 : -1 328 + e.lg(5).lg(7).ilt().if_(); 329 + e.i32c(1).ls(11); 330 + e.else_(); 331 + e.i32c(-1).ls(11); 332 + e.end(); 206 333 207 - addType(params, results) { 208 - const key = `${params.join(",")}->${results.join(",")}`; 209 - if (this.typeMap.has(key)) return this.typeMap.get(key); 210 - const idx = this.types.length; 211 - this.types.push({ params, results }); 212 - this.typeMap.set(key, idx); 213 - return idx; 214 - } 334 + // err = dx - dy 335 + e.lg(8).lg(9).isub().ls(12); 215 336 216 - addImport(module, name, paramCount, hasReturn = false) { 217 - const params = Array(paramCount).fill(F32); 218 - const results = hasReturn ? [F32] : []; 219 - const typeIdx = this.addType(params, results); 220 - const funcIdx = this.importCount++; 221 - this.imports.push({ module, name, typeIdx }); 222 - return funcIdx; 223 - } 337 + // Main loop 338 + e.block().loop(); 224 339 225 - setupImports() { 226 - this.funcs = {}; 227 - // Drawing primitives — all f32 params 228 - this.funcs.wipe = this.addImport("env", "wipe", 3); 229 - this.funcs.ink = this.addImport("env", "ink", 3); 230 - this.funcs.line = this.addImport("env", "line", 4); 231 - this.funcs.box = this.addImport("env", "box", 4); 232 - this.funcs.circle = this.addImport("env", "circle", 3); 233 - this.funcs.plot = this.addImport("env", "plot", 2); 234 - this.funcs.tri = this.addImport("env", "tri", 6); 340 + // plot(ix0, iy0) 341 + e.lg(4).lg(5).call(F_SET_PIXEL); 342 + 343 + // if ix0 == ix1 && iy0 == iy1: break 344 + e.lg(4).lg(6).ieq(); 345 + e.lg(5).lg(7).ieq(); 346 + e.iand().brif(1); 347 + 348 + // e2 = 2 * err 349 + e.lg(12).i32c(2).imul().ls(13); 350 + 351 + // if e2 > -dy: err -= dy; ix0 += sx 352 + e.lg(13).i32c(0).lg(9).isub().igt().if_(); 353 + e.lg(12).lg(9).isub().ls(12); 354 + e.lg(4).lg(10).iadd().ls(4); 355 + e.end(); 356 + 357 + // if e2 < dx: err += dx; iy0 += sy 358 + e.lg(13).lg(8).ilt().if_(); 359 + e.lg(12).lg(8).iadd().ls(12); 360 + e.lg(5).lg(11).iadd().ls(5); 361 + e.end(); 362 + 363 + e.br(0); 364 + e.end().end(); // loop, block 365 + e.end(); 366 + return { locals: [[10, I32]], code: e.bytes() }; 367 + } 235 368 236 - // paint(w, h, frame) → () 237 - this.paintTypeIdx = this.addType([F32, F32, F32], []); 369 + // $box(x: f32, y: f32, w: f32, h: f32) 370 + function emitBox() { 371 + const e = new E(); 372 + // params: 0=x, 1=y, 2=w, 3=h 373 + // locals: 4=ix, 5=iy, 6=ex, 7=ey, 8=py, 9=px 374 + e.lg(0).f2i().ls(4); // ix 375 + e.lg(1).f2i().ls(5); // iy 376 + e.lg(0).lg(2).fadd().f2i().ls(6); // ex = x + w 377 + e.lg(1).lg(3).fadd().f2i().ls(7); // ey = y + h 378 + // py = iy 379 + e.lg(5).ls(8); 380 + // outer loop (rows) 381 + e.block().loop(); 382 + e.lg(8).lg(7).ige().brif(1); 383 + // px = ix 384 + e.lg(4).ls(9); 385 + // inner loop (cols) 386 + e.block().loop(); 387 + e.lg(9).lg(6).ige().brif(1); 388 + e.lg(9).lg(8).call(F_SET_PIXEL); 389 + e.lg(9).i32c(1).iadd().ls(9); 390 + e.br(0); 391 + e.end().end(); // inner loop, inner block 392 + e.lg(8).i32c(1).iadd().ls(8); 393 + e.br(0); 394 + e.end().end(); // outer loop, outer block 395 + e.end(); 396 + return { locals: [[6, I32]], code: e.bytes() }; 397 + } 398 + 399 + // $circle(cx: f32, cy: f32, r: f32) — brute force filled 400 + function emitCircle() { 401 + const e = new E(); 402 + // params: 0=cx, 1=cy, 2=r 403 + // locals: 3=icx, 4=icy, 5=ir, 6=dy, 7=dx, 8=r2 404 + e.lg(0).f2i().ls(3); 405 + e.lg(1).f2i().ls(4); 406 + e.lg(2).f2i().ls(5); 407 + // r2 = ir * ir 408 + e.lg(5).lg(5).imul().ls(8); 409 + // dy = -ir 410 + e.i32c(0).lg(5).isub().ls(6); 411 + // outer loop 412 + e.block().loop(); 413 + e.lg(6).lg(5).igt().brif(1); 414 + // dx = -ir 415 + e.i32c(0).lg(5).isub().ls(7); 416 + // inner loop 417 + e.block().loop(); 418 + e.lg(7).lg(5).igt().brif(1); 419 + // if dx*dx + dy*dy <= r2 420 + e.lg(7).lg(7).imul().lg(6).lg(6).imul().iadd(); 421 + e.lg(8).ile().if_(); 422 + e.lg(3).lg(7).iadd(); // cx + dx 423 + e.lg(4).lg(6).iadd(); // cy + dy 424 + e.call(F_SET_PIXEL); 425 + e.end(); 426 + e.lg(7).i32c(1).iadd().ls(7); 427 + e.br(0); 428 + e.end().end(); // inner 429 + e.lg(6).i32c(1).iadd().ls(6); 430 + e.br(0); 431 + e.end().end(); // outer 432 + e.end(); 433 + return { locals: [[6, I32]], code: e.bytes() }; 434 + } 435 + 436 + // $tri(x0,y0,x1,y1,x2,y2) — scanline fill 437 + function emitTri() { 438 + const e = new E(); 439 + // params: 0=x0,1=y0,2=x1,3=y1,4=x2,5=y2 440 + // locals: 6=iy0,7=iy1,8=iy2,9=minY,10=maxY,11=y,12=minX,13=maxX,14=x 441 + // 15=ix0,16=ix1,17=ix2,18=tmp 442 + 443 + // Convert to int 444 + e.lg(0).f2i().ls(15); 445 + e.lg(1).f2i().ls(6); 446 + e.lg(2).f2i().ls(16); 447 + e.lg(3).f2i().ls(7); 448 + e.lg(4).f2i().ls(17); 449 + e.lg(5).f2i().ls(8); 450 + 451 + // minY = min(iy0, iy1, iy2), clamped to 0 452 + e.lg(6).ls(9); 453 + e.lg(7).lg(9).ilt().if_().lg(7).ls(9).end(); 454 + e.lg(8).lg(9).ilt().if_().lg(8).ls(9).end(); 455 + e.lg(9).i32c(0).ilt().if_().i32c(0).ls(9).end(); 456 + 457 + // maxY = max(iy0, iy1, iy2), clamped to height-1 458 + e.lg(6).ls(10); 459 + e.lg(7).lg(10).igt().if_().lg(7).ls(10).end(); 460 + e.lg(8).lg(10).igt().if_().lg(8).ls(10).end(); 461 + e.gg(G_H).i32c(1).isub().ls(18); 462 + e.lg(10).lg(18).igt().if_().lg(18).ls(10).end(); 463 + 464 + // y = minY 465 + e.lg(9).ls(11); 466 + e.block().loop(); 467 + e.lg(11).lg(10).igt().brif(1); 468 + 469 + // Reset scan extents 470 + e.gg(G_W).ls(12); // minX = width (will be narrowed) 471 + e.i32c(0).ls(13); // maxX = 0 472 + 473 + // Check each of 3 edges — inlined to avoid extra functions 474 + // Edge 0→1 475 + e.lg(6).lg(11).ile().lg(7).lg(11).igt().iand() 476 + .lg(7).lg(11).ile().lg(6).lg(11).igt().iand() 477 + .ior().if_(); 478 + // x = ix0 + (y - iy0) * (ix1 - ix0) / (iy1 - iy0) 479 + e.lg(11).lg(6).isub().lg(16).lg(15).isub().imul(); 480 + e.lg(7).lg(6).isub(); 481 + // avoid div by zero 482 + e.ls(18); 483 + e.lg(18).ieqz().if_().i32c(1).ls(18).end(); 484 + e.lg(18).idiv(); 485 + e.lg(15).iadd().ls(14); 486 + e.lg(14).lg(12).ilt().if_().lg(14).ls(12).end(); 487 + e.lg(14).lg(13).igt().if_().lg(14).ls(13).end(); 488 + e.end(); 489 + 490 + // Edge 1→2 491 + e.lg(7).lg(11).ile().lg(8).lg(11).igt().iand() 492 + .lg(8).lg(11).ile().lg(7).lg(11).igt().iand() 493 + .ior().if_(); 494 + e.lg(11).lg(7).isub().lg(17).lg(16).isub().imul(); 495 + e.lg(8).lg(7).isub(); 496 + e.ls(18); 497 + e.lg(18).ieqz().if_().i32c(1).ls(18).end(); 498 + e.lg(18).idiv(); 499 + e.lg(16).iadd().ls(14); 500 + e.lg(14).lg(12).ilt().if_().lg(14).ls(12).end(); 501 + e.lg(14).lg(13).igt().if_().lg(14).ls(13).end(); 502 + e.end(); 503 + 504 + // Edge 2→0 505 + e.lg(8).lg(11).ile().lg(6).lg(11).igt().iand() 506 + .lg(6).lg(11).ile().lg(8).lg(11).igt().iand() 507 + .ior().if_(); 508 + e.lg(11).lg(8).isub().lg(15).lg(17).isub().imul(); 509 + e.lg(6).lg(8).isub(); 510 + e.ls(18); 511 + e.lg(18).ieqz().if_().i32c(1).ls(18).end(); 512 + e.lg(18).idiv(); 513 + e.lg(17).iadd().ls(14); 514 + e.lg(14).lg(12).ilt().if_().lg(14).ls(12).end(); 515 + e.lg(14).lg(13).igt().if_().lg(14).ls(13).end(); 516 + e.end(); 517 + 518 + // Clamp and fill scanline 519 + e.lg(12).i32c(0).ilt().if_().i32c(0).ls(12).end(); 520 + e.gg(G_W).i32c(1).isub().ls(18); 521 + e.lg(13).lg(18).igt().if_().lg(18).ls(13).end(); 522 + 523 + // Fill from minX to maxX 524 + e.lg(12).ls(14); 525 + e.block().loop(); 526 + e.lg(14).lg(13).igt().brif(1); 527 + e.lg(14).lg(11).call(F_SET_PIXEL); 528 + e.lg(14).i32c(1).iadd().ls(14); 529 + e.br(0); 530 + e.end().end(); 531 + 532 + e.lg(11).i32c(1).iadd().ls(11); 533 + e.br(0); 534 + e.end().end(); // y loop 535 + e.end(); 536 + return { locals: [[13, I32]], code: e.bytes() }; 537 + } 538 + 539 + // ─── Compiler ─────────────────────────────────────────────────────── 540 + 541 + export class Compiler { 542 + constructor() { 543 + this.code = new E(); // bytecode for paint body 238 544 } 239 545 240 - // Emit bytecode that pushes a value onto the WASM stack. 241 - compileExpr(expr) { 242 - if (expr.type === "number") { 243 - this.code.push(OP.F32_CONST, ...f32Bytes(expr.value)); 244 - } else if (expr.type === "symbol") { 245 - this.compileSymbol(expr.value); 246 - } else if (expr.type === "list") { 247 - this.compileCall(expr); 546 + // Emit piece expression 547 + compileExpr(node) { 548 + if (node.t === "num") { 549 + this.code.f32c(node.v); 550 + } else if (node.t === "sym") { 551 + this.compileSym(node.v); 552 + } else if (node.t === "list") { 553 + this.compileCall(node); 248 554 } 249 555 } 250 556 251 - compileSymbol(name) { 252 - // paint params: 0=w, 1=h, 2=frame 253 - if (name === "w") { 254 - this.code.push(OP.LOCAL_GET, ...uleb128(0)); 255 - return; 256 - } 257 - if (name === "h") { 258 - this.code.push(OP.LOCAL_GET, ...uleb128(1)); 259 - return; 260 - } 261 - if (name === "frame" || name === "f") { 262 - this.code.push(OP.LOCAL_GET, ...uleb128(2)); 557 + compileSym(name) { 558 + if (name === "w") { this.code.lg(0); return; } 559 + if (name === "h") { this.code.lg(1); return; } 560 + if (name === "frame" || name === "f") { this.code.lg(2); return; } 561 + 562 + // Division shorthand: w/2, h/3, etc. 563 + const dm = name.match(/^(\w+)\/(\d+(?:\.\d+)?)$/); 564 + if (dm) { 565 + this.compileSym(dm[1]); 566 + this.code.f32c(parseFloat(dm[2])); 567 + this.code.fdiv(); 263 568 return; 264 569 } 265 570 266 - // Division shorthand: w/2, h/3, etc. 267 - const divMatch = name.match(/^(\w+)\/(\d+(?:\.\d+)?)$/); 268 - if (divMatch) { 269 - this.compileSymbol(divMatch[1]); 270 - this.code.push(OP.F32_CONST, ...f32Bytes(parseFloat(divMatch[2]))); 271 - this.code.push(OP.F32_DIV); 571 + // Multiplication shorthand: w*2, h*3, etc. 572 + const mm = name.match(/^(\w+)\*(\d+(?:\.\d+)?)$/); 573 + if (mm) { 574 + this.compileSym(mm[1]); 575 + this.code.f32c(parseFloat(mm[2])); 576 + this.code.fmul(); 272 577 return; 273 578 } 274 579 275 - // Color names → push 3 f32 values (r, g, b) 580 + // Color name → 3 f32 values 276 581 if (COLORS[name]) { 277 582 const [r, g, b] = COLORS[name]; 278 - this.code.push(OP.F32_CONST, ...f32Bytes(r)); 279 - this.code.push(OP.F32_CONST, ...f32Bytes(g)); 280 - this.code.push(OP.F32_CONST, ...f32Bytes(b)); 583 + this.code.f32c(r).f32c(g).f32c(b); 281 584 return; 282 585 } 283 586 284 587 throw new Error(`Unknown symbol: ${name}`); 285 588 } 286 589 287 - compileCall(expr) { 288 - if (expr.items.length === 0) return; 289 - const head = expr.items[0]; 290 - if (head.type !== "symbol") { 291 - throw new Error(`Expected function name, got ${JSON.stringify(head)}`); 292 - } 590 + compileCall(node) { 591 + if (node.items.length === 0) return; 592 + const head = node.items[0]; 593 + if (head.t !== "sym") throw new Error(`Expected function name, got ${JSON.stringify(head)}`); 293 594 294 - const name = head.value; 295 - const args = expr.items.slice(1); 595 + const name = head.v; 596 + const args = node.items.slice(1); 296 597 297 598 // Arithmetic 298 - const arithOp = { "+": OP.F32_ADD, "-": OP.F32_SUB, "*": OP.F32_MUL, "/": OP.F32_DIV }; 299 - if (arithOp[name]) { 599 + const arith = { "+": "fadd", "-": "fsub", "*": "fmul", "/": "fdiv" }; 600 + if (arith[name]) { 300 601 this.compileExpr(args[0]); 301 602 this.compileExpr(args[1]); 302 - this.code.push(arithOp[name]); 603 + this.code[arith[name]](); 303 604 return; 304 605 } 305 606 306 607 // Math builtins 307 - if (name === "sqrt") { 308 - this.compileExpr(args[0]); 309 - this.code.push(OP.F32_SQRT); 310 - return; 311 - } 312 - if (name === "abs") { 313 - this.compileExpr(args[0]); 314 - this.code.push(OP.F32_ABS); 315 - return; 316 - } 317 - if (name === "neg") { 318 - this.compileExpr(args[0]); 319 - this.code.push(OP.F32_NEG); 320 - return; 321 - } 322 - if (name === "floor") { 323 - this.compileExpr(args[0]); 324 - this.code.push(OP.F32_FLOOR); 325 - return; 326 - } 608 + if (name === "sqrt") { this.compileExpr(args[0]); this.code.fsqrt(); return; } 609 + if (name === "abs") { this.compileExpr(args[0]); this.code.fabs(); return; } 610 + if (name === "neg") { this.compileExpr(args[0]); this.code.fneg(); return; } 611 + if (name === "floor"){ this.compileExpr(args[0]); this.code.ffloor(); return; } 327 612 328 - // Drawing functions 329 - if (this.funcs[name] !== undefined) { 613 + // Drawing functions → internal function calls 614 + const funcMap = { 615 + wipe: F_WIPE, ink: F_INK, plot: F_PLOT, 616 + line: F_LINE, box: F_BOX, circle: F_CIRCLE, tri: F_TRI, 617 + }; 618 + if (funcMap[name] !== undefined) { 330 619 for (const arg of args) this.compileExpr(arg); 331 - this.code.push(OP.CALL, ...uleb128(this.funcs[name])); 620 + this.code.call(funcMap[name]); 332 621 return; 333 622 } 334 623 ··· 336 625 } 337 626 338 627 compile(source) { 339 - const tokens = tokenize(source); 340 - const ast = parse(tokens); 628 + const ast = parse(tokenize(source)); 341 629 342 - for (const expr of ast) { 343 - if (expr.type === "list") { 344 - this.compileCall(expr); 345 - } else if (expr.type === "symbol" && COLORS[expr.value]) { 346 - // Bare color name on a line → wipe with that color 347 - const [r, g, b] = COLORS[expr.value]; 348 - this.code.push(OP.F32_CONST, ...f32Bytes(r)); 349 - this.code.push(OP.F32_CONST, ...f32Bytes(g)); 350 - this.code.push(OP.F32_CONST, ...f32Bytes(b)); 351 - this.code.push(OP.CALL, ...uleb128(this.funcs.wipe)); 630 + // Compile piece code into paint body 631 + // Paint starts by setting globals from params 632 + this.code.lg(0).f2i().gs(G_W); // width = param 0 633 + this.code.lg(1).f2i().gs(G_H); // height = param 1 634 + 635 + for (const node of ast) { 636 + if (node.t === "list") { 637 + this.compileCall(node); 638 + } else if (node.t === "sym" && COLORS[node.v]) { 639 + // Bare color name → wipe 640 + const [r, g, b] = COLORS[node.v]; 641 + this.code.f32c(r).f32c(g).f32c(b).call(F_WIPE); 352 642 } 353 643 } 354 644 355 - return this.emit(); 645 + return this.buildModule(); 356 646 } 357 647 358 - emit() { 359 - const bytes = []; 648 + buildModule() { 649 + const out = []; 360 650 361 651 // Magic + version 362 - bytes.push(0x00, 0x61, 0x73, 0x6d); // \0asm 363 - bytes.push(0x01, 0x00, 0x00, 0x00); // version 1 652 + out.push(0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00); 653 + 654 + // ── Types ── 655 + const types = [ 656 + [0x60, 2, I32, I32, 0], // 0: (i32,i32)→() 657 + [0x60, 3, F32, F32, F32, 0], // 1: (f32,f32,f32)→() 658 + [0x60, 2, F32, F32, 0], // 2: (f32,f32)→() 659 + [0x60, 4, F32, F32, F32, F32, 0], // 3: (f32,f32,f32,f32)→() 660 + [0x60, 6, F32, F32, F32, F32, F32, F32, 0], // 4: (f32,f32,f32,f32,f32,f32)→() 661 + ]; 662 + // Format: 0x60 paramcount paramtypes... resultcount resulttypes... 663 + const typeEntries = types.map(t => { 664 + const tag = t[0]; 665 + const paramCount = t[1]; 666 + const params = t.slice(2, 2 + paramCount); 667 + const resultCount = t[2 + paramCount]; 668 + const results = t.slice(3 + paramCount); 669 + return [tag, ...uleb128(paramCount), ...params, ...uleb128(resultCount), ...results]; 670 + }); 671 + out.push(...section(1, vecOf(typeEntries))); 364 672 365 - // ── Type section (1) ── 366 - const typeEntries = this.types.map((t) => [ 367 - 0x60, 368 - ...uleb128(t.params.length), 369 - ...t.params, 370 - ...uleb128(t.results.length), 371 - ...t.results, 372 - ]); 373 - bytes.push(...section(1, vec(typeEntries))); 673 + // ── Functions (no imports!) ── 674 + // Map each function to its type index 675 + const funcTypes = [ 676 + 0, // set_pixel: (i32,i32)→() 677 + 1, // wipe: (f32,f32,f32)→() 678 + 1, // ink: (f32,f32,f32)→() 679 + 2, // plot: (f32,f32)→() 680 + 3, // line: (f32,f32,f32,f32)→() 681 + 3, // box: (f32,f32,f32,f32)→() 682 + 1, // circle: (f32,f32,f32)→() 683 + 4, // tri: (f32,f32,f32,f32,f32,f32)→() 684 + 1, // paint: (f32,f32,f32)→() 685 + ]; 686 + out.push(...section(3, vecOf(funcTypes.map(t => [...uleb128(t)])))); 374 687 375 - // ── Import section (2) ── 376 - const importEntries = this.imports.map((imp) => [ 377 - ...encodeString(imp.module), 378 - ...encodeString(imp.name), 379 - 0x00, // func 380 - ...uleb128(imp.typeIdx), 381 - ]); 382 - bytes.push(...section(2, vec(importEntries))); 688 + // ── Memory ── 689 + // 16 pages = 1MB, enough for 512x512 RGBA 690 + out.push(...section(5, vecOf([[0x00, ...uleb128(16)]]))); 383 691 384 - // ── Function section (3) — declare paint ── 385 - bytes.push(...section(3, vec([[...uleb128(this.paintTypeIdx)]]))); 692 + // ── Globals ── 693 + const globals = [ 694 + [I32, 0x01, 0x41, ...sleb128(0), 0x0b], // width: i32 mut = 0 695 + [I32, 0x01, 0x41, ...sleb128(0), 0x0b], // height: i32 mut = 0 696 + [I32, 0x01, 0x41, ...sleb128(255), 0x0b], // ink_r: i32 mut = 255 697 + [I32, 0x01, 0x41, ...sleb128(255), 0x0b], // ink_g: i32 mut = 255 698 + [I32, 0x01, 0x41, ...sleb128(255), 0x0b], // ink_b: i32 mut = 255 699 + ]; 700 + out.push(...section(6, vecOf(globals))); 386 701 387 - // ── Export section (7) ── 388 - const paintIdx = this.importCount; // first non-import func 389 - const exportEntries = [ 390 - [...encodeString("paint"), 0x00, ...uleb128(paintIdx)], 702 + // ── Exports ── 703 + const exports = [ 704 + [...encodeString("paint"), 0x00, ...uleb128(F_PAINT)], 705 + [...encodeString("memory"), 0x02, ...uleb128(0)], 391 706 ]; 392 - bytes.push(...section(7, vec(exportEntries))); 707 + out.push(...section(7, vecOf(exports))); 393 708 394 - // ── Code section (10) ── 395 - const body = [ 396 - 0x00, // 0 local declarations 397 - ...this.code, 398 - OP.END, 709 + // ── Code ── 710 + const runtimeFuncs = [ 711 + emitSetPixel(), // 0 712 + emitWipe(), // 1 713 + emitInk(), // 2 714 + emitPlot(), // 3 715 + emitLine(), // 4 716 + emitBox(), // 5 717 + emitCircle(), // 6 718 + emitTri(), // 7 399 719 ]; 400 - const codeEntry = [...uleb128(body.length), ...body]; 401 - bytes.push(...section(10, vec([codeEntry]))); 720 + 721 + // Paint function body 722 + const paintBody = { locals: [], code: [...this.code.bytes(), 0x0b] }; 723 + 724 + const allFuncs = [...runtimeFuncs, paintBody]; 725 + 726 + const codeBodies = allFuncs.map(fn => { 727 + // Local declarations: groups of (count, type) 728 + const localDecl = fn.locals.length > 0 729 + ? [...uleb128(fn.locals.length), ...fn.locals.flatMap(([count, type]) => [...uleb128(count), type])] 730 + : [0x00]; 731 + const body = [...localDecl, ...fn.code]; 732 + return [...uleb128(body.length), ...body]; 733 + }); 402 734 403 - return new Uint8Array(bytes); 735 + out.push(...section(10, vecOf(codeBodies))); 736 + 737 + return new Uint8Array(out); 404 738 } 405 739 } 406 740
+26
kidlisp-wasm/face.lisp
··· 1 + wipe 255 220 180 2 + ; head outline 3 + ink 40 30 20 4 + circle w/2 h/2 110 5 + ink 255 220 180 6 + circle w/2 h/2 105 7 + ; eyes 8 + ink 255 255 255 9 + circle 95 105 22 10 + circle 160 105 22 11 + ink 40 30 20 12 + circle 95 108 10 13 + circle 160 108 10 14 + ink 20 15 10 15 + circle 95 108 5 16 + circle 160 108 5 17 + ; nose 18 + ink 200 170 140 19 + circle w/2 140 8 20 + ; mouth 21 + ink 200 80 80 22 + box 100 170 56 8 23 + ; eyebrows 24 + ink 60 40 20 25 + box 75 78 40 5 26 + box 142 78 40 5
+31
kidlisp-wasm/grid.lisp
··· 1 + wipe 240 235 220 2 + ink 60 60 80 3 + ; vertical lines 4 + line 32 0 32 h 5 + line 64 0 64 h 6 + line 96 0 96 h 7 + line 128 0 128 h 8 + line 160 0 160 h 9 + line 192 0 192 h 10 + line 224 0 224 h 11 + ; horizontal lines 12 + line 0 32 w 32 13 + line 0 64 w 64 14 + line 0 96 w 96 15 + line 0 128 w 128 16 + line 0 160 w 160 17 + line 0 192 w 192 18 + line 0 224 w 224 19 + ; colored squares 20 + ink 255 80 80 21 + box 33 33 31 31 22 + ink 80 200 120 23 + box 65 33 31 31 24 + ink 80 120 255 25 + box 97 33 31 31 26 + ink 255 200 50 27 + box 33 65 31 31 28 + ink 200 80 255 29 + box 65 65 31 31 30 + ink 50 220 220 31 + box 97 65 31 31
+38
kidlisp-wasm/render.mjs
··· 1 + #!/usr/bin/env node 2 + // Render KidLisp pieces to PNG via self-contained WASM. 3 + 4 + import { readFileSync, mkdirSync } from "fs"; 5 + import { basename } from "path"; 6 + import sharp from "sharp"; 7 + import { Compiler } from "./compiler.mjs"; 8 + 9 + const OUT_DIR = new URL("./output/", import.meta.url).pathname; 10 + mkdirSync(OUT_DIR, { recursive: true }); 11 + 12 + const pieces = process.argv.slice(2); 13 + if (pieces.length === 0) pieces.push("hello.lisp"); 14 + 15 + const WIDTH = 256; 16 + const HEIGHT = 256; 17 + 18 + for (const input of pieces) { 19 + const path = new URL(input, import.meta.url).pathname; 20 + const source = readFileSync(path, "utf-8"); 21 + const name = basename(input, ".lisp"); 22 + 23 + const compiler = new Compiler(); 24 + const wasmBytes = compiler.compile(source); 25 + const { instance } = await WebAssembly.instantiate(wasmBytes, {}); 26 + instance.exports.paint(WIDTH, HEIGHT, 0); 27 + 28 + const mem = new Uint8Array(instance.exports.memory.buffer); 29 + const pixels = mem.slice(0, WIDTH * HEIGHT * 4); 30 + 31 + const png = await sharp(Buffer.from(pixels), { 32 + raw: { width: WIDTH, height: HEIGHT, channels: 4 }, 33 + }).png().toBuffer(); 34 + 35 + const outPath = `${OUT_DIR}${name}.png`; 36 + await sharp(png).toFile(outPath); 37 + console.log(`${name}.png (${WIDTH}x${HEIGHT}, ${wasmBytes.length}B wasm → ${png.length}B png)`); 38 + }
+13
kidlisp-wasm/rings.lisp
··· 1 + wipe 10 10 30 2 + ink 255 50 50 3 + circle w/2 h/2 100 4 + ink 10 10 30 5 + circle w/2 h/2 80 6 + ink 50 200 255 7 + circle w/2 h/2 70 8 + ink 10 10 30 9 + circle w/2 h/2 50 10 + ink 255 220 50 11 + circle w/2 h/2 40 12 + ink 10 10 30 13 + circle w/2 h/2 20
+40 -152
kidlisp-wasm/run.mjs
··· 1 1 #!/usr/bin/env node 2 - // KidLisp WASM Runner 3 - // Compiles a .lisp file, runs the WASM, outputs a PPM image. 2 + // KidLisp WASM Runner — Verifiable Visual Compute 3 + // 4 + // The WASM module contains everything: renderer + pixel buffer + piece code. 5 + // This host only provides memory and reads pixels out. Nothing to fake. 4 6 5 7 import { readFileSync, writeFileSync } from "fs"; 6 8 import { Compiler } from "./compiler.mjs"; 7 9 8 - const WIDTH = 128; 9 - const HEIGHT = 128; 10 - 11 - // ─── Pixel Buffer ─────────────────────────────────────────────────── 12 - 13 - const fb = new Uint8Array(WIDTH * HEIGHT * 4); // RGBA 14 - 15 - function setPixel(x, y, r, g, b) { 16 - x = Math.round(x); 17 - y = Math.round(y); 18 - if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return; 19 - const i = (y * WIDTH + x) * 4; 20 - fb[i] = r; 21 - fb[i + 1] = g; 22 - fb[i + 2] = b; 23 - fb[i + 3] = 255; 24 - } 25 - 26 - // ─── Drawing State ────────────────────────────────────────────────── 27 - 28 - let inkR = 255, 29 - inkG = 255, 30 - inkB = 255; 31 - 32 - // ─── Host Functions ───────────────────────────────────────────────── 33 - 34 - function wipe(r, g, b) { 35 - r = Math.round(r); 36 - g = Math.round(g); 37 - b = Math.round(b); 38 - for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) { 39 - fb[i] = r; 40 - fb[i + 1] = g; 41 - fb[i + 2] = b; 42 - fb[i + 3] = 255; 43 - } 44 - } 45 - 46 - function ink(r, g, b) { 47 - inkR = Math.round(r); 48 - inkG = Math.round(g); 49 - inkB = Math.round(b); 50 - } 51 - 52 - function plot(x, y) { 53 - setPixel(x, y, inkR, inkG, inkB); 54 - } 55 - 56 - function line(x0, y0, x1, y1) { 57 - x0 = Math.round(x0); 58 - y0 = Math.round(y0); 59 - x1 = Math.round(x1); 60 - y1 = Math.round(y1); 61 - const dx = Math.abs(x1 - x0); 62 - const dy = Math.abs(y1 - y0); 63 - const sx = x0 < x1 ? 1 : -1; 64 - const sy = y0 < y1 ? 1 : -1; 65 - let err = dx - dy; 66 - while (true) { 67 - setPixel(x0, y0, inkR, inkG, inkB); 68 - if (x0 === x1 && y0 === y1) break; 69 - const e2 = 2 * err; 70 - if (e2 > -dy) { 71 - err -= dy; 72 - x0 += sx; 73 - } 74 - if (e2 < dx) { 75 - err += dx; 76 - y0 += sy; 77 - } 78 - } 79 - } 80 - 81 - function box(x, y, w, h) { 82 - x = Math.round(x); 83 - y = Math.round(y); 84 - w = Math.round(w); 85 - h = Math.round(h); 86 - for (let py = y; py < y + h; py++) { 87 - for (let px = x; px < x + w; px++) { 88 - setPixel(px, py, inkR, inkG, inkB); 89 - } 90 - } 91 - } 92 - 93 - function circle(cx, cy, r) { 94 - cx = Math.round(cx); 95 - cy = Math.round(cy); 96 - r = Math.round(r); 97 - for (let y = -r; y <= r; y++) { 98 - for (let x = -r; x <= r; x++) { 99 - if (x * x + y * y <= r * r) { 100 - setPixel(cx + x, cy + y, inkR, inkG, inkB); 101 - } 102 - } 103 - } 104 - } 105 - 106 - function tri(x0, y0, x1, y1, x2, y2) { 107 - // Scanline triangle fill 108 - x0 = Math.round(x0); y0 = Math.round(y0); 109 - x1 = Math.round(x1); y1 = Math.round(y1); 110 - x2 = Math.round(x2); y2 = Math.round(y2); 111 - const minY = Math.max(0, Math.min(y0, y1, y2)); 112 - const maxY = Math.min(HEIGHT - 1, Math.max(y0, y1, y2)); 113 - for (let y = minY; y <= maxY; y++) { 114 - let minX = WIDTH, maxX = 0; 115 - const edges = [[x0,y0,x1,y1],[x1,y1,x2,y2],[x2,y2,x0,y0]]; 116 - for (const [ax,ay,bx,by] of edges) { 117 - if ((ay <= y && by > y) || (by <= y && ay > y)) { 118 - const t = (y - ay) / (by - ay); 119 - const x = Math.round(ax + t * (bx - ax)); 120 - if (x < minX) minX = x; 121 - if (x > maxX) maxX = x; 122 - } 123 - } 124 - for (let x = Math.max(0, minX); x <= Math.min(WIDTH - 1, maxX); x++) { 125 - setPixel(x, y, inkR, inkG, inkB); 126 - } 127 - } 128 - } 129 - 130 - // ─── Compile & Run ────────────────────────────────────────────────── 10 + const WIDTH = parseInt(process.argv[3]) || 128; 11 + const HEIGHT = parseInt(process.argv[4]) || 128; 131 12 132 13 const input = process.argv[2] || "hello.lisp"; 133 - const source = readFileSync( 134 - new URL(input, import.meta.url).pathname, 135 - "utf-8", 136 - ); 14 + const source = readFileSync(new URL(input, import.meta.url).pathname, "utf-8"); 137 15 138 16 console.log(`Compiling ${input}...`); 139 17 const compiler = new Compiler(); 140 18 const wasmBytes = compiler.compile(source); 141 - console.log(`WASM binary: ${wasmBytes.length} bytes`); 19 + console.log(`WASM binary: ${wasmBytes.length} bytes (self-contained renderer)`); 142 20 143 - const { instance } = await WebAssembly.instantiate(wasmBytes, { 144 - env: { wipe, ink, line, box, circle, plot, tri }, 145 - }); 21 + // Instantiate — NO imports. The module has everything. 22 + const { instance } = await WebAssembly.instantiate(wasmBytes, {}); 146 23 147 - console.log("Running paint..."); 24 + console.log(`Running paint(${WIDTH}, ${HEIGHT}, 0)...`); 148 25 instance.exports.paint(WIDTH, HEIGHT, 0); 149 26 27 + // Read pixels directly from WASM linear memory 28 + const mem = new Uint8Array(instance.exports.memory.buffer); 29 + 150 30 // ─── Output PPM ───────────────────────────────────────────────────── 151 31 152 - const ppm = Buffer.alloc(15 + WIDTH * HEIGHT * 3); // header + pixels 153 32 const header = `P6\n${WIDTH} ${HEIGHT}\n255\n`; 33 + const ppm = Buffer.alloc(header.length + WIDTH * HEIGHT * 3); 154 34 ppm.write(header); 155 35 let offset = header.length; 156 36 for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) { 157 - ppm[offset++] = fb[i]; 158 - ppm[offset++] = fb[i + 1]; 159 - ppm[offset++] = fb[i + 2]; 37 + ppm[offset++] = mem[i]; 38 + ppm[offset++] = mem[i + 1]; 39 + ppm[offset++] = mem[i + 2]; 160 40 } 161 41 162 42 const outFile = input.replace(/\.lisp$/, ".ppm"); 163 43 writeFileSync(new URL(outFile, import.meta.url).pathname, ppm.slice(0, offset)); 164 44 console.log(`Wrote ${outFile} (${WIDTH}x${HEIGHT})`); 165 45 166 - // ─── Terminal Preview (ANSI) ──────────────────────────────────────── 46 + // ─── Terminal Preview ─────────────────────────────────────────────── 167 47 168 - const PREVIEW_W = Math.min(WIDTH, 64); 169 - const scaleX = WIDTH / PREVIEW_W; 170 - const scaleY = (HEIGHT / PREVIEW_W) * 2; // 2 rows per char with ▀ 48 + const PW = Math.min(WIDTH, 64); 49 + const sx = WIDTH / PW; 50 + const sy = (HEIGHT / PW) * 2; 171 51 172 - console.log(`\nPreview (${PREVIEW_W} cols):`); 173 - for (let row = 0; row < PREVIEW_W; row++) { 52 + console.log(`\nPreview (${PW} cols):`); 53 + for (let row = 0; row < PW; row++) { 174 54 let line = ""; 175 - for (let col = 0; col < PREVIEW_W; col++) { 176 - const tx = Math.floor(col * scaleX); 177 - const ty = Math.floor(row * scaleY); 178 - const by = Math.floor(row * scaleY + scaleY / 2); 55 + for (let col = 0; col < PW; col++) { 56 + const tx = Math.floor(col * sx); 57 + const ty = Math.floor(row * sy); 58 + const by = Math.min(Math.floor(row * sy + sy / 2), HEIGHT - 1); 179 59 const ti = (ty * WIDTH + tx) * 4; 180 - const bi = (Math.min(by, HEIGHT - 1) * WIDTH + tx) * 4; 181 - line += `\x1b[38;2;${fb[ti]};${fb[ti + 1]};${fb[ti + 2]};48;2;${fb[bi]};${fb[bi + 1]};${fb[bi + 2]}m\u2580`; 60 + const bi = (by * WIDTH + tx) * 4; 61 + line += `\x1b[38;2;${mem[ti]};${mem[ti+1]};${mem[ti+2]};48;2;${mem[bi]};${mem[bi+1]};${mem[bi+2]}m\u2580`; 182 62 } 183 - line += "\x1b[0m"; 184 - process.stdout.write(line + "\n"); 63 + process.stdout.write(line + "\x1b[0m\n"); 185 64 } 65 + 66 + // ─── Verify ───────────────────────────────────────────────────────── 67 + 68 + // Hash the pixel buffer for verifiability 69 + const { createHash } = await import("crypto"); 70 + const pixelData = mem.slice(0, WIDTH * HEIGHT * 4); 71 + const hash = createHash("sha256").update(pixelData).digest("hex").slice(0, 16); 72 + console.log(`\nPixel hash: ${hash}`); 73 + console.log(`Module size: ${wasmBytes.length} bytes | Buffer: ${WIDTH}x${HEIGHT} RGBA`);