diff --git a/kidlisp/PIECE-IL.md b/kidlisp/PIECE-IL.md index 76d7613784..73f2e0d580 100644 --- a/kidlisp/PIECE-IL.md +++ b/kidlisp/PIECE-IL.md @@ -184,3 +184,10 @@ literal or state-derived bound that `repeat` expresses. Steps 1 and 2 are language work in `lib/kidlisp.mjs` and the conformance corpus. Step 3 is the one that decides the rest. + +## 7. Measured + +The two biggest pieces in the corpus were translated by hand and benchmarked +against their JavaScript in the production runtime on 2026-10-10: +`kidlisp/reports/whistlegraph-benchmark-2026-10-10.md`. Size is a wash; +the reference evaluator is 12 to 30 times slower; both pieces run bounded. diff --git a/kidlisp/examples/whistlegraph/fia.lisp b/kidlisp/examples/whistlegraph/fia.lisp index 1ca336461d..f13442315d 100644 --- a/kidlisp/examples/whistlegraph/fia.lisp +++ b/kidlisp/examples/whistlegraph/fia.lisp @@ -4,6 +4,8 @@ ; KidLisp with pools, for kidlisp/PIECE-IL.md. Same drawing calls, same ; renderer; closures became global transform state plus small functions. +; a unit random: KidLisp's random gives integers +(later rnd01 (/ (random 1000000) 1000000)) ; ---- time and the camera, once a frame -------------------------------- (def tt 0) (def tc 0) (def kc 0) (def th 0) (def cT 1) (def sT 0) (def zm 1) (def pit 0) (def cP 1) (def sP 0) (def gh 1) (def uu 1) (def gfloor 1) @@ -54,7 +56,7 @@ ; ---- the cast --------------------------------------------------------- (pool stars 60 sa srad sy sr sph ssp shue zs) -(once (repeat 60 ii (spawn stars (sa (* (random) 6.283)) (srad (+ 0.1 (* (random) 0.36))) (sy (random)) (sr (+ 0.6 (* (random) 1.4))) (sph (* (random) 6.28)) (ssp (+ 1 (* (random) 3))) (shue (random))))) +(once (repeat 60 ii (spawn stars (sa (* (rnd01) 6.283)) (srad (+ 0.1 (* (rnd01) 0.36))) (sy (rnd01)) (sr (+ 0.6 (* (rnd01) 1.4))) (sph (* (rnd01) 6.28)) (ssp (+ 1 (* (rnd01) 3))) (shue (rnd01))))) (pool fwends 10 fr fg fb ear off ph rad zs ang) (once (spawn fwends (fr 250) (fg 160) (fb 50) (ear 1) (off 0) (ph 0) (rad 0.37)) @@ -72,9 +74,11 @@ (later bandB k (if (= k 0) 130 else (if (= k 1) 30 else (if (= k 2) 190 else (if (= k 3) 60 else 200))))) ; ---- music: a tune at 240 bpm and a harp on tap -------------------------- -(0.25s (pluck (choose e4 g4 a4 g4 e4 d4 c4 d4 e4 g4 a4 c5))) -(0.5s (sub (choose c3 c3 a2 g2))) -(tap (bell (choose e4 g4 a4 c5))) +(def noteIdx 0) +(0.25s (now noteIdx (mod (+ noteIdx 1) 8)) + (if (= noteIdx 0) (pluck e4) else (if (= noteIdx 1) (pluck g4) else (if (= noteIdx 2) (pluck a4) else (if (= noteIdx 3) (pluck g4) else (if (= noteIdx 4) (pluck e4) else (if (= noteIdx 5) (pluck d4) else (if (= noteIdx 6) (pluck c4) else (pluck d4))))))))) +(0.5s (sub c3)) +(tap (bell e4)) ; ---- extruded letters: back layers first, dark; front layer bright ----- (later rr v size (round (* v size))) diff --git a/kidlisp/examples/whistlegraph/shooter.lisp b/kidlisp/examples/whistlegraph/shooter.lisp index 3f8b902d4f..61772d5324 100644 --- a/kidlisp/examples/whistlegraph/shooter.lisp +++ b/kidlisp/examples/whistlegraph/shooter.lisp @@ -5,6 +5,8 @@ ; hashed brick courses, box soldiers and the painter's order are all here, ; drawn with the same tri and line calls through the same renderer. +; a unit random: KidLisp's random gives integers +(later rnd01 (/ (random 1000000) 1000000)) ; ---- constants and the world ------------------------------------------------ (def EYE 1.6) (def AX 18) (def AZ 90) (def NEAR 0.2) (def WH 12) (def PH 1.2) (pool pillars 24 qx qz qh) @@ -36,7 +38,7 @@ (pool targets 7 tx ty tz ton tgy tr tdead tseed tcr tcg tcb tox toy) (later setCam (now WW width) (now HH height) (now FF (* WW 0.9)) (now sy (sin yaw)) (now cy (cos yaw)) (now sp (sin pitch)) (now cp (cos pitch))) -(later rnd aa bb (+ aa (* (random) (- bb aa)))) +(later rnd aa bb (+ aa (* (rnd01) (- bb aa)))) (later wrap aa (atan2 (sin aa) (cos aa))) ; camera space of a world point: crt (right), cd (depth), cup (up) (def crt 0) (def cd 0) (def cup 0) @@ -145,14 +147,14 @@ (now freeCount 0) (each pillars (if (> (pillarFree slot) 0) (now freeCount (+ freeCount 1)))) (def placed 0) (now onIdx -1) (def gy 0) - (if (> freeCount 0) (if (< (random) 0.5) - (now pickN (floor (* (random) freeCount))) (now pickI 0) + (if (> freeCount 0) (if (< (rnd01) 0.5) + (now pickN (floor (* (rnd01) freeCount))) (now pickI 0) (each pillars (if (> (pillarFree slot) 0) (if (= pickI pickN) (now pickX qx) (now pickZ qz) (now pickH qh) (now onIdx slot)) (now pickI (+ pickI 1)))) (def placed 1) (def gy pickH))) (if (< placed 1) (now freeCount 0) (each caves (if (< (hypot (- cx px) (- cz pz)) 36) (if (> cz (- pz 4)) (now freeCount (+ freeCount 1))))) - (if (> freeCount 0) (if (< (random) 0.5) - (now pickN (floor (* (random) freeCount))) (now pickI 0) + (if (> freeCount 0) (if (< (rnd01) 0.5) + (now pickN (floor (* (rnd01) freeCount))) (now pickI 0) (each caves (if (< (hypot (- cx px) (- cz pz)) 36) (if (> cz (- pz 4)) (if (= pickI pickN) (def jj (rnd -1.2 1.2)) (def oo (rnd 1.2 2.2)) (now pickX (+ cx (* cdx jj) (* cnx oo))) (now pickZ (+ cz (* cdz jj) (* cnz oo)))) (now pickI (+ pickI 1))))) @@ -164,9 +166,9 @@ (now pickX (clamp (+ px (* (sin aa) dd)) (+ (- 0 AX) 1) (- AX 1))) (now pickZ (clamp (+ pz (* (cos aa) dd)) (+ (- 0 AZ) 1) (- AZ 1))) (if (< (inPillar pickX pickZ 1) 1) (def tries 1))))) - (def cidx (floor (* (random) 4))) + (def cidx (floor (* (rnd01) 4))) (def kreaim (if (< shots 4) 0 else 1.2)) (def rr (rnd 0.45 0.55)) - (spawn targets (tx pickX) (tz pickZ) (ton onIdx) (tgy gy) (ty (+ gy 1.1)) (tr rr) (tdead 0) (tseed (* (random) 100)) + (spawn targets (tx pickX) (tz pickZ) (ton onIdx) (tgy gy) (ty (+ gy 1.1)) (tr rr) (tdead 0) (tseed (* (rnd01) 100)) (tcr (if (= cidx 0) 150 else (if (= cidx 1) 50 else (if (= cidx 2) 90 else 130)))) (tcg (if (= cidx 0) 50 else (if (= cidx 1) 90 else (if (= cidx 2) 120 else 80)))) (tcb (if (= cidx 0) 50 else (if (= cidx 1) 150 else (if (= cidx 2) 50 else 140)))) diff --git a/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md b/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md new file mode 100644 index 0000000000..fb6cefc8ae --- /dev/null +++ b/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md @@ -0,0 +1,54 @@ +# Two Whistlegraph pieces, JavaScript against KidLisp + +Measured 2026-10-10 in the production runtime, headless Chrome, with +`kidlisp/tools/bench-pieces.mjs` (fps from bios's own frame counter over +8 s at each size, after 3 s to settle). Both languages run through the same +renderer and the same loop; only the piece language differs. The KidLisp +versions are hand translations in `kidlisp/examples/whistlegraph/`, same +drawing calls, closures turned into global transform state, lists turned +into pools. + +| piece | language | chars | lines | fps 390×520 | fps 1280×720 | +| --- | --- | --- | --- | --- | --- | +| Fía (wgDuram v18) | JavaScript | 27,581 | 513 | 60 | 30 | +| Fía | KidLisp | 26,396 | 409 | 5 | 5 | +| Shooter (wgNirin v7) | JavaScript | 18,586 | 385 | 60 | 56 | +| Shooter | KidLisp | 22,369 | 382 | 2 | 2 | + +Per frame the KidLisp Fía makes about 2,900 ink, 1,600 line, 730 oval and +30 shape calls; the shooter about 2,400 ink and 5,000 tri calls. The +JavaScript pieces make the same calls. + +## What it says + +- **Size is a wash.** Prefix arithmetic costs what closures saved. A line + of `(+ gx (* sd 2.2 uu) sx)` is no shorter than `gx + sd * 2.2 * u + sx`. + The language wins on size only where it has a form the piece lacked: the + pools replaced forty lines of push/filter/sort with four. +- **The reference evaluator is 12 to 30 times slower than compiled + JavaScript** on these pieces. It re-evaluates the whole program every + paint, copies an environment on every function call, and dispatches + every arithmetic node through a string lookup. That is the gap the + compiled path has to close; it is not a property of the language. +- **The pieces run.** No browser global, no import, nothing the controlled + profile could not bound. The shooter's whole 3D pipeline fits in pools of + 16 and 32 points. + +## What the translation taught the evaluator + +Four faults fixed on the way (commits 91b9151bd1, e943697da8): a `repeat` +body form that did not name the iterator was hoisted and run once even +when it drew or spawned; `if` returned a flag instead of its branch's +value; `shape` could not take a pool; and the function-call path logged to +the console on every call. Rules the parser imposes: a line may not begin +with `else`; names may not contain a hyphen; single-letter names collide +with notes and colors; `>=` does not exist; `random` returns integers; +`quad` is a built-in. + +## Next + +The compiled path, in the order the census ranks the work: compile a +piece's arithmetic and pools to the numeric plan and the Wasm frame +(`lib/kidlisp-plan.mjs`, `kidlisp/benchmarks/raytrace-frame.c`), keep the +drawing calls as a command list the host rasterizes, and measure these two +pieces again. 60 fps at 1280×720 for the shooter is the bar. diff --git a/kidlisp/tools/bench-pieces.mjs b/kidlisp/tools/bench-pieces.mjs index 6d8d9b0063..58119d254a 100644 --- a/kidlisp/tools/bench-pieces.mjs +++ b/kidlisp/tools/bench-pieces.mjs @@ -36,6 +36,11 @@ async function measure(browser, file, [width, height]) { await page.setViewport({width, height, deviceScaleFactor: 1}); const errors = []; page.on('pageerror', e => errors.push(String(e).slice(0, 200))); + page.on('console', async m => { + if (!(m.type() === 'error' || /⛔|Unknown KidLisp|Evaluation failure/.test(m.text()))) return; + const parts = await Promise.all(m.args().map(a => a.evaluate(v => v instanceof Error ? v.message + ' @ ' + String(v.stack || '').split('\n').slice(1, 3).join(' | ') : String(v)).catch(() => '?'))); + errors.push((parts.join(' ') || m.text()).slice(0, 400)); + }); // The runtime is the page, as the render pool loads it. bios reports fps to // a parent window; `parent` is replaceable, so a stand-in parent collects it. await page.goto(RUNTIME, {waitUntil: 'domcontentloaded', timeout: 90_000}); @@ -58,7 +63,7 @@ async function measure(browser, file, [width, height]) { samples: fps.length, fpsMedian: sorted[Math.floor(sorted.length / 2)] ?? 0, fpsMin: sorted[0] ?? 0, fpsMax: sorted.at(-1) ?? 0, errors, shot}; } -const browser = await puppeteer.launch({headless: true, executablePath: CHROME, args: ['--no-sandbox', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--autoplay-policy=no-user-gesture-required', '--mute-audio']}); +const browser = await puppeteer.launch({headless: true, executablePath: CHROME, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--autoplay-policy=no-user-gesture-required', '--mute-audio']}); const results = []; try { for (const file of files) for (const size of SIZES) {