/** * 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) { // ================================================================ // 8 FFI Host Primitives // ================================================================ 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]; return v === undefined ? null : 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]); } }); 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) { return function() { var a = Array.prototype.slice.call(arguments); return K.callFn(fn, a); }; } 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"; }); // ================================================================ // 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; } } } })(); /** * 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; } } /** * Try loading a pre-compiled bytecode module. * Tries .sxbc.json first, then .sxbc (SX s-expression format). * Returns true on success, null on failure (caller falls back to .sx source). */ function loadBytecodeFile(path) { console.log("[sx-platform] loadBytecodeFile:", path, "(sxbc-only, no json)"); // .sxbc.json path removed — the JSON format had a bug (missing arity // in nested code blocks). Use .sxbc (SX text) format only. // Try .sxbc (SX s-expression format, loaded via load-sxbc primitive) var sxbcPath = path.replace(/\.sx$/, '.sxbc'); var sxbcUrl = _baseUrl + sxbcPath + _sxbcCacheBust; try { var xhr2 = new XMLHttpRequest(); xhr2.open("GET", sxbcUrl, false); xhr2.send(); if (xhr2.status === 200) { // Store text in global, parse via SX to avoid JS string escaping window.__sxbcText = xhr2.responseText; var result2 = K.eval('(load-sxbc (first (parse (host-global "__sxbcText"))))'); delete window.__sxbcText; if (typeof result2 !== 'string' || result2.indexOf('Error') !== 0) { console.log("[sx-platform] ok " + path + " (bytecode-sx)"); return true; } console.warn("[sx-platform] bytecode-sx FAIL " + path + ":", result2); } } catch(e) { delete window.__sxbcText; /* fall through to source */ } return null; } /** * 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; } } /** * Load all web adapter .sx files in dependency order. * Tries pre-compiled bytecode first, falls back to source. */ function loadWebStack() { var files = [ // Spec modules "sx/render.sx", "sx/core-signals.sx", "sx/signals.sx", "sx/deps.sx", "sx/router.sx", "sx/page-helpers.sx", // Freeze scope (signal persistence) "sx/freeze.sx", // Bytecode compiler + VM "sx/bytecode.sx", "sx/compiler.sx", "sx/vm.sx", // Web libraries (use 8 FFI primitives) "sx/dom.sx", "sx/browser.sx", // Web adapters "sx/adapter-html.sx", "sx/adapter-sx.sx", "sx/adapter-dom.sx", // Client libraries (CSSX etc. — needed by page components) "sx/cssx.sx", // Boot helpers (platform functions in pure SX) "sx/boot-helpers.sx", "sx/hypersx.sx", // Test harness (for inline test runners) "sx/harness.sx", "sx/harness-reactive.sx", "sx/harness-web.sx", // Web framework "sx/engine.sx", "sx/orchestration.sx", "sx/boot.sx", ]; var loaded = 0, bcCount = 0, srcCount = 0; var inBatch = false; for (var i = 0; i < files.length; i++) { if (!inBatch && K.beginModuleLoad) { K.beginModuleLoad(); inBatch = true; } var r = loadBytecodeFile(files[i]); if (r) { bcCount++; continue; } // Bytecode not available — end batch, load source if (inBatch && K.endModuleLoad) { K.endModuleLoad(); inBatch = false; } r = loadSxFile(files[i]); if (typeof r === "number") { loaded += r; srcCount++; } } if (inBatch && K.endModuleLoad) K.endModuleLoad(); console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)"); return loaded; } // ================================================================ // 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(); }, // 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)"); 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); } // Fallback popstate handler for back/forward navigation. // Only fires before SX engine boots — after boot, boot.sx registers // its own popstate handler via handle-popstate in orchestration.sx. window.addEventListener("popstate", function() { if (document.documentElement.hasAttribute("data-sx-ready")) return; var url = location.pathname + location.search; var target = document.querySelector("#main-panel"); if (!target) return; fetch(url) .then(function(r) { return r.text(); }) .then(function(html) { if (!html) return; var parser = new DOMParser(); var doc = parser.parseFromString(html, "text/html"); var srcPanel = doc.querySelector("#main-panel"); var srcNav = doc.querySelector("#sx-nav"); if (srcPanel) { target.outerHTML = srcPanel.outerHTML; } var navTarget = document.querySelector("#sx-nav"); if (srcNav && navTarget) { navTarget.outerHTML = srcNav.outerHTML; } }) .catch(function(e) { console.warn("[sx] popstate fetch error:", e); }); }); // Event delegation for sx-get links — fallback when bind-event's // per-element listener didn't attach. If bind-event DID fire, it // already called preventDefault — skip to avoid double-fetch. document.addEventListener("click", function(e) { var el = e.target.closest("a[sx-get]"); if (!el) return; if (e.defaultPrevented) return; if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); var url = el.getAttribute("href") || el.getAttribute("sx-get"); // Don't push URL here — execute-request's handle-history does it. // Double-push causes popstate handler to clobber the SX swap. // Store the element reference for SX to pick up window.__sxClickEl = el; try { K.eval('(execute-request (host-global "__sxClickEl") nil nil)'); } catch(ex) { console.warn("[sx] click delegation error:", ex); location.href = el.href; } delete window.__sxClickEl; }); // Signal boot complete document.documentElement.setAttribute("data-sx-ready", "true"); document.dispatchEvent(new CustomEvent("sx:boot-done")); 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 setTimeout(function() { 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); })();