/** * sx-platform.js — Browser platform layer for the SX WASM kernel. * * Registers the 8 FFI host primitives and loads web adapter .sx files. * This is the only JS needed beyond the WASM kernel itself. * * Usage: * * * * Or for js_of_ocaml mode: * * */ (function() { "use strict"; function boot(K) { // ================================================================ // FFI Host Primitives // ================================================================ // Lazy module loading — islands/components call this to declare dependencies K.registerNative("load-library!", function(args) { var name = args[0]; if (!name) return false; return __sxLoadLibrary(name) || false; }); K.registerNative("host-global", function(args) { var name = args[0]; if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name]; if (typeof window !== "undefined" && name in window) return window[name]; return null; }); K.registerNative("host-get", function(args) { var obj = args[0], prop = args[1]; if (obj == null) return null; var v = obj[prop]; if (v === undefined) return null; // Functions can't cross the WASM boundary — return true as a truthy // sentinel so (host-get el "getAttribute") works as a guard. // Use host-call to actually invoke the method. if (typeof v === "function") return true; return v; }); K.registerNative("host-set!", function(args) { var obj = args[0], prop = args[1], val = args[2]; if (obj != null) obj[prop] = val; }); K.registerNative("host-call", function(args) { var obj = args[0], method = args[1]; var callArgs = []; for (var i = 2; i < args.length; i++) callArgs.push(args[i]); if (obj == null) { // Global function call var fn = typeof globalThis !== "undefined" ? globalThis[method] : window[method]; if (typeof fn === "function") return fn.apply(null, callArgs); return null; } if (typeof obj[method] === "function") { try { return obj[method].apply(obj, callArgs); } catch(e) { console.error("[sx] host-call error:", e); return null; } } return null; }); K.registerNative("host-new", function(args) { var name = args[0]; var cArgs = args.slice(1); var Ctor = typeof globalThis !== "undefined" ? globalThis[name] : window[name]; if (typeof Ctor !== "function") return null; switch (cArgs.length) { case 0: return new Ctor(); case 1: return new Ctor(cArgs[0]); case 2: return new Ctor(cArgs[0], cArgs[1]); case 3: return new Ctor(cArgs[0], cArgs[1], cArgs[2]); default: return new Ctor(cArgs[0], cArgs[1], cArgs[2], cArgs[3]); } }); // IO suspension driver — resumes suspended callFn results (wait, fetch, etc.) if (!window._driveAsync) { window._driveAsync = function driveAsync(result) { if (!result || !result.suspended) return; var req = result.request; var items = req && (req.items || req); var op = items && items[0]; var opName = typeof op === "string" ? op : (op && op.name) || String(op); var arg = items && items[1]; if (opName === "io-sleep" || opName === "wait") { setTimeout(function() { try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); } }, typeof arg === "number" ? arg : 0); } else if (opName === "io-fetch") { var fetchUrl = typeof arg === "string" ? arg : ""; var fetchMethod = (items && items[2]) || "GET"; var fetchBody = items && items[3]; var fetchHeaders = items && items[4]; var fetchOpts = { method: typeof fetchMethod === "string" ? fetchMethod : "GET" }; if (fetchBody && typeof fetchBody !== "boolean") { fetchOpts.body = typeof fetchBody === "string" ? fetchBody : JSON.stringify(fetchBody); } if (fetchHeaders && typeof fetchHeaders === "object") { var h = {}; var keys = fetchHeaders._keys || Object.keys(fetchHeaders); for (var fi = 0; fi < keys.length; fi++) { var k = keys[fi], v = fetchHeaders[k]; if (typeof k === "string" && typeof v === "string") h[k] = v; } fetchOpts.headers = h; } fetch(fetchUrl, fetchOpts).then(function(r) { var hdrs = {}; try { r.headers.forEach(function(v, k) { hdrs[k] = v; }); } catch(e) {} return r.text().then(function(t) { return { status: r.status, body: t, headers: hdrs, ok: r.ok }; }); }).then(function(resp) { try { driveAsync(result.resume(resp)); } catch(e) { console.error("[sx] driveAsync:", e.message); } }).catch(function(e) { try { driveAsync(result.resume({status: 0, body: "", headers: {}, ok: false})); } catch(e2) { console.error("[sx] driveAsync:", e2.message); } }); } else if (opName === "io-navigate") { // navigation — don't resume } else if (opName === "text-measure") { // Pretext: measure text using offscreen canvas var font = arg; var size = items && items[2]; var text = items && items[3]; var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.font = (size || 16) + "px " + (font || "serif"); var m = ctx.measureText(text || ""); try { driveAsync(result.resume({ width: m.width, height: m.actualBoundingBoxAscent + m.actualBoundingBoxDescent, ascent: m.actualBoundingBoxAscent, descent: m.actualBoundingBoxDescent })); } catch(e) { console.error("[sx] driveAsync:", e.message); } } else { console.warn("[sx] unhandled IO:", opName); } }; } K.registerNative("host-callback", function(args) { var fn = args[0]; // Native JS function — pass through if (typeof fn === "function") return fn; // SX callable (has __sx_handle) — wrap as JS function if (fn && fn.__sx_handle !== undefined) { var wrappedFn = function() { var a = Array.prototype.slice.call(arguments); var r = K.callFn(fn, a); if (window._driveAsync) window._driveAsync(r); return r; }; wrappedFn.__host_callback = true; return wrappedFn; } return function() {}; }); K.registerNative("host-typeof", function(args) { var obj = args[0]; if (obj == null) return "nil"; if (obj instanceof Element) return "element"; if (obj instanceof Text) return "text"; if (obj instanceof DocumentFragment) return "fragment"; if (obj instanceof Document) return "document"; if (obj instanceof Event) return "event"; if (obj instanceof Promise) return "promise"; if (obj instanceof AbortController) return "abort-controller"; return typeof obj; }); K.registerNative("host-await", function(args) { var promise = args[0], callback = args[1]; if (promise && typeof promise.then === "function") { var cb; if (typeof callback === "function") cb = callback; else if (callback && callback.__sx_handle !== undefined) cb = function(v) { return K.callFn(callback, [v]); }; else cb = function() {}; promise.then(cb); } }); // ================================================================ // Constants expected by .sx files // ================================================================ K.eval('(define SX_VERSION "wasm-1.0")'); K.eval('(define SX_ENGINE "ocaml-vm-wasm")'); K.eval('(define parse sx-parse)'); K.eval('(define serialize sx-serialize)'); // ================================================================ // DOM query helpers used by boot.sx / orchestration.sx // (These are JS-native in the transpiled bundle; here via FFI.) // ================================================================ K.registerNative("query-sx-scripts", function(args) { var root = (args[0] && args[0] !== null) ? args[0] : document; if (typeof root.querySelectorAll !== "function") root = document; return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"]')); }); K.registerNative("query-page-scripts", function(args) { return Array.prototype.slice.call(document.querySelectorAll('script[type="text/sx-pages"]')); }); K.registerNative("query-component-scripts", function(args) { var root = (args[0] && args[0] !== null) ? args[0] : document; if (typeof root.querySelectorAll !== "function") root = document; return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"][data-components]')); }); // localStorage K.registerNative("local-storage-get", function(args) { try { var v = localStorage.getItem(args[0]); return v === null ? null : v; } catch(e) { return null; } }); K.registerNative("local-storage-set", function(args) { try { localStorage.setItem(args[0], args[1]); } catch(e) {} }); K.registerNative("local-storage-remove", function(args) { try { localStorage.removeItem(args[0]); } catch(e) {} }); // log-info/log-warn defined in browser.sx; log-error as native fallback K.registerNative("log-error", function(args) { console.error.apply(console, ["[sx]"].concat(args)); }); // Cookie access (browser-side) K.registerNative("get-cookie", function(args) { var name = args[0]; var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)')); return match ? decodeURIComponent(match[1]) : null; }); K.registerNative("set-cookie", function(args) { document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax"; }); // IntersectionObserver — native JS to avoid bytecode callback issues K.registerNative("observe-intersection", function(args) { var el = args[0], callback = args[1], once = args[2], delay = args[3]; var obs = new IntersectionObserver(function(entries) { for (var i = 0; i < entries.length; i++) { if (entries[i].isIntersecting) { var d = (delay && delay !== null) ? delay : 0; setTimeout(function() { K.callFn(callback, []); }, d); if (once) obs.unobserve(el); } } }); obs.observe(el); return obs; }); // ================================================================ // Load SX web libraries and adapters // ================================================================ // Load order follows dependency graph: // 1. Core spec files (parser, render, primitives already compiled into WASM kernel) // 2. Spec modules: signals, deps, router, page-helpers // 3. Bytecode compiler + VM (for JIT in browser) // 4. Web libraries: dom.sx, browser.sx (built on 8 FFI primitives) // 5. Web adapters: adapter-html, adapter-sx, adapter-dom // 6. Web framework: engine, orchestration, boot var _baseUrl = ""; // Detect base URL and cache-bust params from current script tag. // _cacheBust comes from the script's own ?v= query string (used for .sx source fallback). // _sxbcCacheBust comes from data-sxbc-hash attribute — a separate content hash // covering all .sxbc files so each file gets its own correct cache buster. var _cacheBust = ""; var _sxbcCacheBust = ""; (function() { if (typeof document !== "undefined") { var scripts = document.getElementsByTagName("script"); for (var i = scripts.length - 1; i >= 0; i--) { var src = scripts[i].src || ""; if (src.indexOf("sx-platform") !== -1) { _baseUrl = src.substring(0, src.lastIndexOf("/") + 1); var qi = src.indexOf("?"); if (qi !== -1) _cacheBust = src.substring(qi); var sxbcHash = scripts[i].getAttribute("data-sxbc-hash"); if (sxbcHash) _sxbcCacheBust = "?v=" + sxbcHash; break; } } // Content-addressed boot: script loaded from /sx/h/{hash}, not /static/wasm/. // Fall back to /static/wasm/ base URL for module-manifest.sx and .sx sources. if (!_baseUrl || _baseUrl.indexOf("/sx/h/") !== -1) { _baseUrl = "/static/wasm/"; } } })(); /** * Deserialize type-tagged JSON constant back to JS value for loadModule. */ function deserializeConstant(c) { if (!c || !c.t) return null; switch (c.t) { case 's': return c.v; case 'n': return c.v; case 'b': return c.v; case 'nil': return null; case 'sym': return { _type: 'symbol', name: c.v }; case 'kw': return { _type: 'keyword', name: c.v }; case 'list': return { _type: 'list', items: (c.v || []).map(deserializeConstant) }; case 'code': return { _type: 'dict', bytecode: { _type: 'list', items: c.v.bytecode }, constants: { _type: 'list', items: (c.v.constants || []).map(deserializeConstant) }, arity: c.v.arity || 0, 'upvalue-count': c.v['upvalue-count'] || 0, locals: c.v.locals || 0, }; case 'dict': { var d = { _type: 'dict' }; for (var k in c.v) d[k] = deserializeConstant(c.v[k]); return d; } default: return null; } } /** * Convert a parsed SX code form ({_type:"list", items:[symbol"code", ...]}) * into the dict format that K.loadModule / js_to_value expects. * Mirrors the OCaml convert_code/convert_const in sx_browser.ml. */ function convertCodeForm(form) { if (!form || form._type !== "list" || !form.items || !form.items.length) return null; var items = form.items; if (!items[0] || items[0]._type !== "symbol" || items[0].name !== "code") return null; var d = { _type: "dict", arity: 0, "upvalue-count": 0 }; for (var i = 1; i < items.length; i++) { var item = items[i]; if (item && item._type === "keyword" && i + 1 < items.length) { var val = items[i + 1]; if (item.name === "arity" || item.name === "upvalue-count") { d[item.name] = (typeof val === "number") ? val : 0; } else if (item.name === "bytecode" && val && val._type === "list") { d.bytecode = val; // {_type:"list", items:[numbers...]} } else if (item.name === "constants" && val && val._type === "list") { d.constants = { _type: "list", items: (val.items || []).map(convertConst) }; } i++; // skip value } } return d; } function convertConst(c) { if (!c || typeof c !== "object") return c; // number, string, boolean, null pass through if (c._type === "list" && c.items && c.items.length > 0) { var head = c.items[0]; if (head && head._type === "symbol" && head.name === "code") { return convertCodeForm(c); } if (head && head._type === "symbol" && head.name === "list") { return { _type: "list", items: c.items.slice(1).map(convertConst) }; } } return c; // symbols, keywords, etc. pass through } /** * Try loading a pre-compiled .sxbc bytecode module (SX text format). * Uses K.loadModule which handles VM suspension (import requests). * Content-addressed: checks localStorage by hash, fetches /sx/h/{hash} on miss. * Returns true on success, null on failure (caller falls back to .sx source). */ function loadBytecodeFile(path) { var sxbcPath = path.replace(/\.sx$/, '.sxbc'); var sxbcFile = sxbcPath.split('/').pop(); // e.g. "dom.sxbc" // Content-addressed resolution: manifest → localStorage → fetch by hash var text = null; var manifest = loadPageManifest(); if (manifest && manifest.modules && manifest.modules[sxbcFile]) { var hash = manifest.modules[sxbcFile]; var lsKey = "sx:h:" + hash; try { text = localStorage.getItem(lsKey); } catch(e) {} if (!text) { // Fetch by content hash try { var xhr2 = new XMLHttpRequest(); xhr2.open("GET", "/sx/h/" + hash, false); xhr2.send(); if (xhr2.status === 200) { text = xhr2.responseText; // Strip comment line if present if (text.charAt(0) === ';') { var nl = text.indexOf('\n'); if (nl >= 0) text = text.substring(nl + 1); } try { localStorage.setItem(lsKey, text); } catch(e) {} } } catch(e) {} } } // Fallback: fetch by URL (pre-content-addressed path) if (!text) { var url = _baseUrl + sxbcPath + _sxbcCacheBust; try { var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); xhr.send(); if (xhr.status !== 200) return null; text = xhr.responseText; } catch(e) { return null; } } try { // Parse the sxbc text to get the SX tree var parsed = K.parse(text); if (!parsed || !parsed.length) return null; var sxbc = parsed[0]; // (sxbc version hash (code ...)) if (!sxbc || sxbc._type !== "list" || !sxbc.items) return null; // Extract the code form — 3rd or 4th item (after sxbc, version, optional hash) var codeForm = null; for (var i = 1; i < sxbc.items.length; i++) { var item = sxbc.items[i]; if (item && item._type === "list" && item.items && item.items.length > 0 && item.items[0] && item.items[0]._type === "symbol" && item.items[0].name === "code") { codeForm = item; break; } } if (!codeForm) return null; // Convert the SX code form to a dict for loadModule var moduleDict = convertCodeForm(codeForm); if (!moduleDict) return null; // Load via K.loadModule which handles VmSuspended var result = K.loadModule(moduleDict); // Handle import suspensions — fetch missing libraries on demand while (result && result.suspended && result.op === "import") { var req = result.request; var libName = req && req.library; if (libName) { // Try to find and load the library from the manifest var loaded = handleImportSuspension(libName); if (!loaded) { console.warn("[sx-platform] lazy import: library not found:", libName); } } // Resume the suspended module (null = library is now in env) result = result.resume(null); } if (typeof result === 'string' && result.indexOf('Error') === 0) { console.warn("[sx-platform] bytecode FAIL " + path + ":", result); return null; } return true; } catch(e) { console.warn("[sx-platform] bytecode FAIL " + path + ":", e.message || e); return null; } } /** * Handle an import suspension by finding and loading the library. * The library name may be an SX value (list/string) — normalize to manifest key. */ function handleImportSuspension(libSpec) { // libSpec from the kernel is the library name spec, e.g. {_type:"list", items:[{name:"sx"},{name:"dom"}]} // or a string like "sx dom" var key; if (typeof libSpec === "string") { key = libSpec; } else if (libSpec && libSpec._type === "list" && libSpec.items) { key = libSpec.items.map(function(item) { return (item && item.name) ? item.name : String(item); }).join(" "); } else if (libSpec && libSpec._type === "dict") { // Dict with key/name fields key = libSpec.key || libSpec.name || ""; } else { key = String(libSpec); } if (_loadedLibs[key]) return true; // already loaded if (!_manifest) loadManifest(); if (!_manifest || !_manifest[key]) { console.warn("[sx-platform] lazy import: unknown library key '" + key + "'"); return false; } // Load the library (and its deps) on demand return loadLibrary(key, {}); } /** * Load an .sx file synchronously via XHR (boot-time only). * Returns the number of expressions loaded, or an error string. */ function loadSxFile(path) { var url = _baseUrl + path + _cacheBust; try { var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); // synchronous xhr.send(); if (xhr.status === 200) { var result = K.load(xhr.responseText); if (typeof result === "string" && result.indexOf("Error") === 0) { console.error("[sx-platform] FAIL " + path + ":", result); return 0; } console.log("[sx-platform] ok " + path + " (" + result + " exprs)"); return result; } else { console.error("[sx] Failed to fetch " + path + ": HTTP " + xhr.status); return null; } } catch(e) { console.error("[sx] Failed to load " + path + ":", e); return null; } } // ================================================================ // Manifest-driven module loader — only loads what's needed // ================================================================ var _manifest = null; var _loadedLibs = {}; /** * Convert K.parse output (tagged {_type, ...} objects) to plain JS. * SX nil (from empty lists `()`) becomes []. */ function sxDataToJs(v) { if (v === null || v === undefined) return []; if (typeof v !== "object") return v; if (v._type === "list") return (v.items || []).map(sxDataToJs); if (v._type === "dict") { var out = {}; for (var k in v) if (k !== "_type") out[k] = sxDataToJs(v[k]); return out; } return v; } /** * Fetch and parse the module manifest (library deps + file paths). */ function loadManifest() { if (_manifest) return _manifest; try { var xhr = new XMLHttpRequest(); xhr.open("GET", _baseUrl + "sx/module-manifest.sx" + _cacheBust, false); xhr.send(); if (xhr.status === 200) { var parsed = K.parse(xhr.responseText); if (parsed && parsed.length > 0) { _manifest = sxDataToJs(parsed[0]); return _manifest; } } } catch(e) {} console.warn("[sx-platform] No manifest found, falling back to full load"); return null; } /** * Load a single library and all its dependencies (recursive). * Cycle-safe: tracks in-progress loads to break circular deps. * Functions in cyclic modules resolve symbols at call time via global env. */ function loadLibrary(name, loading) { if (_loadedLibs[name]) return true; if (loading[name]) return true; // cycle — skip loading[name] = true; var info = _manifest[name]; if (!info) { console.warn("[sx-platform] Unknown library: " + name); return false; } // Resolve deps first for (var i = 0; i < info.deps.length; i++) { loadLibrary(info.deps[i], loading); } // Mark as loaded BEFORE executing — self-imports (define-library re-exports) // will see it as already loaded and skip rather than infinite-looping. _loadedLibs[name] = true; // Load this module (bytecode first, fallback to source) var ok = loadBytecodeFile("sx/" + info.file); if (!ok) { var sxFile = info.file.replace(/\.sxbc$/, '.sx'); ok = loadSxFile("sx/" + sxFile); } return !!ok; } /** * Load web stack using the module manifest. * Only downloads libraries that the entry point transitively depends on. */ function loadWebStack() { var manifest = loadManifest(); if (!manifest) return loadWebStackFallback(); var entry = manifest["_entry"]; if (!entry) { console.warn("[sx-platform] No _entry in manifest, falling back"); return loadWebStackFallback(); } var loading = {}; var t0 = performance.now(); if (K.beginModuleLoad) K.beginModuleLoad(); // Load all entry point deps recursively for (var i = 0; i < entry.deps.length; i++) { loadLibrary(entry.deps[i], loading); } // Load entry point itself (boot.sx — not a library, just defines + init) loadBytecodeFile("sx/" + entry.file) || loadSxFile("sx/" + entry.file.replace(/\.sxbc$/, '.sx')); if (K.endModuleLoad) K.endModuleLoad(); var count = Object.keys(_loadedLibs).length + 1; // +1 for entry var dt = Math.round(performance.now() - t0); console.log("[sx-platform] Loaded " + count + " modules in " + dt + "ms (manifest-driven)"); } /** * Fallback: load all files in hardcoded order (pre-manifest compat). */ function loadWebStackFallback() { var files = [ "sx/render.sx", "sx/core-signals.sx", "sx/signals.sx", "sx/deps.sx", "sx/router.sx", "sx/page-helpers.sx", "sx/freeze.sx", "sx/highlight.sx", "sx/bytecode.sx", "sx/compiler.sx", "sx/vm.sx", "sx/dom.sx", "sx/browser.sx", "sx/adapter-html.sx", "sx/adapter-sx.sx", "sx/adapter-dom.sx", "sx/boot-helpers.sx", "sx/hypersx.sx", "sx/harness.sx", "sx/harness-reactive.sx", "sx/harness-web.sx", "sx/engine.sx", "sx/orchestration.sx", "sx/boot.sx", ]; if (K.beginModuleLoad) K.beginModuleLoad(); for (var i = 0; i < files.length; i++) { if (!loadBytecodeFile(files[i])) loadSxFile(files[i]); } if (K.endModuleLoad) K.endModuleLoad(); console.log("[sx-platform] Loaded " + files.length + " files (fallback)"); } /** * Load an optional library on demand (e.g., highlight, harness). * Can be called after boot for pages that need extra modules. */ globalThis.__sxLoadLibrary = function(name) { if (!_manifest) loadManifest(); if (!_manifest) return false; if (_loadedLibs[name]) return true; if (K.beginModuleLoad) K.beginModuleLoad(); var ok = loadLibrary(name, {}); if (K.endModuleLoad) K.endModuleLoad(); return ok; }; // ================================================================ // Transparent lazy loading — symbol → library index // // When the VM hits an undefined symbol, the resolve hook checks this // index, loads the library that exports it, and returns the value. // The programmer just calls the function — loading is invisible. // ================================================================ var _symbolIndex = null; // symbol name → library key function buildSymbolIndex() { if (_symbolIndex) return _symbolIndex; if (!_manifest) loadManifest(); if (!_manifest) return null; _symbolIndex = {}; for (var key in _manifest) { if (key.startsWith('_')) continue; var entry = _manifest[key]; if (entry.exports) { for (var i = 0; i < entry.exports.length; i++) { _symbolIndex[entry.exports[i]] = key; } } } return _symbolIndex; } // ================================================================ // Content-addressed definition loader // // The page manifest maps component names to content hashes. // When a ~component symbol is missing, we resolve its hash, // check localStorage, fetch from /sx/h/{hash} if needed, // then load the definition (recursively resolving @h: deps). // ================================================================ var _pageManifest = null; // { defs: { "~name": "hash", ... } } var _hashToName = {}; // hash → "~name" var _hashCache = {}; // hash → definition text (in-memory) var _loadedHashes = {}; // hash → true (already K.load'd) function loadPageManifest() { if (_pageManifest) return _pageManifest; var el = document.querySelector('script[data-sx-manifest]'); if (!el) return null; try { _pageManifest = JSON.parse(el.textContent); var defs = _pageManifest.defs || {}; for (var name in defs) { _hashToName[defs[name]] = name; } return _pageManifest; } catch(e) { console.warn("[sx] Failed to parse manifest:", e); return null; } } // Merge definitions from a new page's manifest (called during navigation) function mergeManifest(el) { if (!el) return; try { var incoming = JSON.parse(el.textContent); var newDefs = incoming.defs || {}; // Ensure base manifest is loaded if (!_pageManifest) loadPageManifest(); if (!_pageManifest) _pageManifest = { defs: {} }; if (!_pageManifest.defs) _pageManifest.defs = {}; for (var name in newDefs) { _pageManifest.defs[name] = newDefs[name]; _hashToName[newDefs[name]] = name; } // Merge hash store entries if (incoming.store) { if (!_pageManifest.store) _pageManifest.store = {}; for (var h in incoming.store) { _pageManifest.store[h] = incoming.store[h]; } } } catch(e) { console.warn("[sx] Failed to merge manifest:", e); } } function resolveHash(hash) { // 1. In-memory cache if (_hashCache[hash]) return _hashCache[hash]; // 2. localStorage var key = "sx:h:" + hash; try { var cached = localStorage.getItem(key); if (cached) { _hashCache[hash] = cached; return cached; } } catch(e) {} // 3. Fetch from server try { var xhr = new XMLHttpRequest(); xhr.open("GET", "/sx/h/" + hash, false); xhr.send(); if (xhr.status === 200) { var def = xhr.responseText; _hashCache[hash] = def; try { localStorage.setItem(key, def); } catch(e) {} return def; } } catch(e) { console.warn("[sx] Failed to fetch hash " + hash + ":", e); } return null; } function loadDefinitionByHash(hash) { if (_loadedHashes[hash]) return true; // Mark in-progress immediately to prevent circular recursion _loadedHashes[hash] = "loading"; var def = resolveHash(hash); if (!def) { delete _loadedHashes[hash]; return false; } // Strip comment line (;; ~name\n) from start var src = def; if (src.charAt(0) === ';') { var nl = src.indexOf('\n'); if (nl >= 0) src = src.substring(nl + 1); } // Find and recursively load @h: dependencies before loading this one var hashRe = /@h:([0-9a-f]{16})/g; var match; while ((match = hashRe.exec(src)) !== null) { var depHash = match[1]; if (!_loadedHashes[depHash]) { loadDefinitionByHash(depHash); } } // Rewrite @h:xxx back to ~names for the SX evaluator var rewritten = src.replace(/@h:([0-9a-f]{16})/g, function(_m, h) { return _hashToName[h] || ("@h:" + h); }); // Eagerly pre-load any plain manifest symbols referenced in this definition. // The CEK evaluator doesn't call __resolve-symbol, so deps must be present // before the definition is called. Scan for word boundaries matching manifest keys. if (_pageManifest && _pageManifest.defs) { var words = rewritten.match(/[a-zA-Z_][a-zA-Z0-9_?!-]*/g) || []; for (var wi = 0; wi < words.length; wi++) { var w = words[wi]; if (w !== name && _pageManifest.defs[w] && !_loadedHashes[_pageManifest.defs[w]]) { loadDefinitionByHash(_pageManifest.defs[w]); } } } // Prepend the component name back into the definition. // Only for single-definition forms (defcomp/defisland/defmacro) where // the name was stripped for hashing. Multi-define files (client libs) // already contain named (define name ...) forms. var name = _hashToName[hash]; if (name) { // Check if this is a multi-define file (client lib with top-level defines). // Only top-level (define ...) forms count — nested ones inside defisland/defcomp // bodies should NOT suppress name insertion. var startsWithDef = /^\((defcomp|defisland|defmacro)\s/.test(rewritten); var isMultiDefine = !startsWithDef && /\(define\s+[a-zA-Z]/.test(rewritten); if (!isMultiDefine) { rewritten = rewritten.replace( /^\((defcomp|defisland|defmacro|define)\s/, function(_m, kw) { return "(" + kw + " " + name + " "; } ); } } try { var loadRv = K.load(rewritten); if (typeof loadRv === "string" && loadRv.indexOf("Error") >= 0) { console.warn("[sx] K.load error for", (_hashToName[hash] || hash) + ":", loadRv); delete _loadedHashes[hash]; return false; } _loadedHashes[hash] = true; return true; } catch(e) { console.warn("[sx] Failed to load hash " + hash + " (" + (name || "?") + "):", e); return false; } } // Eagerly pre-load island definitions from the manifest. // Called from boot.sx before hydration. Scans the DOM for data-sx-island // attributes and loads definitions via the content-addressed manifest. // Unlike __resolve-symbol (called from inside env_get), this runs at the // top level so K.load can register bindings without reentrancy issues. K.registerNative("preload-island-defs", function() { var manifest = loadPageManifest(); if (!manifest || !manifest.defs) return null; var els = document.querySelectorAll('[data-sx-island]'); for (var i = 0; i < els.length; i++) { var name = "~" + els[i].getAttribute("data-sx-island"); if (manifest.defs[name] && !_loadedHashes[manifest.defs[name]]) { loadDefinitionByHash(manifest.defs[name]); } } return null; }); // Register the resolve hook — called by the VM when GLOBAL_GET fails K.registerNative("__resolve-symbol", function(args) { var name = args[0]; if (!name) return null; // Content-addressed resolution — components, libraries, macros var manifest = loadPageManifest(); if (manifest && manifest.defs && manifest.defs[name]) { var hash = manifest.defs[name]; if (!_loadedHashes[hash]) { loadDefinitionByHash(hash); return null; // VM re-lookups after hook } } // Library-level resolution (existing path — .sxbc modules) var idx = buildSymbolIndex(); if (!idx || !idx[name]) return null; var lib = idx[name]; if (_loadedLibs[lib]) return null; // already loaded but symbol still missing — real error // Load the library __sxLoadLibrary(lib); // Return null — the VM will re-lookup in globals after the hook loads the module return null; }); // ================================================================ // Compatibility shim — expose Sx global matching current JS API // ================================================================ globalThis.Sx = { VERSION: "wasm-1.0", parse: function(src) { return K.parse(src); }, eval: function(src) { return K.eval(src); }, load: function(src) { return K.load(src); }, renderToHtml: function(expr) { return K.renderToHtml(expr); }, callFn: function(fn, args) { return K.callFn(fn, args); }, engine: function() { return K.engine(); }, mergeManifest: function(el) { return mergeManifest(el); }, // Boot entry point (called by auto-init or manually) init: function() { if (typeof K.eval === "function") { // Check boot-init exists // Step through boot manually console.log("[sx] init-css-tracking..."); K.eval("(init-css-tracking)"); console.log("[sx] process-page-scripts..."); K.eval("(process-page-scripts)"); console.log("[sx] routes after pages:", K.eval("(len _page-routes)")); console.log("[sx] process-sx-scripts..."); K.eval("(process-sx-scripts nil)"); console.log("[sx] sx-hydrate-elements..."); K.eval("(sx-hydrate-elements nil)"); // Pre-load island definitions from manifest before hydration. // Must happen at JS level (not from inside SX eval) to avoid // K.load reentrancy issues with the symbol resolve hook. var manifest = loadPageManifest(); if (manifest && manifest.defs) { var islandEls = document.querySelectorAll("[data-sx-island]"); for (var ii = 0; ii < islandEls.length; ii++) { var iname = "~" + islandEls[ii].getAttribute("data-sx-island"); var ihash = manifest.defs[iname]; if (ihash && !_loadedHashes[ihash]) { loadDefinitionByHash(ihash); } } } console.log("[sx] sx-hydrate-islands..."); K.eval("(sx-hydrate-islands nil)"); console.log("[sx] process-elements..."); K.eval("(process-elements nil)"); // Debug islands console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)")); console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)")); // Island count (JS-side, avoids VM overhead) console.log("[sx] manual island query:", document.querySelectorAll("[data-sx-island]").length); // Try hydrating again console.log("[sx] retry hydrate-islands..."); K.eval("(sx-hydrate-islands nil)"); // Check if links are boosted var links = document.querySelectorAll("a[href]"); var boosted = 0; for (var i = 0; i < links.length; i++) { if (links[i]._sxBoundboost) boosted++; } console.log("[sx] boosted links:", boosted, "/", links.length); // Check island state var islands = document.querySelectorAll("[data-sx-island]"); console.log("[sx] islands:", islands.length); for (var j = 0; j < islands.length; j++) { console.log("[sx] island:", islands[j].getAttribute("data-sx-island"), "hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"], "children:", islands[j].children.length); } // Register popstate handler for back/forward navigation window.addEventListener("popstate", function(e) { var state = e.state; var scrollY = (state && state.scrollY) ? state.scrollY : 0; K.eval("(handle-popstate " + scrollY + ")"); }); // Wire up streaming suspense resolution var _resolveFn = K.eval("resolve-suspense"); Sx.resolveSuspense = function(id, sx) { try { K.callFn(_resolveFn, [id, sx]); } catch(e) { console.error("[sx] resolveSuspense error:", e); } }; // Drain any pending resolves that arrived before boot if (window.__sxPending) { for (var pi = 0; pi < window.__sxPending.length; pi++) { Sx.resolveSuspense(window.__sxPending[pi].id, window.__sxPending[pi].sx); } window.__sxPending = null; } window.__sxResolve = function(id, sx) { Sx.resolveSuspense(id, sx); }; // Signal boot complete document.documentElement.setAttribute("data-sx-ready", "true"); console.log("[sx] boot done"); } } }; // ================================================================ // Auto-init: load web stack and boot on DOMContentLoaded // ================================================================ if (typeof document !== "undefined") { var _doInit = function() { loadWebStack(); Sx.init(); // Enable JIT after all boot code has run. // Lazy-load the compiler first — JIT needs it to compile functions. setTimeout(function() { if (K.beginModuleLoad) K.beginModuleLoad(); loadLibrary("sx compiler", {}); if (K.endModuleLoad) K.endModuleLoad(); K.eval('(enable-jit!)'); }, 0); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", _doInit); } else { _doInit(); } } } // end boot // SxKernel is available synchronously (js_of_ocaml) or after async // WASM init. Poll briefly to handle both cases. var K = globalThis.SxKernel; if (K) { boot(K); return; } var tries = 0; var poll = setInterval(function() { K = globalThis.SxKernel; if (K) { clearInterval(poll); boot(K); } else if (++tries > 100) { clearInterval(poll); console.error("[sx-platform] SxKernel not found after 5s"); } }, 50); })();