Fix JIT compiler, CSSX browser support, double-fetch, SPA layout

JIT compiler:
- Fix jit_compile_lambda: resolve `compile` via symbol lookup in env
  instead of embedding VmClosure in AST (CEK dispatches differently)
- Register eval-defcomp/eval-defisland/eval-defmacro runtime helpers
  in browser kernel for bytecoded defcomp forms
- Disable broken .sxbc.json path (missing arity in nested code blocks),
  use .sxbc text format only
- Mark JIT-failed closures as sentinel to stop retrying

CSSX in browser:
- Add cssx.sx symlink + cssx.sxbc to browser web stack
- Add flush-cssx! to orchestration.sx post-swap for SPA nav
- Add cssx.sx to compile-modules.js and mcp_tree.ml bytecode lists

SPA navigation:
- Fix double-fetch: check e.defaultPrevented in click delegation
  (bind-event already handled the click)
- Fix layout destruction: change nav links from outerHTML to innerHTML
  swap (outerHTML destroyed #main-panel when response lacked it)
- Guard JS popstate handler when SX engine is booted
- Rename sx-platform.js → sx-platform-2.js to bust immutable cache

Playwright tests:
- Add trackErrors() helper to all test specs
- Add SPA DOM comparison test (SPA nav vs fresh load)
- Add single-fetch + no-duplicate-elements test
- Improve MCP tool output: show failure details and error messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 20:48:43 +00:00
parent 5b55b75a9a
commit d81a518732
37 changed files with 688 additions and 405 deletions

View File

@@ -233,31 +233,9 @@
* Returns true on success, null on failure (caller falls back to .sx source).
*/
function loadBytecodeFile(path) {
// Try .sxbc.json (JSON dict format)
var jsonPath = path.replace(/\.sx$/, '.sxbc.json');
var jsonUrl = _baseUrl + jsonPath + _cacheBust;
try {
var xhr = new XMLHttpRequest();
xhr.open("GET", jsonUrl, false);
xhr.send();
if (xhr.status === 200) {
var json = JSON.parse(xhr.responseText);
if (json.module && json.magic === 'SXBC') {
var module = {
_type: 'dict',
arity: json.module.arity || 0,
bytecode: { _type: 'list', items: json.module.bytecode },
constants: { _type: 'list', items: json.module.constants.map(deserializeConstant) },
};
var result = K.loadModule(module);
if (typeof result !== 'string' || result.indexOf('Error') !== 0) {
console.log("[sx-platform] ok " + path + " (bytecode-json)");
return true;
}
console.warn("[sx-platform] bytecode-json FAIL " + path + ":", result);
}
}
} catch(e) { /* fall through to .sxbc */ }
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');
@@ -336,6 +314,8 @@
"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",
@@ -418,22 +398,18 @@
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
"children:", islands[j].children.length);
}
// Register popstate handler for back/forward navigation.
// Fetch HTML (not SX) and extract #main-panel content.
// 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;
// Try client-side route first
var clientHandled = false;
try { clientHandled = K.eval('(try-client-route "' + url.replace(/"/g, '\\"') + '" "#main-panel")'); } catch(e) {}
if (clientHandled) return;
// Server fetch — request full HTML (no SX-Request header)
fetch(url)
.then(function(r) { return r.text(); })
.then(function(html) {
if (!html) return;
// Parse the full HTML and extract #main-panel
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
var srcPanel = doc.querySelector("#main-panel");
@@ -441,26 +417,20 @@
if (srcPanel) {
target.outerHTML = srcPanel.outerHTML;
}
// Also update nav if present
var navTarget = document.querySelector("#sx-nav");
if (srcNav && navTarget) {
navTarget.outerHTML = srcNav.outerHTML;
}
// Re-hydrate
var newTarget = document.querySelector("#main-panel");
if (newTarget) {
try { K.eval("(post-swap (dom-query \"#main-panel\"))"); } catch(e) {}
try { K.eval("(sx-hydrate-islands (dom-query \"#main-panel\"))"); } catch(e) {}
}
})
.catch(function(e) { console.warn("[sx] popstate fetch error:", e); });
});
// Event delegation for sx-get links — bytecoded bind-event doesn't
// attach per-element listeners (VM closure issue), so catch clicks
// at the document level and route through the SX engine.
// 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");

View File

@@ -0,0 +1 @@
../../../../shared/sx/templates/cssx.sx

File diff suppressed because one or more lines are too long

View File

@@ -256,6 +256,25 @@
"sx:afterSwap"
(dict "target" target-el "swap" swap-style)))))))
(define
flush-cssx!
:effects (mutation io)
(fn
()
(let
((rules (collected "cssx")))
(clear-collected! "cssx")
(when
(not (empty? rules))
(let
((style (dom-query "#sx-css")))
(when
style
(dom-set-prop
style
"textContent"
(str (dom-get-prop style "textContent") (join "" rules)))))))))
(define
handle-sx-response
:effects (mutation io)
@@ -508,7 +527,8 @@
(sx-hydrate root)
(sx-hydrate-islands root)
(run-post-render-hooks)
(process-elements root)))
(process-elements root)
(flush-cssx!)))
(define
process-settle-hooks

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
}
(globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-15eb71d8",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-0c758f5b",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-eb076217",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-36a151d2",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",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