diff --git a/hosts/ocaml/browser/compile-modules.js b/hosts/ocaml/browser/compile-modules.js index 3894a14b..a4321fdf 100644 --- a/hosts/ocaml/browser/compile-modules.js +++ b/hosts/ocaml/browser/compile-modules.js @@ -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) diff --git a/hosts/ocaml/browser/sx-platform.js b/hosts/ocaml/browser/sx-platform.js index ae466eec..21da6d72 100644 --- a/hosts/ocaml/browser/sx-platform.js +++ b/hosts/ocaml/browser/sx-platform.js @@ -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 // ================================================================ diff --git a/hosts/ocaml/browser/sx_browser.ml b/hosts/ocaml/browser/sx_browser.ml index aa29360f..48b7aeb5 100644 --- a/hosts/ocaml/browser/sx_browser.ml +++ b/hosts/ocaml/browser/sx_browser.ml @@ -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 *) (* ================================================================== *) diff --git a/hosts/ocaml/lib/sx_ref.ml b/hosts/ocaml/lib/sx_ref.ml index 946e1fc9..ed7ab2f2 100644 --- a/hosts/ocaml/lib/sx_ref.ml +++ b/hosts/ocaml/lib/sx_ref.ml @@ -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 = diff --git a/hosts/ocaml/lib/sx_types.ml b/hosts/ocaml/lib/sx_types.ml index aa9e83f1..f3f23e70 100644 --- a/hosts/ocaml/lib/sx_types.ml +++ b/hosts/ocaml/lib/sx_types.ml @@ -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 diff --git a/hosts/ocaml/lib/sx_vm_ref.ml b/hosts/ocaml/lib/sx_vm_ref.ml index 4c9e4c2a..6a7b9041 100644 --- a/hosts/ocaml/lib/sx_vm_ref.ml +++ b/hosts/ocaml/lib/sx_vm_ref.ml @@ -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 diff --git a/shared/static/wasm/sx-platform.js b/shared/static/wasm/sx-platform.js index ae466eec..21da6d72 100644 --- a/shared/static/wasm/sx-platform.js +++ b/shared/static/wasm/sx-platform.js @@ -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 // ================================================================ diff --git a/shared/static/wasm/sx/module-manifest.json b/shared/static/wasm/sx/module-manifest.json index fc3d7c15..ae41a639 100644 --- a/shared/static/wasm/sx/module-manifest.json +++ b/shared/static/wasm/sx/module-manifest.json @@ -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": { diff --git a/sx/sx/nav-tools.sx b/sx/sx/nav-tools.sx index dc395d85..743d9b2b 100644 --- a/sx/sx/nav-tools.sx +++ b/sx/sx/nav-tools.sx @@ -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))"))) diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index 064ab020..46a96e60 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -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 diff --git a/sx/sx/playground.sx b/sx/sx/playground.sx index e6c4282a..f486169f 100644 --- a/sx/sx/playground.sx +++ b/sx/sx/playground.sx @@ -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"))))))