From 1c9d74b407811893ebd1ec9a2b68ac2a11d9e836 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 14 Jun 2026 12:46:06 -0400 Subject: [PATCH] wat iter-4: embedded Lisp prelude + append-only race output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WAT prelude (evaluated after primitive binding at init) adds: map, filter, fold-left, fold-right, for-each, any, every, count, find, sort (quicksort), vector-map, vector-for-each, vector-fill!, string-split, string-trim, string->list, random-state, assert-equal/true/false. Higher-order ops are now Lisp-defined, not primitive bloat. Eval-time parse + bind happens once per WASM instance startup. Playground output: per fox, single append-only column instead of 3-up grid. Tiers still race in parallel workers; whichever finishes first appears first in the output. Live ms counters move to the status bar (python 312ms · c 47ms · asm 89ms). --- wasm/app/app.js | 71 ++++++++++++++--------------- wasm/app/style.css | 11 +---- wasm/asm/lumbda.wat | 56 ++++++++++++++++++++++- www/playground/app.js | 71 ++++++++++++++--------------- www/playground/asm/lumbda-asm.wasm | Bin 13446 -> 16291 bytes www/playground/style.css | 11 +---- 6 files changed, 125 insertions(+), 95 deletions(-) diff --git a/wasm/app/app.js b/wasm/app/app.js index 5d7d503..5504c0e 100644 --- a/wasm/app/app.js +++ b/wasm/app/app.js @@ -128,43 +128,24 @@ function cancelCurrentRun() { } } -// ─── Per-tier output block with live ms counter ──────────────────── - -function makeLiveBlock(tierName) { +// Append a finished tier-block to the output. Order = finish order, so +// the fastest tier appears first naturally. +function appendBlock(tierName, elapsedMs, text, kind) { const block = document.createElement("div"); block.className = "tier-block"; const h = document.createElement("h3"); h.textContent = TIERS[tierName] || tierName; const t = document.createElement("span"); t.className = "time"; - t.textContent = " loading…"; + if (kind === "cancelled") t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`; + else t.textContent = ` (${elapsedMs.toFixed(0)} ms)`; h.appendChild(t); block.appendChild(h); const pre = document.createElement("pre"); - pre.textContent = ""; + if (kind === "error") pre.className = "err"; + pre.textContent = text; block.appendChild(pre); outputEl.appendChild(block); - - let raf = 0; - let start = 0; - function tickerLoop() { - const ms = (performance.now() - start) | 0; - t.textContent = ` ${ms} ms…`; - raf = requestAnimationFrame(tickerLoop); - } - return { - startTimer() { start = performance.now(); tickerLoop(); return start; }, - stop(elapsedMs) { - if (raf) cancelAnimationFrame(raf); - t.textContent = ` (${elapsedMs.toFixed(0)} ms)`; - }, - cancelled(elapsedMs) { - if (raf) cancelAnimationFrame(raf); - t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`; - }, - setOutput(text) { pre.textContent = text; }, - setError(msg) { pre.className = "err"; pre.textContent = msg; }, - }; } // ─── Run / cancel ────────────────────────────────────────────────── @@ -180,43 +161,57 @@ async function runAll() { outputEl.innerHTML = ""; const tiers = selectedTiers(); const src = getEditorText(); - // Each tier runs in its own worker — Promise.all so they race. + // Each tier in its own worker. Append-only output: as each tier finishes, + // we append its block — so the fastest tier shows up first. + const startTimes = {}; + const tickStatus = () => { + const parts = []; + for (const t of tiers) { + if (startTimes[t] !== undefined) { + parts.push(`${t} ${((performance.now() - startTimes[t]) | 0)}ms`); + } + } + setStatus(parts.join(" · "), "busy"); + }; + let raf = requestAnimationFrame(function loop() { + tickStatus(); + raf = requestAnimationFrame(loop); + }); const tierPromises = tiers.map((t) => { - const live = makeLiveBlock(t); - const startMark = live.startTimer(); + startTimes[t] = performance.now(); return runOnTierInWorker(t, src, (loadingTier) => { - // Status reflects whichever tier most recently announced a load. setStatus(`loading ${loadingTier}…`, "busy"); }) .then((output) => { - const elapsed = performance.now() - startMark; - live.stop(elapsed); - live.setOutput(output); + const elapsed = performance.now() - startTimes[t]; + delete startTimes[t]; + appendBlock(t, elapsed, output, "ok"); return { tier: t, ok: true, elapsed }; }) .catch((e) => { - const elapsed = performance.now() - startMark; + const elapsed = performance.now() - startTimes[t]; + delete startTimes[t]; if (e.message === "cancelled") { - live.cancelled(elapsed); + appendBlock(t, elapsed, "(cancelled)", "cancelled"); return { tier: t, cancelled: true, elapsed }; } - live.stop(elapsed); - live.setError(e.message || String(e)); + appendBlock(t, elapsed, e.message || String(e), "error"); return { tier: t, error: e.message, elapsed }; }); }); try { const results = await Promise.all(tierPromises); + cancelAnimationFrame(raf); const cancelled = results.some((r) => r.cancelled); const anyErr = results.some((r) => r.error); if (cancelled) setStatus("cancelled", "warn"); else if (anyErr) setStatus("completed with errors", "err"); else { - // Winner of the race for ok styling. const fastest = results.reduce((a, b) => (a.elapsed < b.elapsed ? a : b)); setStatus(`ok — ${fastest.tier} won in ${fastest.elapsed.toFixed(0)} ms`, "ok"); } } catch (e) { + cancelAnimationFrame(raf); setStatus(`fatal: ${e.message}`, "err"); } finally { inFlight = false; diff --git a/wasm/app/style.css b/wasm/app/style.css index 665d1ff..7760cc1 100644 --- a/wasm/app/style.css +++ b/wasm/app/style.css @@ -233,16 +233,9 @@ header code { padding: 0.6rem 0.8rem; font-size: 13px; line-height: 1.35; - display: grid; - gap: 0.6rem; - align-content: start; } -/* When 3 tier blocks render (All three mode), lay them out as 3 columns - * for race-style comparison. With fewer, they stack as a single column. */ -#output:has(.tier-block:nth-child(3)) { - grid-template-columns: 1fr 1fr 1fr; -} -#output .tier-block { margin-bottom: 0; } +/* Append-only: results arrive in finish order — fastest tier shows up first. */ +#output .tier-block { margin-bottom: 1rem; } #output .tier-block:last-child { margin-bottom: 0; } #output .tier-block h3 { margin: 0 0 0.25rem; diff --git a/wasm/asm/lumbda.wat b/wasm/asm/lumbda.wat index 78e2982..994df4c 100644 --- a/wasm/asm/lumbda.wat +++ b/wasm/asm/lumbda.wat @@ -68,6 +68,11 @@ (global $sym_case (mut i32) (i32.const 0)) (global $sym_do (mut i32) (i32.const 0)) + ;; Prelude — Lisp source evaluated after primitive binding to define + ;; higher-order functions on top of the C-level primitives. + (global $prelude_ptr i32 (i32.const 0x50000)) + (global $prelude_len (mut i32) (i32.const 0)) + ;; Constants (immediate value addresses) (global $NIL i32 (i32.const 4)) (global $TRUE i32 (i32.const 8)) @@ -2761,6 +2766,30 @@ (data (i32.const 0xE590) "hash-table-values") (data (i32.const 0xE5A4) "hash-table->alist") + ;; ── Embedded Lisp prelude. Evaluated at end of init. ───────────── + (data (i32.const 0x50000) + "(define (map f xs) (if (null? xs) (quote ()) (cons (f (car xs)) (map f (cdr xs)))))\n" + "(define (filter p xs) (cond ((null? xs) (quote ())) ((p (car xs)) (cons (car xs) (filter p (cdr xs)))) (else (filter p (cdr xs)))))\n" + "(define (fold-left f z xs) (if (null? xs) z (fold-left f (f z (car xs)) (cdr xs))))\n" + "(define (fold-right f z xs) (if (null? xs) z (f (car xs) (fold-right f z (cdr xs)))))\n" + "(define (for-each f xs) (cond ((null? xs) (void)) (else (f (car xs)) (for-each f (cdr xs)))))\n" + "(define (any p xs) (cond ((null? xs) #f) ((p (car xs)) #t) (else (any p (cdr xs)))))\n" + "(define (every p xs) (cond ((null? xs) #t) ((p (car xs)) (every p (cdr xs))) (else #f)))\n" + "(define (count p xs) (fold-left (lambda (acc x) (if (p x) (+ acc 1) acc)) 0 xs))\n" + "(define (find p xs) (cond ((null? xs) #f) ((p (car xs)) (car xs)) (else (find p (cdr xs)))))\n" + "(define (string-split s sep) (let loop ((i 0) (start 0) (acc (quote ()))) (cond ((>= i (string-length s)) (reverse (cons (substring s start i) acc))) ((char=? (string-ref s i) sep) (loop (+ i 1) (+ i 1) (cons (substring s start i) acc))) (else (loop (+ i 1) start acc)))))\n" + "(define (string-trim s) (let* ((len (string-length s)) (start (let loop ((i 0)) (cond ((>= i len) i) ((char-whitespace? (string-ref s i)) (loop (+ i 1))) (else i)))) (end (let loop ((i (- len 1))) (cond ((< i start) start) ((char-whitespace? (string-ref s i)) (loop (- i 1))) (else (+ i 1)))))) (substring s start end)))\n" + "(define (string->list s) (let loop ((i 0) (acc (quote ()))) (if (>= i (string-length s)) (reverse acc) (loop (+ i 1) (cons (string-ref s i) acc)))))\n" + "(define (random-state) (cons 12345 67890))\n" + "(define (vector-map f v) (let* ((n (vector-length v)) (r (make-vector n 0))) (let loop ((i 0)) (cond ((>= i n) r) (else (vector-set! r i (f (vector-ref v i))) (loop (+ i 1)))))))\n" + "(define (vector-for-each f v) (let ((n (vector-length v))) (let loop ((i 0)) (cond ((>= i n) (void)) (else (f (vector-ref v i)) (loop (+ i 1)))))))\n" + "(define (vector-fill! v x) (let ((n (vector-length v))) (let loop ((i 0)) (cond ((>= i n) (void)) (else (vector-set! v i x) (loop (+ i 1)))))))\n" + "(define (sort xs less?) (cond ((null? xs) (quote ())) ((null? (cdr xs)) xs) (else (let ((p (car xs)) (rest (cdr xs))) (append (sort (filter (lambda (x) (less? x p)) rest) less?) (cons p (sort (filter (lambda (x) (not (less? x p))) rest) less?)))))))\n" + "(define (assert-equal expected actual) (cond ((equal? expected actual) #t) (else (display \"FAIL expected=\") (display expected) (display \" actual=\") (display actual) (newline) #f)))\n" + "(define (assert-true v) (cond (v #t) (else (display \"FAIL expected truthy got=\") (display v) (newline) #f)))\n" + "(define (assert-false v) (cond ((not v) #t) (else (display \"FAIL expected #f got=\") (display v) (newline) #f)))\n" + ) + (func $bind_prim (param $name_ptr i32) (param $name_len i32) (param $id i32) (local $sym i32) (local.set $sym (call $intern (local.get $name_ptr) (local.get $name_len))) @@ -2769,6 +2798,30 @@ (local.get $sym) (call $make_primitive (local.get $id))))) + ;; Length of a NUL-terminated byte sequence (used for prelude embed). + (func $strlen_nul (param $p i32) (result i32) + (local $i i32) + (local.set $i (i32.const 0)) + (block $done + (loop $l + (br_if $done (i32.eqz (i32.load8_u (i32.add (local.get $p) (local.get $i))))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $l))) + (local.get $i)) + + ;; Evaluate the embedded prelude. Source bounds restored by next lumbda_eval. + (func $eval_prelude + (local $val i32) + (global.set $prelude_len (call $strlen_nul (global.get $prelude_ptr))) + (global.set $source_ptr (global.get $prelude_ptr)) + (global.set $source_end (i32.add (global.get $prelude_ptr) (global.get $prelude_len))) + (block $done + (loop $loop + (call $skip_ws) + (br_if $done (i32.ge_u (global.get $source_ptr) (global.get $source_end))) + (local.set $val (call $eval (call $read) (global.get $NIL))) + (br $loop)))) + (func $lumbda_init (export "lumbda_init") (if (global.get $initialized) (then (return))) (global.set $initialized (i32.const 1)) @@ -2897,7 +2950,8 @@ (call $bind_prim (i32.const 0xE570) (i32.const 15) (i32.const 103)) ;; hash-table-size (call $bind_prim (i32.const 0xE580) (i32.const 15) (i32.const 104)) ;; hash-table-keys (call $bind_prim (i32.const 0xE590) (i32.const 17) (i32.const 105)) ;; hash-table-values - (call $bind_prim (i32.const 0xE5A4) (i32.const 17) (i32.const 106))) ;; hash-table->alist + (call $bind_prim (i32.const 0xE5A4) (i32.const 17) (i32.const 106)) ;; hash-table->alist + (call $eval_prelude)) ;; ─── Public eval entry ───────────────────────────────────────── ;; JS writes UTF-8 source into 0x20000 and calls lumbda_eval(len). diff --git a/www/playground/app.js b/www/playground/app.js index 5d7d503..5504c0e 100644 --- a/www/playground/app.js +++ b/www/playground/app.js @@ -128,43 +128,24 @@ function cancelCurrentRun() { } } -// ─── Per-tier output block with live ms counter ──────────────────── - -function makeLiveBlock(tierName) { +// Append a finished tier-block to the output. Order = finish order, so +// the fastest tier appears first naturally. +function appendBlock(tierName, elapsedMs, text, kind) { const block = document.createElement("div"); block.className = "tier-block"; const h = document.createElement("h3"); h.textContent = TIERS[tierName] || tierName; const t = document.createElement("span"); t.className = "time"; - t.textContent = " loading…"; + if (kind === "cancelled") t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`; + else t.textContent = ` (${elapsedMs.toFixed(0)} ms)`; h.appendChild(t); block.appendChild(h); const pre = document.createElement("pre"); - pre.textContent = ""; + if (kind === "error") pre.className = "err"; + pre.textContent = text; block.appendChild(pre); outputEl.appendChild(block); - - let raf = 0; - let start = 0; - function tickerLoop() { - const ms = (performance.now() - start) | 0; - t.textContent = ` ${ms} ms…`; - raf = requestAnimationFrame(tickerLoop); - } - return { - startTimer() { start = performance.now(); tickerLoop(); return start; }, - stop(elapsedMs) { - if (raf) cancelAnimationFrame(raf); - t.textContent = ` (${elapsedMs.toFixed(0)} ms)`; - }, - cancelled(elapsedMs) { - if (raf) cancelAnimationFrame(raf); - t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`; - }, - setOutput(text) { pre.textContent = text; }, - setError(msg) { pre.className = "err"; pre.textContent = msg; }, - }; } // ─── Run / cancel ────────────────────────────────────────────────── @@ -180,43 +161,57 @@ async function runAll() { outputEl.innerHTML = ""; const tiers = selectedTiers(); const src = getEditorText(); - // Each tier runs in its own worker — Promise.all so they race. + // Each tier in its own worker. Append-only output: as each tier finishes, + // we append its block — so the fastest tier shows up first. + const startTimes = {}; + const tickStatus = () => { + const parts = []; + for (const t of tiers) { + if (startTimes[t] !== undefined) { + parts.push(`${t} ${((performance.now() - startTimes[t]) | 0)}ms`); + } + } + setStatus(parts.join(" · "), "busy"); + }; + let raf = requestAnimationFrame(function loop() { + tickStatus(); + raf = requestAnimationFrame(loop); + }); const tierPromises = tiers.map((t) => { - const live = makeLiveBlock(t); - const startMark = live.startTimer(); + startTimes[t] = performance.now(); return runOnTierInWorker(t, src, (loadingTier) => { - // Status reflects whichever tier most recently announced a load. setStatus(`loading ${loadingTier}…`, "busy"); }) .then((output) => { - const elapsed = performance.now() - startMark; - live.stop(elapsed); - live.setOutput(output); + const elapsed = performance.now() - startTimes[t]; + delete startTimes[t]; + appendBlock(t, elapsed, output, "ok"); return { tier: t, ok: true, elapsed }; }) .catch((e) => { - const elapsed = performance.now() - startMark; + const elapsed = performance.now() - startTimes[t]; + delete startTimes[t]; if (e.message === "cancelled") { - live.cancelled(elapsed); + appendBlock(t, elapsed, "(cancelled)", "cancelled"); return { tier: t, cancelled: true, elapsed }; } - live.stop(elapsed); - live.setError(e.message || String(e)); + appendBlock(t, elapsed, e.message || String(e), "error"); return { tier: t, error: e.message, elapsed }; }); }); try { const results = await Promise.all(tierPromises); + cancelAnimationFrame(raf); const cancelled = results.some((r) => r.cancelled); const anyErr = results.some((r) => r.error); if (cancelled) setStatus("cancelled", "warn"); else if (anyErr) setStatus("completed with errors", "err"); else { - // Winner of the race for ok styling. const fastest = results.reduce((a, b) => (a.elapsed < b.elapsed ? a : b)); setStatus(`ok — ${fastest.tier} won in ${fastest.elapsed.toFixed(0)} ms`, "ok"); } } catch (e) { + cancelAnimationFrame(raf); setStatus(`fatal: ${e.message}`, "err"); } finally { inFlight = false; diff --git a/www/playground/asm/lumbda-asm.wasm b/www/playground/asm/lumbda-asm.wasm index 5c8c6ad00e8559c56361d66a0f59896e6a120139..2cb64c1a354f7bf71d71220e253cb02eb0090910 100644 GIT binary patch delta 5070 zcma)9U2Gd!74~@S#EIKx^4r9*n>!vmwUaoT{*djeORVQJM?Xn&rd^4jCXsbjmI zahk3`ld1?1ylk0$0SPUul@N>Ahb6oqA%p}H5<-Z#eL^5WNJz^IS}j_{1K+v#dOTxi zx6Q-#%=!7wIo~<=-pSW%Z#%DF3g6v%yjh+I4G#~gsv3c8>x$yA5L2`nU1h3~bpNBCh5b?_858zK;_znV zAz}P6`UX~WsK8faJrzF>=kOKv3JV#DYsVE9(sV-6bf)lFh?Fr>h!{e|5RaI0KN$Mp zd;;p?!C%y3SX(8vL9vRj`vgf6{Jzp8OoBh-hibRJ%A9<>BHBx%GTd@0JEQ=i899y0haDdwAtM#vAb(q}JE~XQ@ zVkCObk}XX)C^MBo>Hb8^=U@UewHu>rCKKhx5e5_Y%w)#Z$LE2*G5+!zXFmq^qb!Pb zh(*|tkQ4G|_h2D^_Wcg@l5nwA?8)!wWSVYbDw*KP=E4!Auh5Um!c0X{*cG>@ zmY*keq^RuZ5kAQRcZB1P^l;}8W{zeEdOnKD_?MK3tik=|)C`}+ZpO36qv%gm^alxt zr`MEu_uRCuB;A*$3wVAsoyPMIr@si|Kc2pT_3X^|7Ag1;8@?{f>7_*}ztD~D{(I)D z*aRwo|2#i)JgJTH35sH)C%%)+9{%Fwy9&T}=GL*kHJ8Qu+quskBXHEt!QJV^ZH)xI z5JvK_lQ0zzc$QZrLGNJFd(9%oaV{vDE+<{o{V@5=Cz+*vGyX7{aX(5Sd?g|<6ww}IIJz;L|XVf=fqPD`&3`(s@yB5 zW|+@^#AkHVGuk_~?(jpxXL&+cV#v^TFUS~|G8I35vBt$>@O;+e0%J`an8gXa7Du&$ ze4Uh9F2b0vEIf*GXZL3dS5f+Z7m8RfG0X8mE(j;6?gdj|T}8+%UQniciax^9TmdEx znUK*H$P>Pp5dufmZeUzO#jG^IkH7jwF4mNw^z@O^oRCI?ws|3q1*J3^AB`P9gs zluzHvepx>KD|;JQn~TrM_pdA_F7PX`_&2(pXn*3arJIJ%Sbjj_BI1nwz2Ct7!=k~2 zGr!u7a5m=M@zWcKc#LMX)dADvnXKV+2nD&1qVS~)Xk%x1#I ztTDHf;TMi}&IlXot>cQej!k$`T-fn%9PXTsMmMxdyaY$hEQT^l@!%kq3De3#aK^33 zL-qs6h`3FEymCo>Kl9DQf3AFg*a^XLD*oPdu3}be4U^?|OD$Gq`&ORiYE_nNbn5lX z;`LsqX~Q57O}W{ySPtrPsZC0VgkP>)5rTX^Kk8vtYjxXfvzEYtbA{yuveO3*pj^uj zD5Iy=JXKw9C@H2oP19p7YV@rN)D9FPs5n zquSpC!}j6`&!bVzJ+tlM4#E;hu(jmikVg+s)`J}U<5O;S8g@71UWan^5`B)dT&Y}U z`+%o;#f;%;nUU;Fo_@g68gb^Qw}uJi(}?~LraHiYZwR`h#sPYX`_V1CU2AL=tX92d zGmBYf3mMf-o7J056q~EDH3%%b)V7&JkCbc9+g);h(H7a9WS)3R+`*NG+pY0yjJ}a7Qdd!^wTR&;`z{7g{jQ5yrb`6R9sunT5&4P(& zLnKC1f?B4nvo_eL3rotVqCE@`_zzZ&82IZQ@Y3FQrpnAeA#Z{UZgoifQ;Q69y~Eq# z7uP^qPnlG$Ue{3uop==VXeOo7clbck&B&n>CYTas<|-uJEpK?f3H`>cij zl6&5tnnibx@O9)zL%_MwL^xiGKnOzc0r5>p$+FD0T`=!;N_A%Lw=m^q1$)ZIdyfjP zzC56JnD2J2)Np5%9j@|B< uj7C~a#NLzhVQ{gxAF!>a9SBJvPl#46(T7?uTCQ;uRO9`LBq05#3HX2fbgviy delta 2257 zcmZuyOK4nG7{2E|CX=~s=1kJ0_c7_5GiN4`rYX^u5KXGNLaH{gi7r|M+mM&GHMKQu zR1ir@tp!(Yj|ξX+&#A0rB`#EnoyP;nzF1<{p)i*D47-#Pcr%$?NDf6x8j|M&m@ zIqz<~bn?oCdGf^7dzNTh#^EmmJ7+> znf3E$*U!(NJ-=qGY~ii0JY_sy=bbyVzP7ZY^`6LGKXRh>6}dq&-l&;nhAcR~{LmVy z`UmYvX60RG(lR%<8Lle=ca)F~W-vl~xWz0HNXNdDcyi1xW@CNf)gH4i5b%C+UIBe4 zBV31D=A;E@LjRA3?tZw@EJJt)w-aViuKZEXh>nVw6%it~f!KzQ9m;<)f9&@ljy7*v zkxBO|u~1Q?D=9{x7;mQ-p$xLOkV(G%773-D+l%u)&`>Vs_7r^OCHmcfl?d{#2<2P3 zpB;}QF?qoqZucnTCgOOt&)jK~2loRMo0e6kbUg3qbD7)BMj>z!O3`r?uAlFCAGDgE z>kQpN6e~h*)(e)G^Ht<YzeIDEL*^>M}XeSwyTyJBL6obq;RC%@_V{B`I@2ZE6>) z7tQ7~(;c~eVwwi3DQ4OLlR87J4F%j5R5}}(D*T#|9POG#!`|x>Sq+JT^}DV+s(L)A z4gZANQ}K)>9ypyzQk4Tcih)jez_d{UQ6Gz$%tB(=k>fjt7M0kJ$SH2?X^#E2YrpMH zzdpjKi58ISaSYsJ#BS8V4DRSoI7kL-gO;^+cDsY7IdRia0qq_kzKps#3FK1uFwR%I z%Q%11{Rq0z)-wh=+4Fjz$|l3wrjz=bd8!&7tfzBr=L;A!EHSquBWWLsomKmbg)P#Y zPR7xD-chX9ZtT2H5bZgC6!ZhX3i^Y8-!>Hu=;&xt1anj|nlVLCyBMqx?4Y_Y{tP|6 zgLSX-Scqm%RZ|e2P`(9;%J3X;L#Mjp^=V8&%A(wY-5B-M7{Qh5Acas4^a=np)4Ln= z*Ax=0@0)x%Qu?J1?h4W$=% zjc({GC8gOBYhoNRAdMh-&1DuoX*SjDctlg$h1kbPe7#1U_>FROIQi{Lq?(0=xVv=G z(Z=%2(iBS1vIz7LJGYT?S&EHH5qY{b1y&arjpAd-ctfxSu(B2aNs%NZ>Wt()h$=Bd zPVgP+9QtC9q&0#5|K$$ET&GocFy>}kxw~U-1`TdoYZVRkXc?X$eQ7nM@;Ck@Kzg`5 z4*)+Ze+;@T{!U