Fix HS browser activation: host-get function sentinel, runtime symbol shadow, lazy dep chain

Three bugs fixed:
1. host-get in sx-platform.js: return true for function-valued properties
   so dom-get-attr/dom-set-attr guards pass (functions can't cross WASM boundary)
2. hs-runtime.sx: renamed host-get→hs-host-get and dom-query→hs-dom-query to
   stop shadowing platform natives when loaded as .sx source
3. compile-modules.js: HS dependency chain (integration→runtime→compiler→parser→tokenizer)
   so lazy loading pulls in all deps. Non-library modules load as .sx source
   for CEK env visibility.

Result: 8/8 elements activate, hs-on attaches listeners. Click handler needs
IO suspension support (VmSuspended in sx_browser.ml) to fire — next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 20:50:10 +00:00
parent 7f273dc7c2
commit c6df054957
7 changed files with 86 additions and 27 deletions

View File

@@ -382,9 +382,16 @@ for (const file of FILES) {
const defines = extractDefines(src);
if (defines.length > 0) {
const key = file.replace(/\.sx$/, '');
// HS modules form a dependency chain — loading one loads all predecessors.
const HS_DEPS = {
'hs-parser': ['hs-tokenizer'],
'hs-compiler': ['hs-tokenizer', 'hs-parser'],
'hs-runtime': ['hs-tokenizer', 'hs-parser', 'hs-compiler'],
'hs-integration': ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime'],
};
manifest[key] = {
file: sxbcFile,
deps: [],
deps: HS_DEPS[key] || [],
exports: defines,
};
}

View File

@@ -40,7 +40,12 @@
var obj = args[0], prop = args[1];
if (obj == null) return null;
var v = obj[prop];
return v === undefined ? null : v;
if (v === undefined) return null;
// Functions can't cross the WASM boundary — return true as a truthy
// sentinel so (host-get el "getAttribute") works as a guard.
// Use host-call to actually invoke the method.
if (typeof v === "function") return true;
return v;
});
K.registerNative("host-set!", function(args) {
@@ -474,9 +479,19 @@
// will see it as already loaded and skip rather than infinite-looping.
_loadedLibs[name] = true;
// Load this module
var ok = loadBytecodeFile("sx/" + info.file);
if (!ok) {
// Load this module.
// Non-library modules (no space in name, e.g. "hs-runtime") use source
// loading so their defines go into the CEK global env — needed when
// eval-expr-cek evaluates compiled hyperscript at runtime.
var isLibrary = name.indexOf(' ') >= 0 || name === '_entry';
var ok;
if (isLibrary) {
ok = loadBytecodeFile("sx/" + info.file);
if (!ok) {
var sxFile = info.file.replace(/\.sxbc$/, '.sx');
ok = loadSxFile("sx/" + sxFile);
}
} else {
var sxFile = info.file.replace(/\.sxbc$/, '.sx');
ok = loadSxFile("sx/" + sxFile);
}

View File

@@ -49,7 +49,9 @@
;; Toggle a single class on an element.
(define
hs-toggle-class!
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
(fn
(target cls)
(host-call (hs-host-get target "classList") "toggle" cls)))
;; Toggle between two classes — exactly one is active at a time.
(define
@@ -136,7 +138,7 @@
(find-prev sibling)))))
;; First element matching selector within a scope.
(define hs-query-first (fn (sel) (dom-query sel)))
(define hs-query-first (fn (sel) (hs-dom-query sel)))
;; Last element matching selector.
(define
@@ -466,9 +468,11 @@
d))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define host-get (fn (obj key) (if (= key "length") (len obj) (get obj key))))
(define
hs-host-get
(fn (obj key) (if (= key "length") (len obj) (get obj key))))
;; DOM query stub — sandbox returns empty list
(define dom-query (fn (selector) (list)))
(define hs-dom-query (fn (selector) (list)))
;; Method dispatch — obj.method(args)
(define hs-method-call (fn (obj method &rest args)
(cond
@@ -488,7 +492,7 @@
;; beep! — debug logging, returns value unchanged
(define hs-beep (fn (v) v))
;; Property-based is — check obj.key truthiness
(define hs-prop-is (fn (obj key) (not (hs-falsy? (host-get obj key)))))
(define hs-prop-is (fn (obj key) (not (hs-falsy? (hs-host-get obj key)))))
;; Array slicing (inclusive both ends)
(define hs-slice (fn (col start end)
(let ((s (if (nil? start) 0 start))

View File

@@ -40,7 +40,12 @@
var obj = args[0], prop = args[1];
if (obj == null) return null;
var v = obj[prop];
return v === undefined ? null : v;
if (v === undefined) return null;
// Functions can't cross the WASM boundary — return true as a truthy
// sentinel so (host-get el "getAttribute") works as a guard.
// Use host-call to actually invoke the method.
if (typeof v === "function") return true;
return v;
});
K.registerNative("host-set!", function(args) {
@@ -474,9 +479,19 @@
// will see it as already loaded and skip rather than infinite-looping.
_loadedLibs[name] = true;
// Load this module
var ok = loadBytecodeFile("sx/" + info.file);
if (!ok) {
// Load this module.
// Non-library modules (no space in name, e.g. "hs-runtime") use source
// loading so their defines go into the CEK global env — needed when
// eval-expr-cek evaluates compiled hyperscript at runtime.
var isLibrary = name.indexOf(' ') >= 0 || name === '_entry';
var ok;
if (isLibrary) {
ok = loadBytecodeFile("sx/" + info.file);
if (!ok) {
var sxFile = info.file.replace(/\.sxbc$/, '.sx');
ok = loadSxFile("sx/" + sxFile);
}
} else {
var sxFile = info.file.replace(/\.sxbc$/, '.sx');
ok = loadSxFile("sx/" + sxFile);
}

View File

@@ -49,7 +49,9 @@
;; Toggle a single class on an element.
(define
hs-toggle-class!
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
(fn
(target cls)
(host-call (hs-host-get target "classList") "toggle" cls)))
;; Toggle between two classes — exactly one is active at a time.
(define
@@ -136,7 +138,7 @@
(find-prev sibling)))))
;; First element matching selector within a scope.
(define hs-query-first (fn (sel) (dom-query sel)))
(define hs-query-first (fn (sel) (hs-dom-query sel)))
;; Last element matching selector.
(define
@@ -466,9 +468,11 @@
d))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define host-get (fn (obj key) (if (= key "length") (len obj) (get obj key))))
(define
hs-host-get
(fn (obj key) (if (= key "length") (len obj) (get obj key))))
;; DOM query stub — sandbox returns empty list
(define dom-query (fn (selector) (list)))
(define hs-dom-query (fn (selector) (list)))
;; Method dispatch — obj.method(args)
(define hs-method-call (fn (obj method &rest args)
(cond
@@ -488,7 +492,7 @@
;; beep! — debug logging, returns value unchanged
(define hs-beep (fn (v) v))
;; Property-based is — check obj.key truthiness
(define hs-prop-is (fn (obj key) (not (hs-falsy? (host-get obj key)))))
(define hs-prop-is (fn (obj key) (not (hs-falsy? (hs-host-get obj key)))))
;; Array slicing (inclusive both ends)
(define hs-slice (fn (col start end)
(let ((s (if (nil? start) 0 start))

File diff suppressed because one or more lines are too long

View File

@@ -927,7 +927,9 @@
},
"hs-parser": {
"file": "hs-parser.sxbc",
"deps": [],
"deps": [
"hs-tokenizer"
],
"exports": [
"hs-parse",
"hs-compile"
@@ -935,7 +937,10 @@
},
"hs-compiler": {
"file": "hs-compiler.sxbc",
"deps": [],
"deps": [
"hs-tokenizer",
"hs-parser"
],
"exports": [
"hs-to-sx",
"hs-to-sx-from-source"
@@ -943,7 +948,11 @@
},
"hs-runtime": {
"file": "hs-runtime.sxbc",
"deps": [],
"deps": [
"hs-tokenizer",
"hs-parser",
"hs-compiler"
],
"exports": [
"hs-on",
"hs-on-every",
@@ -982,8 +991,8 @@
"hs-last",
"hs-template",
"hs-make-object",
"host-get",
"dom-query",
"hs-host-get",
"hs-dom-query",
"hs-method-call",
"hs-beep",
"hs-prop-is",
@@ -998,7 +1007,12 @@
},
"hs-integration": {
"file": "hs-integration.sxbc",
"deps": [],
"deps": [
"hs-tokenizer",
"hs-parser",
"hs-compiler",
"hs-runtime"
],
"exports": [
"hs-handler",
"hs-activate!",