Transparent lazy module loading — code loads like data

When the VM or CEK hits an undefined symbol, it checks a symbol→library
index (built from manifest exports at boot), loads the library that
exports it, and returns the value. Execution continues as if the module
was always loaded. No import statements, no load-library! calls, no
Suspense boundaries — just call the function.

This is the same mechanism as IO suspension for data fetching. The
programmer doesn't distinguish between calling a local function and
calling one that needs its module fetched first. The runtime treats
code as just another resource.

Implementation:
- _symbol_resolve_hook in sx_types.ml — called by env_get_id (CEK path)
  and vm_global_get (VM path) when a symbol isn't found
- Symbol→library index built from manifest exports in sx-platform.js
- __resolve-symbol native calls __sxLoadLibrary, module loads, symbol
  appears in globals, execution resumes
- compile-modules.js extracts export lists into module-manifest.json
- Playground page demonstrates: (freeze-scope) triggers freeze.sxbc
  download transparently on first use

2650/2650 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 22:14:19 +00:00
parent f4f8715d06
commit 2f3e727a6f
11 changed files with 961 additions and 67 deletions

View File

@@ -279,6 +279,20 @@ function extractImportDeps(source) {
return deps;
}
// Extract exported symbol names from (export name1 name2 ...) clause
function extractExports(source) {
const exports = [];
const m = source.match(/\(export\s+([\s\S]*?)\)\s*\(/);
if (!m) return exports;
// Parse symbol names from the export list (skip keywords, nested forms)
const tokens = m[1].split(/\s+/).filter(t => t && !t.startsWith(':') && !t.startsWith('(') && !t.startsWith(')'));
for (const t of tokens) {
const clean = t.replace(/[()]/g, '');
if (clean && !clean.startsWith(':')) exports.push(clean);
}
return exports;
}
// Flatten library spec: "(sx dom)" → "sx dom"
function libKey(spec) {
return spec.replace(/^\(/, '').replace(/\)$/, '');
@@ -296,9 +310,11 @@ for (const file of FILES) {
const sxbcFile = file.replace(/\.sx$/, '.sxbc');
if (libName) {
const exports = extractExports(src);
manifest[libKey(libName)] = {
file: sxbcFile,
deps: deps.map(libKey),
exports: exports,
};
} else if (deps.length > 0) {
// Entry point (no define-library, has imports)

View File

@@ -550,6 +550,47 @@
return ok;
};
// ================================================================
// Transparent lazy loading — symbol → library index
//
// When the VM hits an undefined symbol, the resolve hook checks this
// index, loads the library that exports it, and returns the value.
// The programmer just calls the function — loading is invisible.
// ================================================================
var _symbolIndex = null; // symbol name → library key
function buildSymbolIndex() {
if (_symbolIndex) return _symbolIndex;
if (!_manifest) loadManifest();
if (!_manifest) return null;
_symbolIndex = {};
for (var key in _manifest) {
if (key.startsWith('_')) continue;
var entry = _manifest[key];
if (entry.exports) {
for (var i = 0; i < entry.exports.length; i++) {
_symbolIndex[entry.exports[i]] = key;
}
}
}
return _symbolIndex;
}
// Register the resolve hook — called by the VM when GLOBAL_GET fails
K.registerNative("__resolve-symbol", function(args) {
var name = args[0];
if (!name) return null;
var idx = buildSymbolIndex();
if (!idx || !idx[name]) return null;
var lib = idx[name];
if (_loadedLibs[lib]) return null; // already loaded but symbol still missing — real error
// Load the library
__sxLoadLibrary(lib);
// Return null — the VM will re-lookup in globals after the hook loads the module
return null;
});
// ================================================================
// Compatibility shim — expose Sx global matching current JS API
// ================================================================

View File

@@ -229,6 +229,21 @@ let () =
Sx_types._vm_global_set_hook := Some (fun name v ->
Hashtbl.replace global_env.bindings (Sx_types.intern name) v)
(* Symbol resolve hook: transparent lazy module loading.
When GLOBAL_GET can't find a symbol, this calls the JS __resolve-symbol
native which checks the manifest's symbol→library index and loads the
library that exports it. After loading, the symbol is in _vm_globals. *)
let () =
Sx_types._symbol_resolve_hook := Some (fun name ->
match Hashtbl.find_opt Sx_primitives.primitives "__resolve-symbol" with
| None -> None
| Some resolve_fn ->
(try ignore (resolve_fn [String name]) with _ -> ());
(* Check if the symbol appeared in globals after the load *)
match Hashtbl.find_opt _vm_globals name with
| Some v -> Some v
| None -> None)
(* ================================================================== *)
(* Core API *)
(* ================================================================== *)

View File

@@ -500,7 +500,7 @@ and cek_step state =
(* step-eval *)
and step_eval state =
(let expr = (cek_control (state)) in let env = (cek_env (state)) in let kont = (cek_kont (state)) in (let _match_val = (type_of (expr)) in (if _match_val = (String "number") then (make_cek_value (expr) (env) (kont)) else (if _match_val = (String "string") then (make_cek_value (expr) (env) (kont)) else (if _match_val = (String "boolean") then (make_cek_value (expr) (env) (kont)) else (if _match_val = (String "nil") then (make_cek_value (Nil) (env) (kont)) else (if _match_val = (String "symbol") then (let name = (symbol_name (expr)) in (let val' = (if sx_truthy ((env_has (env) (name))) then (env_get (env) (name)) else (if sx_truthy ((is_primitive (name))) then (get_primitive (name)) else (if sx_truthy ((prim_call "=" [name; (String "true")])) then (Bool true) else (if sx_truthy ((prim_call "=" [name; (String "false")])) then (Bool false) else (if sx_truthy ((prim_call "=" [name; (String "nil")])) then Nil else (raise (Eval_error (value_to_str (String (sx_str [(String "Undefined symbol: "); name])))))))))) in (let () = ignore ((if sx_truthy ((let _and = (is_nil (val')) in if not (sx_truthy _and) then _and else (prim_call "starts-with?" [name; (String "~")]))) then (debug_log ((String "Component not found:")) (name)) else Nil)) in (make_cek_value (val') (env) (kont))))) else (if _match_val = (String "keyword") then (make_cek_value ((keyword_name (expr))) (env) (kont)) else (if _match_val = (String "dict") then (let ks = (prim_call "keys" [expr]) in (if sx_truthy ((empty_p (ks))) then (make_cek_value ((Dict (Hashtbl.create 0))) (env) (kont)) else (let first_key = (first (ks)) in let remaining_entries = ref ((List [])) in (let () = ignore ((List.iter (fun k -> ignore ((remaining_entries := sx_append_b !remaining_entries (List [k; (get (expr) (k))]); Nil))) (sx_to_list (rest (ks))); Nil)) in (make_cek_state ((get (expr) (first_key))) (env) ((kont_push ((make_dict_frame (!remaining_entries) ((List [(List [first_key])])) (env))) (kont)))))))) else (if _match_val = (String "list") then (if sx_truthy ((empty_p (expr))) then (make_cek_value ((List [])) (env) (kont)) else (step_eval_list (expr) (env) (kont))) else (make_cek_value (expr) (env) (kont))))))))))))
(let expr = (cek_control (state)) in let env = (cek_env (state)) in let kont = (cek_kont (state)) in (let _match_val = (type_of (expr)) in (if _match_val = (String "number") then (make_cek_value (expr) (env) (kont)) else (if _match_val = (String "string") then (make_cek_value (expr) (env) (kont)) else (if _match_val = (String "boolean") then (make_cek_value (expr) (env) (kont)) else (if _match_val = (String "nil") then (make_cek_value (Nil) (env) (kont)) else (if _match_val = (String "symbol") then (let name = (symbol_name (expr)) in (let val' = (if sx_truthy ((env_has (env) (name))) then (env_get (env) (name)) else (if sx_truthy ((is_primitive (name))) then (get_primitive (name)) else (if sx_truthy ((prim_call "=" [name; (String "true")])) then (Bool true) else (if sx_truthy ((prim_call "=" [name; (String "false")])) then (Bool false) else (if sx_truthy ((prim_call "=" [name; (String "nil")])) then Nil else (match !_symbol_resolve_hook with Some hook -> (match hook (value_to_str name) with Some v -> v | None -> raise (Eval_error ("Undefined symbol: " ^ value_to_str name))) | None -> raise (Eval_error ("Undefined symbol: " ^ value_to_str name)))))))) in (let () = ignore ((if sx_truthy ((let _and = (is_nil (val')) in if not (sx_truthy _and) then _and else (prim_call "starts-with?" [name; (String "~")]))) then (debug_log ((String "Component not found:")) (name)) else Nil)) in (make_cek_value (val') (env) (kont))))) else (if _match_val = (String "keyword") then (make_cek_value ((keyword_name (expr))) (env) (kont)) else (if _match_val = (String "dict") then (let ks = (prim_call "keys" [expr]) in (if sx_truthy ((empty_p (ks))) then (make_cek_value ((Dict (Hashtbl.create 0))) (env) (kont)) else (let first_key = (first (ks)) in let remaining_entries = ref ((List [])) in (let () = ignore ((List.iter (fun k -> ignore ((remaining_entries := sx_append_b !remaining_entries (List [k; (get (expr) (k))]); Nil))) (sx_to_list (rest (ks))); Nil)) in (make_cek_state ((get (expr) (first_key))) (env) ((kont_push ((make_dict_frame (!remaining_entries) ((List [(List [first_key])])) (env))) (kont)))))))) else (if _match_val = (String "list") then (if sx_truthy ((empty_p (expr))) then (make_cek_value ((List [])) (env) (kont)) else (step_eval_list (expr) (env) (kont))) else (make_cek_value (expr) (env) (kont))))))))))))
(* step-sf-raise *)
and step_sf_raise args env kont =

View File

@@ -259,6 +259,13 @@ let _vm_global_set_hook : (string -> value -> unit) option ref = ref None
If set, the hook loads the library and returns true; cek_run then resumes. *)
let _import_hook : (value -> bool) option ref = ref None
(* Optional hook: called by vm_global_get when a symbol isn't found.
Receives the symbol name. If the hook can resolve it (e.g. by loading a
library that exports it), it returns Some value. Otherwise None.
This enables transparent lazy module loading — just use a symbol and
the VM loads whatever module provides it. *)
let _symbol_resolve_hook : (string -> value option) option ref = ref None
let env_bind env name v =
Hashtbl.replace env.bindings (intern name) v;
(match !_env_bind_hook with Some f -> f env name v | None -> ());
@@ -278,7 +285,16 @@ let rec env_get_id env id name =
match env.parent with
| Some p -> env_get_id p id name
| None ->
raise (Eval_error ("Undefined symbol: " ^ name))
(* Symbol not in any scope — try the resolve hook (transparent lazy loading).
The hook loads the module that exports this symbol, making it available. *)
match !_symbol_resolve_hook with
| Some hook ->
(match hook name with
| Some v ->
(* Cache in the root env so subsequent lookups are instant *)
Hashtbl.replace env.bindings id v; v
| None -> raise (Eval_error ("Undefined symbol: " ^ name)))
| None -> raise (Eval_error ("Undefined symbol: " ^ name))
let env_get env name = env_get_id env (intern name) name

View File

@@ -204,20 +204,26 @@ let vm_global_get vm_val frame_val name =
| None ->
(* Walk closure env chain *)
let f = unwrap_frame frame_val in
let not_found () =
(* Try evaluator's primitive table *)
try prim_call n [] with _ ->
(* Try symbol resolve hook — transparent lazy module loading *)
match !_symbol_resolve_hook with
| Some hook ->
(match hook n with
| Some v -> v
| None -> raise (Eval_error ("VM undefined: " ^ n)))
| None -> raise (Eval_error ("VM undefined: " ^ n))
in
(match f.vf_closure.vm_closure_env with
| Some env ->
let id = intern n in
let rec find_env e =
match Hashtbl.find_opt e.bindings id with
| Some v -> v
| None -> (match e.parent with Some p -> find_env p | None ->
(* Try evaluator's primitive table as last resort *)
(try prim_call n [] with _ ->
raise (Eval_error ("VM undefined: " ^ n))))
| None -> (match e.parent with Some p -> find_env p | None -> not_found ())
in find_env env
| None ->
(try prim_call n [] with _ ->
raise (Eval_error ("VM undefined: " ^ n))))
| None -> not_found ())
let vm_global_set vm_val frame_val name v =
let m = unwrap_vm vm_val in

View File

@@ -550,6 +550,47 @@
return ok;
};
// ================================================================
// Transparent lazy loading — symbol → library index
//
// When the VM hits an undefined symbol, the resolve hook checks this
// index, loads the library that exports it, and returns the value.
// The programmer just calls the function — loading is invisible.
// ================================================================
var _symbolIndex = null; // symbol name → library key
function buildSymbolIndex() {
if (_symbolIndex) return _symbolIndex;
if (!_manifest) loadManifest();
if (!_manifest) return null;
_symbolIndex = {};
for (var key in _manifest) {
if (key.startsWith('_')) continue;
var entry = _manifest[key];
if (entry.exports) {
for (var i = 0; i < entry.exports.length; i++) {
_symbolIndex[entry.exports[i]] = key;
}
}
}
return _symbolIndex;
}
// Register the resolve hook — called by the VM when GLOBAL_GET fails
K.registerNative("__resolve-symbol", function(args) {
var name = args[0];
if (!name) return null;
var idx = buildSymbolIndex();
if (!idx || !idx[name]) return null;
var lib = idx[name];
if (_loadedLibs[lib]) return null; // already loaded but symbol still missing — real error
// Load the library
__sxLoadLibrary(lib);
// Return null — the VM will re-lookup in globals after the hook loads the module
return null;
});
// ================================================================
// Compatibility shim — expose Sx global matching current JS API
// ================================================================

View File

@@ -1,65 +1,529 @@
{
"sx render": {
"file": "render.sxbc",
"deps": []
"deps": [],
"exports": [
"HTML_TAGS",
"VOID_ELEMENTS",
"BOOLEAN_ATTRS",
"*definition-form-extensions*",
"definition-form?",
"parse-element-args",
"render-attrs",
"eval-cond",
"eval-cond-scheme",
"eval-cond-clojure",
"process-bindings",
"is-render-expr?",
"merge-spread-attrs",
"escape-html",
"escape-attr"
]
},
"sx signals": {
"file": "core-signals.sxbc",
"deps": []
"deps": [],
"exports": [
"make-signal",
"signal?",
"signal-value",
"signal-set-value!",
"signal-subscribers",
"signal-add-sub!",
"signal-remove-sub!",
"signal-deps",
"signal-set-deps!",
"signal",
"deref",
"reset!",
"swap!",
"computed",
"effect",
"*batch-depth*",
"*batch-queue*",
"batch",
"notify-subscribers",
"flush-subscribers",
"dispose-computed",
"with-island-scope",
"register-in-scope"
]
},
"sx signals-web": {
"file": "signals.sxbc",
"deps": [
"sx dom",
"sx browser"
],
"exports": [
"with-marsh-scope",
"dispose-marsh-scope",
"emit-event",
"on-event",
"bridge-event",
"resource"
]
},
"web deps": {
"file": "deps.sxbc",
"deps": []
"deps": [],
"exports": [
"scan-refs",
"scan-refs-walk",
"transitive-deps-walk",
"transitive-deps",
"compute-all-deps",
"scan-components-from-source",
"components-needed",
"page-component-bundle",
"page-css-classes",
"scan-io-refs-walk",
"scan-io-refs",
"transitive-io-refs-walk",
"transitive-io-refs",
"compute-all-io-refs",
"component-io-refs-cached",
"component-pure?",
"render-target",
"page-render-plan",
"env-components"
]
},
"web router": {
"file": "router.sxbc",
"deps": []
"deps": [],
"exports": [
"split-path-segments",
"make-route-segment",
"parse-route-pattern",
"match-route-segments",
"match-route",
"find-matching-route",
"_fn-to-segment",
"sx-url-to-path",
"_count-leading-dots",
"_strip-trailing-close",
"_index-of-safe",
"_last-index-of",
"_pop-sx-url-level",
"_pop-sx-url-levels",
"_split-pos-kw",
"_parse-relative-body",
"_extract-innermost",
"_find-kw-in-tokens",
"_find-keyword-value",
"_replace-kw-in-tokens",
"_set-keyword-in-content",
"_is-delta-value?",
"_apply-delta",
"_apply-kw-pairs",
"_apply-keywords-to-url",
"_normalize-relative",
"resolve-relative-url",
"relative-sx-url?",
"_url-special-forms",
"url-special-form?",
"parse-sx-url",
"url-special-form-name",
"url-special-form-inner",
"url-to-expr",
"auto-quote-unknowns",
"prepare-url-expr"
]
},
"web page-helpers": {
"file": "page-helpers.sxbc",
"deps": []
"deps": [],
"exports": [
"special-form-category-map",
"extract-define-kwargs",
"categorize-special-forms",
"build-ref-items-with-href",
"build-reference-data",
"build-attr-detail",
"build-header-detail",
"build-event-detail",
"build-component-source",
"build-bundle-analysis",
"build-routing-analysis",
"build-affinity-analysis"
]
},
"sx freeze": {
"file": "freeze.sxbc",
"deps": []
"deps": [],
"exports": [
"freeze-registry",
"freeze-signal",
"freeze-scope",
"cek-freeze-scope",
"cek-freeze-all",
"cek-thaw-scope",
"cek-thaw-all",
"freeze-to-sx",
"thaw-from-sx"
]
},
"sx bytecode": {
"file": "bytecode.sxbc",
"deps": []
"deps": [],
"exports": [
"OP_CONST",
"OP_NIL",
"OP_TRUE",
"OP_FALSE",
"OP_POP",
"OP_DUP",
"OP_LOCAL_GET",
"OP_LOCAL_SET",
"OP_UPVALUE_GET",
"OP_UPVALUE_SET",
"OP_GLOBAL_GET",
"OP_GLOBAL_SET",
"OP_JUMP",
"OP_JUMP_IF_FALSE",
"OP_JUMP_IF_TRUE",
"OP_CALL",
"OP_TAIL_CALL",
"OP_RETURN",
"OP_CLOSURE",
"OP_CALL_PRIM",
"OP_APPLY",
"OP_LIST",
"OP_DICT",
"OP_APPEND_BANG",
"OP_ITER_INIT",
"OP_ITER_NEXT",
"OP_MAP_OPEN",
"OP_MAP_APPEND",
"OP_MAP_CLOSE",
"OP_FILTER_TEST",
"OP_HO_MAP",
"OP_HO_FILTER",
"OP_HO_REDUCE",
"OP_HO_FOR_EACH",
"OP_HO_SOME",
"OP_HO_EVERY",
"OP_SCOPE_PUSH",
"OP_SCOPE_POP",
"OP_PROVIDE_PUSH",
"OP_PROVIDE_POP",
"OP_CONTEXT",
"OP_EMIT",
"OP_EMITTED",
"OP_RESET",
"OP_SHIFT",
"OP_DEFINE",
"OP_DEFCOMP",
"OP_DEFISLAND",
"OP_DEFMACRO",
"OP_EXPAND_MACRO",
"OP_STR_CONCAT",
"OP_STR_JOIN",
"OP_SERIALIZE",
"OP_ADD",
"OP_SUB",
"OP_MUL",
"OP_DIV",
"OP_EQ",
"OP_LT",
"OP_GT",
"OP_NOT",
"OP_LEN",
"OP_FIRST",
"OP_REST",
"OP_NTH",
"OP_CONS",
"OP_NEG",
"OP_INC",
"OP_DEC",
"OP_ASER_TAG",
"OP_ASER_FRAG",
"BYTECODE_MAGIC",
"BYTECODE_VERSION",
"CONST_NUMBER",
"CONST_STRING",
"CONST_BOOL",
"CONST_NIL",
"CONST_SYMBOL",
"CONST_KEYWORD",
"CONST_LIST",
"CONST_DICT",
"CONST_CODE",
"opcode-name"
]
},
"sx compiler": {
"file": "compiler.sxbc",
"deps": []
"deps": [],
"exports": [
"make-pool",
"pool-add",
"make-scope",
"scope-define-local",
"scope-resolve",
"make-emitter",
"emit-byte",
"emit-u16",
"emit-i16",
"emit-op",
"emit-const",
"current-offset",
"patch-i16",
"compile-expr",
"compile-symbol",
"compile-dict",
"compile-list",
"compile-if",
"compile-when",
"compile-and",
"compile-or",
"compile-begin",
"compile-let",
"desugar-let-match",
"compile-letrec",
"compile-lambda",
"compile-define",
"compile-set",
"compile-quote",
"compile-cond",
"compile-case",
"compile-case-clauses",
"compile-match",
"compile-thread",
"compile-thread-step",
"compile-defcomp",
"compile-defmacro",
"compile-quasiquote",
"compile-qq-expr",
"compile-qq-list",
"compile-call",
"compile",
"compile-module"
]
},
"sx vm": {
"file": "vm.sxbc",
"deps": []
"deps": [],
"exports": [
"make-upvalue-cell",
"uv-get",
"uv-set!",
"make-vm-code",
"make-vm-closure",
"make-vm-frame",
"make-vm",
"vm-push",
"vm-pop",
"vm-peek",
"frame-read-u8",
"frame-read-u16",
"frame-read-i16",
"vm-push-frame",
"code-from-value",
"vm-closure?",
"*active-vm*",
"*jit-compile-fn*",
"lambda?",
"lambda-compiled",
"lambda-set-compiled!",
"lambda-name",
"cek-call-or-suspend",
"try-jit-call",
"vm-call",
"frame-local-get",
"frame-local-set",
"frame-upvalue-get",
"frame-upvalue-set",
"frame-ip",
"frame-set-ip!",
"frame-base",
"frame-closure",
"closure-code",
"closure-upvalues",
"closure-env",
"code-bytecode",
"code-constants",
"code-locals",
"vm-sp",
"vm-set-sp!",
"vm-stack",
"vm-set-stack!",
"vm-frames",
"vm-set-frames!",
"vm-globals-ref",
"collect-n-from-stack",
"collect-n-pairs",
"pad-n-nils",
"vm-global-get",
"vm-resolve-ho-form",
"vm-call-external",
"vm-global-set",
"env-walk",
"env-walk-set!",
"vm-create-closure",
"vm-run",
"vm-step",
"vm-call-closure",
"vm-execute-module",
"vm-resume-module"
]
},
"sx dom": {
"file": "dom.sxbc",
"deps": []
"deps": [],
"exports": [
"dom-document",
"dom-window",
"dom-body",
"dom-head",
"dom-create-element",
"create-text-node",
"create-fragment",
"create-comment",
"dom-append",
"dom-prepend",
"dom-insert-before",
"dom-insert-after",
"dom-remove",
"dom-is-active-element?",
"dom-is-input-element?",
"dom-is-child-of?",
"dom-attr-list",
"dom-remove-child",
"dom-replace-child",
"dom-clone",
"dom-query",
"dom-query-all",
"dom-query-by-id",
"dom-closest",
"dom-matches?",
"dom-get-attr",
"dom-set-attr",
"dom-remove-attr",
"dom-has-attr?",
"dom-add-class",
"dom-remove-class",
"dom-has-class?",
"dom-text-content",
"dom-set-text-content",
"dom-inner-html",
"dom-set-inner-html",
"dom-outer-html",
"dom-insert-adjacent-html",
"dom-get-style",
"dom-set-style",
"dom-get-prop",
"dom-set-prop",
"dom-tag-name",
"dom-node-type",
"dom-node-name",
"dom-id",
"dom-parent",
"dom-first-child",
"dom-next-sibling",
"dom-child-list",
"dom-is-fragment?",
"dom-child-nodes",
"dom-remove-children-after",
"dom-focus",
"dom-parse-html",
"dom-listen",
"dom-add-listener",
"dom-dispatch",
"event-detail",
"prevent-default",
"stop-propagation",
"event-modifier-key?",
"element-value",
"error-message",
"dom-get-data",
"dom-set-data",
"dom-append-to-head",
"set-document-title"
]
},
"sx browser": {
"file": "browser.sxbc",
"deps": []
"deps": [],
"exports": [
"browser-location-href",
"browser-location-pathname",
"browser-location-origin",
"browser-same-origin?",
"url-pathname",
"browser-push-state",
"browser-replace-state",
"browser-reload",
"browser-navigate",
"local-storage-get",
"local-storage-set",
"local-storage-remove",
"set-timeout",
"set-interval",
"clear-timeout",
"clear-interval",
"request-animation-frame",
"fetch-request",
"new-abort-controller",
"controller-signal",
"controller-abort",
"promise-then",
"promise-resolve",
"promise-delayed",
"browser-confirm",
"browser-prompt",
"browser-media-matches?",
"json-parse",
"log-info",
"log-warn",
"console-log",
"now-ms",
"schedule-idle",
"set-cookie",
"get-cookie"
]
},
"web adapter-html": {
"file": "adapter-html.sxbc",
"deps": [
"sx render"
],
"exports": [
"render-to-html",
"render-value-to-html",
"RENDER_HTML_FORMS",
"render-html-form?",
"render-list-to-html",
"dispatch-html-form",
"render-lambda-html",
"render-html-component",
"render-html-element",
"render-html-lake",
"render-html-marsh",
"render-html-island",
"serialize-island-state"
]
},
"web adapter-sx": {
"file": "adapter-sx.sxbc",
"deps": [
"web boot-helpers"
],
"exports": [
"render-to-sx",
"aser",
"aser-list",
"aser-reserialize",
"aser-fragment",
"aser-call",
"aser-expand-component",
"SPECIAL_FORM_NAMES",
"HO_FORM_NAMES",
"special-form?",
"ho-form?",
"aser-special",
"eval-case-aser"
]
},
"web adapter-dom": {
@@ -67,6 +531,41 @@
"deps": [
"sx dom",
"sx render"
],
"exports": [
"SVG_NS",
"MATH_NS",
"island-scope?",
"contains-deref?",
"dom-on",
"render-to-dom",
"render-dom-list",
"render-dom-element",
"render-dom-component",
"render-dom-fragment",
"render-dom-raw",
"render-dom-unknown-component",
"RENDER_DOM_FORMS",
"render-dom-form?",
"dispatch-render-form",
"render-lambda-dom",
"render-dom-island",
"render-dom-lake",
"render-dom-marsh",
"reactive-text",
"reactive-attr",
"reactive-spread",
"reactive-fragment",
"render-list-item",
"extract-key",
"reactive-list",
"bind-input",
"*use-cek-reactive*",
"enable-cek-reactive!",
"cek-reactive-text",
"cek-reactive-attr",
"render-dom-portal",
"render-dom-error-boundary"
]
},
"web boot-helpers": {
@@ -75,23 +574,178 @@
"sx dom",
"sx browser",
"web adapter-dom"
],
"exports": [
"_sx-bound-prefix",
"mark-processed!",
"is-processed?",
"clear-processed!",
"callable?",
"to-kebab",
"sx-load-components",
"call-expr",
"base-env",
"get-render-env",
"merge-envs",
"sx-render-with-env",
"parse-env-attr",
"store-env-attr",
"resolve-mount-target",
"remove-head-element",
"set-sx-comp-cookie",
"clear-sx-comp-cookie",
"log-parse-error",
"loaded-component-names",
"csrf-token",
"validate-for-request",
"build-request-body",
"abort-previous-target",
"abort-previous",
"track-controller",
"track-controller-target",
"new-abort-controller",
"abort-signal",
"apply-optimistic",
"revert-optimistic",
"dom-has-attr?",
"show-indicator",
"disable-elements",
"clear-loading-state",
"abort-error?",
"promise-catch",
"fetch-request",
"fetch-location",
"fetch-and-restore",
"fetch-preload",
"fetch-streaming",
"dom-parse-html-document",
"dom-body-inner-html",
"create-script-clone",
"cross-origin?",
"browser-scroll-to",
"with-transition",
"event-source-connect",
"event-source-listen",
"bind-boost-link",
"bind-boost-form",
"bind-client-route-click",
"sw-post-message",
"try-parse-json",
"strip-component-scripts",
"extract-response-css",
"sx-render",
"sx-hydrate",
"sx-process-scripts",
"select-from-container",
"children-to-fragment",
"select-html-from-doc",
"register-io-deps",
"resolve-page-data",
"parse-sx-data",
"try-eval-content",
"try-async-eval-content",
"try-rerender-page",
"execute-action",
"bind-preload",
"persist-offline-data",
"retrieve-offline-data"
]
},
"sx hypersx": {
"file": "hypersx.sxbc",
"deps": []
"deps": [],
"exports": [
"hsx-indent",
"hsx-sym-name",
"hsx-kw-name",
"hsx-is-element?",
"hsx-is-component?",
"hsx-extract-css",
"hsx-tag-str",
"hsx-atom",
"hsx-inline",
"hsx-attrs-str",
"hsx-children",
"sx->hypersx-node",
"sx->hypersx"
]
},
"sx harness": {
"file": "harness.sxbc",
"deps": []
"deps": [],
"exports": [
"assert",
"assert=",
"default-platform",
"make-harness",
"harness-reset!",
"harness-log",
"harness-get",
"harness-set!",
"make-interceptor",
"install-interceptors",
"io-calls",
"io-call-count",
"io-call-nth",
"io-call-args",
"io-call-result",
"assert-io-called",
"assert-no-io",
"assert-io-count",
"assert-io-args",
"assert-io-result",
"assert-state"
]
},
"sx harness-reactive": {
"file": "harness-reactive.sxbc",
"deps": []
"deps": [],
"exports": [
"assert-signal-value",
"assert-signal-has-subscribers",
"assert-signal-no-subscribers",
"assert-signal-subscriber-count",
"simulate-signal-set!",
"simulate-signal-swap!",
"assert-computed-dep-count",
"assert-computed-depends-on",
"count-effect-runs",
"make-test-signal",
"assert-batch-coalesces"
]
},
"sx harness-web": {
"file": "harness-web.sxbc",
"deps": []
"deps": [],
"exports": [
"mock-element",
"mock-set-text!",
"mock-append-child!",
"mock-set-attr!",
"mock-get-attr",
"mock-add-listener!",
"simulate-click",
"simulate-input",
"simulate-event",
"assert-text",
"assert-attr",
"assert-class",
"assert-no-class",
"assert-child-count",
"assert-event-fired",
"assert-no-event",
"event-fire-count",
"make-web-harness",
"is-renderable?",
"is-render-leak?",
"assert-renderable",
"render-body-audit",
"assert-render-body-clean",
"mock-render",
"mock-render-fragment",
"assert-single-render-root",
"assert-tag"
]
},
"web engine": {
"file": "engine.sxbc",
@@ -99,6 +753,41 @@
"web boot-helpers",
"sx dom",
"sx browser"
],
"exports": [
"ENGINE_VERBS",
"DEFAULT_SWAP",
"parse-time",
"parse-trigger-spec",
"default-trigger",
"get-verb-info",
"build-request-headers",
"process-response-headers",
"parse-swap-spec",
"parse-retry-spec",
"next-retry-ms",
"filter-params",
"resolve-target",
"apply-optimistic",
"revert-optimistic",
"find-oob-swaps",
"morph-node",
"sync-attrs",
"morph-children",
"morph-island-children",
"morph-marsh",
"process-signal-updates",
"swap-dom-nodes",
"insert-remaining-siblings",
"swap-html-string",
"handle-history",
"PRELOAD_TTL",
"preload-cache-get",
"preload-cache-set",
"classify-trigger",
"should-boost-link?",
"should-boost-form?",
"parse-sse-swap"
]
},
"web orchestration": {
@@ -109,6 +798,67 @@
"sx browser",
"web adapter-dom",
"web engine"
],
"exports": [
"_preload-cache",
"dispatch-trigger-events",
"execute-request",
"do-fetch",
"handle-fetch-success",
"flush-collected-styles",
"handle-sx-response",
"handle-html-response",
"handle-retry",
"bind-triggers",
"bind-event",
"post-swap",
"process-settle-hooks",
"activate-scripts",
"process-oob-swaps",
"hoist-head-elements",
"process-boosted",
"boost-descendants",
"_page-data-cache",
"_page-data-cache-ttl",
"page-data-cache-key",
"page-data-cache-get",
"page-data-cache-set",
"invalidate-page-cache",
"invalidate-all-page-cache",
"update-page-cache",
"process-cache-directives",
"_optimistic-snapshots",
"optimistic-cache-update",
"optimistic-cache-revert",
"optimistic-cache-confirm",
"submit-mutation",
"_is-online",
"_offline-queue",
"offline-is-online?",
"offline-set-online!",
"offline-queue-mutation",
"offline-sync",
"offline-pending-count",
"offline-aware-mutation",
"current-page-layout",
"swap-rendered-content",
"resolve-route-target",
"deps-satisfied?",
"try-client-route",
"bind-client-route-link",
"process-sse",
"bind-sse",
"bind-sse-swap",
"bind-inline-handlers",
"bind-preload-for",
"do-preload",
"VERB_SELECTOR",
"process-elements",
"process-one",
"process-emit-elements",
"save-scroll-position",
"handle-popstate",
"engine-init"
]
},
"_entry": {

View File

@@ -3,4 +3,5 @@
(define tools-nav-items
(list
(dict :label "SX Tools" :href "/sx/(tools.(sx-tools))")
(dict :label "Services" :href "/sx/(tools.(services))")))
(dict :label "Services" :href "/sx/(tools.(services))")
(dict :label "Playground" :href "/sx/(tools.(playground))")))

View File

@@ -625,6 +625,15 @@
(define tools (fn (content) (or content "")))
(define
playground
(fn
(&key title &rest args)
(quasiquote
(~playground/content
:title (unquote (or title "Playground"))
(splice-unquote args)))))
(define
services
(fn

View File

@@ -1,53 +1,52 @@
;; Playground — demonstrates lazy module loading on navigation
;; Playground — demonstrates transparent lazy module loading
(defcomp ~playground/content (&key (title "Playground"))
(~docs/page :title title
(h1 "Playground")
(p "This page lazy-loads the " (code "sx freeze") " and " (code "sx compiler")
" modules when you navigate here. They are NOT loaded at boot.")
(p "An interactive SX REPL. The " (code "sx freeze") " module "
"loads transparently when this island hydrates — no import needed.")
(span :data-sx-island "playground/repl"
(div (~tw :tokens "mt-6 border border-stone-200 rounded-lg overflow-hidden")
(div (~tw :tokens "bg-stone-100 px-4 py-2 text-sm font-mono text-stone-500")
"sx> ")
(div (~tw :tokens "p-4 font-mono text-sm min-h-[120px] text-stone-400")
"Loading modules...")))))
"Loading...")))))
(defisland ~playground/repl ()
(do
;; These two modules are NOT in the boot manifest — they load on demand
(load-library! "sx freeze")
(load-library! "sx compiler")
(let ((input (signal ""))
(output (signal "(ready)"))
(history (signal (list))))
(let ((eval-input
(fn (e)
(let ((src (deref input)))
(when (not (empty? src))
(let ((result (cek-try
(fn () (str (cek-eval (first (sx-parse src)))))
(fn (err) (str "Error: " err)))))
(reset! output result)
(swap! history (fn (h) (append h (list (dict :src src :result result)))))
(reset! input "")))))))
(div (~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(div (~tw :tokens "bg-stone-800 text-stone-100 p-4 font-mono text-sm min-h-[120px] max-h-[300px] overflow-y-auto")
(for-each
(fn (entry)
(div
(div (~tw :tokens "text-emerald-400") (str "sx> " (get entry "src")))
(div (~tw :tokens "text-stone-300 mb-2") (get entry "result"))))
(deref history))
(div (~tw :tokens "text-amber-400") (str "=> " (deref output))))
(div (~tw :tokens "flex border-t border-stone-200")
(span (~tw :tokens "bg-stone-100 px-3 py-2 text-stone-500 font-mono text-sm") "sx>")
(input :type "text"
:value (deref input)
:on-input (fn (e) (reset! input (element-value (host-get e "target"))))
:on-keydown (fn (e) (when (= (host-get e "key") "Enter") (eval-input e)))
(~tw :tokens "flex-1 px-3 py-2 font-mono text-sm outline-none")
:placeholder "Type an expression..."
:autofocus "true")
(button :on-click eval-input
(~tw :tokens "px-4 py-2 bg-violet-600 text-white font-mono text-sm hover:bg-violet-700")
"Eval")))))))
(let ((input (signal ""))
(output (signal "(ready)"))
(history (signal (list)))
(modules-loaded (signal
(str "freeze: " (type-of freeze-registry)))))
(let ((eval-input
(fn (e)
(let ((src (deref input)))
(when (not (empty? src))
(let ((result (cek-try
(fn () (str (cek-eval (first (sx-parse src)))))
(fn (err) (str "Error: " err)))))
(reset! output result)
(swap! history (fn (h) (append h (list (dict :src src :result result)))))
(reset! input "")))))))
(div (~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(div (~tw :tokens "bg-stone-800 text-stone-100 p-4 font-mono text-sm min-h-[120px] max-h-[300px] overflow-y-auto")
(for-each
(fn (entry)
(div
(div (~tw :tokens "text-emerald-400") (str "sx> " (get entry "src")))
(div (~tw :tokens "text-stone-300 mb-2") (get entry "result"))))
(deref history))
(div (~tw :tokens "text-amber-400") (str "=> " (deref output)))
(div (~tw :tokens "text-stone-600 text-xs mt-2") (deref modules-loaded)))
(div (~tw :tokens "flex border-t border-stone-200")
(span (~tw :tokens "bg-stone-100 px-3 py-2 text-stone-500 font-mono text-sm") "sx>")
(input :type "text"
:value (deref input)
:on-input (fn (e) (reset! input (element-value (host-get e "target"))))
:on-keydown (fn (e) (when (= (host-get e "key") "Enter") (eval-input e)))
(~tw :tokens "flex-1 px-3 py-2 font-mono text-sm outline-none")
:placeholder "Type an expression... (try: (+ 1 2) or (map inc (list 1 2 3)))"
:autofocus "true")
(button :on-click eval-input
(~tw :tokens "px-4 py-2 bg-violet-600 text-white font-mono text-sm hover:bg-violet-700")
"Eval"))))))