Fix WASM browser click handlers: 8 bugs, 50 new VM tests

The sx-get links were doing full page refreshes because click handlers
never attached. Root causes: VM frame management bug, missing primitives,
CEK/VM type dispatch mismatch, and silent error swallowing.

Fixes:
- VM frame exhaustion: frames <- [] now properly pops to rest_frames
- length primitive: add alias for len in OCaml primitives
- call_sx_fn: use sx_call directly instead of eval_expr (CEK checks
  for type "lambda" but VmClosure reports "function")
- Boot error surfacing: Sx.init() now has try/catch + failure summary
- Callback error surfacing: catch-all handler for non-Eval_error exceptions
- Silent JIT failures: log before CEK fallback instead of swallowing
- vm→env sync: loadModule now calls sync_vm_to_env()
- sx_build_bytecode MCP tool added for bytecode compilation

Tests: 50 new tests across test-vm.sx and test-vm-primitives.sx covering
nested VM calls, frame integrity, CEK bridge, primitive availability,
cross-module symbol resolution, and callback dispatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 00:37:21 +00:00
parent 00de248ee9
commit c923a34fa8
38 changed files with 6016 additions and 4513 deletions

View File

@@ -257,6 +257,7 @@
console.log("[sx-platform] ok " + path + " (bytecode)");
return true;
} catch(e) {
console.error("[sx-platform] bytecode EXCEPTION " + path + ":", e);
return null;
}
}
@@ -329,18 +330,23 @@
];
var loaded = 0, bcCount = 0, srcCount = 0;
if (K.beginModuleLoad) K.beginModuleLoad();
var t0 = performance.now();
var forceSrc = (typeof window !== "undefined" && window.location.search.indexOf("nosxbc") !== -1);
if (!forceSrc && K.beginModuleLoad) K.beginModuleLoad();
for (var i = 0; i < files.length; i++) {
var r = loadBytecodeFile(files[i]);
if (r) { bcCount++; continue; }
if (!forceSrc) {
var r = loadBytecodeFile(files[i]);
if (r) { bcCount++; continue; }
}
// Bytecode not available — end batch, load source, restart batch
if (K.endModuleLoad) K.endModuleLoad();
if (!forceSrc && K.endModuleLoad) K.endModuleLoad();
r = loadSxFile(files[i]);
if (typeof r === "number") { loaded += r; srcCount++; }
if (K.beginModuleLoad) K.beginModuleLoad();
if (!forceSrc && K.beginModuleLoad) K.beginModuleLoad();
}
if (K.endModuleLoad) K.endModuleLoad();
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
if (!forceSrc && K.endModuleLoad) K.endModuleLoad();
var elapsed = Math.round(performance.now() - t0);
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs) in " + elapsed + "ms");
return loaded;
}
@@ -358,46 +364,33 @@
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)"));
// Try manual island query
console.log("[sx] manual island query:", K.eval("(len (dom-query-all (dom-body) \"[data-sx-island]\"))"));
// 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++;
if (typeof K.eval !== "function") return;
var steps = [
'(log-info (str "sx-browser " SX_VERSION))',
'(init-css-tracking)',
'(process-page-scripts)',
'(process-sx-scripts nil)',
'(sx-hydrate-elements nil)',
'(sx-hydrate-islands nil)',
'(run-post-render-hooks)',
'(process-elements nil)',
'(dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0)))'
];
var failures = [];
for (var i = 0; i < steps.length; i++) {
try {
var r = K.eval(steps[i]);
if (typeof r === "string" && r.indexOf("Error") === 0) {
console.error("[sx] boot step " + i + " FAILED: " + steps[i].substring(0, 60), r);
failures.push({ step: i, expr: steps[i], error: r });
}
} catch(e) {
console.error("[sx] boot step " + i + " THREW: " + steps[i].substring(0, 60), e);
failures.push({ step: i, expr: steps[i], error: String(e) });
}
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);
}
console.log("[sx] boot done");
}
if (failures.length > 0) {
console.error("[sx] BOOT FAILED: " + failures.length + " step(s) errored:", failures.map(function(f) { return "step " + f.step + ": " + f.error; }).join("; "));
}
}
};
@@ -410,8 +403,8 @@
var _doInit = function() {
loadWebStack();
Sx.init();
// Enable JIT after all boot code has run
setTimeout(function() { K.eval('(enable-jit!)'); }, 0);
// JIT disabled for debugging
// setTimeout(function() { K.eval('(enable-jit!)'); }, 0);
};
if (document.readyState === "loading") {