Fix hydration: effect was a no-op primitive, bytecode compiler emitted CALL_PRIM

Root cause: sx_primitives.ml registered "effect" as a native no-op (for SSR).
The bytecode compiler's (primitive? "effect") returned true, so it emitted
OP_CALL_PRIM instead of OP_GLOBAL_GET + OP_CALL. The VM's CALL_PRIM handler
found the native Nil-returning stub and never called the real effect function
from core-signals.sx.

Fix: Remove effect and register-in-scope from the primitives table. The server
overrides them via env_bind in sx_server.ml (after compilation), which doesn't
affect primitive? checks.

Also: VM CALL_PRIM now falls back to cek_call for non-NativeFn values (safety
net for any other functions that get misclassified).

15/15 source mode, 15/15 bytecode mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 16:56:31 +00:00
parent 4cb4551753
commit a7efcaf679
28 changed files with 232 additions and 199 deletions

View File

@@ -234,36 +234,37 @@
}
/**
* Try loading a pre-compiled bytecode module.
* Tries .sxbc.json first, then .sxbc (SX s-expression format).
* Try loading a pre-compiled .sxbc.json bytecode module.
* 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;
var bcPath = path.replace(/\.sx$/, '.sxbc.json');
var url = _baseUrl + bcPath + _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 */ }
var xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.send();
if (xhr.status !== 200) return null;
return null;
var json = JSON.parse(xhr.responseText);
if (!json.module || json.magic !== 'SXBC') return null;
var module = {
_type: 'dict',
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.warn("[sx-platform] bytecode FAIL " + path + ":", result);
return null;
}
console.log("[sx-platform] ok " + path + " (bytecode)");
return true;
} catch(e) {
return null;
}
}
/**
@@ -320,8 +321,6 @@
"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",
@@ -336,17 +335,15 @@
];
var loaded = 0, bcCount = 0, srcCount = 0;
var inBatch = false;
if (K.beginModuleLoad) K.beginModuleLoad();
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; }
// Bytecode not available — load source inside the batch (don't break it)
r = loadSxFile(files[i]);
if (typeof r === "number") { loaded += r; srcCount++; }
}
if (inBatch && K.endModuleLoad) K.endModuleLoad();
if (K.endModuleLoad) K.endModuleLoad();
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
return loaded;
}
@@ -404,58 +401,6 @@
"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");
}
}