(function(Object){ typeof globalThis !== "object" && (this ? get() : (Object.defineProperty (Object.prototype, "_T_", {configurable: true, get: get}), _T_)); function get(){ var global = this || self; global.globalThis = global; delete Object.prototype._T_; } } (Object)); (js=> async args=>{ "use strict"; const {link, src, generated, disable_effects} = args, isNode = globalThis.process?.versions?.node, math = {cos: Math.cos, sin: Math.sin, tan: Math.tan, acos: Math.acos, asin: Math.asin, atan: Math.atan, cosh: Math.cosh, sinh: Math.sinh, tanh: Math.tanh, acosh: Math.acosh, asinh: Math.asinh, atanh: Math.atanh, cbrt: Math.cbrt, exp: Math.exp, expm1: Math.expm1, log: Math.log, log1p: Math.log1p, log2: Math.log2, log10: Math.log10, atan2: Math.atan2, hypot: Math.hypot, pow: Math.pow, fmod: (x, y)=>x % y}, typed_arrays = [Float32Array, Float64Array, Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Int32Array, Int32Array, Int32Array, Float32Array, Float64Array, Uint8Array, Uint16Array, Uint8ClampedArray], fs = isNode && require("node:fs"), fs_cst = fs?.constants, access_flags = fs ? [fs_cst.R_OK, fs_cst.W_OK, fs_cst.X_OK, fs_cst.F_OK] : [], open_flags = fs ? [fs_cst.O_RDONLY, fs_cst.O_WRONLY, fs_cst.O_RDWR, fs_cst.O_APPEND, fs_cst.O_CREAT, fs_cst.O_TRUNC, fs_cst.O_EXCL, fs_cst.O_NONBLOCK, fs_cst.O_NOCTTY, fs_cst.O_DSYNC, fs_cst.O_SYNC] : []; var out_channels = {map: new WeakMap(), set: new Set(), finalization: new FinalizationRegistry(ref=>out_channels.set.delete(ref))}; function register_channel(ch){ const ref = new WeakRef(ch); out_channels.map.set(ch, ref); out_channels.set.add(ref); out_channels.finalization.register(ch, ref, ch); } function unregister_channel(ch){ const ref = out_channels.map.get(ch); if(ref){ out_channels.map.delete(ch); out_channels.set.delete(ref); out_channels.finalization.unregister(ch); } } function channel_list(){ return [...out_channels.set].map(ref=>ref.deref()).filter(ch=>ch); } var start_fiber; function make_suspending(f){ return WebAssembly?.Suspending ? new WebAssembly.Suspending(f) : f; } function make_promising(f){ return ! disable_effects && WebAssembly?.promising && f ? WebAssembly.promising(f) : f; } const decoder = new TextDecoder("utf-8", {ignoreBOM: 1}), encoder = new TextEncoder(); function hash_int(h, d){ d = Math.imul(d, 0xcc9e2d51 | 0); d = d << 15 | d >>> 17; d = Math.imul(d, 0x1b873593); h ^= d; h = h << 13 | h >>> 19; return (h + (h << 2) | 0) + (0xe6546b64 | 0) | 0; } function hash_string(h, s){ for(var i = 0; i < s.length; i++) h = hash_int(h, s.charCodeAt(i)); return h ^ s.length; } function getenv(n){ if(isNode && globalThis.process.env[n] !== undefined) return globalThis.process.env[n]; return globalThis.jsoo_env?.[n]; } let record_backtrace_flag = 0; for(const l of getenv("OCAMLRUNPARAM")?.split(",") || []){ if(l === "b") record_backtrace_flag = 1; if(l.startsWith("b=")) record_backtrace_flag = + l.slice(2) ? 1 : 0; } function alloc_stat(s, large){ var kind; if(s.isFile()) kind = 0; else if(s.isDirectory()) kind = 1; else if(s.isCharacterDevice()) kind = 2; else if(s.isBlockDevice()) kind = 3; else if(s.isSymbolicLink()) kind = 4; else if(s.isFIFO()) kind = 5; else if(s.isSocket()) kind = 6; return caml_alloc_stat (large, s.dev, s.ino | 0, kind, s.mode, s.nlink, s.uid, s.gid, s.rdev, BigInt(s.size), s.atimeMs / 1000, s.mtimeMs / 1000, s.ctimeMs / 1000); } const on_windows = isNode && globalThis.process.platform === "win32", call = Function.prototype.call, DV = DataView.prototype, bindings = {jstag: WebAssembly.JSTag || new WebAssembly.Tag({parameters: ["externref"], results: []}), identity: x=>x, from_bool: x=>! ! x, get: (x, y)=>x[y], set: (x, y, z)=>x[y] = z, delete: (x, y)=>delete x[y], instanceof: (x, y)=>x instanceof y, typeof: x=>typeof x, equals: (x, y)=>x == y, strict_equals: (x, y)=>x === y, fun_call: (f, o, args)=>f.apply(o, args), meth_call: (o, f, args)=>o[f].apply(o, args), new_array: n=>new Array(n), new_obj: ()=>({}), new: (c, args)=>new c(...args), global_this: globalThis, iter_props: (o, f)=>{for(var nm in o) if(Object.hasOwn(o, nm)) f(nm);}, array_length: a=>a.length, array_get: (a, i)=>a[i], array_set: (a, i, v)=>a[i] = v, read_string: l=>decoder.decode(new Uint8Array(buffer, 0, l)), read_string_stream: (l, stream)=> decoder.decode(new Uint8Array(buffer, 0, l), {stream: stream}), append_string: (s1, s2)=>s1 + s2, write_string: s=>{ var start = 0, len = s.length; for(;;){ const {read, written} = encoder.encodeInto(s.slice(start), out_buffer); len -= read; if(! len) return written; caml_extract_bytes(written); start += read; }}, ta_create: (k, sz)=>new typed_arrays[k](sz), ta_normalize: a=> a instanceof Uint32Array ? new Int32Array(a.buffer, a.byteOffset, a.length) : a, ta_kind: a=>typed_arrays.findIndex(c=>a instanceof c), ta_length: a=>a.length, ta_get_i32: (a, i)=>a[i], ta_fill: (a, v)=>a.fill(v), ta_blit: (s, d)=>d.set(s), ta_subarray: (a, i, j)=>a.subarray(i, j), ta_set: (a, b, i)=>a.set(b, i), ta_new: len=>new Uint8Array(len), ta_copy: (ta, t, s, e)=>ta.copyWithin(t, s, e), ta_bytes: a=> new Uint8Array (a.buffer, a.byteOffset, a.length * a.BYTES_PER_ELEMENT), ta_blit_from_bytes: (s, p1, a, p2, l)=>{ for(let i = 0; i < l; i++) a[p2 + i] = bytes_get(s, p1 + i);}, ta_blit_to_bytes: (a, p1, s, p2, l)=>{ for(let i = 0; i < l; i++) bytes_set(s, p2 + i, a[p1 + i]);}, dv_make: a=>new DataView(a.buffer, a.byteOffset, a.byteLength), dv_get_f64: call.bind(DV.getFloat64), dv_get_f32: call.bind(DV.getFloat32), dv_get_i64: call.bind(DV.getBigInt64), dv_get_i32: call.bind(DV.getInt32), dv_get_i16: call.bind(DV.getInt16), dv_get_ui16: call.bind(DV.getUint16), dv_get_i8: call.bind(DV.getInt8), dv_get_ui8: call.bind(DV.getUint8), dv_set_f64: call.bind(DV.setFloat64), dv_set_f32: call.bind(DV.setFloat32), dv_set_i64: call.bind(DV.setBigInt64), dv_set_i32: call.bind(DV.setInt32), dv_set_i16: call.bind(DV.setInt16), dv_set_i8: call.bind(DV.setInt8), littleEndian: new Uint8Array(new Uint32Array([1]).buffer)[0], wrap_callback: f=> function(...args){ if(args.length === 0) args = [undefined]; return caml_callback(f, args.length, args, 1); }, wrap_callback_args: f=>function(...args){return caml_callback(f, 1, [args], 0);}, wrap_callback_strict: (arity, f)=> function(...args){ args.length = arity; return caml_callback(f, arity, args, 0); }, wrap_callback_unsafe: f=>function(...args){return caml_callback(f, args.length, args, 2);}, wrap_meth_callback: f=> function(...args){ args.unshift(this); return caml_callback(f, args.length, args, 1); }, wrap_meth_callback_args: f=>function(...args){return caml_callback(f, 2, [this, args], 0);}, wrap_meth_callback_strict: (arity, f)=> function(...args){ args.length = arity; args.unshift(this); return caml_callback(f, args.length, args, 0); }, wrap_meth_callback_unsafe: f=> function(...args){ args.unshift(this); return caml_callback(f, args.length, args, 2); }, wrap_fun_arguments: f=>function(...args){return f(args);}, format_float: (prec, conversion, pad, x)=>{ function toFixed(x, dp){ if(Math.abs(x) < 1.0) return x.toFixed(dp); else{ var e = Number.parseInt(x.toString().split("+")[1]); if(e > 20){ e -= 20; x /= Math.pow(10, e); x += new Array(e + 1).join("0"); if(dp > 0) x = x + "." + new Array(dp + 1).join("0"); return x; } else return x.toFixed(dp); } } switch(conversion){ case 0: var s = x.toExponential(prec), i = s.length; if(s.charAt(i - 3) === "e") s = s.slice(0, i - 1) + "0" + s.slice(i - 1); break; case 1: s = toFixed(x, prec); break; case 2: prec = prec ? prec : 1; s = x.toExponential(prec - 1); var j = s.indexOf("e"), exp = + s.slice(j + 1); if(exp < - 4 || x >= 1e21 || x.toFixed(0).length > prec){ var i = j - 1; while(s.charAt(i) === "0") i--; if(s.charAt(i) === ".") i--; s = s.slice(0, i + 1) + s.slice(j); i = s.length; if(s.charAt(i - 3) === "e") s = s.slice(0, i - 1) + "0" + s.slice(i - 1); break; } else{ var p = prec; if(exp < 0){ p -= exp + 1; s = x.toFixed(p); } else while(s = x.toFixed(p), s.length > prec + 1) p--; if(p){ var i = s.length - 1; while(s.charAt(i) === "0") i--; if(s.charAt(i) === ".") i--; s = s.slice(0, i + 1); } } break; } return pad ? " " + s : s;}, gettimeofday: ()=>new Date().getTime() / 1000, times: ()=>{ if(globalThis.process?.cpuUsage){ var t = globalThis.process.cpuUsage(); return caml_alloc_times(t.user / 1e6, t.system / 1e6); } else{ var t = performance.now() / 1000; return caml_alloc_times(t, 0); }}, gmtime: t=>{ var d = new Date(t * 1000), d_num = d.getTime(), januaryfirst = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)).getTime(), doy = Math.floor((d_num - januaryfirst) / 86400000); return caml_alloc_tm (d.getUTCSeconds(), d.getUTCMinutes(), d.getUTCHours(), d.getUTCDate(), d.getUTCMonth(), d.getUTCFullYear() - 1900, d.getUTCDay(), doy, false);}, localtime: t=>{ var d = new Date(t * 1000), d_num = d.getTime(), januaryfirst = new Date(d.getFullYear(), 0, 1).getTime(), doy = Math.floor((d_num - januaryfirst) / 86400000), jan = new Date(d.getFullYear(), 0, 1), jul = new Date(d.getFullYear(), 6, 1), stdTimezoneOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); return caml_alloc_tm (d.getSeconds(), d.getMinutes(), d.getHours(), d.getDate(), d.getMonth(), d.getFullYear() - 1900, d.getDay(), doy, d.getTimezoneOffset() < stdTimezoneOffset);}, mktime: (year, month, day, h, m, s)=> new Date(year, month, day, h, m, s).getTime(), random_seed: ()=>crypto.getRandomValues(new Int32Array(12)), access: (p, flags)=> fs.accessSync (p, access_flags.reduce((f, v, i)=>flags & 1 << i ? f | v : f, 0)), open: (p, flags, perm)=> fs.openSync (p, open_flags.reduce((f, v, i)=>flags & 1 << i ? f | v : f, 0), perm), close: fd=>fs.closeSync(fd), write: (fd, b, o, l, p)=> fs ? fs.writeSync(fd, b, o, l, p === null ? p : Number(p)) : (console [fd === 2 ? "error" : "log"] (typeof b === "string" ? b : decoder.decode(b.slice(o, o + l))), l), read: (fd, b, o, l, p)=>fs.readSync(fd, b, o, l, p), fsync: fd=>fs.fsyncSync(fd), file_size: fd=>fs.fstatSync(fd, {bigint: true}).size, register_channel: register_channel, unregister_channel: unregister_channel, channel_list: channel_list, exit: n=>isNode && globalThis.process.exit(n), argv: ()=>isNode ? globalThis.process.argv.slice(1) : ["a.out"], on_windows: + on_windows, getenv: getenv, backtrace_status: ()=>record_backtrace_flag, record_backtrace: b=>record_backtrace_flag = b, system: c=>{ var res = require("node:child_process").spawnSync (c, {shell: true, stdio: "inherit"}); if(res.error) throw res.error; return res.signal ? 255 : res.status;}, isatty: fd=>+ require("node:tty").isatty(fd), time: ()=>performance.now(), getcwd: ()=>isNode ? globalThis.process.cwd() : "/static", chdir: x=>globalThis.process.chdir(x), mkdir: (p, m)=>fs.mkdirSync(p, m), rmdir: p=>fs.rmdirSync(p), link: (d, s)=>fs.linkSync(d, s), symlink: (t, p, kind)=>fs.symlinkSync(t, p, [null, "file", "dir"][kind]), readlink: p=>fs.readlinkSync(p), unlink: p=>fs.unlinkSync(p), read_dir: p=>fs.readdirSync(p), opendir: p=>fs.opendirSync(p), readdir: d=>{var n = d.readSync()?.name; return n === undefined ? null : n;}, closedir: d=>d.closeSync(), stat: (p, l)=>alloc_stat(fs.statSync(p), l), lstat: (p, l)=>alloc_stat(fs.lstatSync(p), l), fstat: (fd, l)=>alloc_stat(fs.fstatSync(fd), l), chmod: (p, perms)=>fs.chmodSync(p, perms), fchmod: (p, perms)=>fs.fchmodSync(p, perms), file_exists: p=>+ fs.existsSync(p), is_directory: p=>+ fs.lstatSync(p).isDirectory(), is_file: p=>+ fs.lstatSync(p).isFile(), utimes: (p, a, m)=>fs.utimesSync(p, a, m), truncate: (p, l)=>fs.truncateSync(p, l), ftruncate: (fd, l)=>fs.ftruncateSync(fd, l), rename: (o, n)=>{ var n_stat; if (on_windows && (n_stat = fs.statSync(n, {throwIfNoEntry: false})) && fs.statSync(o, {throwIfNoEntry: false})?.isDirectory()) if(n_stat.isDirectory()){ if(! n.startsWith(o)) try{fs.rmdirSync(n);}catch{} } else{ var e = new Error(`ENOTDIR: not a directory, rename '${o}' -> '${n}'`); throw Object.assign (e, {errno: - 20, code: "ENOTDIR", syscall: "rename", path: n}); } fs.renameSync(o, n);}, tmpdir: ()=>require("node:os").tmpdir(), start_fiber: x=>start_fiber(x), suspend_fiber: make_suspending((f, env)=>new Promise(k=>f(k, env))), resume_fiber: (k, v)=>k(v), weak_new: v=>new WeakRef(v), weak_deref: w=>{var v = w.deref(); return v === undefined ? null : v;}, weak_map_new: ()=>new WeakMap(), map_new: ()=>new Map(), map_get: (m, x)=>{var v = m.get(x); return v === undefined ? null : v;}, map_set: (m, x, v)=>m.set(x, v), map_delete: (m, x)=>m.delete(x), hash_string: hash_string, log: x=>console.log(x)}, string_ops = {test: v=>+ (typeof v === "string"), compare: (s1, s2)=>s1 < s2 ? - 1 : + (s1 > s2), decodeStringFromUTF8Array: ()=>"", encodeStringToUTF8Array: ()=>0, fromCharCodeArray: ()=>""}, imports = Object.assign ({Math: math, bindings: bindings, js: js, "wasm:js-string": string_ops, "wasm:text-decoder": string_ops, "wasm:text-encoder": string_ops, str: new globalThis.Proxy({}, {get(_, prop){return prop;}}), env: {}}, generated), options = {builtins: ["js-string", "text-decoder", "text-encoder"], importedStringConstants: "str"}; function loadRelative(src){ const path = require("node:path"), f = path.join(path.dirname(require.main.filename), src); return require("node:fs/promises").readFile(f); } const fetchBase = globalThis?.document?.currentScript?.src; function fetchRelative(src){ const url = fetchBase ? new URL(src, fetchBase) : src; return fetch(url); } const loadCode = isNode ? loadRelative : fetchRelative; async function instantiateModule(code){ return isNode ? WebAssembly.instantiate(await code, imports, options) : WebAssembly.instantiateStreaming(code, imports, options); } async function instantiateFromDir(){ imports.OCaml = {}; const deps = []; async function loadModule(module, isRuntime){ const sync = module[1].constructor !== Array; async function instantiate(){ const code = loadCode(src + "/" + module[0] + ".wasm"); await Promise.all(sync ? deps : module[1].map(i=>deps[i])); const wasmModule = await instantiateModule(code); Object.assign (isRuntime ? imports.env : imports.OCaml, wasmModule.instance.exports); } const promise = instantiate(); deps.push(promise); return promise; } async function loadModules(lst){ for(const module of lst) await loadModule(module); } await loadModule(link[0], 1); if(link.length > 1){ await loadModule(link[1]); const workers = new Array(20).fill(link.slice(2).values()).map(loadModules); await Promise.all(workers); } return {instance: {exports: Object.assign(imports.env, imports.OCaml)}}; } const wasmModule = await instantiateFromDir(); var {caml_callback, caml_alloc_times, caml_alloc_tm, caml_alloc_stat, caml_start_fiber, caml_handle_uncaught_exception, caml_buffer, caml_extract_bytes, bytes_get, bytes_set, _initialize} = wasmModule.instance.exports, buffer = caml_buffer?.buffer, out_buffer = buffer && new Uint8Array(buffer, 0, buffer.length); start_fiber = make_promising(caml_start_fiber); var _initialize = make_promising(_initialize); if(globalThis.process?.on) globalThis.process.on ("uncaughtException", (err, _origin)=>caml_handle_uncaught_exception(err)); else if(globalThis.addEventListener) globalThis.addEventListener ("error", event=>event.error && caml_handle_uncaught_exception(event.error)); await _initialize();}) (function(globalThis){ "use strict"; var blake2b = function(){ function ADD64AA(v, a, b){ const o0 = v[a] + v[b]; let o1 = v[a + 1] + v[b + 1]; if(o0 >= 0x100000000) o1++; v[a] = o0; v[a + 1] = o1; } function ADD64AC(v, a, b0, b1){ let o0 = v[a] + b0; if(b0 < 0) o0 += 0x100000000; let o1 = v[a + 1] + b1; if(o0 >= 0x100000000) o1++; v[a] = o0; v[a + 1] = o1; } function B2B_GET32(arr, i){ return arr[i] ^ arr[i + 1] << 8 ^ arr[i + 2] << 16 ^ arr[i + 3] << 24; } function B2B_G(a, b, c, d, ix, iy){ const x0 = m[ix], x1 = m[ix + 1], y0 = m[iy], y1 = m[iy + 1]; ADD64AA(v, a, b); ADD64AC(v, a, x0, x1); let xor0 = v[d] ^ v[a], xor1 = v[d + 1] ^ v[a + 1]; v[d] = xor1; v[d + 1] = xor0; ADD64AA(v, c, d); xor0 = v[b] ^ v[c]; xor1 = v[b + 1] ^ v[c + 1]; v[b] = xor0 >>> 24 ^ xor1 << 8; v[b + 1] = xor1 >>> 24 ^ xor0 << 8; ADD64AA(v, a, b); ADD64AC(v, a, y0, y1); xor0 = v[d] ^ v[a]; xor1 = v[d + 1] ^ v[a + 1]; v[d] = xor0 >>> 16 ^ xor1 << 16; v[d + 1] = xor1 >>> 16 ^ xor0 << 16; ADD64AA(v, c, d); xor0 = v[b] ^ v[c]; xor1 = v[b + 1] ^ v[c + 1]; v[b] = xor1 >>> 31 ^ xor0 << 1; v[b + 1] = xor0 >>> 31 ^ xor1 << 1; } const BLAKE2B_IV32 = new Uint32Array ([0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, 0xfe94f82b, 0x3c6ef372, 0x5f1d36f1, 0xa54ff53a, 0xade682d1, 0x510e527f, 0x2b3e6c1f, 0x9b05688c, 0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19]), SIGMA8 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], SIGMA82 = new Uint8Array(SIGMA8.map(function(x){return x * 2;})), v = new Uint32Array(32), m = new Uint32Array(32); function blake2bCompress(ctx, last){ let i = 0; for(i = 0; i < 16; i++){ v[i] = ctx.h[i]; v[i + 16] = BLAKE2B_IV32[i]; } v[24] = v[24] ^ ctx.t; v[25] = v[25] ^ ctx.t / 0x100000000; if(last){v[28] = ~ v[28]; v[29] = ~ v[29];} for(i = 0; i < 32; i++) m[i] = B2B_GET32(ctx.b, 4 * i); for(i = 0; i < 12; i++){ B2B_G(0, 8, 16, 24, SIGMA82[i * 16 + 0], SIGMA82[i * 16 + 1]); B2B_G(2, 10, 18, 26, SIGMA82[i * 16 + 2], SIGMA82[i * 16 + 3]); B2B_G(4, 12, 20, 28, SIGMA82[i * 16 + 4], SIGMA82[i * 16 + 5]); B2B_G(6, 14, 22, 30, SIGMA82[i * 16 + 6], SIGMA82[i * 16 + 7]); B2B_G(0, 10, 20, 30, SIGMA82[i * 16 + 8], SIGMA82[i * 16 + 9]); B2B_G(2, 12, 22, 24, SIGMA82[i * 16 + 10], SIGMA82[i * 16 + 11]); B2B_G(4, 14, 16, 26, SIGMA82[i * 16 + 12], SIGMA82[i * 16 + 13]); B2B_G(6, 8, 18, 28, SIGMA82[i * 16 + 14], SIGMA82[i * 16 + 15]); } for(i = 0; i < 16; i++) ctx.h[i] = ctx.h[i] ^ v[i] ^ v[i + 16]; } const parameterBlock = new Uint8Array ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); function blake2bInit(outlen, key){ if(outlen === 0 || outlen > 64) throw new Error("Illegal output length, expected 0 < length <= 64"); if(key.length > 64) throw new Error ("Illegal key, expected Uint8Array with 0 < length <= 64"); const ctx = {b: new Uint8Array(128), h: new Uint32Array(16), t: 0, c: 0, outlen: outlen}; parameterBlock.fill(0); parameterBlock[0] = outlen; parameterBlock[1] = key.length; parameterBlock[2] = 1; parameterBlock[3] = 1; for(let i = 0; i < 16; i++) ctx.h[i] = BLAKE2B_IV32[i] ^ B2B_GET32(parameterBlock, i * 4); if(key.length > 0){blake2bUpdate(ctx, key); ctx.c = 128;} return ctx; } function blake2bUpdate(ctx, input){ for(let i = 0; i < input.length; i++){ if(ctx.c === 128){ ctx.t += ctx.c; blake2bCompress(ctx, false); ctx.c = 0; } ctx.b[ctx.c++] = input[i]; } } function blake2bFinal(ctx){ ctx.t += ctx.c; while(ctx.c < 128) ctx.b[ctx.c++] = 0; blake2bCompress(ctx, true); const out = new Uint8Array(ctx.outlen); for(let i = 0; i < ctx.outlen; i++) out[i] = ctx.h[i >> 2] >> 8 * (i & 3); return out; } return {Init: blake2bInit, Update: blake2bUpdate, Final: blake2bFinal}; } (); function caml_ml_string_length(s){return s.length;} function caml_string_unsafe_get(s, i){return s.charCodeAt(i);} function caml_uint8_array_of_string(s){ var l = caml_ml_string_length(s), a = new Uint8Array(l), i = 0; for(; i < l; i++) a[i] = caml_string_unsafe_get(s, i); return a; } function caml_blake2_create(hashlen, key){ key = caml_uint8_array_of_string(key); if(key.length > 64) key.subarray(0, 64); return blake2b.Init(hashlen, key); } function caml_string_of_jsbytes(x){return x;} function blake2_js_for_wasm_create(hashlen, key){ const key_jsoo_string = caml_string_of_jsbytes(key); return caml_blake2_create(hashlen, key_jsoo_string); } function caml_sub_uint8_array_to_jsbytes(a, i, len){ var f = String.fromCharCode; if(i === 0 && len <= 4096 && len === a.length) return f.apply(null, a); var s = ""; for(; 0 < len; i += 1024, len -= 1024) s += f.apply(null, a.subarray(i, i + Math.min(len, 1024))); return s; } function caml_string_of_uint8_array(a){ return caml_sub_uint8_array_to_jsbytes(a, 0, a.length); } function caml_blake2_final(ctx, _hashlen){ var r = blake2b.Final(ctx); return caml_string_of_uint8_array(r); } function caml_jsbytes_of_string(x){return x;} function blake2_js_for_wasm_final(ctx, hashlen){ return caml_jsbytes_of_string(caml_blake2_final(ctx, hashlen)); } function caml_blake2_update(ctx, buf, ofs, len){ var input = caml_uint8_array_of_string(buf); input = input.subarray(ofs, ofs + len); blake2b.Update(ctx, input); return 0; } function blake2_js_for_wasm_update(ctx, buf, ofs, len){ const buf_jsoo_string = caml_string_of_jsbytes(buf); return caml_blake2_update(ctx, buf_jsoo_string, ofs, len); } function caml_js_html_entities(s){ var entity = /^&#?[0-9a-zA-Z]+;$/; if(s.match(entity)){ var str, temp = document.createElement("p"); temp.innerHTML = s; str = temp.textContent || temp.innerText; temp = null; return str; } else return null; } var caml_js_regexps = {amp: /&/g, lt: /= 0){ const code = unix_error[errno]; return util.getSystemErrorMap().entries().find(x=>x[1][0] === code)[1] [1]; } else return util.getSystemErrorMessage(errno); } var zstd_decompress = function(){ var ab = ArrayBuffer, u8 = Uint8Array, u16 = Uint16Array, i16 = Int16Array, i32 = Int32Array; function slc(v, s, e){ if(u8.prototype.slice) return u8.prototype.slice.call(v, s, e); if(s == null || s < 0) s = 0; if(e == null || e > v.length) e = v.length; var n = new u8(e - s); n.set(v.subarray(s, e)); return n; } function fill(v, n, s, e){ if(u8.prototype.fill) return u8.prototype.fill.call(v, n, s, e); if(s == null || s < 0) s = 0; if(e == null || e > v.length) e = v.length; for(; s < e; ++s) v[s] = n; return v; } function cpw(v, t, s, e){ if(u8.prototype.copyWithin) return u8.prototype.copyWithin.call(v, t, s, e); if(s == null || s < 0) s = 0; if(e == null || e > v.length) e = v.length; while(s < e) v[t++] = v[s++]; } var ec = ["invalid zstd data", "window size too large (>2046MB)", "invalid block type", "FSE accuracy too high", "match distance too far back", "unexpected EOF"]; function err(ind, msg, nt){ var e = new Error(msg || ec[ind]); e.code = ind; if(! nt) throw e; return e; } function rb(d, b, n){ var i = 0, o = 0; for(; i < n; ++i) o |= d[b++] << (i << 3); return o; } function b4(d, b){ return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0; } function rzfh(dat, w){ var n3 = dat[0] | dat[1] << 8 | dat[2] << 16; if(n3 === 0x2fb528 && dat[3] === 253){ var flg = dat[4], ss = flg >> 5 & 1, cc = flg >> 2 & 1, df = flg & 3, fcf = flg >> 6; if(flg & 8) err(0); var bt = 6 - ss, db = df === 3 ? 4 : df, di = rb(dat, bt, db); bt += db; var fsb = fcf ? 1 << fcf : ss, fss = rb(dat, bt, fsb) + (fcf === 1 && 256), ws = fss; if(! ss){ var wb = 1 << 10 + (dat[5] >> 3); ws = wb + (wb >> 3) * (dat[5] & 7); } if(ws > 2145386496) err(1); var buf = new u8((w === 1 ? fss || ws : w ? 0 : ws) + 12); buf[0] = 1, buf[4] = 4, buf[8] = 8; return {b: bt + fsb, y: 0, l: 0, d: di, w: w && w !== 1 ? w : buf.subarray(12), e: ws, o: new i32(buf.buffer, 0, 3), u: fss, c: cc, m: Math.min(131072, ws)}; } else if((n3 >> 4 | dat[3] << 20) === 0x184d2a5) return b4(dat, 4) + 8; err(0); } function msb(val){ var bits = 0; for(; 1 << bits <= val; ++bits) ; return bits - 1; } function rfse(dat, bt, mal){ var tpos = (bt << 3) + 4, al = (dat[bt] & 15) + 5; if(al > mal) err(3); var sz = 1 << al, probs = sz, sym = - 1, re = - 1, i = - 1, ht = sz, buf = new ab(512 + (sz << 2)), freq = new i16(buf, 0, 256), dstate = new u16(buf, 0, 256), nstate = new u16(buf, 512, sz), bb1 = 512 + (sz << 1), syms = new u8(buf, bb1, sz), nbits = new u8(buf, bb1 + sz); while(sym < 255 && probs > 0){ var bits = msb(probs + 1), cbt = tpos >> 3, msk = (1 << bits + 1) - 1, val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (tpos & 7) & msk, msk1fb = (1 << bits) - 1, msv = msk - probs - 1, sval = val & msk1fb; if(sval < msv) tpos += bits, val = sval; else{tpos += bits + 1; if(val > msk1fb) val -= msv;} freq[++sym] = --val; if(val === - 1){probs += val; syms[--ht] = sym;} else probs -= val; if(! val) do{ var rbt = tpos >> 3; re = (dat[rbt] | dat[rbt + 1] << 8) >> (tpos & 7) & 3; tpos += 2; sym += re; } while (re === 3); } if(sym > 255 || probs) err(0); var sympos = 0, sstep = (sz >> 1) + (sz >> 3) + 3, smask = sz - 1; for(var s = 0; s <= sym; ++s){ var sf = freq[s]; if(sf < 1){dstate[s] = - sf; continue;} for(i = 0; i < sf; ++i){ syms[sympos] = s; do sympos = sympos + sstep & smask;while(sympos >= ht); } } if(sympos) err(0); for(i = 0; i < sz; ++i){ var ns = dstate[syms[i]]++, nb = nbits[i] = al - msb(ns); nstate[i] = (ns << nb) - sz; } return [tpos + 7 >> 3, {b: al, s: syms, n: nbits, t: nstate}]; } function rhu(dat, bt){ var i = 0, wc = - 1, buf = new u8(292), hb = dat[bt], hw = buf.subarray(0, 256), rc = buf.subarray(256, 268), ri = new u16(buf.buffer, 268); if(hb < 128){ var _a = rfse(dat, bt + 1, 6), ebt = _a[0], fdt = _a[1]; bt += hb; var epos = ebt << 3, lb = dat[bt]; if(! lb) err(0); var st1 = 0, st2 = 0, btr1 = fdt.b, btr2 = btr1, fpos = (++bt << 3) - 8 + msb(lb); for(;;){ fpos -= btr1; if(fpos < epos) break; var cbt = fpos >> 3; st1 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr1) - 1; hw[++wc] = fdt.s[st1]; fpos -= btr2; if(fpos < epos) break; cbt = fpos >> 3; st2 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr2) - 1; hw[++wc] = fdt.s[st2]; btr1 = fdt.n[st1]; st1 = fdt.t[st1]; btr2 = fdt.n[st2]; st2 = fdt.t[st2]; } if(++wc > 255) err(0); } else{ wc = hb - 127; for(; i < wc; i += 2){ var byte = dat[++bt]; hw[i] = byte >> 4; hw[i + 1] = byte & 15; } ++bt; } var wes = 0; for(i = 0; i < wc; ++i){ var wt = hw[i]; if(wt > 11) err(0); wes += wt && 1 << wt - 1; } var mb = msb(wes) + 1, ts = 1 << mb, rem = ts - wes; if(rem & rem - 1) err(0); hw[wc++] = msb(rem) + 1; for(i = 0; i < wc; ++i){ var wt = hw[i]; ++rc[hw[i] = wt && mb + 1 - wt]; } var hbuf = new u8(ts << 1), syms = hbuf.subarray(0, ts), nb = hbuf.subarray(ts); ri[mb] = 0; for(i = mb; i > 0; --i){ var pv = ri[i]; fill(nb, i, pv, ri[i - 1] = pv + rc[i] * (1 << mb - i)); } if(ri[0] !== ts) err(0); for(i = 0; i < wc; ++i){ var bits = hw[i]; if(bits){ var code = ri[bits]; fill(syms, i, code, ri[bits] = code + (1 << mb - bits)); } } return [bt, {n: nb, b: mb, s: syms}]; } var dllt = rfse (new u8 ([81, 16, 99, 140, 49, 198, 24, 99, 12, 33, 196, 24, 99, 102, 102, 134, 70, 146, 4]), 0, 6) [1], dmlt = rfse (new u8 ([33, 20, 196, 24, 99, 140, 33, 132, 16, 66, 8, 33, 132, 16, 66, 8, 33, 68, 68, 68, 68, 68, 68, 68, 68, 36, 9]), 0, 6) [1], doct = rfse (new u8([32, 132, 16, 66, 102, 70, 68, 68, 68, 68, 36, 73, 2]), 0, 5) [1]; function b2bl(b, s){ var len = b.length, bl = new i32(len); for(var i = 0; i < len; ++i){bl[i] = s; s += 1 << b[i];} return bl; } var llb = new u8 (new i32 ([0, 0, 0, 0, 16843009, 50528770, 134678020, 202050057, 269422093]).buffer, 0, 36), llbl = b2bl(llb, 0), mlb = new u8 (new i32 ([0, 0, 0, 0, 0, 0, 0, 0, 16843009, 50528770, 117769220, 185207048, 252579084, 16]).buffer, 0, 53), mlbl = b2bl(mlb, 3); function dhu(dat, out, hu){ var len = dat.length, ss = out.length, lb = dat[len - 1], msk = (1 << hu.b) - 1, eb = - hu.b; if(! lb) err(0); var st = 0, btr = hu.b, pos = (len << 3) - 8 + msb(lb) - btr, i = - 1; while(pos > eb && i < ss){ var cbt = pos >> 3, val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (pos & 7); st = (st << btr | val) & msk; out[++i] = hu.s[st]; pos -= btr = hu.n[st]; } if(pos !== eb || i + 1 !== ss) err(0); } function dhu4(dat, out, hu){ var bt = 6, ss = out.length, sz1 = ss + 3 >> 2, sz2 = sz1 << 1, sz3 = sz1 + sz2; dhu (dat.subarray(bt, bt += dat[0] | dat[1] << 8), out.subarray(0, sz1), hu); dhu (dat.subarray(bt, bt += dat[2] | dat[3] << 8), out.subarray(sz1, sz2), hu); dhu (dat.subarray(bt, bt += dat[4] | dat[5] << 8), out.subarray(sz2, sz3), hu); dhu(dat.subarray(bt), out.subarray(sz3), hu); } function rzb(dat, st, out){ var _a, bt = st.b, b0 = dat[bt], btype = b0 >> 1 & 3; st.l = b0 & 1; var sz = b0 >> 3 | dat[bt + 1] << 5 | dat[bt + 2] << 13, ebt = (bt += 3) + sz; if(btype === 1){ if(bt >= dat.length) return; st.b = bt + 1; if(out){fill(out, dat[bt], st.y, st.y += sz); return out;} return fill(new u8(sz), dat[bt]); } if(ebt > dat.length) return; if(btype === 0){ st.b = ebt; if(out){ out.set(dat.subarray(bt, ebt), st.y); st.y += sz; return out; } return slc(dat, bt, ebt); } if(btype === 2){ var b3 = dat[bt], lbt = b3 & 3, sf = b3 >> 2 & 3, lss = b3 >> 4, lcs = 0, s4 = 0; if(lbt < 2) if(sf & 1) lss |= dat[++bt] << 4 | (sf & 2 && dat[++bt] << 12); else lss = b3 >> 3; else{ s4 = sf; if(sf < 2) lss |= (dat[++bt] & 63) << 4, lcs = dat[bt] >> 6 | dat[++bt] << 2; else if(sf === 2) lss |= dat[++bt] << 4 | (dat[++bt] & 3) << 12, lcs = dat[bt] >> 2 | dat[++bt] << 6; else lss |= dat[++bt] << 4 | (dat[++bt] & 63) << 12, lcs = dat[bt] >> 6 | dat[++bt] << 2 | dat[++bt] << 10; } ++bt; var buf = out ? out.subarray(st.y, st.y + st.m) : new u8(st.m), spl = buf.length - lss; if(lbt === 0) buf.set(dat.subarray(bt, bt += lss), spl); else if(lbt === 1) fill(buf, dat[bt++], spl); else{ var hu = st.h; if(lbt === 2){ var hud = rhu(dat, bt); lcs += bt - (bt = hud[0]); st.h = hu = hud[1]; } else if(! hu) err(0); (s4 ? dhu4 : dhu) (dat.subarray(bt, bt += lcs), buf.subarray(spl), hu); } var ns = dat[bt++]; if(ns){ if(ns === 255) ns = (dat[bt++] | dat[bt++] << 8) + 0x7f00; else if(ns > 127) ns = ns - 128 << 8 | dat[bt++]; var scm = dat[bt++]; if(scm & 3) err(0); var dts = [dmlt, doct, dllt]; for(var i = 2; i > - 1; --i){ var md = scm >> (i << 1) + 2 & 3; if(md === 1){ var rbuf = new u8([0, 0, dat[bt++]]); dts[i] = {s: rbuf.subarray(2, 3), n: rbuf.subarray(0, 1), t: new u16(rbuf.buffer, 0, 1), b: 0}; } else if(md === 2) _a = rfse(dat, bt, 9 - (i & 1)), bt = _a[0], dts[i] = _a[1]; else if(md === 3){if(! st.t) err(0); dts[i] = st.t[i];} } var _b = st.t = dts, mlt = _b[0], oct = _b[1], llt = _b[2], lb = dat[ebt - 1]; if(! lb) err(0); var spos = (ebt << 3) - 8 + msb(lb) - llt.b, cbt = spos >> 3, oubt = 0, lst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << llt.b) - 1; cbt = (spos -= oct.b) >> 3; var ost = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << oct.b) - 1; cbt = (spos -= mlt.b) >> 3; var mst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mlt.b) - 1; for(++ns; --ns;){ var llc = llt.s[lst], lbtr = llt.n[lst], mlc = mlt.s[mst], mbtr = mlt.n[mst], ofc = oct.s[ost], obtr = oct.n[ost]; cbt = (spos -= ofc) >> 3; var ofp = 1 << ofc, off = ofp + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16 | dat[cbt + 3] << 24) >>> (spos & 7) & ofp - 1); cbt = (spos -= mlb[mlc]) >> 3; var ml = mlbl[mlc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << mlb[mlc]) - 1); cbt = (spos -= llb[llc]) >> 3; var ll = llbl[llc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << llb[llc]) - 1); cbt = (spos -= lbtr) >> 3; lst = llt.t[lst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << lbtr) - 1); cbt = (spos -= mbtr) >> 3; mst = mlt.t[mst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mbtr) - 1); cbt = (spos -= obtr) >> 3; ost = oct.t[ost] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << obtr) - 1); if(off > 3){ st.o[2] = st.o[1]; st.o[1] = st.o[0]; st.o[0] = off -= 3; } else{ var idx = off - (ll !== 0); if(idx){ off = idx === 3 ? st.o[0] - 1 : st.o[idx]; if(idx > 1) st.o[2] = st.o[1]; st.o[1] = st.o[0]; st.o[0] = off; } else off = st.o[0]; } for(var i = 0; i < ll; ++i) buf[oubt + i] = buf[spl + i]; oubt += ll, spl += ll; var stin = oubt - off; if(stin < 0){ var len = - stin, bs = st.e + stin; if(len > ml) len = ml; for(var i = 0; i < len; ++i) buf[oubt + i] = st.w[bs + i]; oubt += len, ml -= len, stin = 0; } for(var i = 0; i < ml; ++i) buf[oubt + i] = buf[stin + i]; oubt += ml; } if(oubt !== spl) while(spl < buf.length) buf[oubt++] = buf[spl++]; else oubt = buf.length; if(out) st.y += oubt; else buf = slc(buf, 0, oubt); } else if(out){ st.y += lss; if(spl) for(var i = 0; i < lss; ++i) buf[i] = buf[spl + i]; } else if(spl) buf = slc(buf, spl); st.b = ebt; return buf; } err(2); } function cct(bufs, ol){ if(bufs.length === 1) return bufs[0]; var buf = new u8(ol); for(var i = 0, b = 0; i < bufs.length; ++i){ var chk = bufs[i]; buf.set(chk, b); b += chk.length; } return buf; } return function(dat, buf){ var bt = 0, bufs = [], nb = + ! buf, ol = 0; while(dat.length){ var st = rzfh(dat, nb || buf); if(typeof st === "object"){ if(nb){ buf = null; if(st.w.length === st.u){bufs.push(buf = st.w); ol += st.u;} } else{bufs.push(buf); st.e = 0;} while(! st.l){ var blk = rzb(dat, st, buf); if(! blk) err(5); if(buf) st.e = st.y; else{ bufs.push(blk); ol += blk.length; cpw(st.w, 0, blk.length); st.w.set(blk, st.w.length - blk.length); } } bt = st.b + st.c * 4; } else bt = st; dat = dat.subarray(bt); } return cct(bufs, ol);}; } (); return {zstd_decompress: zstd_decompress, unix_error: unix_error, caml_strerror: caml_strerror, caml_js_html_escape: caml_js_html_escape, caml_js_html_entities: caml_js_html_entities, blake2_js_for_wasm_update: blake2_js_for_wasm_update, blake2_js_for_wasm_final: blake2_js_for_wasm_final, blake2_js_for_wasm_create: blake2_js_for_wasm_create}; } (globalThis)) ({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-a462ed04",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-5f19f371",[2,3,5]],["std_exit-10fb8830",[2]],["start-9afa06f6",0]],"generated":(b=>{var c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new Error("caml_int64_create_lo_mi_hi not implemented")},"caml_jsoo_flags_effects":()=>{throw new Error("caml_jsoo_flags_effects not implemented")},"caml_list_mount_point":()=>{throw new Error("caml_list_mount_point not implemented")},"caml_ml_set_channel_output":()=>{throw new Error("caml_ml_set_channel_output not implemented")},"caml_ml_set_channel_refill":()=>{throw new Error("caml_ml_set_channel_refill not implemented")},"caml_unmount":()=>{throw new Error("caml_unmount not implemented")}},"Js_of_ocaml__Js.fragments":{"fun_call_1":(a,b)=>a(b),"get_Array":a=>a.Array,"get_Date":a=>a.Date,"get_Error":a=>a.Error,"get_JSON":a=>a.JSON,"get_Math":a=>a.Math,"get_Object":a=>a.Object,"get_RegExp":a=>a.RegExp,"get_String":a=>a.String,"get_decodeURI":a=>a.decodeURI,"get_decodeURIComponent":a=>a.decodeURIComponent,"get_encodeURI":a=>a.encodeURI,"get_encodeURIComponent":a=>a.encodeURIComponent,"get_escape":a=>a.escape,"get_isNaN":a=>a.isNaN,"get_length":a=>a.length,"get_message":a=>a.message,"get_name":a=>a.name,"get_parseFloat":a=>a.parseFloat,"get_parseInt":a=>a.parseInt,"get_stack":a=>a.stack,"get_unescape":a=>a.unescape,"js_expr_12c48ca8":()=>b,"js_expr_21711c2a":()=>a,"js_expr_26f07992":()=>null,"js_expr_28647a4c":()=>false,"js_expr_34edcf72":()=>true,"js_expr_ba692c1":()=>undefined,"meth_call_0_toString":a=>a.toString(),"meth_call_1_forEach":(a,b)=>a.forEach(b),"meth_call_1_keys":(a,b)=>a.keys(b),"meth_call_1_map":(a,b)=>a.map(b)},"Js_of_ocaml__Dom.fragments":{"call_1":(a,b,c)=>a.call(b,c),"get_CustomEvent":a=>a.CustomEvent,"get_addEventListener":a=>a.addEventListener,"get_length":a=>a.length,"get_nodeType":a=>a.nodeType,"get_srcElement":a=>a.srcElement,"get_target":a=>a.target,"meth_call_0_preventDefault":a=>a.preventDefault(),"meth_call_1_appendChild":(a,b)=>a.appendChild(b),"meth_call_1_concat":(a,b)=>a.concat(b),"meth_call_1_item":(a,b)=>a.item(b),"meth_call_1_removeChild":(a,b)=>a.removeChild(b),"meth_call_2_attachEvent":(a,b,c)=>a.attachEvent(b,c),"meth_call_2_detachEvent":(a,b,c)=>a.detachEvent(b,c),"meth_call_2_insertBefore":(a,b,c)=>a.insertBefore(b,c),"meth_call_2_replaceChild":(a,b,c)=>a.replaceChild(b,c),"meth_call_3_addEventListener":(a,b,c,d)=>a.addEventListener(b,c,d),"meth_call_3_removeEventListener":(a,b,c,d)=>a.removeEventListener(b,c,d),"new_2":(a,b,c)=>new a(b,c),"obj_0":()=>({}),"obj_1":()=>({}),"set_bubbles":(a,b)=>a.bubbles=b,"set_cancelable":(a,b)=>a.cancelable=b,"set_capture":(a,b)=>a.capture=b,"set_detail":(a,b)=>a.detail=b,"set_once":(a,b)=>a.once=b,"set_passive":(a,b)=>a.passive=b},"Js_of_ocaml__Typed_array.fragments":{"get_ArrayBuffer":a=>a.ArrayBuffer,"get_DataView":a=>a.DataView,"get_Float32Array":a=>a.Float32Array,"get_Float64Array":a=>a.Float64Array,"get_Int16Array":a=>a.Int16Array,"get_Int32Array":a=>a.Int32Array,"get_Int8Array":a=>a.Int8Array,"get_Uint16Array":a=>a.Uint16Array,"get_Uint32Array":a=>a.Uint32Array,"get_Uint8Array":a=>a.Uint8Array,"new_1":(a,b)=>new a(b)},"Js_of_ocaml__File.fragments":{"get_Blob":a=>a.Blob,"get_Document":a=>a.Document,"get_FileReader":a=>a.FileReader,"get_fileName":a=>a.fileName,"get_name":a=>a.name,"new_2":(a,b,c)=>new a(b,c)},"Js_of_ocaml__Dom_html.fragments":{"fun_call_1":(a,b)=>a(b),"get_HTMLElement":a=>a.HTMLElement,"get_KeyboardEvent":a=>a.KeyboardEvent,"get_MessageEvent":a=>a.MessageEvent,"get_MouseEvent":a=>a.MouseEvent,"get_MouseScrollEvent":a=>a.MouseScrollEvent,"get_PopStateEvent":a=>a.PopStateEvent,"get_WheelEvent":a=>a.WheelEvent,"get_body":a=>a.body,"get_button":a=>a.button,"get_charCode":a=>a.charCode,"get_clientLeft":a=>a.clientLeft,"get_clientTop":a=>a.clientTop,"get_clientX":a=>a.clientX,"get_clientY":a=>a.clientY,"get_code":a=>a.code,"get_document":a=>a.document,"get_documentElement":a=>a.documentElement,"get_getContext":a=>a.getContext,"get_history":a=>a.history,"get_key":a=>a.key,"get_keyCode":a=>a.keyCode,"get_left":a=>a.left,"get_length":a=>a.length,"get_location":a=>a.location,"get_mozRequestAnimationFrame":a=>a.mozRequestAnimationFrame,"get_msRequestAnimationFrame":a=>a.msRequestAnimationFrame,"get_name":a=>a.name,"get_oRequestAnimationFrame":a=>a.oRequestAnimationFrame,"get_origin":a=>a.origin,"get_pageX":a=>a.pageX,"get_pageY":a=>a.pageY,"get_placeholder":a=>a.placeholder,"get_pushState":a=>a.pushState,"get_readyState":a=>a.readyState,"get_relatedTarget":a=>a.relatedTarget,"get_requestAnimationFrame":a=>a.requestAnimationFrame,"get_required":a=>a.required,"get_scrollLeft":a=>a.scrollLeft,"get_scrollTop":a=>a.scrollTop,"get_stopPropagation":a=>a.stopPropagation,"get_tagName":a=>a.tagName,"get_top":a=>a.top,"get_webkitRequestAnimationFrame":a=>a.webkitRequestAnimationFrame,"get_wheelDelta":a=>a.wheelDelta,"get_wheelDeltaX":a=>a.wheelDeltaX,"get_wheelDeltaY":a=>a.wheelDeltaY,"get_which":a=>a.which,"js_expr_4c8b1c6":()=>[].slice,"meth_call_0_getBoundingClientRect":a=>a.getBoundingClientRect(),"meth_call_0_getTime":a=>a.getTime(),"meth_call_0_stopPropagation":a=>a.stopPropagation(),"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_call":(a,b)=>a.call(b),"meth_call_1_charCodeAt":(a,b)=>a.charCodeAt(b),"meth_call_1_clearTimeout":(a,b)=>a.clearTimeout(b),"meth_call_1_createElement":(a,b)=>a.createElement(b),"meth_call_1_getElementById":(a,b)=>a.getElementById(b),"meth_call_1_join":(a,b)=>a.join(b),"meth_call_1_push":(a,b)=>a.push(b),"meth_call_2_push":(a,b,c)=>a.push(b,c),"meth_call_2_setTimeout":(a,b,c)=>a.setTimeout(b,c),"meth_call_3_push":(a,b,c,d)=>a.push(b,c,d),"new_0":a=>new a(),"set_cancelBubble":(a,b)=>a.cancelBubble=b,"set_name":(a,b)=>a.name=b,"set_type":(a,b)=>a.type=b},"Js_of_ocaml__Form.fragments":{"get_FormData":a=>a.FormData,"get_checked":a=>a.checked,"get_disabled":a=>a.disabled,"get_elements":a=>a.elements,"get_files":a=>a.files,"get_length":a=>a.length,"get_multiple":a=>a.multiple,"get_name":a=>a.name,"get_options":a=>a.options,"get_selected":a=>a.selected,"get_type":a=>a.type,"get_value":a=>a.value,"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_item":(a,b)=>a.item(b),"meth_call_2_append":(a,b,c)=>a.append(b,c),"new_0":a=>new a()},"Js_of_ocaml__Worker.fragments":{"get_Worker":a=>a.Worker,"get_data":a=>a.data,"get_importScripts":a=>a.importScripts,"get_onmessage":a=>a.onmessage,"get_postMessage":a=>a.postMessage,"meth_call_1_postMessage":(a,b)=>a.postMessage(b),"new_1":(a,b)=>new a(b),"set_onmessage":(a,b)=>a.onmessage=b},"Js_of_ocaml__WebSockets.fragments":{"get_WebSocket":a=>a.WebSocket},"Js_of_ocaml__WebGL.fragments":{"meth_call_1_getContext":(a,b)=>a.getContext(b),"meth_call_2_getContext":(a,b,c)=>a.getContext(b,c),"obj_2":(a,b,c,d,e,f,g,h)=>({alpha:a,depth:b,stencil:c,antialias:d,premultipliedAlpha:e,preserveDrawingBuffer:f,preferLowPowerToHighPerformance:g,failIfMajorPerformanceCaveat:h})},"Js_of_ocaml__Regexp.fragments":{"get_ignoreCase":a=>a.ignoreCase,"get_index":a=>a.index,"get_length":a=>a.length,"get_multiline":a=>a.multiline,"get_source":a=>a.source,"meth_call_1_exec":(a,b)=>a.exec(b),"meth_call_1_split":(a,b)=>a.split(b),"meth_call_2_replace":(a,b,c)=>a.replace(b,c),"meth_call_2_split":(a,b,c)=>a.split(b,c),"new_2":(a,b,c)=>new a(b,c),"set_lastIndex":(a,b)=>a.lastIndex=b},"Js_of_ocaml__Url.fragments":{"get_hash":a=>a.hash,"get_hostname":a=>a.hostname,"get_href":a=>a.href,"get_length":a=>a.length,"get_location":a=>a.location,"get_pathname":a=>a.pathname,"get_port":a=>a.port,"get_protocol":a=>a.protocol,"get_search":a=>a.search,"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_charAt":(a,b)=>a.charAt(b),"meth_call_1_exec":(a,b)=>a.exec(b),"meth_call_1_indexOf":(a,b)=>a.indexOf(b),"meth_call_1_slice":(a,b)=>a.slice(b),"meth_call_1_split":(a,b)=>a.split(b),"meth_call_2_replace":(a,b,c)=>a.replace(b,c),"meth_call_2_slice":(a,b,c)=>a.slice(b,c),"new_1":(a,b)=>new a(b),"new_2":(a,b,c)=>new a(b,c),"obj_3":(a,b,c,d,e,f,g,h,i,j,k,l)=>({href:a,protocol:b,host:c,hostname:d,port:e,pathname:f,search:g,hash:h,origin:i,reload:j,replace:k,assign:l}),"set_hash":(a,b)=>a.hash=b,"set_href":(a,b)=>a.href=b,"set_lastIndex":(a,b)=>a.lastIndex=b},"Js_of_ocaml__ResizeObserver.fragments":{"get_ResizeObserver":a=>a.ResizeObserver,"meth_call_1_observe":(a,b)=>a.observe(b),"meth_call_2_observe":(a,b,c)=>a.observe(b,c),"new_1":(a,b)=>new a(b),"obj_4":()=>({}),"obj_5":()=>({}),"set_box":(a,b)=>a.box=b},"Js_of_ocaml__PerformanceObserver.fragments":{"get_PerformanceObserver":a=>a.PerformanceObserver,"meth_call_1_observe":(a,b)=>a.observe(b),"new_1":(a,b)=>new a(b),"obj_6":()=>({}),"set_entryTypes":(a,b)=>a.entryTypes=b},"Js_of_ocaml__MutationObserver.fragments":{"get_MutationObserver":a=>a.MutationObserver,"meth_call_2_observe":(a,b,c)=>a.observe(b,c),"new_1":(a,b)=>new a(b),"obj_7":()=>({}),"obj_8":()=>({}),"set_attributeFilter":(a,b)=>a.attributeFilter=b,"set_attributeOldValue":(a,b)=>a.attributeOldValue=b,"set_attributes":(a,b)=>a.attributes=b,"set_characterData":(a,b)=>a.characterData=b,"set_characterDataOldValue":(a,b)=>a.characterDataOldValue=b,"set_childList":(a,b)=>a.childList=b,"set_subtree":(a,b)=>a.subtree=b},"Js_of_ocaml__Jstable.fragments":{"get_Object":a=>a.Object,"get_length":a=>a.length,"meth_call_1_concat":(a,b)=>a.concat(b),"meth_call_1_keys":(a,b)=>a.keys(b),"meth_call_2_substring":(a,b,c)=>a.substring(b,c),"new_0":a=>new a()},"Js_of_ocaml__Json.fragments":{"get_JSON":a=>a.JSON,"get_constructor":a=>a.constructor,"get_hi":a=>a.hi,"get_length":a=>a.length,"get_lo":a=>a.lo,"get_mi":a=>a.mi,"meth_call_1_stringify":(a,b)=>a.stringify(b),"meth_call_2_parse":(a,b,c)=>a.parse(b,c),"meth_call_2_stringify":(a,b,c)=>a.stringify(b,c)},"Js_of_ocaml__CSS.fragments":{"meth_call_1_test":(a,b)=>a.test(b),"new_1":(a,b)=>new a(b)},"Js_of_ocaml__Dom_svg.fragments":{"get_SVGElement":a=>a.SVGElement,"get_document":a=>a.document,"get_tagName":a=>a.tagName,"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_getElementById":(a,b)=>a.getElementById(b),"meth_call_2_createElementNS":(a,b,c)=>a.createElementNS(b,c)},"Js_of_ocaml__EventSource.fragments":{"get_EventSource":a=>a.EventSource,"obj_9":()=>({}),"set_withCredentials":(a,b)=>a.withCredentials=b},"Js_of_ocaml__Geolocation.fragments":{"get_geolocation":a=>a.geolocation,"get_navigator":a=>a.navigator,"obj_10":()=>({})},"Js_of_ocaml__IntersectionObserver.fragments":{"get_IntersectionObserver":a=>a.IntersectionObserver,"obj_11":()=>({})},"Js_of_ocaml__Intl.fragments":{"get_Collator":a=>a.Collator,"get_DateTimeFormat":a=>a.DateTimeFormat,"get_Intl":a=>a.Intl,"get_NumberFormat":a=>a.NumberFormat,"get_PluralRules":a=>a.PluralRules,"obj_12":a=>({localeMatcher:a}),"obj_13":(a,b,c,d,e,f)=>({localeMatcher:a,usage:b,sensitivity:c,ignorePunctuation:d,numeric:e,caseFirst:f}),"obj_14":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t)=>({dateStyle:a,timeStyle:b,calendar:c,dayPeriod:d,numberingSystem:e,localeMatcher:f,timeZone:g,hour12:h,hourCycle:i,formatMatcher:j,weekday:k,era:l,year:m,month:n,day:o,hour:p,minute:q,second:r,fractionalSecondDigits:s,timeZoneName:t}),"obj_15":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)=>({compactDisplay:a,currency:b,currencyDisplay:c,currencySign:d,localeMatcher:e,notation:f,numberingSystem:g,signDisplay:h,style:i,unit:j,unitDisplay:k,useGrouping:l,roundingMode:m,roundingPriority:n,roundingIncrement:o,trailingZeroDisplay:p,minimumIntegerDigits:q,minimumFractionDigits:r,maximumFractionDigits:s,minimumSignificantDigits:t,maximumSignificantDigits:u}),"obj_16":(a,b)=>({localeMatcher:a,type:b})},"Dune__exe__Sx_browser.fragments":{"fun_call_1":(a,b)=>a(b),"fun_call_3":(a,b,c,d)=>a(b,c,d),"get_Array":a=>a.Array,"get_Object":a=>a.Object,"get___sxTagFn":a=>a.__sxTagFn,"get___sx_handle":a=>a.__sx_handle,"get__type":a=>a._type,"get_console":a=>a.console,"get_items":a=>a.items,"get_length":a=>a.length,"get_name":a=>a.name,"js_expr_288a41e0":()=>function(a){return function(){b.__sxR=undefined;var c=a.apply(null,arguments);return b.__sxR!==undefined?b.__sxR:c}},"js_expr_36506fc1":()=>function(a,b,c){a.__sx_handle=b;a._type=c;return a},"meth_call_1_error":(a,b)=>a.error(b),"meth_call_1_isArray":(a,b)=>a.isArray(b),"meth_call_1_keys":(a,b)=>a.keys(b),"obj_0":()=>({}),"obj_1":(a,b)=>({_type:a,items:b}),"obj_2":(a,b)=>({_type:a,__sx_handle:b}),"obj_3":(a,b,c)=>({_type:a,name:b,__sx_handle:c}),"obj_4":(a,b,c)=>({_type:a,name:b,__sx_handle:c}),"obj_5":()=>({}),"obj_6":(a,b)=>({_type:a,items:b}),"obj_7":(a,b)=>({_type:a,name:b}),"obj_8":(a,b)=>({_type:a,name:b}),"obj_9":(a,b)=>({_type:a,__sx_handle:b}),"set_SxKernel":(a,b)=>a.SxKernel=b,"set___sxR":(a,b)=>a.__sxR=b,"set___sxTagFn":(a,b)=>a.__sxTagFn=b,"set__type":(a,b)=>a._type=b,"set_callFn":(a,b)=>a.callFn=b,"set_cekRun":(a,b)=>a.cekRun=b,"set_engine":(a,b)=>a.engine=b,"set_eval":(a,b)=>a.eval=b,"set_evalExpr":(a,b)=>a.evalExpr=b,"set_fnArity":(a,b)=>a.fnArity=b,"set_inspect":(a,b)=>a.inspect=b,"set_isCallable":(a,b)=>a.isCallable=b,"set_load":(a,b)=>a.load=b,"set_loadSource":(a,b)=>a.loadSource=b,"set_parse":(a,b)=>a.parse=b,"set_registerNative":(a,b)=>a.registerNative=b,"set_renderToHtml":(a,b)=>a.renderToHtml=b,"set_stringify":(a,b)=>a.stringify=b,"set_typeOf":(a,b)=>a.typeOf=b}}})(globalThis),"src":"sx-wasm-assets"}); /** * sx-platform.js — Thin JS platform layer for the OCaml SX WASM engine. * * This file provides browser-native primitives (DOM, fetch, timers, etc.) * to the WASM-compiled OCaml CEK machine. It: * 1. Loads the WASM module (SxKernel) * 2. Registers ~80 native browser functions via registerNative * 3. Loads web adapters (.sx files) into the engine * 4. Exports the public Sx API * * Both wasm_of_ocaml and js_of_ocaml targets bind to this same layer. */ (function(global) { "use strict"; function initPlatform() { var K = global.SxKernel; if (!K) { // WASM loader is async — wait and retry setTimeout(initPlatform, 20); return; } var _hasDom = typeof document !== "undefined"; var NIL = null; var SVG_NS = "http://www.w3.org/2000/svg"; // ========================================================================= // Helper: wrap SX lambda for use as JS callback // ========================================================================= function wrapLambda(fn) { // For now, SX lambdas from registerNative are opaque — we can't call them // directly from JS. They need to go through the engine. // TODO: add callLambda API to SxKernel return fn; } // ========================================================================= // 1. DOM Creation & Manipulation // ========================================================================= K.registerNative("dom-create-element", function(args) { if (!_hasDom) return NIL; var tag = args[0], ns = args[1]; if (ns && ns !== NIL) return document.createElementNS(ns, tag); return document.createElement(tag); }); K.registerNative("create-text-node", function(args) { return _hasDom ? document.createTextNode(args[0] || "") : NIL; }); K.registerNative("create-comment", function(args) { return _hasDom ? document.createComment(args[0] || "") : NIL; }); K.registerNative("create-fragment", function(_args) { return _hasDom ? document.createDocumentFragment() : NIL; }); K.registerNative("dom-clone", function(args) { var node = args[0]; return node && node.cloneNode ? node.cloneNode(true) : node; }); K.registerNative("dom-parse-html", function(args) { if (!_hasDom) return NIL; var tpl = document.createElement("template"); tpl.innerHTML = args[0] || ""; return tpl.content; }); K.registerNative("dom-parse-html-document", function(args) { if (!_hasDom) return NIL; var parser = new DOMParser(); return parser.parseFromString(args[0] || "", "text/html"); }); // ========================================================================= // 2. DOM Queries // ========================================================================= K.registerNative("dom-query", function(args) { return _hasDom ? document.querySelector(args[0]) || NIL : NIL; }); K.registerNative("dom-query-all", function(args) { var root = args[0] || (_hasDom ? document : null); if (!root || !root.querySelectorAll) return []; return Array.prototype.slice.call(root.querySelectorAll(args[1] || args[0])); }); K.registerNative("dom-query-by-id", function(args) { return _hasDom ? document.getElementById(args[0]) || NIL : NIL; }); K.registerNative("dom-body", function(_args) { return _hasDom ? document.body : NIL; }); K.registerNative("dom-ensure-element", function(args) { if (!_hasDom) return NIL; var sel = args[0]; var el = document.querySelector(sel); if (el) return el; if (sel.charAt(0) === "#") { el = document.createElement("div"); el.id = sel.slice(1); document.body.appendChild(el); return el; } return NIL; }); // ========================================================================= // 3. DOM Attributes // ========================================================================= K.registerNative("dom-get-attr", function(args) { var el = args[0], name = args[1]; if (!el || !el.getAttribute) return NIL; var v = el.getAttribute(name); return v === null ? NIL : v; }); K.registerNative("dom-set-attr", function(args) { var el = args[0], name = args[1], val = args[2]; if (el && el.setAttribute) el.setAttribute(name, val); return NIL; }); K.registerNative("dom-remove-attr", function(args) { if (args[0] && args[0].removeAttribute) args[0].removeAttribute(args[1]); return NIL; }); K.registerNative("dom-has-attr?", function(args) { return !!(args[0] && args[0].hasAttribute && args[0].hasAttribute(args[1])); }); K.registerNative("dom-attr-list", function(args) { var el = args[0]; if (!el || !el.attributes) return []; var r = []; for (var i = 0; i < el.attributes.length; i++) { r.push([el.attributes[i].name, el.attributes[i].value]); } return r; }); // ========================================================================= // 4. DOM Content // ========================================================================= K.registerNative("dom-text-content", function(args) { var el = args[0]; return el ? el.textContent || el.nodeValue || "" : ""; }); K.registerNative("dom-set-text-content", function(args) { var el = args[0], s = args[1]; if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } return NIL; }); K.registerNative("dom-inner-html", function(args) { return args[0] && args[0].innerHTML != null ? args[0].innerHTML : ""; }); K.registerNative("dom-set-inner-html", function(args) { if (args[0]) args[0].innerHTML = args[1] || ""; return NIL; }); K.registerNative("dom-insert-adjacent-html", function(args) { var el = args[0], pos = args[1], html = args[2]; if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html); return NIL; }); K.registerNative("dom-body-inner-html", function(args) { var doc = args[0]; return doc && doc.body ? doc.body.innerHTML : ""; }); // ========================================================================= // 5. DOM Structure & Navigation // ========================================================================= K.registerNative("dom-parent", function(args) { return args[0] ? args[0].parentNode || NIL : NIL; }); K.registerNative("dom-first-child", function(args) { return args[0] ? args[0].firstChild || NIL : NIL; }); K.registerNative("dom-next-sibling", function(args) { return args[0] ? args[0].nextSibling || NIL : NIL; }); K.registerNative("dom-id", function(args) { return args[0] && args[0].id ? args[0].id : NIL; }); K.registerNative("dom-node-type", function(args) { return args[0] ? args[0].nodeType : 0; }); K.registerNative("dom-node-name", function(args) { return args[0] ? args[0].nodeName : ""; }); K.registerNative("dom-tag-name", function(args) { return args[0] && args[0].tagName ? args[0].tagName : ""; }); K.registerNative("dom-child-list", function(args) { var el = args[0]; if (!el || !el.childNodes) return []; return Array.prototype.slice.call(el.childNodes); }); K.registerNative("dom-child-nodes", function(args) { var el = args[0]; if (!el || !el.childNodes) return []; return Array.prototype.slice.call(el.childNodes); }); // ========================================================================= // 6. DOM Insertion & Removal // ========================================================================= K.registerNative("dom-append", function(args) { var parent = args[0], child = args[1]; if (parent && child) parent.appendChild(child); return NIL; }); K.registerNative("dom-prepend", function(args) { var parent = args[0], child = args[1]; if (parent && child) parent.insertBefore(child, parent.firstChild); return NIL; }); K.registerNative("dom-insert-before", function(args) { var parent = args[0], node = args[1], ref = args[2]; if (parent && node) parent.insertBefore(node, ref || null); return NIL; }); K.registerNative("dom-insert-after", function(args) { var ref = args[0], node = args[1]; if (ref && ref.parentNode && node) { ref.parentNode.insertBefore(node, ref.nextSibling); } return NIL; }); K.registerNative("dom-remove", function(args) { var node = args[0]; if (node && node.parentNode) node.parentNode.removeChild(node); return NIL; }); K.registerNative("dom-remove-child", function(args) { var parent = args[0], child = args[1]; if (parent && child && child.parentNode === parent) parent.removeChild(child); return NIL; }); K.registerNative("dom-replace-child", function(args) { var parent = args[0], newC = args[1], oldC = args[2]; if (parent && newC && oldC) parent.replaceChild(newC, oldC); return NIL; }); K.registerNative("dom-remove-children-after", function(args) { var marker = args[0]; if (!marker || !marker.parentNode) return NIL; var parent = marker.parentNode; while (marker.nextSibling) parent.removeChild(marker.nextSibling); return NIL; }); K.registerNative("dom-append-to-head", function(args) { if (_hasDom && args[0]) document.head.appendChild(args[0]); return NIL; }); // ========================================================================= // 7. DOM Type Checks // ========================================================================= K.registerNative("dom-is-fragment?", function(args) { return args[0] ? args[0].nodeType === 11 : false; }); K.registerNative("dom-is-child-of?", function(args) { return !!(args[1] && args[0] && args[0].parentNode === args[1]); }); K.registerNative("dom-is-active-element?", function(args) { return _hasDom && args[0] === document.activeElement; }); K.registerNative("dom-is-input-element?", function(args) { if (!args[0] || !args[0].tagName) return false; var t = args[0].tagName; return t === "INPUT" || t === "TEXTAREA" || t === "SELECT"; }); // ========================================================================= // 8. DOM Styles & Classes // ========================================================================= K.registerNative("dom-get-style", function(args) { return args[0] && args[0].style ? args[0].style[args[1]] || "" : ""; }); K.registerNative("dom-set-style", function(args) { if (args[0] && args[0].style) args[0].style[args[1]] = args[2]; return NIL; }); K.registerNative("dom-add-class", function(args) { if (args[0] && args[0].classList) args[0].classList.add(args[1]); return NIL; }); K.registerNative("dom-remove-class", function(args) { if (args[0] && args[0].classList) args[0].classList.remove(args[1]); return NIL; }); K.registerNative("dom-has-class?", function(args) { return !!(args[0] && args[0].classList && args[0].classList.contains(args[1])); }); // ========================================================================= // 9. DOM Properties & Data // ========================================================================= K.registerNative("dom-get-prop", function(args) { return args[0] ? args[0][args[1]] : NIL; }); K.registerNative("dom-set-prop", function(args) { if (args[0]) args[0][args[1]] = args[2]; return NIL; }); K.registerNative("dom-set-data", function(args) { var el = args[0], key = args[1], val = args[2]; if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; } return NIL; }); K.registerNative("dom-get-data", function(args) { var el = args[0], key = args[1]; return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : NIL) : NIL; }); K.registerNative("dom-call-method", function(args) { var obj = args[0], method = args[1]; var callArgs = args.slice(2); if (obj && typeof obj[method] === "function") { try { return obj[method].apply(obj, callArgs); } catch(e) { return NIL; } } return NIL; }); // ========================================================================= // 10. DOM Events // ========================================================================= K.registerNative("dom-listen", function(args) { var el = args[0], name = args[1], handler = args[2]; if (!_hasDom || !el) return function() {}; // handler is a wrapped SX lambda (JS function with __sx_handle). // Wrap it to: // - Pass the event object as arg (or no args for 0-arity handlers) // - Catch errors from the CEK machine var arity = K.fnArity(handler); var wrapped; if (arity === 0) { wrapped = function(_e) { try { K.callFn(handler, []); } catch(err) { console.error("[sx] event handler error:", name, err); } }; } else { wrapped = function(e) { try { K.callFn(handler, [e]); } catch(err) { console.error("[sx] event handler error:", name, err); } }; } el.addEventListener(name, wrapped); return function() { el.removeEventListener(name, wrapped); }; }); K.registerNative("dom-dispatch", function(args) { if (!_hasDom || !args[0]) return false; var evt = new CustomEvent(args[1], { bubbles: true, cancelable: true, detail: args[2] || {} }); return args[0].dispatchEvent(evt); }); K.registerNative("event-detail", function(args) { return (args[0] && args[0].detail != null) ? args[0].detail : NIL; }); // ========================================================================= // 11. Browser Navigation & History // ========================================================================= K.registerNative("browser-location-href", function(_args) { return typeof location !== "undefined" ? location.href : ""; }); K.registerNative("browser-same-origin?", function(args) { try { return new URL(args[0], location.href).origin === location.origin; } catch (e) { return true; } }); K.registerNative("browser-push-state", function(args) { if (typeof history !== "undefined") { try { history.pushState({ sxUrl: args[0], scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", args[0]); } catch (e) {} } return NIL; }); K.registerNative("browser-replace-state", function(args) { if (typeof history !== "undefined") { try { history.replaceState({ sxUrl: args[0], scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", args[0]); } catch (e) {} } return NIL; }); K.registerNative("browser-navigate", function(args) { if (typeof location !== "undefined") location.assign(args[0]); return NIL; }); K.registerNative("browser-reload", function(_args) { if (typeof location !== "undefined") location.reload(); return NIL; }); K.registerNative("browser-scroll-to", function(args) { if (typeof window !== "undefined") window.scrollTo(args[0] || 0, args[1] || 0); return NIL; }); K.registerNative("browser-media-matches?", function(args) { if (typeof window === "undefined") return false; return window.matchMedia(args[0]).matches; }); K.registerNative("browser-confirm", function(args) { if (typeof window === "undefined") return false; return window.confirm(args[0]); }); K.registerNative("browser-prompt", function(args) { if (typeof window === "undefined") return NIL; var r = window.prompt(args[0]); return r === null ? NIL : r; }); // ========================================================================= // 12. Timers // ========================================================================= K.registerNative("set-timeout", function(args) { var fn = args[0], ms = args[1] || 0; var cb = (typeof fn === "function" && fn.__sx_handle != null) ? function() { try { K.callFn(fn, []); } catch(e) { console.error("[sx] timeout error:", e); } } : fn; return setTimeout(cb, ms); }); K.registerNative("set-interval", function(args) { var fn = args[0], ms = args[1] || 1000; var cb = (typeof fn === "function" && fn.__sx_handle != null) ? function() { try { K.callFn(fn, []); } catch(e) { console.error("[sx] interval error:", e); } } : fn; return setInterval(cb, ms); }); K.registerNative("clear-timeout", function(args) { clearTimeout(args[0]); return NIL; }); K.registerNative("clear-interval", function(args) { clearInterval(args[0]); return NIL; }); K.registerNative("now-ms", function(_args) { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }); K.registerNative("request-animation-frame", function(args) { var fn = args[0]; var cb = (typeof fn === "function" && fn.__sx_handle != null) ? function() { try { K.callFn(fn, []); } catch(e) { console.error("[sx] raf error:", e); } } : fn; if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb); else setTimeout(cb, 16); return NIL; }); // ========================================================================= // 13. Promises // ========================================================================= K.registerNative("promise-resolve", function(args) { return Promise.resolve(args[0]); }); K.registerNative("promise-then", function(args) { var p = args[0]; if (!p || !p.then) return p; var onResolve = function(v) { return K.callFn(args[1], [v]); }; var onReject = args[2] ? function(e) { return K.callFn(args[2], [e]); } : undefined; return onReject ? p.then(onResolve, onReject) : p.then(onResolve); }); K.registerNative("promise-catch", function(args) { if (!args[0] || !args[0].catch) return args[0]; return args[0].catch(function(e) { return K.callFn(args[1], [e]); }); }); K.registerNative("promise-delayed", function(args) { return new Promise(function(resolve) { setTimeout(function() { resolve(args[1]); }, args[0]); }); }); // ========================================================================= // 14. Abort Controllers // ========================================================================= var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; var _targetControllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; K.registerNative("new-abort-controller", function(_args) { return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} }; }); K.registerNative("abort-previous", function(args) { if (_controllers) { var prev = _controllers.get(args[0]); if (prev) prev.abort(); } return NIL; }); K.registerNative("track-controller", function(args) { if (_controllers) _controllers.set(args[0], args[1]); return NIL; }); K.registerNative("abort-previous-target", function(args) { if (_targetControllers) { var prev = _targetControllers.get(args[0]); if (prev) prev.abort(); } return NIL; }); K.registerNative("track-controller-target", function(args) { if (_targetControllers) _targetControllers.set(args[0], args[1]); return NIL; }); K.registerNative("controller-signal", function(args) { return args[0] ? args[0].signal : NIL; }); K.registerNative("is-abort-error", function(args) { return args[0] && args[0].name === "AbortError"; }); // ========================================================================= // 15. Fetch // ========================================================================= K.registerNative("fetch-request", function(args) { var config = args[0], successFn = args[1], errorFn = args[2]; var opts = { method: config.method, headers: config.headers }; if (config.signal) opts.signal = config.signal; if (config.body && config.method !== "GET") opts.body = config.body; if (config["cross-origin"]) opts.credentials = "include"; return fetch(config.url, opts).then(function(resp) { return resp.text().then(function(text) { var getHeader = function(name) { var v = resp.headers.get(name); return v === null ? NIL : v; }; return K.callFn(successFn, [resp.ok, resp.status, getHeader, text]); }); }).catch(function(err) { return K.callFn(errorFn, [err]); }); }); K.registerNative("csrf-token", function(_args) { if (!_hasDom) return NIL; var m = document.querySelector('meta[name="csrf-token"]'); return m ? m.getAttribute("content") : NIL; }); K.registerNative("is-cross-origin", function(args) { try { var h = new URL(args[0], location.href).hostname; return h !== location.hostname && (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0); } catch (e) { return false; } }); // ========================================================================= // 16. localStorage // ========================================================================= K.registerNative("local-storage-get", function(args) { try { var v = localStorage.getItem(args[0]); return v === null ? NIL : v; } catch(e) { return NIL; } }); K.registerNative("local-storage-set", function(args) { try { localStorage.setItem(args[0], args[1]); } catch(e) {} return NIL; }); K.registerNative("local-storage-remove", function(args) { try { localStorage.removeItem(args[0]); } catch(e) {} return NIL; }); // ========================================================================= // 17. Document Head & Title // ========================================================================= K.registerNative("set-document-title", function(args) { if (_hasDom) document.title = args[0] || ""; return NIL; }); K.registerNative("remove-head-element", function(args) { if (_hasDom) { var el = document.head.querySelector(args[0]); if (el) el.remove(); } return NIL; }); // ========================================================================= // 18. Logging // ========================================================================= K.registerNative("log-info", function(args) { console.log("[sx]", args[0]); return NIL; }); K.registerNative("log-warn", function(args) { console.warn("[sx]", args[0]); return NIL; }); K.registerNative("log-error", function(args) { console.error("[sx]", args[0]); return NIL; }); // ========================================================================= // 19. JSON // ========================================================================= K.registerNative("json-parse", function(args) { try { return JSON.parse(args[0]); } catch(e) { return {}; } }); K.registerNative("try-parse-json", function(args) { try { return JSON.parse(args[0]); } catch(e) { return NIL; } }); // ========================================================================= // 20. Processing markers // ========================================================================= K.registerNative("mark-processed!", function(args) { var el = args[0], key = args[1] || "sx"; if (el) { if (!el._sxProcessed) el._sxProcessed = {}; el._sxProcessed[key] = true; } return NIL; }); K.registerNative("is-processed?", function(args) { var el = args[0], key = args[1] || "sx"; return !!(el && el._sxProcessed && el._sxProcessed[key]); }); // ========================================================================= // Public Sx API (wraps SxKernel for compatibility with existing code) // ========================================================================= var Sx = { // Core (delegated to WASM engine) parse: K.parse, eval: K.eval, evalExpr: K.evalExpr, load: K.load, loadSource: K.loadSource, renderToHtml: K.renderToHtml, typeOf: K.typeOf, inspect: K.inspect, engine: K.engine, // Will be populated after web adapters load: // mount, hydrate, processElements, etc. }; global.Sx = Sx; global.SxKernel = K; // Keep kernel available for direct access console.log("[sx-platform] registered, engine:", K.engine()); } // end initPlatform initPlatform(); })(typeof globalThis !== "undefined" ? globalThis : this); // ========================================================================= // Embedded web adapters (loaded into WASM engine at boot) // ========================================================================= globalThis.__sxAdapters = {}; globalThis.__sxAdapters["signals"] = ";; ==========================================================================\n;; signals.sx \u2014 Reactive signal runtime specification\n;;\n;; Defines the signal primitive: a container for a value that notifies\n;; subscribers when it changes. Signals are the reactive state primitive\n;; for SX islands.\n;;\n;; Signals are pure computation \u2014 no DOM, no IO. The reactive rendering\n;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server\n;; adapter (adapter-html.sx) reads signal values without subscribing.\n;;\n;; Signals are plain dicts with a \"__signal\" marker key. No platform\n;; primitives needed \u2014 all signal operations are pure SX.\n;;\n;; Reactive tracking and island lifecycle use the general scoped effects\n;; system (scope-push!/scope-pop!/context) instead of separate globals.\n;; Two scope names:\n;; \"sx-reactive\" \u2014 tracking context for computed/effect dep discovery\n;; \"sx-island-scope\" \u2014 island disposable collector\n;;\n;; Scope-based tracking:\n;; (scope-push! \"sx-reactive\" {:deps (list) :notify fn}) \u2192 void\n;; (scope-pop! \"sx-reactive\") \u2192 void\n;; (context \"sx-reactive\" nil) \u2192 dict or nil\n;;\n;; CEK callable dispatch:\n;; (cek-call f args) \u2192 any \u2014 call f with args list via CEK.\n;; Dispatches through cek-run for SX\n;; lambdas, apply for native callables.\n;; Defined in cek.sx.\n;;\n;; ==========================================================================\n\n\n;; --------------------------------------------------------------------------\n;; Signal container \u2014 plain dict with marker key\n;; --------------------------------------------------------------------------\n;;\n;; A signal is a dict: {\"__signal\" true, \"value\" v, \"subscribers\" [], \"deps\" []}\n;; type-of returns \"dict\". Use signal? to distinguish from regular dicts.\n\n(define make-signal (fn (value)\n (dict \"__signal\" true \"value\" value \"subscribers\" (list) \"deps\" (list))))\n\n(define signal? (fn (x)\n (and (dict? x) (has-key? x \"__signal\"))))\n\n(define signal-value (fn (s) (get s \"value\")))\n(define signal-set-value! (fn (s v) (dict-set! s \"value\" v)))\n(define signal-subscribers (fn (s) (get s \"subscribers\")))\n\n(define signal-add-sub! (fn (s f)\n (when (not (contains? (get s \"subscribers\") f))\n (append! (get s \"subscribers\") f))))\n\n(define signal-remove-sub! (fn (s f)\n (dict-set! s \"subscribers\"\n (filter (fn (sub) (not (identical? sub f)))\n (get s \"subscribers\")))))\n\n(define signal-deps (fn (s) (get s \"deps\")))\n(define signal-set-deps! (fn (s deps) (dict-set! s \"deps\" deps)))\n\n\n;; --------------------------------------------------------------------------\n;; 1. signal \u2014 create a reactive container\n;; --------------------------------------------------------------------------\n\n(define signal :effects []\n (fn ((initial-value :as any))\n (make-signal initial-value)))\n\n\n;; --------------------------------------------------------------------------\n;; 2. deref \u2014 read signal value, subscribe current reactive context\n;; --------------------------------------------------------------------------\n;;\n;; In a reactive context (inside effect or computed), deref registers the\n;; signal as a dependency. Outside reactive context, deref just returns\n;; the current value \u2014 no subscription, no overhead.\n\n(define deref :effects []\n (fn ((s :as any))\n (if (not (signal? s))\n s ;; non-signal values pass through\n (let ((ctx (context \"sx-reactive\" nil)))\n (when ctx\n ;; Register this signal as a dependency of the current context\n (let ((dep-list (get ctx \"deps\"))\n (notify-fn (get ctx \"notify\")))\n (when (not (contains? dep-list s))\n (append! dep-list s)\n (signal-add-sub! s notify-fn))))\n (signal-value s)))))\n\n\n;; --------------------------------------------------------------------------\n;; 3. reset! \u2014 write a new value, notify subscribers\n;; --------------------------------------------------------------------------\n\n(define reset! :effects [mutation]\n (fn ((s :as signal) value)\n (when (signal? s)\n (let ((old (signal-value s)))\n (when (not (identical? old value))\n (signal-set-value! s value)\n (notify-subscribers s))))))\n\n\n;; --------------------------------------------------------------------------\n;; 4. swap! \u2014 update signal via function\n;; --------------------------------------------------------------------------\n\n(define swap! :effects [mutation]\n (fn ((s :as signal) (f :as lambda) &rest args)\n (when (signal? s)\n (let ((old (signal-value s))\n (new-val (apply f (cons old args))))\n (when (not (identical? old new-val))\n (signal-set-value! s new-val)\n (notify-subscribers s))))))\n\n\n;; --------------------------------------------------------------------------\n;; 5. computed \u2014 derived signal with automatic dependency tracking\n;; --------------------------------------------------------------------------\n;;\n;; A computed signal wraps a zero-arg function. It re-evaluates when any\n;; of its dependencies change. The dependency set is discovered automatically\n;; by tracking deref calls during evaluation.\n\n(define computed :effects [mutation]\n (fn ((compute-fn :as lambda))\n (let ((s (make-signal nil))\n (deps (list))\n (compute-ctx nil))\n\n ;; The notify function \u2014 called when a dependency changes\n (let ((recompute\n (fn ()\n ;; Unsubscribe from old deps\n (for-each\n (fn ((dep :as signal)) (signal-remove-sub! dep recompute))\n (signal-deps s))\n (signal-set-deps! s (list))\n\n ;; Push scope-based tracking context for this computed\n (let ((ctx (dict \"deps\" (list) \"notify\" recompute)))\n (scope-push! \"sx-reactive\" ctx)\n (let ((new-val (cek-call compute-fn nil)))\n (scope-pop! \"sx-reactive\")\n ;; Save discovered deps\n (signal-set-deps! s (get ctx \"deps\"))\n ;; Update value + notify downstream\n (let ((old (signal-value s)))\n (signal-set-value! s new-val)\n (when (not (identical? old new-val))\n (notify-subscribers s))))))))\n\n ;; Initial computation\n (recompute)\n ;; Auto-register disposal with island scope\n (register-in-scope (fn () (dispose-computed s)))\n s))))\n\n\n;; --------------------------------------------------------------------------\n;; 6. effect \u2014 side effect that runs when dependencies change\n;; --------------------------------------------------------------------------\n;;\n;; Like computed, but doesn't produce a signal value. Returns a dispose\n;; function that tears down the effect.\n\n(define effect :effects [mutation]\n (fn ((effect-fn :as lambda))\n (let ((deps (list))\n (disposed false)\n (cleanup-fn nil))\n\n (let ((run-effect\n (fn ()\n (when (not disposed)\n ;; Run previous cleanup if any\n (when cleanup-fn (cek-call cleanup-fn nil))\n\n ;; Unsubscribe from old deps\n (for-each\n (fn ((dep :as signal)) (signal-remove-sub! dep run-effect))\n deps)\n (set! deps (list))\n\n ;; Push scope-based tracking context\n (let ((ctx (dict \"deps\" (list) \"notify\" run-effect)))\n (scope-push! \"sx-reactive\" ctx)\n (let ((result (cek-call effect-fn nil)))\n (scope-pop! \"sx-reactive\")\n (set! deps (get ctx \"deps\"))\n ;; If effect returns a function, it's the cleanup\n (when (callable? result)\n (set! cleanup-fn result))))))))\n\n ;; Initial run\n (run-effect)\n\n ;; Return dispose function\n (let ((dispose-fn\n (fn ()\n (set! disposed true)\n (when cleanup-fn (cek-call cleanup-fn nil))\n (for-each\n (fn ((dep :as signal)) (signal-remove-sub! dep run-effect))\n deps)\n (set! deps (list)))))\n ;; Auto-register with island scope so disposal happens on swap\n (register-in-scope dispose-fn)\n dispose-fn)))))\n\n\n;; --------------------------------------------------------------------------\n;; 7. batch \u2014 group multiple signal writes into one notification pass\n;; --------------------------------------------------------------------------\n;;\n;; During a batch, signal writes are deferred. Subscribers are notified\n;; once at the end, after all values have been updated.\n\n(define *batch-depth* 0)\n(define *batch-queue* (list))\n\n(define batch :effects [mutation]\n (fn ((thunk :as lambda))\n (set! *batch-depth* (+ *batch-depth* 1))\n (cek-call thunk nil)\n (set! *batch-depth* (- *batch-depth* 1))\n (when (= *batch-depth* 0)\n (let ((queue *batch-queue*))\n (set! *batch-queue* (list))\n ;; Collect unique subscribers across all queued signals,\n ;; then notify each exactly once.\n (let ((seen (list))\n (pending (list)))\n (for-each\n (fn ((s :as signal))\n (for-each\n (fn ((sub :as lambda))\n (when (not (contains? seen sub))\n (append! seen sub)\n (append! pending sub)))\n (signal-subscribers s)))\n queue)\n (for-each (fn ((sub :as lambda)) (sub)) pending))))))\n\n\n;; --------------------------------------------------------------------------\n;; 8. notify-subscribers \u2014 internal notification dispatch\n;; --------------------------------------------------------------------------\n;;\n;; If inside a batch, queues the signal. Otherwise, notifies immediately.\n\n(define notify-subscribers :effects [mutation]\n (fn ((s :as signal))\n (if (> *batch-depth* 0)\n (when (not (contains? *batch-queue* s))\n (append! *batch-queue* s))\n (flush-subscribers s))))\n\n(define flush-subscribers :effects [mutation]\n (fn ((s :as signal))\n (for-each\n (fn ((sub :as lambda)) (sub))\n (signal-subscribers s))))\n\n\n;; --------------------------------------------------------------------------\n;; 9. Reactive tracking context\n;; --------------------------------------------------------------------------\n;;\n;; Tracking is now scope-based. computed/effect push a dict\n;; {:deps (list) :notify fn} onto the \"sx-reactive\" scope stack via\n;; scope-push!/scope-pop!. deref reads it via (context \"sx-reactive\" nil).\n;; No platform primitives needed \u2014 uses the existing scope infrastructure.\n\n\n;; --------------------------------------------------------------------------\n;; 10. dispose \u2014 tear down a computed signal\n;; --------------------------------------------------------------------------\n;;\n;; For computed signals, unsubscribe from all dependencies.\n;; For effects, the dispose function is returned by effect itself.\n\n(define dispose-computed :effects [mutation]\n (fn ((s :as signal))\n (when (signal? s)\n (for-each\n (fn ((dep :as signal)) (signal-remove-sub! dep nil))\n (signal-deps s))\n (signal-set-deps! s (list)))))\n\n\n;; --------------------------------------------------------------------------\n;; 11. Island scope \u2014 automatic cleanup of signals within an island\n;; --------------------------------------------------------------------------\n;;\n;; When an island is created, all signals, effects, and computeds created\n;; within it are tracked. When the island is removed from the DOM, they\n;; are all disposed.\n;;\n;; Uses \"sx-island-scope\" scope name. The scope value is a collector\n;; function (fn (disposable) ...) that appends to the island's disposer list.\n\n(define with-island-scope :effects [mutation]\n (fn ((scope-fn :as lambda) (body-fn :as lambda))\n (scope-push! \"sx-island-scope\" scope-fn)\n (let ((result (body-fn)))\n (scope-pop! \"sx-island-scope\")\n result)))\n\n;; Hook into signal/effect/computed creation for scope tracking.\n\n(define register-in-scope :effects [mutation]\n (fn ((disposable :as lambda))\n (let ((collector (context \"sx-island-scope\" nil)))\n (when collector\n (cek-call collector (list disposable))))))\n\n\n;; ==========================================================================\n;; 12. Marsh scopes \u2014 child scopes within islands\n;; ==========================================================================\n;;\n;; Marshes are zones inside islands where server content is re-evaluated\n;; in the island's reactive context. When a marsh is re-morphed with new\n;; content, its old effects and computeds must be disposed WITHOUT disturbing\n;; the island's own reactive graph.\n;;\n;; Scope hierarchy: island \u2192 marsh \u2192 effects/computeds\n;; Disposing a marsh disposes its subscope. Disposing an island disposes\n;; all its marshes. The signal graph is a tree, not a flat list.\n;;\n;; Platform interface required:\n;; (dom-set-data el key val) \u2192 void \u2014 store JS value on element\n;; (dom-get-data el key) \u2192 any \u2014 retrieve stored value\n\n(define with-marsh-scope :effects [mutation io]\n (fn (marsh-el (body-fn :as lambda))\n ;; Execute body-fn collecting all disposables into a marsh-local list.\n ;; Nested under the current island scope \u2014 if the island is disposed,\n ;; the marsh is disposed too (because island scope collected the marsh's\n ;; own dispose function).\n (let ((disposers (list)))\n (with-island-scope\n (fn (d) (append! disposers d))\n body-fn)\n ;; Store disposers on the marsh element for later cleanup\n (dom-set-data marsh-el \"sx-marsh-disposers\" disposers))))\n\n(define dispose-marsh-scope :effects [mutation io]\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope.\n ;; Parent island scope and sibling marshes are unaffected.\n (let ((disposers (dom-get-data marsh-el \"sx-marsh-disposers\")))\n (when disposers\n (for-each (fn ((d :as lambda)) (cek-call d nil)) disposers)\n (dom-set-data marsh-el \"sx-marsh-disposers\" nil)))))\n\n\n;; ==========================================================================\n;; 13. Named stores \u2014 page-level signal containers (L3)\n;; ==========================================================================\n;;\n;; Stores persist across island creation/destruction. They live at page\n;; scope, not island scope. When an island is swapped out and re-created,\n;; it reconnects to the same store instance.\n;;\n;; The store registry is global page-level state. It survives island\n;; disposal but is cleared on full page navigation.\n\n(define *store-registry* (dict))\n\n(define def-store :effects [mutation]\n (fn ((name :as string) (init-fn :as lambda))\n (let ((registry *store-registry*))\n ;; Only create the store once \u2014 subsequent calls return existing\n (when (not (has-key? registry name))\n (set! *store-registry* (assoc registry name (cek-call init-fn nil))))\n (get *store-registry* name))))\n\n(define use-store :effects []\n (fn ((name :as string))\n (if (has-key? *store-registry* name)\n (get *store-registry* name)\n (error (str \"Store not found: \" name\n \". Call (def-store ...) before (use-store ...).\")))))\n\n(define clear-stores :effects [mutation]\n (fn ()\n (set! *store-registry* (dict))))\n\n\n;; ==========================================================================\n;; 13. Event bridge \u2014 DOM event communication for lake\u2192island\n;; ==========================================================================\n;;\n;; Server-rendered content (\"htmx lakes\") inside reactive islands can\n;; communicate with island signals via DOM custom events. The bridge\n;; pattern:\n;;\n;; 1. Server renders a button/link with data-sx-emit=\"event-name\"\n;; 2. When clicked, the client dispatches a CustomEvent on the element\n;; 3. The event bubbles up to the island container\n;; 4. An island effect listens for the event and updates signals\n;;\n;; This keeps server content pure HTML \u2014 no signal references needed.\n;; The island effect is the only reactive code.\n;;\n;; Platform interface required:\n;; (dom-listen el event-name handler) \u2192 remove-fn\n;; (dom-dispatch el event-name detail) \u2192 void\n;; (event-detail e) \u2192 any\n;;\n;; These are platform primitives because they require browser DOM APIs.\n\n(define emit-event :effects [io]\n (fn (el (event-name :as string) detail)\n (dom-dispatch el event-name detail)))\n\n(define on-event :effects [io]\n (fn (el (event-name :as string) (handler :as lambda))\n (dom-listen el event-name handler)))\n\n;; Convenience: create an effect that listens for a DOM event on an\n;; element and writes the event detail (or a transformed value) into\n;; a target signal. Returns the effect's dispose function.\n;; When the effect is disposed (island teardown), the listener is\n;; removed automatically via the cleanup return.\n\n(define bridge-event :effects [mutation io]\n (fn (el (event-name :as string) (target-signal :as signal) transform-fn)\n (effect (fn ()\n (let ((remove (dom-listen el event-name\n (fn (e)\n (let ((detail (event-detail e))\n (new-val (if transform-fn\n (cek-call transform-fn (list detail))\n detail)))\n (reset! target-signal new-val))))))\n ;; Return cleanup \u2014 removes listener on dispose/re-run\n remove)))))\n\n\n;; ==========================================================================\n;; 14. Resource \u2014 async signal with loading/resolved/error states\n;; ==========================================================================\n;;\n;; A resource wraps an async operation (fetch, computation) and exposes\n;; its state as a signal. The signal transitions through:\n;; {:loading true :data nil :error nil} \u2014 initial/loading\n;; {:loading false :data result :error nil} \u2014 success\n;; {:loading false :data nil :error err} \u2014 failure\n;;\n;; Usage:\n;; (let ((user (resource (fn () (fetch-json \"/api/user\")))))\n;; (cond\n;; (get (deref user) \"loading\") (div \"Loading...\")\n;; (get (deref user) \"error\") (div \"Error: \" (get (deref user) \"error\"))\n;; :else (div (get (deref user) \"data\"))))\n;;\n;; Platform interface required:\n;; (promise-then promise on-resolve on-reject) \u2192 void\n\n(define resource :effects [mutation io]\n (fn ((fetch-fn :as lambda))\n (let ((state (signal (dict \"loading\" true \"data\" nil \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (cek-call fetch-fn nil)\n (fn (data) (reset! state (dict \"loading\" false \"data\" data \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false \"data\" nil \"error\" err))))\n state)))\n\n\n"; globalThis.__sxAdapters["deps"] = ";; ==========================================================================\n;; deps.sx \u2014 Component dependency analysis specification\n;;\n;; Pure functions for analyzing component dependency graphs.\n;; Used by the bundling system to compute per-page component bundles\n;; instead of sending every definition to every page.\n;;\n;; All functions are pure \u2014 no IO, no platform-specific operations.\n;; Each host bootstraps this to native code alongside eval.sx/render.sx.\n;;\n;; From eval.sx platform (already provided by every host):\n;; (type-of x) \u2192 type string\n;; (symbol-name s) \u2192 string name of symbol\n;; (component-body c) \u2192 unevaluated AST of component body\n;; (component-name c) \u2192 string name (without ~)\n;; (macro-body m) \u2192 macro body AST\n;; (env-get env k) \u2192 value or nil\n;;\n;; New platform functions for deps (each host implements):\n;; (component-deps c) \u2192 cached deps list (may be empty)\n;; (component-set-deps! c d)\u2192 cache deps on component\n;; (component-css-classes c)\u2192 pre-scanned CSS class list\n;; (regex-find-all pat src) \u2192 list of capture group 1 matches\n;; (scan-css-classes src) \u2192 list of CSS class strings from source\n;; ==========================================================================\n\n\n;; --------------------------------------------------------------------------\n;; 1. AST scanning \u2014 collect ~component references from an AST node\n;; --------------------------------------------------------------------------\n;; Walks all branches of control flow (if/when/cond/case) to find\n;; every component that *could* be rendered.\n\n(define scan-refs :effects []\n (fn (node)\n (let ((refs (list)))\n (scan-refs-walk node refs)\n refs)))\n\n\n(define scan-refs-walk :effects []\n (fn (node (refs :as list))\n (cond\n ;; Symbol starting with ~ \u2192 component reference\n (= (type-of node) \"symbol\")\n (let ((name (symbol-name node)))\n (when (starts-with? name \"~\")\n (when (not (contains? refs name))\n (append! refs name))))\n\n ;; List \u2192 recurse into all elements (covers all control flow branches)\n (= (type-of node) \"list\")\n (for-each (fn (item) (scan-refs-walk item refs)) node)\n\n ;; Dict \u2192 recurse into values\n (= (type-of node) \"dict\")\n (for-each (fn (key) (scan-refs-walk (dict-get node key) refs))\n (keys node))\n\n ;; Literals (number, string, boolean, nil, keyword) \u2192 no refs\n :else nil)))\n\n\n;; --------------------------------------------------------------------------\n;; 2. Transitive dependency closure\n;; --------------------------------------------------------------------------\n;; Given a component name and an environment, compute all components\n;; that it can transitively render. Handles cycles via seen-set.\n\n(define transitive-deps-walk :effects []\n (fn ((n :as string) (seen :as list) (env :as dict))\n (when (not (contains? seen n))\n (append! seen n)\n (let ((val (env-get env n)))\n (cond\n (or (= (type-of val) \"component\") (= (type-of val) \"island\"))\n (for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env))\n (scan-refs (component-body val)))\n (= (type-of val) \"macro\")\n (for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env))\n (scan-refs (macro-body val)))\n :else nil)))))\n\n\n(define transitive-deps :effects []\n (fn ((name :as string) (env :as dict))\n (let ((seen (list))\n (key (if (starts-with? name \"~\") name (str \"~\" name))))\n (transitive-deps-walk key seen env)\n (filter (fn ((x :as string)) (not (= x key))) seen))))\n\n\n;; --------------------------------------------------------------------------\n;; 3. Compute deps for all components in an environment\n;; --------------------------------------------------------------------------\n;; Iterates env, calls transitive-deps for each component, and\n;; stores the result via the platform's component-set-deps! function.\n;;\n;; Platform interface:\n;; (env-components env) \u2192 list of component names in env\n;; (component-set-deps! comp deps) \u2192 store deps on component\n\n(define compute-all-deps :effects [mutation]\n (fn ((env :as dict))\n (for-each\n (fn ((name :as string))\n (let ((val (env-get env name)))\n (when (or (= (type-of val) \"component\") (= (type-of val) \"island\"))\n (component-set-deps! val (transitive-deps name env)))))\n (env-components env))))\n\n\n;; --------------------------------------------------------------------------\n;; 4. Scan serialized SX source for component references\n;; --------------------------------------------------------------------------\n;; Regex-based extraction of (~name patterns from SX wire format.\n;; Returns list of names WITH ~ prefix.\n;;\n;; Platform interface:\n;; (regex-find-all pattern source) \u2192 list of matched group strings\n\n(define scan-components-from-source :effects []\n (fn ((source :as string))\n (let ((matches (regex-find-all \"\\\\(~([a-zA-Z_][a-zA-Z0-9_\\\\-:/]*)\" source)))\n (map (fn ((m :as string)) (str \"~\" m)) matches))))\n\n\n;; --------------------------------------------------------------------------\n;; 5. Components needed for a page\n;; --------------------------------------------------------------------------\n;; Scans page source for direct component references, then computes\n;; the transitive closure. Returns list of ~names.\n\n(define components-needed :effects []\n (fn ((page-source :as string) (env :as dict))\n (let ((direct (scan-components-from-source page-source))\n (all-needed (list)))\n\n ;; Add each direct ref + its transitive deps\n (for-each\n (fn ((name :as string))\n (when (not (contains? all-needed name))\n (append! all-needed name))\n (let ((val (env-get env name)))\n (let ((deps (if (and (= (type-of val) \"component\")\n (not (empty? (component-deps val))))\n (component-deps val)\n (transitive-deps name env))))\n (for-each\n (fn ((dep :as string))\n (when (not (contains? all-needed dep))\n (append! all-needed dep)))\n deps))))\n direct)\n\n all-needed)))\n\n\n;; --------------------------------------------------------------------------\n;; 6. Build per-page component bundle\n;; --------------------------------------------------------------------------\n;; Given page source and env, returns list of component names needed.\n;; The host uses this list to serialize only the needed definitions\n;; and compute a page-specific hash.\n;;\n;; This replaces the \"send everything\" approach with per-page bundles.\n\n(define page-component-bundle :effects []\n (fn ((page-source :as string) (env :as dict))\n (components-needed page-source env)))\n\n\n;; --------------------------------------------------------------------------\n;; 7. CSS classes for a page\n;; --------------------------------------------------------------------------\n;; Returns the union of CSS classes from components this page uses,\n;; plus classes from the page source itself.\n;;\n;; Platform interface:\n;; (component-css-classes c) \u2192 set/list of class strings\n;; (scan-css-classes source) \u2192 set/list of class strings from source\n\n(define page-css-classes :effects []\n (fn ((page-source :as string) (env :as dict))\n (let ((needed (components-needed page-source env))\n (classes (list)))\n\n ;; Collect classes from needed components\n (for-each\n (fn ((name :as string))\n (let ((val (env-get env name)))\n (when (= (type-of val) \"component\")\n (for-each\n (fn ((cls :as string))\n (when (not (contains? classes cls))\n (append! classes cls)))\n (component-css-classes val)))))\n needed)\n\n ;; Add classes from page source\n (for-each\n (fn ((cls :as string))\n (when (not (contains? classes cls))\n (append! classes cls)))\n (scan-css-classes page-source))\n\n classes)))\n\n\n;; --------------------------------------------------------------------------\n;; 8. IO detection \u2014 scan component ASTs for IO primitive references\n;; --------------------------------------------------------------------------\n;; Extends the dependency walker to detect references to IO primitives.\n;; IO names are provided by the host (from boundary.sx declarations).\n;; A component is \"pure\" if it (transitively) references no IO primitives.\n;;\n;; Platform interface additions:\n;; (component-io-refs c) \u2192 cached IO ref list (may be empty)\n;; (component-set-io-refs! c r) \u2192 cache IO refs on component\n\n(define scan-io-refs-walk :effects []\n (fn (node (io-names :as list) (refs :as list))\n (cond\n ;; Symbol \u2192 check if name is in the IO set\n (= (type-of node) \"symbol\")\n (let ((name (symbol-name node)))\n (when (contains? io-names name)\n (when (not (contains? refs name))\n (append! refs name))))\n\n ;; List \u2192 recurse into all elements\n (= (type-of node) \"list\")\n (for-each (fn (item) (scan-io-refs-walk item io-names refs)) node)\n\n ;; Dict \u2192 recurse into values\n (= (type-of node) \"dict\")\n (for-each (fn (key) (scan-io-refs-walk (dict-get node key) io-names refs))\n (keys node))\n\n ;; Literals \u2192 no IO refs\n :else nil)))\n\n\n(define scan-io-refs :effects []\n (fn (node (io-names :as list))\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))\n\n\n;; --------------------------------------------------------------------------\n;; 9. Transitive IO refs \u2014 follow component deps and union IO refs\n;; --------------------------------------------------------------------------\n\n(define transitive-io-refs-walk :effects []\n (fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list))\n (when (not (contains? seen n))\n (append! seen n)\n (let ((val (env-get env n)))\n (cond\n (= (type-of val) \"component\")\n (do\n ;; Scan this component's body for IO refs\n (for-each\n (fn ((ref :as string))\n (when (not (contains? all-refs ref))\n (append! all-refs ref)))\n (scan-io-refs (component-body val) io-names))\n ;; Recurse into component deps\n (for-each\n (fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names))\n (scan-refs (component-body val))))\n\n (= (type-of val) \"macro\")\n (do\n (for-each\n (fn ((ref :as string))\n (when (not (contains? all-refs ref))\n (append! all-refs ref)))\n (scan-io-refs (macro-body val) io-names))\n (for-each\n (fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names))\n (scan-refs (macro-body val))))\n\n :else nil)))))\n\n\n(define transitive-io-refs :effects []\n (fn ((name :as string) (env :as dict) (io-names :as list))\n (let ((all-refs (list))\n (seen (list))\n (key (if (starts-with? name \"~\") name (str \"~\" name))))\n (transitive-io-refs-walk key seen all-refs env io-names)\n all-refs)))\n\n\n;; --------------------------------------------------------------------------\n;; 10. Compute IO refs for all components in an environment\n;; --------------------------------------------------------------------------\n\n(define compute-all-io-refs :effects [mutation]\n (fn ((env :as dict) (io-names :as list))\n (for-each\n (fn ((name :as string))\n (let ((val (env-get env name)))\n (when (= (type-of val) \"component\")\n (component-set-io-refs! val (transitive-io-refs name env io-names)))))\n (env-components env))))\n\n\n(define component-io-refs-cached :effects []\n (fn ((name :as string) (env :as dict) (io-names :as list))\n (let ((key (if (starts-with? name \"~\") name (str \"~\" name))))\n (let ((val (env-get env key)))\n (if (and (= (type-of val) \"component\")\n (not (nil? (component-io-refs val)))\n (not (empty? (component-io-refs val))))\n (component-io-refs val)\n ;; Fallback: not yet cached (shouldn't happen after compute-all-io-refs)\n (transitive-io-refs name env io-names))))))\n\n(define component-pure? :effects []\n (fn ((name :as string) (env :as dict) (io-names :as list))\n (let ((key (if (starts-with? name \"~\") name (str \"~\" name))))\n (let ((val (env-get env key)))\n (if (and (= (type-of val) \"component\")\n (not (nil? (component-io-refs val))))\n ;; Use cached io-refs (empty list = pure)\n (empty? (component-io-refs val))\n ;; Fallback\n (empty? (transitive-io-refs name env io-names)))))))\n\n\n;; --------------------------------------------------------------------------\n;; 5. Render target \u2014 boundary decision per component\n;; --------------------------------------------------------------------------\n;; Combines IO analysis with affinity annotations to decide where a\n;; component should render:\n;;\n;; :affinity :server \u2192 always \"server\" (auth-sensitive, secrets)\n;; :affinity :client \u2192 \"client\" even if IO-dependent (IO proxy)\n;; :affinity :auto \u2192 \"server\" if IO-dependent, \"client\" if pure\n;;\n;; Returns: \"server\" | \"client\"\n\n(define render-target :effects []\n (fn ((name :as string) (env :as dict) (io-names :as list))\n (let ((key (if (starts-with? name \"~\") name (str \"~\" name))))\n (let ((val (env-get env key)))\n (if (not (= (type-of val) \"component\"))\n \"server\"\n (let ((affinity (component-affinity val)))\n (cond\n (= affinity \"server\") \"server\"\n (= affinity \"client\") \"client\"\n ;; auto: decide from IO analysis\n (not (component-pure? name env io-names)) \"server\"\n :else \"client\")))))))\n\n\n;; --------------------------------------------------------------------------\n;; 6. Page render plan \u2014 pre-computed boundary decisions for a page\n;; --------------------------------------------------------------------------\n;; Given page source + env + IO names, returns a render plan dict:\n;;\n;; {:components {~name \"server\"|\"client\" ...}\n;; :server (list of ~names that render server-side)\n;; :client (list of ~names that render client-side)\n;; :io-deps (list of IO primitives needed by server components)}\n;;\n;; This is computed once at page registration and cached on the page def.\n;; The async evaluator and client router both use it to make decisions\n;; without recomputing at every request.\n\n(define page-render-plan :effects []\n (fn ((page-source :as string) (env :as dict) (io-names :as list))\n (let ((needed (components-needed page-source env))\n (comp-targets (dict))\n (server-list (list))\n (client-list (list))\n (io-deps (list)))\n\n (for-each\n (fn ((name :as string))\n (let ((target (render-target name env io-names)))\n (dict-set! comp-targets name target)\n (if (= target \"server\")\n (do\n (append! server-list name)\n ;; Collect IO deps from server components (use cache)\n (for-each\n (fn ((io-ref :as string))\n (when (not (contains? io-deps io-ref))\n (append! io-deps io-ref)))\n (component-io-refs-cached name env io-names)))\n (append! client-list name))))\n needed)\n\n {:components comp-targets\n :server server-list\n :client client-list\n :io-deps io-deps})))\n\n\n;; --------------------------------------------------------------------------\n;; Host obligation: selective expansion in async partial evaluation\n;; --------------------------------------------------------------------------\n;; The spec classifies components as pure or IO-dependent and provides\n;; per-component render-target decisions. Each host's async partial\n;; evaluator (the server-side rendering path that bridges sync evaluation\n;; with async IO) must use this classification:\n;;\n;; render-target \"server\" \u2192 expand server-side (IO must resolve)\n;; render-target \"client\" \u2192 serialize for client (can render anywhere)\n;; Layout slot context \u2192 expand all (server needs full HTML)\n;;\n;; The spec provides: component-io-refs, component-pure?, render-target,\n;; component-affinity. The host provides the async runtime that acts on it.\n;; This is not SX semantics \u2014 it is host infrastructure. Every host\n;; with a server-side async evaluator implements the same rule.\n;; --------------------------------------------------------------------------\n\n\n;; --------------------------------------------------------------------------\n;; Platform interface summary\n;; --------------------------------------------------------------------------\n;;\n;; From eval.sx (already provided):\n;; (type-of x) \u2192 type string\n;; (symbol-name s) \u2192 string name of symbol\n;; (env-get env k) \u2192 value or nil\n;;\n;; New for deps.sx (each host implements):\n;; (component-body c) \u2192 AST body of component\n;; (component-name c) \u2192 name string\n;; (component-deps c) \u2192 cached deps list (may be empty)\n;; (component-set-deps! c d)\u2192 cache deps on component\n;; (component-css-classes c)\u2192 pre-scanned CSS class list\n;; (component-io-refs c) \u2192 cached IO ref list (may be empty)\n;; (component-set-io-refs! c r)\u2192 cache IO refs on component\n;; (component-affinity c) \u2192 \"auto\" | \"client\" | \"server\"\n;; (macro-body m) \u2192 AST body of macro\n;; (regex-find-all pat src) \u2192 list of capture group matches\n;; (scan-css-classes src) \u2192 list of CSS class strings from source\n;; --------------------------------------------------------------------------\n\n\n;; --------------------------------------------------------------------------\n;; env-components \u2014 list component/macro names in an environment\n;; --------------------------------------------------------------------------\n;; Moved from platform to spec: pure logic using type predicates.\n\n(define env-components :effects []\n (fn ((env :as dict))\n (filter\n (fn ((k :as string))\n (let ((v (env-get env k)))\n (or (component? v) (macro? v))))\n (keys env))))\n"; globalThis.__sxAdapters["page-helpers"] = ";; ==========================================================================\n;; page-helpers.sx \u2014 Pure data-transformation page helpers\n;;\n;; These functions take raw data (from Python I/O edge) and return\n;; structured dicts for page rendering. No I/O \u2014 pure transformations\n;; only. Bootstrapped to every host.\n;; ==========================================================================\n\n\n;; --------------------------------------------------------------------------\n;; categorize-special-forms\n;;\n;; Parses define-special-form declarations from special-forms.sx AST,\n;; categorizes each form by name lookup, returns dict of category \u2192 forms.\n;; --------------------------------------------------------------------------\n\n(define special-form-category-map\n {\"if\" \"Control Flow\" \"when\" \"Control Flow\" \"cond\" \"Control Flow\"\n \"case\" \"Control Flow\" \"and\" \"Control Flow\" \"or\" \"Control Flow\"\n \"let\" \"Binding\" \"let*\" \"Binding\" \"letrec\" \"Binding\"\n \"define\" \"Binding\" \"set!\" \"Binding\"\n \"lambda\" \"Functions & Components\" \"fn\" \"Functions & Components\"\n \"defcomp\" \"Functions & Components\" \"defmacro\" \"Functions & Components\"\n \"begin\" \"Sequencing & Threading\" \"do\" \"Sequencing & Threading\"\n \"->\" \"Sequencing & Threading\"\n \"quote\" \"Quoting\" \"quasiquote\" \"Quoting\"\n \"reset\" \"Continuations\" \"shift\" \"Continuations\"\n \"dynamic-wind\" \"Guards\"\n \"map\" \"Higher-Order Forms\" \"map-indexed\" \"Higher-Order Forms\"\n \"filter\" \"Higher-Order Forms\" \"reduce\" \"Higher-Order Forms\"\n \"some\" \"Higher-Order Forms\" \"every?\" \"Higher-Order Forms\"\n \"for-each\" \"Higher-Order Forms\"\n \"defstyle\" \"Domain Definitions\"\n \"defhandler\" \"Domain Definitions\" \"defpage\" \"Domain Definitions\"\n \"defquery\" \"Domain Definitions\" \"defaction\" \"Domain Definitions\"})\n\n\n(define extract-define-kwargs\n (fn ((expr :as list))\n ;; Extract keyword args from a define-special-form expression.\n ;; Returns dict of keyword-name \u2192 string value.\n ;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value.\n (let ((result {})\n (items (slice expr 2))\n (n (len items)))\n (for-each\n (fn ((idx :as number))\n (when (and (< (+ idx 1) n)\n (= (type-of (nth items idx)) \"keyword\"))\n (let ((key (keyword-name (nth items idx)))\n (val (nth items (+ idx 1))))\n (dict-set! result key\n (if (= (type-of val) \"list\")\n (str \"(\" (join \" \" (map serialize val)) \")\")\n (str val))))))\n (range 0 n))\n result)))\n\n\n(define categorize-special-forms\n (fn ((parsed-exprs :as list))\n ;; parsed-exprs: result of parse-all on special-forms.sx\n ;; Returns dict of category-name \u2192 list of form dicts.\n (let ((categories {}))\n (for-each\n (fn (expr)\n (when (and (= (type-of expr) \"list\")\n (>= (len expr) 2)\n (= (type-of (first expr)) \"symbol\")\n (= (symbol-name (first expr)) \"define-special-form\"))\n (let ((name (nth expr 1))\n (kwargs (extract-define-kwargs expr))\n (category (or (get special-form-category-map name) \"Other\")))\n (when (not (has-key? categories category))\n (dict-set! categories category (list)))\n (append! (get categories category)\n {\"name\" name\n \"syntax\" (or (get kwargs \"syntax\") \"\")\n \"doc\" (or (get kwargs \"doc\") \"\")\n \"tail-position\" (or (get kwargs \"tail-position\") \"\")\n \"example\" (or (get kwargs \"example\") \"\")}))))\n parsed-exprs)\n categories)))\n\n\n;; --------------------------------------------------------------------------\n;; build-reference-data\n;;\n;; Takes a slug and raw reference data, returns structured dict for rendering.\n;; --------------------------------------------------------------------------\n\n(define build-ref-items-with-href\n (fn ((items :as list) (base-path :as string) (detail-keys :as list) (n-fields :as number))\n ;; items: list of lists (tuples), each with n-fields elements\n ;; base-path: e.g. \"/geography/hypermedia/reference/attributes/\"\n ;; detail-keys: list of strings (keys that have detail pages)\n ;; n-fields: 2 or 3 (number of fields per tuple)\n (map\n (fn ((item :as list))\n (if (= n-fields 3)\n ;; [name, desc/value, exists/desc]\n (let ((name (nth item 0))\n (field2 (nth item 1))\n (field3 (nth item 2)))\n {\"name\" name\n \"desc\" field2\n \"exists\" field3\n \"href\" (if (and field3 (some (fn ((k :as string)) (= k name)) detail-keys))\n (str base-path name)\n nil)})\n ;; [name, desc]\n (let ((name (nth item 0))\n (desc (nth item 1)))\n {\"name\" name\n \"desc\" desc\n \"href\" (if (some (fn ((k :as string)) (= k name)) detail-keys)\n (str base-path name)\n nil)})))\n items)))\n\n\n(define build-reference-data\n (fn ((slug :as string) (raw-data :as dict) (detail-keys :as list))\n ;; slug: \"attributes\", \"headers\", \"events\", \"js-api\"\n ;; raw-data: dict with the raw data lists for this slug\n ;; detail-keys: list of names that have detail pages\n (case slug\n \"attributes\"\n {\"req-attrs\" (build-ref-items-with-href\n (get raw-data \"req-attrs\")\n \"/geography/hypermedia/reference/attributes/\" detail-keys 3)\n \"beh-attrs\" (build-ref-items-with-href\n (get raw-data \"beh-attrs\")\n \"/geography/hypermedia/reference/attributes/\" detail-keys 3)\n \"uniq-attrs\" (build-ref-items-with-href\n (get raw-data \"uniq-attrs\")\n \"/geography/hypermedia/reference/attributes/\" detail-keys 3)}\n\n \"headers\"\n {\"req-headers\" (build-ref-items-with-href\n (get raw-data \"req-headers\")\n \"/geography/hypermedia/reference/headers/\" detail-keys 3)\n \"resp-headers\" (build-ref-items-with-href\n (get raw-data \"resp-headers\")\n \"/geography/hypermedia/reference/headers/\" detail-keys 3)}\n\n \"events\"\n {\"events-list\" (build-ref-items-with-href\n (get raw-data \"events-list\")\n \"/geography/hypermedia/reference/events/\" detail-keys 2)}\n\n \"js-api\"\n {\"js-api-list\" (map (fn ((item :as list)) {\"name\" (nth item 0) \"desc\" (nth item 1)})\n (get raw-data \"js-api-list\"))}\n\n ;; default: attributes\n :else\n {\"req-attrs\" (build-ref-items-with-href\n (get raw-data \"req-attrs\")\n \"/geography/hypermedia/reference/attributes/\" detail-keys 3)\n \"beh-attrs\" (build-ref-items-with-href\n (get raw-data \"beh-attrs\")\n \"/geography/hypermedia/reference/attributes/\" detail-keys 3)\n \"uniq-attrs\" (build-ref-items-with-href\n (get raw-data \"uniq-attrs\")\n \"/geography/hypermedia/reference/attributes/\" detail-keys 3)})))\n\n\n;; --------------------------------------------------------------------------\n;; build-attr-detail / build-header-detail / build-event-detail\n;;\n;; Lookup a slug in a detail dict, reshape for page rendering.\n;; --------------------------------------------------------------------------\n\n(define build-attr-detail\n (fn ((slug :as string) detail)\n ;; detail: dict with \"description\", \"example\", \"handler\", \"demo\" keys or nil\n (if (nil? detail)\n {\"attr-not-found\" true}\n {\"attr-not-found\" nil\n \"attr-title\" slug\n \"attr-description\" (get detail \"description\")\n \"attr-example\" (get detail \"example\")\n \"attr-handler\" (get detail \"handler\")\n \"attr-demo\" (get detail \"demo\")\n \"attr-wire-id\" (if (has-key? detail \"handler\")\n (str \"ref-wire-\"\n (replace (replace slug \":\" \"-\") \"*\" \"star\"))\n nil)})))\n\n\n(define build-header-detail\n (fn ((slug :as string) detail)\n (if (nil? detail)\n {\"header-not-found\" true}\n {\"header-not-found\" nil\n \"header-title\" slug\n \"header-direction\" (get detail \"direction\")\n \"header-description\" (get detail \"description\")\n \"header-example\" (get detail \"example\")\n \"header-demo\" (get detail \"demo\")})))\n\n\n(define build-event-detail\n (fn ((slug :as string) detail)\n (if (nil? detail)\n {\"event-not-found\" true}\n {\"event-not-found\" nil\n \"event-title\" slug\n \"event-description\" (get detail \"description\")\n \"event-example\" (get detail \"example\")\n \"event-demo\" (get detail \"demo\")})))\n\n\n;; --------------------------------------------------------------------------\n;; build-component-source\n;;\n;; Reconstruct defcomp/defisland source from component metadata.\n;; --------------------------------------------------------------------------\n\n(define build-component-source\n (fn ((comp-data :as dict))\n ;; comp-data: dict with \"type\", \"name\", \"params\", \"has-children\", \"body-sx\", \"affinity\"\n (let ((comp-type (get comp-data \"type\"))\n (name (get comp-data \"name\"))\n (params (get comp-data \"params\"))\n (has-children (get comp-data \"has-children\"))\n (body-sx (get comp-data \"body-sx\"))\n (affinity (get comp-data \"affinity\")))\n (if (= comp-type \"not-found\")\n (str \";; component \" name \" not found\")\n (let ((param-strs (if (empty? params)\n (if has-children\n (list \"&rest\" \"children\")\n (list))\n (if has-children\n (append (cons \"&key\" params) (list \"&rest\" \"children\"))\n (cons \"&key\" params))))\n (params-sx (str \"(\" (join \" \" param-strs) \")\"))\n (form-name (if (= comp-type \"island\") \"defisland\" \"defcomp\"))\n (affinity-str (if (and (= comp-type \"component\")\n (not (nil? affinity))\n (not (= affinity \"auto\")))\n (str \" :affinity \" affinity)\n \"\")))\n (str \"(\" form-name \" \" name \" \" params-sx affinity-str \"\\n \" body-sx \")\"))))))\n\n\n;; --------------------------------------------------------------------------\n;; build-bundle-analysis\n;;\n;; Compute per-page bundle stats from pre-extracted component data.\n;; --------------------------------------------------------------------------\n\n(define build-bundle-analysis\n (fn ((pages-raw :as list) (components-raw :as dict) (total-components :as number) (total-macros :as number) (pure-count :as number) (io-count :as number))\n ;; pages-raw: list of {:name :path :direct :needed-names}\n ;; components-raw: dict of name \u2192 {:is-pure :affinity :render-target :io-refs :deps :source}\n (let ((pages-data (list)))\n (for-each\n (fn ((page :as dict))\n (let ((needed-names (get page \"needed-names\"))\n (n (len needed-names))\n (pct (if (> total-components 0)\n (round (* (/ n total-components) 100))\n 0))\n (savings (- 100 pct))\n (pure-in-page 0)\n (io-in-page 0)\n (page-io-refs (list))\n (comp-details (list)))\n ;; Walk needed components\n (for-each\n (fn ((comp-name :as string))\n (let ((info (get components-raw comp-name)))\n (when (not (nil? info))\n (if (get info \"is-pure\")\n (set! pure-in-page (+ pure-in-page 1))\n (do\n (set! io-in-page (+ io-in-page 1))\n (for-each\n (fn ((ref :as string)) (when (not (some (fn ((r :as string)) (= r ref)) page-io-refs))\n (append! page-io-refs ref)))\n (or (get info \"io-refs\") (list)))))\n (append! comp-details\n {\"name\" comp-name\n \"is-pure\" (get info \"is-pure\")\n \"affinity\" (get info \"affinity\")\n \"render-target\" (get info \"render-target\")\n \"io-refs\" (or (get info \"io-refs\") (list))\n \"deps\" (or (get info \"deps\") (list))\n \"source\" (get info \"source\")}))))\n needed-names)\n (append! pages-data\n {\"name\" (get page \"name\")\n \"path\" (get page \"path\")\n \"direct\" (get page \"direct\")\n \"needed\" n\n \"pct\" pct\n \"savings\" savings\n \"io-refs\" (len page-io-refs)\n \"pure-in-page\" pure-in-page\n \"io-in-page\" io-in-page\n \"components\" comp-details})))\n pages-raw)\n {\"pages\" pages-data\n \"total-components\" total-components\n \"total-macros\" total-macros\n \"pure-count\" pure-count\n \"io-count\" io-count})))\n\n\n;; --------------------------------------------------------------------------\n;; build-routing-analysis\n;;\n;; Classify pages by routing mode (client vs server).\n;; --------------------------------------------------------------------------\n\n(define build-routing-analysis\n (fn ((pages-raw :as list))\n ;; pages-raw: list of {:name :path :has-data :content-src}\n (let ((pages-data (list))\n (client-count 0)\n (server-count 0))\n (for-each\n (fn ((page :as dict))\n (let ((has-data (get page \"has-data\"))\n (content-src (or (get page \"content-src\") \"\"))\n (mode nil)\n (reason \"\"))\n (cond\n has-data\n (do (set! mode \"server\")\n (set! reason \"Has :data expression \u2014 needs server IO\")\n (set! server-count (+ server-count 1)))\n (empty? content-src)\n (do (set! mode \"server\")\n (set! reason \"No content expression\")\n (set! server-count (+ server-count 1)))\n :else\n (do (set! mode \"client\")\n (set! client-count (+ client-count 1))))\n (append! pages-data\n {\"name\" (get page \"name\")\n \"path\" (get page \"path\")\n \"mode\" mode\n \"has-data\" has-data\n \"content-expr\" (if (> (len content-src) 80)\n (str (slice content-src 0 80) \"...\")\n content-src)\n \"reason\" reason})))\n pages-raw)\n {\"pages\" pages-data\n \"total-pages\" (+ client-count server-count)\n \"client-count\" client-count\n \"server-count\" server-count})))\n\n\n;; --------------------------------------------------------------------------\n;; build-affinity-analysis\n;;\n;; Package component affinity info + page render plans for display.\n;; --------------------------------------------------------------------------\n\n(define build-affinity-analysis\n (fn ((demo-components :as list) (page-plans :as list))\n {\"components\" demo-components\n \"page-plans\" page-plans}))\n"; globalThis.__sxAdapters["router"] = ";; ==========================================================================\n;; router.sx \u2014 Client-side route matching specification\n;;\n;; Pure functions for matching URL paths against Flask-style route patterns.\n;; Used by client-side routing to determine if a page can be rendered\n;; locally without a server roundtrip.\n;;\n;; All functions are pure \u2014 no IO, no platform-specific operations.\n;; Uses only primitives from primitives.sx (string ops, list ops).\n;; ==========================================================================\n\n\n;; --------------------------------------------------------------------------\n;; 1. Split path into segments\n;; --------------------------------------------------------------------------\n;; \"/docs/hello\" \u2192 (\"docs\" \"hello\")\n;; \"/\" \u2192 ()\n;; \"/docs/\" \u2192 (\"docs\")\n\n(define split-path-segments :effects []\n (fn ((path :as string))\n (let ((trimmed (if (starts-with? path \"/\") (slice path 1) path)))\n (let ((trimmed2 (if (and (not (empty? trimmed))\n (ends-with? trimmed \"/\"))\n (slice trimmed 0 (- (len trimmed) 1))\n trimmed)))\n (if (empty? trimmed2)\n (list)\n (split trimmed2 \"/\"))))))\n\n\n;; --------------------------------------------------------------------------\n;; 2. Parse Flask-style route pattern into segment descriptors\n;; --------------------------------------------------------------------------\n;; \"/docs/\" \u2192 ({\"type\" \"literal\" \"value\" \"docs\"}\n;; {\"type\" \"param\" \"value\" \"slug\"})\n\n(define make-route-segment :effects []\n (fn ((seg :as string))\n (if (and (starts-with? seg \"<\") (ends-with? seg \">\"))\n (let ((param-name (slice seg 1 (- (len seg) 1))))\n (let ((d {}))\n (dict-set! d \"type\" \"param\")\n (dict-set! d \"value\" param-name)\n d))\n (let ((d {}))\n (dict-set! d \"type\" \"literal\")\n (dict-set! d \"value\" seg)\n d))))\n\n(define parse-route-pattern :effects []\n (fn ((pattern :as string))\n (let ((segments (split-path-segments pattern)))\n (map make-route-segment segments))))\n\n\n;; --------------------------------------------------------------------------\n;; 3. Match path segments against parsed pattern\n;; --------------------------------------------------------------------------\n;; Returns params dict if match, nil if no match.\n\n(define match-route-segments :effects []\n (fn ((path-segs :as list) (parsed-segs :as list))\n (if (not (= (len path-segs) (len parsed-segs)))\n nil\n (let ((params {})\n (matched true))\n (for-each-indexed\n (fn ((i :as number) (parsed-seg :as dict))\n (when matched\n (let ((path-seg (nth path-segs i))\n (seg-type (get parsed-seg \"type\")))\n (cond\n (= seg-type \"literal\")\n (when (not (= path-seg (get parsed-seg \"value\")))\n (set! matched false))\n (= seg-type \"param\")\n (dict-set! params (get parsed-seg \"value\") path-seg)\n :else\n (set! matched false)))))\n parsed-segs)\n (if matched params nil)))))\n\n\n;; --------------------------------------------------------------------------\n;; 4. Public API: match a URL path against a pattern string\n;; --------------------------------------------------------------------------\n;; Returns params dict (may be empty for exact matches) or nil.\n\n(define match-route :effects []\n (fn ((path :as string) (pattern :as string))\n (let ((path-segs (split-path-segments path))\n (parsed-segs (parse-route-pattern pattern)))\n (match-route-segments path-segs parsed-segs))))\n\n\n;; --------------------------------------------------------------------------\n;; 5. Search a list of route entries for first match\n;; --------------------------------------------------------------------------\n;; Each entry: {\"pattern\" \"/docs/\" \"parsed\" [...] \"name\" \"docs-page\" ...}\n;; Returns matching entry with \"params\" added, or nil.\n\n(define find-matching-route :effects []\n (fn ((path :as string) (routes :as list))\n ;; If path is an SX expression URL, convert to old-style for matching.\n (let ((match-path (if (starts-with? path \"/(\")\n (or (sx-url-to-path path) path)\n path)))\n (let ((path-segs (split-path-segments match-path))\n (result nil))\n (for-each\n (fn ((route :as dict))\n (when (nil? result)\n (let ((params (match-route-segments path-segs (get route \"parsed\"))))\n (when (not (nil? params))\n (let ((matched (merge route {})))\n (dict-set! matched \"params\" params)\n (set! result matched))))))\n routes)\n result))))\n\n\n;; --------------------------------------------------------------------------\n;; 6. SX expression URL \u2192 old-style path conversion\n;; --------------------------------------------------------------------------\n;; Converts /(language.(doc.introduction)) \u2192 /language/docs/introduction\n;; so client-side routing can match SX URLs against Flask-style patterns.\n\n(define _fn-to-segment :effects []\n (fn ((name :as string))\n (case name\n \"doc\" \"docs\"\n \"spec\" \"specs\"\n \"bootstrapper\" \"bootstrappers\"\n \"test\" \"testing\"\n \"example\" \"examples\"\n \"protocol\" \"protocols\"\n \"essay\" \"essays\"\n \"plan\" \"plans\"\n \"reference-detail\" \"reference\"\n :else name)))\n\n(define sx-url-to-path :effects []\n (fn ((url :as string))\n ;; Convert an SX expression URL to an old-style slash path.\n ;; \"/(language.(doc.introduction))\" \u2192 \"/language/docs/introduction\"\n ;; Returns nil for non-SX URLs (those not starting with \"/(\" ).\n (if (not (and (starts-with? url \"/(\") (ends-with? url \")\")))\n nil\n (let ((inner (slice url 2 (- (len url) 1))))\n ;; \"language.(doc.introduction)\" \u2192 dots to slashes, strip parens\n (let ((s (replace (replace (replace inner \".\" \"/\") \"(\" \"\") \")\" \"\")))\n ;; \"language/doc/introduction\" \u2192 split, map names, rejoin\n (let ((segs (filter (fn (s) (not (empty? s))) (split s \"/\"))))\n (str \"/\" (join \"/\" (map _fn-to-segment segs)))))))))\n\n\n;; --------------------------------------------------------------------------\n;; 7. Relative SX URL resolution\n;; --------------------------------------------------------------------------\n;; Resolves relative SX URLs against the current absolute URL.\n;; This is a macro in the deepest sense: SX transforming SX into SX.\n;; The URL is code. Relative resolution is code transformation.\n;;\n;; Relative URLs start with ( or . :\n;; (.slug) \u2192 append slug as argument to innermost call\n;; (..section) \u2192 up 1: replace innermost with new nested call\n;; (...section) \u2192 up 2: replace 2 innermost levels\n;;\n;; Bare-dot shorthand (parens optional):\n;; .slug \u2192 same as (.slug)\n;; .. \u2192 same as (..) \u2014 go up one level\n;; ... \u2192 same as (...) \u2014 go up two levels\n;; .:page.4 \u2192 same as (.:page.4) \u2014 set keyword\n;;\n;; Dot count semantics (parallels filesystem . and ..):\n;; 1 dot = current level (append argument / modify keyword)\n;; 2 dots = up 1 level (sibling call)\n;; 3 dots = up 2 levels\n;; N dots = up N-1 levels\n;;\n;; Keyword operations (set, delta):\n;; (.:page.4) \u2192 set :page to 4 at current level\n;; (.:page.+1) \u2192 increment :page by 1 (delta)\n;; (.:page.-1) \u2192 decrement :page by 1 (delta)\n;; (.slug.:page.1) \u2192 append slug AND set :page=1\n;;\n;; Examples (current = \"/(geography.(hypermedia.(example)))\"):\n;; (.progress-bar) \u2192 /(geography.(hypermedia.(example.progress-bar)))\n;; (..reactive.demo) \u2192 /(geography.(hypermedia.(reactive.demo)))\n;; (...marshes) \u2192 /(geography.(marshes))\n;; (..) \u2192 /(geography.(hypermedia))\n;; (...) \u2192 /(geography)\n;;\n;; Keyword examples (current = \"/(language.(spec.(explore.signals.:page.3)))\"):\n;; (.:page.4) \u2192 /(language.(spec.(explore.signals.:page.4)))\n;; (.:page.+1) \u2192 /(language.(spec.(explore.signals.:page.4)))\n;; (.:page.-1) \u2192 /(language.(spec.(explore.signals.:page.2)))\n;; (..eval) \u2192 /(language.(spec.(eval)))\n;; (..eval.:page.1) \u2192 /(language.(spec.(eval.:page.1)))\n\n(define _count-leading-dots :effects []\n (fn ((s :as string))\n (if (empty? s)\n 0\n (if (starts-with? s \".\")\n (+ 1 (_count-leading-dots (slice s 1)))\n 0))))\n\n(define _strip-trailing-close :effects []\n (fn ((s :as string))\n ;; Strip trailing ) characters: \"/(a.(b.(c\" from \"/(a.(b.(c)))\"\n (if (ends-with? s \")\")\n (_strip-trailing-close (slice s 0 (- (len s) 1)))\n s)))\n\n(define _index-of-safe :effects []\n (fn ((s :as string) (needle :as string))\n ;; Wrapper around index-of that normalizes -1 to nil.\n ;; (index-of returns -1 on some platforms, nil on others.)\n (let ((idx (index-of s needle)))\n (if (or (nil? idx) (< idx 0)) nil idx))))\n\n(define _last-index-of :effects []\n (fn ((s :as string) (needle :as string))\n ;; Find the last occurrence of needle in s. Returns nil if not found.\n (let ((idx (_index-of-safe s needle)))\n (if (nil? idx)\n nil\n (let ((rest-idx (_last-index-of (slice s (+ idx 1)) needle)))\n (if (nil? rest-idx)\n idx\n (+ (+ idx 1) rest-idx)))))))\n\n(define _pop-sx-url-level :effects []\n (fn ((url :as string))\n ;; Remove the innermost nesting level from an absolute SX URL.\n ;; \"/(a.(b.(c)))\" \u2192 \"/(a.(b))\"\n ;; \"/(a.(b))\" \u2192 \"/(a)\"\n ;; \"/(a)\" \u2192 \"/\"\n (let ((stripped (_strip-trailing-close url))\n (close-count (- (len url) (len (_strip-trailing-close url)))))\n (if (<= close-count 1)\n \"/\" ;; at root, popping goes to bare root\n (let ((last-dp (_last-index-of stripped \".(\")))\n (if (nil? last-dp)\n \"/\" ;; single-level URL, pop to root\n ;; Remove from .( to end of stripped, drop one closing paren\n (str (slice stripped 0 last-dp)\n (slice url (- (len url) (- close-count 1))))))))))\n\n(define _pop-sx-url-levels :effects []\n (fn ((url :as string) (n :as number))\n (if (<= n 0)\n url\n (_pop-sx-url-levels (_pop-sx-url-level url) (- n 1)))))\n\n\n;; --------------------------------------------------------------------------\n;; 8. Relative URL body parsing \u2014 positional vs keyword tokens\n;; --------------------------------------------------------------------------\n;; Body \"slug.:page.4\" \u2192 positional \"slug\", keywords ((:page 4))\n;; Body \":page.+1\" \u2192 positional \"\", keywords ((:page +1))\n\n(define _split-pos-kw :effects []\n (fn ((tokens :as list) (i :as number) (pos :as list) (kw :as list))\n ;; Walk tokens: non-: tokens are positional, : tokens consume next as value\n (if (>= i (len tokens))\n {\"positional\" (join \".\" pos) \"keywords\" kw}\n (let ((tok (nth tokens i)))\n (if (starts-with? tok \":\")\n ;; Keyword: take this + next token as a pair\n (let ((val (if (< (+ i 1) (len tokens))\n (nth tokens (+ i 1))\n \"\")))\n (_split-pos-kw tokens (+ i 2) pos\n (append kw (list (list tok val)))))\n ;; Positional token\n (_split-pos-kw tokens (+ i 1)\n (append pos (list tok))\n kw))))))\n\n(define _parse-relative-body :effects []\n (fn ((body :as string))\n ;; Returns {\"positional\" \"keywords\" }\n (if (empty? body)\n {\"positional\" \"\" \"keywords\" (list)}\n (_split-pos-kw (split body \".\") 0 (list) (list)))))\n\n\n;; --------------------------------------------------------------------------\n;; 9. Keyword operations on URL expressions\n;; --------------------------------------------------------------------------\n;; Extract, find, and modify keyword arguments in the innermost expression.\n\n(define _extract-innermost :effects []\n (fn ((url :as string))\n ;; Returns {\"before\" ... \"content\" ... \"suffix\" ...}\n ;; where before + content + suffix = url\n ;; content = the innermost expression's dot-separated tokens\n (let ((stripped (_strip-trailing-close url))\n (suffix (slice url (len (_strip-trailing-close url)))))\n (let ((last-dp (_last-index-of stripped \".(\")))\n (if (nil? last-dp)\n ;; Single-level: /(content)\n {\"before\" \"/(\"\n \"content\" (slice stripped 2)\n \"suffix\" suffix}\n ;; Multi-level: .../.(content)...)\n {\"before\" (slice stripped 0 (+ last-dp 2))\n \"content\" (slice stripped (+ last-dp 2))\n \"suffix\" suffix})))))\n\n(define _find-kw-in-tokens :effects []\n (fn ((tokens :as list) (i :as number) (kw :as string))\n ;; Find value of keyword kw in token list. Returns nil if not found.\n (if (>= i (len tokens))\n nil\n (if (and (= (nth tokens i) kw)\n (< (+ i 1) (len tokens)))\n (nth tokens (+ i 1))\n (_find-kw-in-tokens tokens (+ i 1) kw)))))\n\n(define _find-keyword-value :effects []\n (fn ((content :as string) (kw :as string))\n ;; Find keyword's value in dot-separated content string.\n ;; \"explore.signals.:page.3\" \":page\" \u2192 \"3\"\n (_find-kw-in-tokens (split content \".\") 0 kw)))\n\n(define _replace-kw-in-tokens :effects []\n (fn ((tokens :as list) (i :as number) (kw :as string) (value :as string))\n ;; Replace keyword's value in token list. Returns new token list.\n (if (>= i (len tokens))\n (list)\n (if (and (= (nth tokens i) kw)\n (< (+ i 1) (len tokens)))\n ;; Found \u2014 keep keyword, replace value, concat rest\n (append (list kw value)\n (_replace-kw-in-tokens tokens (+ i 2) kw value))\n ;; Not this keyword \u2014 keep token, continue\n (cons (nth tokens i)\n (_replace-kw-in-tokens tokens (+ i 1) kw value))))))\n\n(define _set-keyword-in-content :effects []\n (fn ((content :as string) (kw :as string) (value :as string))\n ;; Set or replace keyword value in dot-separated content.\n ;; \"a.b.:page.3\" \":page\" \"4\" \u2192 \"a.b.:page.4\"\n ;; \"a.b\" \":page\" \"1\" \u2192 \"a.b.:page.1\"\n (let ((current (_find-keyword-value content kw)))\n (if (nil? current)\n ;; Not found \u2014 append\n (str content \".\" kw \".\" value)\n ;; Found \u2014 replace\n (join \".\" (_replace-kw-in-tokens (split content \".\") 0 kw value))))))\n\n(define _is-delta-value? :effects []\n (fn ((s :as string))\n ;; \"+1\", \"-2\", \"+10\" are deltas. \"-\" alone is not.\n (and (not (empty? s))\n (> (len s) 1)\n (or (starts-with? s \"+\") (starts-with? s \"-\")))))\n\n(define _apply-delta :effects []\n (fn ((current-str :as string) (delta-str :as string))\n ;; Apply numeric delta to current value string.\n ;; \"3\" \"+1\" \u2192 \"4\", \"3\" \"-1\" \u2192 \"2\"\n (let ((cur (parse-int current-str nil))\n (delta (parse-int delta-str nil)))\n (if (or (nil? cur) (nil? delta))\n delta-str ;; fallback: use delta as literal value\n (str (+ cur delta))))))\n\n(define _apply-kw-pairs :effects []\n (fn ((content :as string) (kw-pairs :as list))\n ;; Apply keyword modifications to content, one at a time.\n (if (empty? kw-pairs)\n content\n (let ((pair (first kw-pairs))\n (kw (first pair))\n (raw-val (nth pair 1)))\n (let ((actual-val\n (if (_is-delta-value? raw-val)\n (let ((current (_find-keyword-value content kw)))\n (if (nil? current)\n raw-val ;; no current value, treat delta as literal\n (_apply-delta current raw-val)))\n raw-val)))\n (_apply-kw-pairs\n (_set-keyword-in-content content kw actual-val)\n (rest kw-pairs)))))))\n\n(define _apply-keywords-to-url :effects []\n (fn ((url :as string) (kw-pairs :as list))\n ;; Apply keyword modifications to the innermost expression of a URL.\n (if (empty? kw-pairs)\n url\n (let ((parts (_extract-innermost url)))\n (let ((new-content (_apply-kw-pairs (get parts \"content\") kw-pairs)))\n (str (get parts \"before\") new-content (get parts \"suffix\")))))))\n\n\n;; --------------------------------------------------------------------------\n;; 10. Public API: resolve-relative-url (structural + keywords)\n;; --------------------------------------------------------------------------\n\n(define _normalize-relative :effects []\n (fn ((url :as string))\n ;; Normalize bare-dot shorthand to paren form.\n ;; \"..\" \u2192 \"(..)\"\n ;; \".slug\" \u2192 \"(.slug)\"\n ;; \".:page.4\" \u2192 \"(.:page.4)\"\n ;; \"(.slug)\" \u2192 \"(.slug)\" (already canonical)\n (if (starts-with? url \"(\")\n url\n (str \"(\" url \")\"))))\n\n(define resolve-relative-url :effects []\n (fn ((current :as string) (relative :as string))\n ;; current: absolute SX URL \"/(geography.(hypermedia.(example)))\"\n ;; relative: relative SX URL \"(.progress-bar)\" or \"..\" or \".:page.+1\"\n ;; Returns: absolute SX URL\n (let ((canonical (_normalize-relative relative)))\n (let ((rel-inner (slice canonical 1 (- (len canonical) 1))))\n (let ((dots (_count-leading-dots rel-inner))\n (body (slice rel-inner (_count-leading-dots rel-inner))))\n (if (= dots 0)\n current ;; no dots \u2014 not a relative URL\n ;; Parse body into positional part + keyword pairs\n (let ((parsed (_parse-relative-body body))\n (pos-body (get parsed \"positional\"))\n (kw-pairs (get parsed \"keywords\")))\n ;; Step 1: structural navigation\n (let ((after-nav\n (if (= dots 1)\n ;; One dot = current level\n (if (empty? pos-body)\n current ;; no positional \u2192 stay here (keyword-only)\n ;; Append positional part at current level\n (let ((stripped (_strip-trailing-close current))\n (suffix (slice current (len (_strip-trailing-close current)))))\n (str stripped \".\" pos-body suffix)))\n ;; Two+ dots = pop (dots-1) levels\n (let ((base (_pop-sx-url-levels current (- dots 1))))\n (if (empty? pos-body)\n base ;; no positional \u2192 just pop (cd ..)\n (if (= base \"/\")\n (str \"/(\" pos-body \")\")\n (let ((stripped (_strip-trailing-close base))\n (suffix (slice base (len (_strip-trailing-close base)))))\n (str stripped \".(\" pos-body \")\" suffix))))))))\n ;; Step 2: apply keyword modifications\n (_apply-keywords-to-url after-nav kw-pairs)))))))))\n\n;; Check if a URL is relative (starts with ( but not /( , or starts with .)\n(define relative-sx-url? :effects []\n (fn ((url :as string))\n (or (and (starts-with? url \"(\")\n (not (starts-with? url \"/(\")))\n (starts-with? url \".\"))))\n\n\n;; --------------------------------------------------------------------------\n;; 11. URL special forms (! prefix)\n;; --------------------------------------------------------------------------\n;; Special forms are meta-operations on URL expressions.\n;; Distinguished by `!` prefix to avoid name collisions with sections/pages.\n;;\n;; Known forms:\n;; !source \u2014 show defcomp source code\n;; !inspect \u2014 deps, CSS footprint, render plan, IO\n;; !diff \u2014 side-by-side comparison of two expressions\n;; !search \u2014 grep within a page/spec\n;; !raw \u2014 skip ~sx-doc wrapping, return raw content\n;; !json \u2014 return content as JSON data\n;;\n;; URL examples:\n;; /(!source.(~essay-sx-sucks))\n;; /(!inspect.(language.(doc.primitives)))\n;; /(!diff.(language.(spec.signals)).(language.(spec.eval)))\n;; /(!search.\"define\".:in.(language.(spec.signals)))\n;; /(!raw.(~some-component))\n;; /(!json.(language.(doc.primitives)))\n\n(define _url-special-forms :effects []\n (fn ()\n ;; Returns the set of known URL special form names.\n (list \"!source\" \"!inspect\" \"!diff\" \"!search\" \"!raw\" \"!json\")))\n\n(define url-special-form? :effects []\n (fn ((name :as string))\n ;; Check if a name is a URL special form (starts with ! and is known).\n (and (starts-with? name \"!\")\n (contains? (_url-special-forms) name))))\n\n(define parse-sx-url :effects []\n (fn ((url :as string))\n ;; Parse an SX URL into a structured descriptor.\n ;; Returns a dict with:\n ;; \"type\" \u2014 \"home\" | \"absolute\" | \"relative\" | \"special-form\" | \"direct-component\"\n ;; \"form\" \u2014 special form name (for special-form type), e.g. \"!source\"\n ;; \"inner\" \u2014 inner URL expression string (without the special form wrapper)\n ;; \"raw\" \u2014 original URL string\n ;;\n ;; Examples:\n ;; \"/\" \u2192 {\"type\" \"home\" \"raw\" \"/\"}\n ;; \"/(language.(doc.intro))\" \u2192 {\"type\" \"absolute\" \"raw\" ...}\n ;; \"(.slug)\" \u2192 {\"type\" \"relative\" \"raw\" ...}\n ;; \"..slug\" \u2192 {\"type\" \"relative\" \"raw\" ...}\n ;; \"/(!source.(~essay))\" \u2192 {\"type\" \"special-form\" \"form\" \"!source\" \"inner\" \"(~essay)\" \"raw\" ...}\n ;; \"/(~essay-sx-sucks)\" \u2192 {\"type\" \"direct-component\" \"name\" \"~essay-sx-sucks\" \"raw\" ...}\n (cond\n (= url \"/\")\n {\"type\" \"home\" \"raw\" url}\n (relative-sx-url? url)\n {\"type\" \"relative\" \"raw\" url}\n (and (starts-with? url \"/(!\")\n (ends-with? url \")\"))\n ;; Special form: /(!source.(~essay)) or /(!diff.a.b)\n ;; Extract the form name (first dot-separated token after /()\n (let ((inner (slice url 2 (- (len url) 1))))\n ;; inner = \"!source.(~essay)\" or \"!diff.(a).(b)\"\n (let ((dot-pos (_index-of-safe inner \".\"))\n (paren-pos (_index-of-safe inner \"(\")))\n ;; Form name ends at first . or ( (whichever comes first)\n (let ((end-pos (cond\n (and (nil? dot-pos) (nil? paren-pos)) (len inner)\n (nil? dot-pos) paren-pos\n (nil? paren-pos) dot-pos\n :else (min dot-pos paren-pos))))\n (let ((form-name (slice inner 0 end-pos))\n (rest-part (slice inner end-pos)))\n ;; rest-part starts with \".\" \u2192 strip leading dot\n (let ((inner-expr (if (starts-with? rest-part \".\")\n (slice rest-part 1)\n rest-part)))\n {\"type\" \"special-form\"\n \"form\" form-name\n \"inner\" inner-expr\n \"raw\" url})))))\n (and (starts-with? url \"/(~\")\n (ends-with? url \")\"))\n ;; Direct component: /(~essay-sx-sucks)\n (let ((name (slice url 2 (- (len url) 1))))\n {\"type\" \"direct-component\" \"name\" name \"raw\" url})\n (and (starts-with? url \"/(\")\n (ends-with? url \")\"))\n {\"type\" \"absolute\" \"raw\" url}\n :else\n {\"type\" \"path\" \"raw\" url})))\n\n(define url-special-form-name :effects []\n (fn ((url :as string))\n ;; Extract the special form name from a URL, or nil if not a special form.\n ;; \"/(!source.(~essay))\" \u2192 \"!source\"\n ;; \"/(language.(doc))\" \u2192 nil\n (let ((parsed (parse-sx-url url)))\n (if (= (get parsed \"type\") \"special-form\")\n (get parsed \"form\")\n nil))))\n\n(define url-special-form-inner :effects []\n (fn ((url :as string))\n ;; Extract the inner expression from a special form URL, or nil.\n ;; \"/(!source.(~essay))\" \u2192 \"(~essay)\"\n ;; \"/(!diff.(a).(b))\" \u2192 \"(a).(b)\"\n (let ((parsed (parse-sx-url url)))\n (if (= (get parsed \"type\") \"special-form\")\n (get parsed \"inner\")\n nil))))\n\n\n;; --------------------------------------------------------------------------\n;; 12. URL expression evaluation\n;; --------------------------------------------------------------------------\n;; A URL is an expression. The system is the environment.\n;; eval(url, env) \u2014 that's it.\n;;\n;; The only URL-specific pre-processing:\n;; 1. Surface syntax \u2192 AST (dots to spaces, parse as SX)\n;; 2. Auto-quote unknowns (symbols not in env become strings)\n;;\n;; After that, it's standard eval. The host wires these into its route\n;; handlers (Python catch-all, JS client-side navigation). The same\n;; functions serve both.\n\n(define url-to-expr :effects []\n (fn ((url-path :as string))\n ;; Convert a URL path to an SX expression (AST).\n ;;\n ;; \"/sx/(language.(doc.introduction))\" \u2192 (language (doc introduction))\n ;; \"/(language.(doc.introduction))\" \u2192 (language (doc introduction))\n ;; \"/\" \u2192 (list) ; empty \u2014 home\n ;;\n ;; Steps:\n ;; 1. Strip URL prefix (\"/sx/\" or \"/\") \u2014 host passes the path after prefix\n ;; 2. Dots \u2192 spaces (URL-safe whitespace encoding)\n ;; 3. Parse as SX expression\n ;;\n ;; The caller is responsible for stripping any app-level prefix.\n ;; This function receives the raw expression portion: \"(language.(doc.intro))\"\n ;; or \"/\" for home.\n (if (or (= url-path \"/\") (empty? url-path))\n (list)\n (let ((trimmed (if (starts-with? url-path \"/\")\n (slice url-path 1)\n url-path)))\n ;; Dots \u2192 spaces\n (let ((sx-source (replace trimmed \".\" \" \")))\n ;; Parse \u2014 returns list of expressions, take the first\n (let ((exprs (sx-parse sx-source)))\n (if (empty? exprs)\n (list)\n (first exprs))))))))\n\n\n(define auto-quote-unknowns :effects []\n (fn ((expr :as list) (env :as dict))\n ;; Walk an AST and replace symbols not in env with their name as a string.\n ;; This makes URL slugs work without quoting:\n ;; (language (doc introduction)) ; introduction is not a function\n ;; \u2192 (language (doc \"introduction\"))\n ;;\n ;; Rules:\n ;; - List head (call position) stays as-is \u2014 it's a function name\n ;; - Tail symbols: if in env, keep as symbol; otherwise, string\n ;; - Keywords, strings, numbers, nested lists: pass through\n ;; - Non-list expressions: pass through unchanged\n (if (not (list? expr))\n expr\n (if (empty? expr)\n expr\n ;; Head stays as symbol (function position), quote the rest\n (cons (first expr)\n (map (fn (child)\n (cond\n ;; Nested list \u2014 recurse\n (list? child)\n (auto-quote-unknowns child env)\n ;; Symbol \u2014 check env\n (= (type-of child) \"symbol\")\n (let ((name (symbol-name child)))\n (if (or (env-has? env name)\n ;; Keep keywords, component refs, special forms\n (starts-with? name \":\")\n (starts-with? name \"~\")\n (starts-with? name \"!\"))\n child\n name)) ;; unknown \u2192 string\n ;; Everything else passes through\n :else child))\n (rest expr)))))))\n\n\n(define prepare-url-expr :effects []\n (fn ((url-path :as string) (env :as dict))\n ;; Full pipeline: URL path \u2192 ready-to-eval AST.\n ;;\n ;; \"(language.(doc.introduction))\" + env\n ;; \u2192 (language (doc \"introduction\"))\n ;;\n ;; The result can be fed directly to eval:\n ;; (eval (prepare-url-expr path env) env)\n (let ((expr (url-to-expr url-path)))\n (if (empty? expr)\n expr\n (auto-quote-unknowns expr env)))))\n\n\n;; --------------------------------------------------------------------------\n;; Platform interface\n;; --------------------------------------------------------------------------\n;; Pure primitives used:\n;; split, slice, starts-with?, ends-with?, len, empty?, replace,\n;; map, filter, for-each, for-each-indexed, nth, get, dict-set!, merge,\n;; list, nil?, not, =, case, join, str, index-of, and, or, cons,\n;; first, rest, append, parse-int, contains?, min, cond,\n;; symbol?, symbol-name, list?, env-has?, type-of\n;;\n;; From parser.sx: sx-parse, sx-serialize\n;; --------------------------------------------------------------------------\n"; globalThis.__sxAdapters["adapter-html"] = ";; ==========================================================================\n;; adapter-html.sx \u2014 HTML string rendering adapter\n;;\n;; Renders evaluated SX expressions to HTML strings. Used server-side.\n;;\n;; Depends on:\n;; render.sx \u2014 HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,\n;; parse-element-args, render-attrs, definition-form?\n;; eval.sx \u2014 eval-expr, trampoline, expand-macro, process-bindings,\n;; eval-cond, env-has?, env-get, env-set!, env-merge,\n;; lambda?, component?, island?, macro?,\n;; lambda-closure, lambda-params, lambda-body\n;; ==========================================================================\n\n\n(define render-to-html :effects [render]\n (fn (expr (env :as dict))\n (set-render-active! true)\n (case (type-of expr)\n ;; Literals \u2014 render directly\n \"nil\" \"\"\n \"string\" (escape-html expr)\n \"number\" (str expr)\n \"boolean\" (if expr \"true\" \"false\")\n ;; List \u2014 dispatch to render-list which handles HTML tags, special forms, etc.\n \"list\" (if (empty? expr) \"\" (render-list-to-html expr env))\n ;; Symbol \u2014 evaluate then render\n \"symbol\" (render-value-to-html (trampoline (eval-expr expr env)) env)\n ;; Keyword \u2014 render as text\n \"keyword\" (escape-html (keyword-name expr))\n ;; Raw HTML passthrough\n \"raw-html\" (raw-html-content expr)\n ;; Spread \u2014 emit attrs to nearest element provider\n \"spread\" (do (emit! \"element-attrs\" (spread-attrs expr)) \"\")\n ;; Everything else \u2014 evaluate first\n :else (render-value-to-html (trampoline (eval-expr expr env)) env))))\n\n(define render-value-to-html :effects [render]\n (fn (val (env :as dict))\n (case (type-of val)\n \"nil\" \"\"\n \"string\" (escape-html val)\n \"number\" (str val)\n \"boolean\" (if val \"true\" \"false\")\n \"list\" (render-list-to-html val env)\n \"raw-html\" (raw-html-content val)\n \"spread\" (do (emit! \"element-attrs\" (spread-attrs val)) \"\")\n :else (escape-html (str val)))))\n\n\n;; --------------------------------------------------------------------------\n;; Render-aware form classification\n;; --------------------------------------------------------------------------\n\n(define RENDER_HTML_FORMS\n (list \"if\" \"when\" \"cond\" \"case\" \"let\" \"let*\" \"begin\" \"do\"\n \"define\" \"defcomp\" \"defisland\" \"defmacro\" \"defstyle\" \"defhandler\"\n \"deftype\" \"defeffect\"\n \"map\" \"map-indexed\" \"filter\" \"for-each\" \"scope\" \"provide\"))\n\n(define render-html-form? :effects []\n (fn ((name :as string))\n (contains? RENDER_HTML_FORMS name)))\n\n\n;; --------------------------------------------------------------------------\n;; render-list-to-html \u2014 dispatch on list head\n;; --------------------------------------------------------------------------\n\n(define render-list-to-html :effects [render]\n (fn ((expr :as list) (env :as dict))\n (if (empty? expr)\n \"\"\n (let ((head (first expr)))\n (if (not (= (type-of head) \"symbol\"))\n ;; Data list \u2014 render each item\n (join \"\" (map (fn (x) (render-value-to-html x env)) expr))\n (let ((name (symbol-name head))\n (args (rest expr)))\n (cond\n ;; Fragment\n (= name \"<>\")\n (join \"\" (map (fn (x) (render-to-html x env)) args))\n\n ;; Raw HTML passthrough\n (= name \"raw!\")\n (join \"\" (map (fn (x) (str (trampoline (eval-expr x env)))) args))\n\n ;; Lake \u2014 server-morphable slot within an island\n (= name \"lake\")\n (render-html-lake args env)\n\n ;; Marsh \u2014 reactive server-morphable slot within an island\n (= name \"marsh\")\n (render-html-marsh args env)\n\n ;; HTML tag\n (contains? HTML_TAGS name)\n (render-html-element name args env)\n\n ;; Island (~name) \u2014 reactive component, SSR with hydration markers\n (and (starts-with? name \"~\")\n (env-has? env name)\n (island? (env-get env name)))\n (render-html-island (env-get env name) args env)\n\n ;; Component or macro call (~name)\n (starts-with? name \"~\")\n (let ((val (env-get env name)))\n (cond\n (component? val)\n (render-html-component val args env)\n (macro? val)\n (render-to-html\n (expand-macro val args env)\n env)\n :else\n (error (str \"Unknown component: \" name))))\n\n ;; Render-aware special forms\n (render-html-form? name)\n (dispatch-html-form name expr env)\n\n ;; Macro expansion\n (and (env-has? env name) (macro? (env-get env name)))\n (render-to-html\n (expand-macro (env-get env name) args env)\n env)\n\n ;; Fallback \u2014 evaluate then render result\n :else\n (render-value-to-html\n (trampoline (eval-expr expr env))\n env))))))))\n\n\n;; --------------------------------------------------------------------------\n;; dispatch-html-form \u2014 render-aware special form handling for HTML output\n;; --------------------------------------------------------------------------\n\n(define dispatch-html-form :effects [render]\n (fn ((name :as string) (expr :as list) (env :as dict))\n (cond\n ;; if\n (= name \"if\")\n (let ((cond-val (trampoline (eval-expr (nth expr 1) env))))\n (if cond-val\n (render-to-html (nth expr 2) env)\n (if (> (len expr) 3)\n (render-to-html (nth expr 3) env)\n \"\")))\n\n ;; when \u2014 single body: pass through. Multi: join strings.\n (= name \"when\")\n (if (not (trampoline (eval-expr (nth expr 1) env)))\n \"\"\n (if (= (len expr) 3)\n (render-to-html (nth expr 2) env)\n (join \"\" (map (fn (i) (render-to-html (nth expr i) env))\n (range 2 (len expr))))))\n\n ;; cond\n (= name \"cond\")\n (let ((branch (eval-cond (rest expr) env)))\n (if branch\n (render-to-html branch env)\n \"\"))\n\n ;; case\n (= name \"case\")\n (render-to-html (trampoline (eval-expr expr env)) env)\n\n ;; let / let* \u2014 single body: pass through. Multi: join strings.\n (or (= name \"let\") (= name \"let*\"))\n (let ((local (process-bindings (nth expr 1) env)))\n (if (= (len expr) 3)\n (render-to-html (nth expr 2) local)\n (join \"\" (map (fn (i) (render-to-html (nth expr i) local))\n (range 2 (len expr))))))\n\n ;; begin / do \u2014 single body: pass through. Multi: join strings.\n (or (= name \"begin\") (= name \"do\"))\n (if (= (len expr) 2)\n (render-to-html (nth expr 1) env)\n (join \"\" (map (fn (i) (render-to-html (nth expr i) env))\n (range 1 (len expr)))))\n\n ;; Definition forms \u2014 eval for side effects\n (definition-form? name)\n (do (trampoline (eval-expr expr env)) \"\")\n\n ;; map\n (= name \"map\")\n (let ((f (trampoline (eval-expr (nth expr 1) env)))\n (coll (trampoline (eval-expr (nth expr 2) env))))\n (join \"\"\n (map\n (fn (item)\n (if (lambda? f)\n (render-lambda-html f (list item) env)\n (render-to-html (apply f (list item)) env)))\n coll)))\n\n ;; map-indexed\n (= name \"map-indexed\")\n (let ((f (trampoline (eval-expr (nth expr 1) env)))\n (coll (trampoline (eval-expr (nth expr 2) env))))\n (join \"\"\n (map-indexed\n (fn (i item)\n (if (lambda? f)\n (render-lambda-html f (list i item) env)\n (render-to-html (apply f (list i item)) env)))\n coll)))\n\n ;; filter \u2014 evaluate fully then render\n (= name \"filter\")\n (render-to-html (trampoline (eval-expr expr env)) env)\n\n ;; for-each (render variant)\n (= name \"for-each\")\n (let ((f (trampoline (eval-expr (nth expr 1) env)))\n (coll (trampoline (eval-expr (nth expr 2) env))))\n (join \"\"\n (map\n (fn (item)\n (if (lambda? f)\n (render-lambda-html f (list item) env)\n (render-to-html (apply f (list item)) env)))\n coll)))\n\n ;; scope \u2014 unified render-time dynamic scope\n (= name \"scope\")\n (let ((scope-name (trampoline (eval-expr (nth expr 1) env)))\n (rest-args (slice expr 2))\n (scope-val nil)\n (body-exprs nil))\n ;; Check for :value keyword\n (if (and (>= (len rest-args) 2)\n (= (type-of (first rest-args)) \"keyword\")\n (= (keyword-name (first rest-args)) \"value\"))\n (do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))\n (set! body-exprs (slice rest-args 2)))\n (set! body-exprs rest-args))\n (scope-push! scope-name scope-val)\n (let ((result (if (= (len body-exprs) 1)\n (render-to-html (first body-exprs) env)\n (join \"\" (map (fn (e) (render-to-html e env)) body-exprs)))))\n (scope-pop! scope-name)\n result))\n\n ;; provide \u2014 sugar for scope with value\n (= name \"provide\")\n (let ((prov-name (trampoline (eval-expr (nth expr 1) env)))\n (prov-val (trampoline (eval-expr (nth expr 2) env)))\n (body-start 3)\n (body-count (- (len expr) 3)))\n (scope-push! prov-name prov-val)\n (let ((result (if (= body-count 1)\n (render-to-html (nth expr body-start) env)\n (join \"\" (map (fn (i) (render-to-html (nth expr i) env))\n (range body-start (+ body-start body-count)))))))\n (scope-pop! prov-name)\n result))\n\n ;; Fallback\n :else\n (render-value-to-html (trampoline (eval-expr expr env)) env))))\n\n\n;; --------------------------------------------------------------------------\n;; render-lambda-html \u2014 render a lambda body in HTML context\n;; --------------------------------------------------------------------------\n\n(define render-lambda-html :effects [render]\n (fn ((f :as lambda) (args :as list) (env :as dict))\n (let ((local (env-merge (lambda-closure f) env)))\n (for-each-indexed\n (fn (i p)\n (env-bind! local p (nth args i)))\n (lambda-params f))\n (render-to-html (lambda-body f) local))))\n\n\n;; --------------------------------------------------------------------------\n;; render-html-component \u2014 expand and render a component\n;; --------------------------------------------------------------------------\n\n(define render-html-component :effects [render]\n (fn ((comp :as component) (args :as list) (env :as dict))\n ;; Expand component and render body through HTML adapter.\n ;; Component body contains rendering forms (HTML tags) that only the\n ;; adapter understands, so expansion must happen here, not in eval-expr.\n (let ((kwargs (dict))\n (children (list)))\n ;; Separate keyword args from positional children\n (reduce\n (fn (state arg)\n (let ((skip (get state \"skip\")))\n (if skip\n (assoc state \"skip\" false \"i\" (inc (get state \"i\")))\n (if (and (= (type-of arg) \"keyword\")\n (< (inc (get state \"i\")) (len args)))\n (let ((val (trampoline\n (eval-expr (nth args (inc (get state \"i\"))) env))))\n (dict-set! kwargs (keyword-name arg) val)\n (assoc state \"skip\" true \"i\" (inc (get state \"i\"))))\n (do\n (append! children arg)\n (assoc state \"i\" (inc (get state \"i\"))))))))\n (dict \"i\" 0 \"skip\" false)\n args)\n ;; Build component env: closure + caller env + params\n (let ((local (env-merge (component-closure comp) env)))\n ;; Bind params from kwargs\n (for-each\n (fn (p)\n (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))\n (component-params comp))\n ;; If component accepts children, pre-render them to raw HTML\n (when (component-has-children? comp)\n (env-bind! local \"children\"\n (make-raw-html (join \"\" (map (fn (c) (render-to-html c env)) children)))))\n (render-to-html (component-body comp) local)))))\n\n\n(define render-html-element :effects [render]\n (fn ((tag :as string) (args :as list) (env :as dict))\n (let ((parsed (parse-element-args args env))\n (attrs (first parsed))\n (children (nth parsed 1))\n (is-void (contains? VOID_ELEMENTS tag)))\n (if is-void\n (str \"<\" tag (render-attrs attrs) \" />\")\n ;; Provide scope for spread emit!\n (do\n (scope-push! \"element-attrs\" nil)\n (let ((content (join \"\" (map (fn (c) (render-to-html c env)) children))))\n (for-each\n (fn (spread-dict) (merge-spread-attrs attrs spread-dict))\n (emitted \"element-attrs\"))\n (scope-pop! \"element-attrs\")\n (str \"<\" tag (render-attrs attrs) \">\"\n content\n \"\")))))))\n\n\n;; --------------------------------------------------------------------------\n;; render-html-lake \u2014 SSR rendering of a server-morphable slot\n;; --------------------------------------------------------------------------\n;;\n;; (lake :id \"name\" children...) \u2192
children
\n;;\n;; Lakes are server territory inside islands. The morph can update lake\n;; content while preserving surrounding reactive DOM.\n\n(define render-html-lake :effects [render]\n (fn ((args :as list) (env :as dict))\n (let ((lake-id nil)\n (lake-tag \"div\")\n (children (list)))\n (reduce\n (fn (state arg)\n (let ((skip (get state \"skip\")))\n (if skip\n (assoc state \"skip\" false \"i\" (inc (get state \"i\")))\n (if (and (= (type-of arg) \"keyword\")\n (< (inc (get state \"i\")) (len args)))\n (let ((kname (keyword-name arg))\n (kval (trampoline (eval-expr (nth args (inc (get state \"i\"))) env))))\n (cond\n (= kname \"id\") (set! lake-id kval)\n (= kname \"tag\") (set! lake-tag kval))\n (assoc state \"skip\" true \"i\" (inc (get state \"i\"))))\n (do\n (append! children arg)\n (assoc state \"i\" (inc (get state \"i\"))))))))\n (dict \"i\" 0 \"skip\" false)\n args)\n ;; Provide scope for spread emit!\n (let ((lake-attrs (dict \"data-sx-lake\" (or lake-id \"\"))))\n (scope-push! \"element-attrs\" nil)\n (let ((content (join \"\" (map (fn (c) (render-to-html c env)) children))))\n (for-each\n (fn (spread-dict) (merge-spread-attrs lake-attrs spread-dict))\n (emitted \"element-attrs\"))\n (scope-pop! \"element-attrs\")\n (str \"<\" lake-tag (render-attrs lake-attrs) \">\"\n content\n \"\"))))))\n\n\n;; --------------------------------------------------------------------------\n;; render-html-marsh \u2014 SSR rendering of a reactive server-morphable slot\n;; --------------------------------------------------------------------------\n;;\n;; (marsh :id \"name\" :tag \"div\" :transform fn children...)\n;; \u2192
children
\n;;\n;; Like a lake but reactive: during morph, new content is parsed as SX and\n;; re-evaluated in the island's signal scope. Server renders children normally;\n;; the :transform is a client-only concern.\n\n(define render-html-marsh :effects [render]\n (fn ((args :as list) (env :as dict))\n (let ((marsh-id nil)\n (marsh-tag \"div\")\n (children (list)))\n (reduce\n (fn (state arg)\n (let ((skip (get state \"skip\")))\n (if skip\n (assoc state \"skip\" false \"i\" (inc (get state \"i\")))\n (if (and (= (type-of arg) \"keyword\")\n (< (inc (get state \"i\")) (len args)))\n (let ((kname (keyword-name arg))\n (kval (trampoline (eval-expr (nth args (inc (get state \"i\"))) env))))\n (cond\n (= kname \"id\") (set! marsh-id kval)\n (= kname \"tag\") (set! marsh-tag kval)\n (= kname \"transform\") nil)\n (assoc state \"skip\" true \"i\" (inc (get state \"i\"))))\n (do\n (append! children arg)\n (assoc state \"i\" (inc (get state \"i\"))))))))\n (dict \"i\" 0 \"skip\" false)\n args)\n ;; Provide scope for spread emit!\n (let ((marsh-attrs (dict \"data-sx-marsh\" (or marsh-id \"\"))))\n (scope-push! \"element-attrs\" nil)\n (let ((content (join \"\" (map (fn (c) (render-to-html c env)) children))))\n (for-each\n (fn (spread-dict) (merge-spread-attrs marsh-attrs spread-dict))\n (emitted \"element-attrs\"))\n (scope-pop! \"element-attrs\")\n (str \"<\" marsh-tag (render-attrs marsh-attrs) \">\"\n content\n \"\"))))))\n\n\n;; --------------------------------------------------------------------------\n;; render-html-island \u2014 SSR rendering of a reactive island\n;; --------------------------------------------------------------------------\n;;\n;; Renders the island body as static HTML wrapped in a container element\n;; with data-sx-island and data-sx-state attributes. The client hydrates\n;; this by finding these elements and re-rendering with reactive context.\n;;\n;; On the server, signal/deref/reset!/swap! are simple passthrough:\n;; (signal val) \u2192 returns val (no container needed server-side)\n;; (deref s) \u2192 returns s (signal values are plain values server-side)\n;; (reset! s v) \u2192 no-op\n;; (swap! s f) \u2192 no-op\n\n(define render-html-island :effects [render]\n (fn ((island :as island) (args :as list) (env :as dict))\n ;; Parse kwargs and children (same pattern as render-html-component)\n (let ((kwargs (dict))\n (children (list)))\n (reduce\n (fn (state arg)\n (let ((skip (get state \"skip\")))\n (if skip\n (assoc state \"skip\" false \"i\" (inc (get state \"i\")))\n (if (and (= (type-of arg) \"keyword\")\n (< (inc (get state \"i\")) (len args)))\n (let ((val (trampoline\n (eval-expr (nth args (inc (get state \"i\"))) env))))\n (dict-set! kwargs (keyword-name arg) val)\n (assoc state \"skip\" true \"i\" (inc (get state \"i\"))))\n (do\n (append! children arg)\n (assoc state \"i\" (inc (get state \"i\"))))))))\n (dict \"i\" 0 \"skip\" false)\n args)\n\n ;; Build island env: closure + caller env + params\n (let ((local (env-merge (component-closure island) env))\n (island-name (component-name island)))\n\n ;; Bind params from kwargs\n (for-each\n (fn (p)\n (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))\n (component-params island))\n\n ;; If island accepts children, pre-render them to raw HTML\n (when (component-has-children? island)\n (env-bind! local \"children\"\n (make-raw-html (join \"\" (map (fn (c) (render-to-html c env)) children)))))\n\n ;; Render the island body as HTML\n (let ((body-html (render-to-html (component-body island) local))\n (state-sx (serialize-island-state kwargs)))\n ;; Wrap in container with hydration attributes\n (str \"\"\n body-html\n \"\"))))))\n\n\n;; --------------------------------------------------------------------------\n;; serialize-island-state \u2014 serialize kwargs to SX for hydration\n;; --------------------------------------------------------------------------\n;;\n;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.\n;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.\n\n(define serialize-island-state :effects []\n (fn ((kwargs :as dict))\n (if (empty-dict? kwargs)\n nil\n (sx-serialize kwargs))))\n\n\n;; --------------------------------------------------------------------------\n;; Platform interface \u2014 HTML adapter\n;; --------------------------------------------------------------------------\n;;\n;; Inherited from render.sx:\n;; escape-html, escape-attr, raw-html-content\n;;\n;; From eval.sx:\n;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond\n;; env-has?, env-get, env-set!, env-merge\n;; lambda?, component?, island?, macro?\n;; lambda-closure, lambda-params, lambda-body\n;; component-params, component-body, component-closure,\n;; component-has-children?, component-name\n;;\n;; Raw HTML construction:\n;; (make-raw-html s) \u2192 wrap string as raw HTML (not double-escaped)\n;;\n;; Island state serialization:\n;; (sx-serialize val) \u2192 SX source string (from parser.sx)\n;; (empty-dict? d) \u2192 boolean\n;; (escape-attr s) \u2192 HTML attribute escape\n;;\n;; Iteration:\n;; (for-each-indexed fn coll) \u2192 call fn(index, item) for each element\n;; (map-indexed fn coll) \u2192 map fn(index, item) over each element\n;; --------------------------------------------------------------------------\n"; // ========================================================================= // WASM Boot: load adapters, then process inline