Rebuild WASM: bytecode with pending_cek snapshot fix

All .sxbc recompiled with fixed sx_vm.ml. 32/32 WASM tests, 4/4
bytecode regression tests. hs-repeat-times correctly does 6 io-sleep
suspensions in bytecode mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 21:44:58 +00:00
parent ddc48c6d48
commit 5e708e1b20
24 changed files with 379 additions and 110 deletions

View File

@@ -81,18 +81,62 @@
K.registerNative("host-callback", function(args) { K.registerNative("host-callback", function(args) {
var fn = args[0]; var fn = args[0];
// Native JS function — pass through // Native JS function (not SX-origin) — pass through
if (typeof fn === "function") return fn; if (typeof fn === "function" && fn.__sx_handle === undefined) return fn;
// SX callable (has __sx_handle) — wrap as JS function // SX callable (has __sx_handle) — wrap as JS function with suspension handling
if (fn && fn.__sx_handle !== undefined) { if (fn && fn.__sx_handle !== undefined) {
return function() { return function() {
var a = Array.prototype.slice.call(arguments); var a = Array.prototype.slice.call(arguments);
return K.callFn(fn, a); var result = K.callFn(fn, a);
// Handle IO suspension chain (e.g. wait, fetch, navigate)
_driveAsync(result);
return result;
}; };
} }
return function() {}; return function() {};
}); });
/**
* Drive an async suspension chain to completion.
* When K.callFn returns {suspended: true, request: ..., resume: fn},
* handle the IO operation and resume the VM.
*/
function _driveAsync(result) {
if (!result || !result.suspended) return;
console.log("[sx] IO suspension:", JSON.stringify(result.request, null, 2));
var req = result.request;
if (!req) return;
// req is an SX list — extract items. K returns SX values.
var items = req.items || req;
var op = (items && items[0]) || req;
// Normalize: op might be a string or {name: "..."} symbol
var opName = (typeof op === "string") ? op : (op && op.name) || String(op);
if (opName === "wait" || opName === "io-sleep") {
// (wait ms) or (io-sleep ms) — resume after timeout
var ms = (items && items[1]) || 0;
if (typeof ms !== "number") ms = parseFloat(ms) || 0;
console.log("[sx] IO wait: " + ms + "ms, resuming after timeout");
setTimeout(function() {
try {
var resumed = result.resume(null);
console.log("[sx] IO resumed:", typeof resumed, resumed && resumed.suspended ? "suspended-again" : "done", JSON.stringify(resumed));
_driveAsync(resumed);
} catch(e) {
console.error("[sx] IO resume error:", e);
}
}, ms);
} else if (opName === "navigate") {
// (navigate url) — browser navigation
var url = (items && items[1]) || "/";
if (typeof url !== "string") url = String(url);
window.location.href = url;
} else {
console.warn("[sx] Unhandled IO suspension in callback:", opName, req);
}
}
K.registerNative("host-typeof", function(args) { K.registerNative("host-typeof", function(args) {
var obj = args[0]; var obj = args[0];
if (obj == null) return "nil"; if (obj == null) return "nil";
@@ -526,7 +570,10 @@
"sx/adapter-html.sx", "sx/adapter-sx.sx", "sx/adapter-dom.sx", "sx/adapter-html.sx", "sx/adapter-sx.sx", "sx/adapter-dom.sx",
"sx/boot-helpers.sx", "sx/hypersx.sx", "sx/harness.sx", "sx/boot-helpers.sx", "sx/hypersx.sx", "sx/harness.sx",
"sx/harness-reactive.sx", "sx/harness-web.sx", "sx/harness-reactive.sx", "sx/harness-web.sx",
"sx/engine.sx", "sx/orchestration.sx", "sx/boot.sx", "sx/engine.sx", "sx/orchestration.sx",
"sx/hs-tokenizer.sx", "sx/hs-parser.sx", "sx/hs-compiler.sx",
"sx/hs-runtime.sx", "sx/hs-integration.sx",
"sx/boot.sx",
]; ];
if (K.beginModuleLoad) K.beginModuleLoad(); if (K.beginModuleLoad) K.beginModuleLoad();
for (var i = 0; i < files.length; i++) { for (var i = 0; i < files.length; i++) {
@@ -644,6 +691,13 @@
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"], "hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
"children:", islands[j].children.length); "children:", islands[j].children.length);
} }
// Activate _hyperscript compat on elements with _ attribute
if (document.querySelector('[_]')) {
if (K.beginModuleLoad) K.beginModuleLoad();
loadLibrary("hyperscript integration", {});
if (K.endModuleLoad) K.endModuleLoad();
K.eval("(hs-boot!)");
}
// Register popstate handler for back/forward navigation // Register popstate handler for back/forward navigation
window.addEventListener("popstate", function(e) { window.addEventListener("popstate", function(e) {
var state = e.state; var state = e.state;

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

@@ -444,6 +444,7 @@
(sx-hydrate-islands nil) (sx-hydrate-islands nil)
(run-post-render-hooks) (run-post-render-hooks)
(flush-collected-styles) (flush-collected-styles)
(hs-boot!)
(set-timeout (fn () (process-elements nil)) 0) (set-timeout (fn () (process-elements nil)) 0)
(dom-set-attr (dom-set-attr
(host-get (dom-document) "documentElement") (host-get (dom-document) "documentElement")

File diff suppressed because one or more lines are too long

View File

@@ -599,11 +599,12 @@
(binding) (binding)
(let (let
((name (if (= (type-of (first binding)) "symbol") (symbol-name (first binding)) (first binding))) ((name (if (= (type-of (first binding)) "symbol") (symbol-name (first binding)) (first binding)))
(value (nth binding 1)) (value (nth binding 1)))
(slot (scope-define-local let-scope name)))
(compile-expr em value let-scope false) (compile-expr em value let-scope false)
(emit-op em 17) (let
(emit-byte em slot))) ((slot (scope-define-local let-scope name)))
(emit-op em 17)
(emit-byte em slot))))
bindings) bindings)
(compile-begin em body let-scope tail?))))) (compile-begin em body let-scope tail?)))))
(define (define
@@ -640,29 +641,38 @@
(fn-scope (make-scope scope)) (fn-scope (make-scope scope))
(fn-em (make-emitter))) (fn-em (make-emitter)))
(dict-set! fn-scope "is-function" true) (dict-set! fn-scope "is-function" true)
(for-each
(fn
(p)
(let
((name (cond (= (type-of p) "symbol") (symbol-name p) (and (list? p) (not (empty? p)) (= (type-of (first p)) "symbol")) (symbol-name (first p)) :else p)))
(when
(and (not (= name "&key")) (not (= name "&rest")))
(scope-define-local fn-scope name))))
params)
(compile-begin fn-em body fn-scope true)
(emit-op fn-em 50)
(let (let
((upvals (get fn-scope "upvalues")) ((rest-pos -1) (rest-name nil))
(code {:upvalue-count (len upvals) :arity (len (get fn-scope "locals")) :constants (get (get fn-em "pool") "entries") :bytecode (get fn-em "bytecode")})
(code-idx (pool-add (get em "pool") code)))
(emit-op em 51)
(emit-u16 em code-idx)
(for-each (for-each
(fn (fn
(uv) (p)
(emit-byte em (if (get uv "is-local") 1 0)) (let
(emit-byte em (get uv "index"))) ((name (cond (= (type-of p) "symbol") (symbol-name p) (and (list? p) (not (empty? p)) (= (type-of (first p)) "symbol")) (symbol-name (first p)) :else p)))
upvals))))) (cond
(= name "&rest")
(set! rest-pos (len (get fn-scope "locals")))
(= name "&key")
nil
:else (do
(when
(and (> rest-pos -1) (nil? rest-name))
(set! rest-name name))
(scope-define-local fn-scope name)))))
params)
(compile-begin fn-em body fn-scope true)
(emit-op fn-em 50)
(let
((upvals (get fn-scope "upvalues"))
(code (if (> rest-pos -1) {:upvalue-count (len upvals) :arity (len (get fn-scope "locals")) :constants (get (get fn-em "pool") "entries") :rest-arity rest-pos :bytecode (get fn-em "bytecode")} {:upvalue-count (len upvals) :arity (len (get fn-scope "locals")) :constants (get (get fn-em "pool") "entries") :bytecode (get fn-em "bytecode")}))
(code-idx (pool-add (get em "pool") code)))
(emit-op em 51)
(emit-u16 em code-idx)
(for-each
(fn
(uv)
(emit-byte em (if (get uv "is-local") 1 0))
(emit-byte em (get uv "index")))
upvals))))))
(define (define
compile-define compile-define
(fn (fn
@@ -681,7 +691,7 @@
(and (and
(not (empty? rest-args)) (not (empty? rest-args))
(= (type-of (first rest-args)) "keyword")) (= (type-of (first rest-args)) "keyword"))
(let (letrec
((skip-annotations (fn (items) (if (empty? items) nil (if (= (type-of (first items)) "keyword") (skip-annotations (rest (rest items))) (first items)))))) ((skip-annotations (fn (items) (if (empty? items) nil (if (= (type-of (first items)) "keyword") (skip-annotations (rest (rest items))) (first items))))))
(skip-annotations rest-args)) (skip-annotations rest-args))
(first rest-args))))) (first rest-args)))))
@@ -724,38 +734,41 @@
compile-cond compile-cond
(fn (fn
(em args scope tail?) (em args scope tail?)
"Compile (cond test1 body1 test2 body2 ... :else fallback)." "Compile (cond test1 body1 test2 body2 ... :else fallback).\n Also handles clause syntax: (cond (test1 body1) (test2 body2) ...)."
(if (let
(< (len args) 2) ((flat-args (if (and (not (empty? args)) (list? (first args)) (> (len (first args)) 1) (list? (first (first args)))) (reduce (fn (acc clause) (if (list? clause) (append acc clause) (append acc (list clause)))) (list) args) args)))
(emit-op em 2) (if
(let (< (len flat-args) 2)
((test (first args)) (emit-op em 2)
(body (nth args 1)) (let
(rest-clauses (if (> (len args) 2) (slice args 2) (list)))) ((test (nth flat-args 0))
(if (body (nth flat-args 1))
(or (rest-clauses
(and (if (> (len flat-args) 2) (slice flat-args 2) (list))))
(= (type-of test) "keyword") (if
(= (keyword-name test) "else")) (or
(= test true)) (and
(compile-expr em body scope tail?) (= (type-of test) "keyword")
(do (= (keyword-name test) "else"))
(compile-expr em test scope false) (= test true))
(emit-op em 33) (compile-expr em body scope tail?)
(let (do
((skip (current-offset em))) (compile-expr em test scope false)
(emit-i16 em 0) (emit-op em 33)
(compile-expr em body scope tail?)
(emit-op em 32)
(let (let
((end-jump (current-offset em))) ((skip (current-offset em)))
(emit-i16 em 0) (emit-i16 em 0)
(patch-i16 em skip (- (current-offset em) (+ skip 2))) (compile-expr em body scope tail?)
(compile-cond em rest-clauses scope tail?) (emit-op em 32)
(patch-i16 (let
em ((end-jump (current-offset em)))
end-jump (emit-i16 em 0)
(- (current-offset em) (+ end-jump 2))))))))))) (patch-i16 em skip (- (current-offset em) (+ skip 2)))
(compile-cond em rest-clauses scope tail?)
(patch-i16
em
end-jump
(- (current-offset em) (+ end-jump 2))))))))))))
(define (define
compile-case compile-case
(fn (fn

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

@@ -0,0 +1,66 @@
;; _hyperscript integration — wire _="..." attributes to compiled SX
;;
;; Entry points:
;; (hs-handler src) — compile source to callable (fn (me) ...)
;; (hs-activate! el) — activate hyperscript on a single element
;; (hs-boot!) — scan DOM, activate all _="..." elements
;; (hs-boot-subtree! root) — activate within a subtree (for HTMX swaps)
;; ── Compile source to a handler function ────────────────────────
;; Returns a function (fn (me) ...) that can be called with a DOM element.
;; Uses eval-expr-cek to turn the SX data structure into a live closure.
(define
hs-handler
(fn
(src)
(let
((sx (hs-to-sx-from-source src)))
(eval-expr-cek
(list
(quote fn)
(list (quote me))
(list
(quote let)
(list (list (quote it) nil) (list (quote event) nil))
sx))))))
;; ── Activate a single element ───────────────────────────────────
;; Reads the _="..." attribute, compiles, and executes with me=element.
;; Marks the element to avoid double-activation.
(define
hs-activate!
(fn
(el)
(let
((src (dom-get-attr el "_")))
(when
(and src (not (dom-get-data el "hs-active")))
(dom-set-data el "hs-active" true)
(let ((handler (hs-handler src))) (handler el))))))
;; ── Boot: scan entire document ──────────────────────────────────
;; Called once at page load. Finds all elements with _ attribute,
;; compiles their hyperscript, and activates them.
(define
hs-boot!
(fn
()
(let
((elements (dom-query-all (dom-body) "[_]")))
(for-each (fn (el) (hs-activate! el)) elements))))
;; ── Boot subtree: for dynamic content ───────────────────────────
;; Called after HTMX swaps or dynamic DOM insertion.
;; Only activates elements within the given root.
(define
hs-boot-subtree!
(fn
(root)
(let
((elements (dom-query-all root "[_]")))
(for-each (fn (el) (hs-activate! el)) elements))
(when (dom-get-attr root "_") (hs-activate! root))))

View File

@@ -0,0 +1,3 @@
(sxbc 1 "af8c1b333d6af000"
(code
:constants ("hs-handler" {:upvalue-count 0 :arity 1 :constants ("hs-to-sx-from-source" "eval-expr-cek" "list" fn me let it event) :bytecode (20 0 0 16 0 48 1 17 1 20 1 0 1 3 0 1 4 0 52 2 0 1 1 5 0 1 6 0 2 52 2 0 2 1 7 0 2 52 2 0 2 52 2 0 2 16 1 52 2 0 3 52 2 0 3 49 1 50)} "hs-activate!" {:upvalue-count 0 :arity 1 :constants ("dom-get-attr" "_" "not" "dom-get-data" "hs-active" "dom-set-data" "hs-handler") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 6 33 15 0 5 20 3 0 16 0 1 4 0 48 2 52 2 0 1 33 30 0 20 5 0 16 0 1 4 0 3 48 3 5 20 6 0 16 1 48 1 17 2 16 2 16 0 49 1 32 1 0 2 50)} "hs-boot!" {:upvalue-count 0 :arity 0 :constants ("dom-query-all" "dom-body" "[_]" "for-each" {:upvalue-count 0 :arity 1 :constants ("hs-activate!") :bytecode (20 0 0 16 0 49 1 50)}) :bytecode (20 0 0 20 1 0 48 0 1 2 0 48 2 17 0 51 4 0 16 0 52 3 0 2 50)} "hs-boot-subtree!" {:upvalue-count 0 :arity 1 :constants ("dom-query-all" "[_]" "for-each" {:upvalue-count 0 :arity 1 :constants ("hs-activate!") :bytecode (20 0 0 16 0 49 1 50)} "dom-get-attr" "_" "hs-activate!") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 51 3 0 16 1 52 2 0 2 5 20 4 0 16 0 1 5 0 48 2 33 10 0 20 6 0 16 0 49 1 32 1 0 2 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 50)))

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

@@ -797,7 +797,8 @@
"sx dom", "sx dom",
"sx browser", "sx browser",
"web adapter-dom", "web adapter-dom",
"web engine" "web engine",
"hyperscript integration"
], ],
"exports": [ "exports": [
"_preload-cache", "_preload-cache",
@@ -861,6 +862,89 @@
"engine-init" "engine-init"
] ]
}, },
"hyperscript tokenizer": {
"file": "hs-tokenizer.sxbc",
"deps": [],
"exports": [
"hs-tokenize",
"hs-make-token",
"hs-keywords",
"hs-keyword?",
"hs-digit?",
"hs-letter?",
"hs-ident-start?",
"hs-ident-char?",
"hs-ws?"
]
},
"hyperscript parser": {
"file": "hs-parser.sxbc",
"deps": [
"hyperscript tokenizer"
],
"exports": [
"hs-parse",
"hs-compile"
]
},
"hyperscript compiler": {
"file": "hs-compiler.sxbc",
"deps": [
"hyperscript parser"
],
"exports": [
"hs-to-sx",
"hs-to-sx-from-source"
]
},
"hyperscript runtime": {
"file": "hs-runtime.sxbc",
"deps": [
"sx dom",
"sx browser"
],
"exports": [
"hs-on",
"hs-on-every",
"hs-init",
"hs-wait",
"hs-wait-for",
"hs-settle",
"hs-toggle-class!",
"hs-toggle-between!",
"hs-take!",
"hs-put!",
"hs-navigate!",
"hs-next",
"hs-previous",
"hs-query-first",
"hs-query-last",
"hs-first",
"hs-last",
"hs-repeat-times",
"hs-repeat-forever",
"hs-fetch",
"hs-coerce",
"hs-make",
"hs-install",
"hs-measure",
"hs-transition"
]
},
"hyperscript integration": {
"file": "hs-integration.sxbc",
"deps": [
"hyperscript compiler",
"hyperscript runtime",
"sx dom"
],
"exports": [
"hs-handler",
"hs-activate!",
"hs-boot!",
"hs-boot-subtree!"
]
},
"_entry": { "_entry": {
"file": "boot.sxbc", "file": "boot.sxbc",
"deps": [ "deps": [

View File

@@ -6,6 +6,9 @@
(import (web adapter-dom)) (import (web adapter-dom))
(import (web engine)) (import (web engine))
(import (hyperscript integration)) ;; end define-library
;; Re-export to global namespace for backward compatibility
(define-library (define-library
(web orchestration) (web orchestration)
(export (export
@@ -626,7 +629,8 @@
(sx-hydrate-islands root) (sx-hydrate-islands root)
(run-post-render-hooks) (run-post-render-hooks)
(flush-collected-styles) (flush-collected-styles)
(process-elements root))) (process-elements root)
(hs-boot-subtree! root)))
(define (define
process-settle-hooks process-settle-hooks
:effects (mutation io) :effects (mutation io)
@@ -1632,7 +1636,6 @@
(do (do
(sx-process-scripts nil) (sx-process-scripts nil)
(sx-hydrate nil) (sx-hydrate nil)
(process-elements nil)))))) ;; end define-library (process-elements nil))))))
;; Re-export to global namespace for backward compatibility
(import (web orchestration)) (import (web orchestration))

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

@@ -137,18 +137,21 @@
code-from-value code-from-value
(fn (fn
(v) (v)
"Convert a compiler output dict to a vm-code object." "Convert a compiler output dict to a vm-code dict. Idempotent — if v\n already has vm-code keys (vc-bytecode), returns as-is."
(if (if
(not (dict? v)) (not (dict? v))
(make-vm-code 0 16 (list) (list)) (make-vm-code 0 16 (list) (list))
(let (if
((bc-raw (get v "bytecode")) (has-key? v "vc-bytecode")
(bc (if (nil? bc-raw) (list) bc-raw)) v
(consts-raw (get v "constants")) (let
(consts (if (nil? consts-raw) (list) consts-raw)) ((bc-raw (get v "bytecode"))
(arity-raw (get v "arity")) (bc (if (nil? bc-raw) (list) bc-raw))
(arity (if (nil? arity-raw) 0 arity-raw))) (consts-raw (get v "constants"))
(make-vm-code arity (+ arity 16) bc consts))))) (consts (if (nil? consts-raw) (list) consts-raw))
(arity-raw (get v "arity"))
(arity (if (nil? arity-raw) 0 arity-raw)))
(make-vm-code arity (+ arity 16) bc consts))))))
(define vm-closure? (fn (v) (and (dict? v) (has-key? v "vm-code")))) (define vm-closure? (fn (v) (and (dict? v) (has-key? v "vm-code"))))
(define *active-vm* nil) (define *active-vm* nil)
(define *jit-compile-fn* nil) (define *jit-compile-fn* nil)
@@ -297,27 +300,32 @@
vm-global-get vm-global-get
(fn (fn
(vm frame name) (vm frame name)
"Look up a global: globals table → closure env → primitives → HO wrappers" "Look up a global: closure env → globals table → primitives → HO forms"
(let (let
((globals (vm-globals-ref vm))) ((closure-env (get (frame-closure frame) "vm-closure-env")))
(if (if
(has-key? globals name) (nil? closure-env)
(get globals name)
(let (let
((closure-env (-> frame frame-closure closure-env))) ((globals (vm-globals-ref vm)))
(if (if
(nil? closure-env) (has-key? globals name)
(get globals name)
(cek-try (cek-try
(fn () (get-primitive name)) (fn () (get-primitive name))
(fn (e) (vm-resolve-ho-form vm name))) (fn (e) (vm-resolve-ho-form vm name)))))
(let
((found (env-walk closure-env name)))
(if
(nil? found)
(let (let
((found (env-walk closure-env name))) ((globals (vm-globals-ref vm)))
(if (if
(nil? found) (has-key? globals name)
(get globals name)
(cek-try (cek-try
(fn () (get-primitive name)) (fn () (get-primitive name))
(fn (e) (vm-resolve-ho-form vm name))) (fn (e) (vm-resolve-ho-form vm name)))))
found)))))))) found))))))
(define (define
vm-resolve-ho-form vm-resolve-ho-form
(fn (fn

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}; blake2_js_for_wasm_create: blake2_js_for_wasm_create};
} }
(globalThis)) (globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-f96dc336",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-b7a948a6",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var ({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-9799aa33",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-b7a948a6",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new 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_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 Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new