diff --git a/hosts/ocaml/browser/sx-platform.js b/hosts/ocaml/browser/sx-platform.js index cb7b74a9..ae466eec 100644 --- a/hosts/ocaml/browser/sx-platform.js +++ b/hosts/ocaml/browser/sx-platform.js @@ -19,9 +19,16 @@ function boot(K) { // ================================================================ - // 8 FFI Host Primitives + // FFI Host Primitives // ================================================================ + // Lazy module loading — islands/components call this to declare dependencies + K.registerNative("load-library!", function(args) { + var name = args[0]; + if (!name) return false; + return __sxLoadLibrary(name) || false; + }); + K.registerNative("host-global", function(args) { var name = args[0]; if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name]; @@ -463,13 +470,16 @@ loadLibrary(info.deps[i], loading); } + // Mark as loaded BEFORE executing — self-imports (define-library re-exports) + // 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) { var sxFile = info.file.replace(/\.sxbc$/, '.sx'); ok = loadSxFile("sx/" + sxFile); } - _loadedLibs[name] = true; return !!ok; } @@ -614,8 +624,14 @@ var _doInit = function() { loadWebStack(); Sx.init(); - // Enable JIT after all boot code has run - setTimeout(function() { K.eval('(enable-jit!)'); }, 0); + // Enable JIT after all boot code has run. + // Lazy-load the compiler first — JIT needs it to compile functions. + setTimeout(function() { + if (K.beginModuleLoad) K.beginModuleLoad(); + loadLibrary("sx compiler", {}); + if (K.endModuleLoad) K.endModuleLoad(); + K.eval('(enable-jit!)'); + }, 0); }; if (document.readyState === "loading") { diff --git a/hosts/ocaml/lib/sx_types.ml b/hosts/ocaml/lib/sx_types.ml index 9aaec6ab..aa9e83f1 100644 --- a/hosts/ocaml/lib/sx_types.ml +++ b/hosts/ocaml/lib/sx_types.ml @@ -255,6 +255,10 @@ let _env_bind_hook : (env -> string -> value -> unit) option ref = ref None Used by browser kernel to sync mutations back to global_env. *) let _vm_global_set_hook : (string -> value -> unit) option ref = ref None +(* Optional hook: called by cek_run on import suspension. + If set, the hook loads the library and returns true; cek_run then resumes. *) +let _import_hook : (value -> bool) 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 -> ()); diff --git a/shared/static/wasm/sx-platform.js b/shared/static/wasm/sx-platform.js index 20fd6975..ae466eec 100644 --- a/shared/static/wasm/sx-platform.js +++ b/shared/static/wasm/sx-platform.js @@ -19,9 +19,16 @@ function boot(K) { // ================================================================ - // 8 FFI Host Primitives + // FFI Host Primitives // ================================================================ + // Lazy module loading — islands/components call this to declare dependencies + K.registerNative("load-library!", function(args) { + var name = args[0]; + if (!name) return false; + return __sxLoadLibrary(name) || false; + }); + K.registerNative("host-global", function(args) { var name = args[0]; if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name]; @@ -617,8 +624,14 @@ var _doInit = function() { loadWebStack(); Sx.init(); - // Enable JIT after all boot code has run - setTimeout(function() { K.eval('(enable-jit!)'); }, 0); + // Enable JIT after all boot code has run. + // Lazy-load the compiler first — JIT needs it to compile functions. + setTimeout(function() { + if (K.beginModuleLoad) K.beginModuleLoad(); + loadLibrary("sx compiler", {}); + if (K.endModuleLoad) K.endModuleLoad(); + K.eval('(enable-jit!)'); + }, 0); }; if (document.readyState === "loading") { diff --git a/sx/sx/playground.sx b/sx/sx/playground.sx new file mode 100644 index 00000000..7263c75d --- /dev/null +++ b/sx/sx/playground.sx @@ -0,0 +1,50 @@ +;; Playground — interactive REPL with lazy-loaded compiler + +(defcomp ~playground/content (&key (title "Playground")) + (~docs/page :title title + (h1 "Playground") + (p "An interactive SX REPL. The compiler module loads on demand when you navigate here.") + (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 compiler..."))))) + +(defisland ~playground/repl () + (do + (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")))))))