diff --git a/hosts/ocaml/browser/compile-modules.js b/hosts/ocaml/browser/compile-modules.js new file mode 100644 index 00000000..c9c9dc21 --- /dev/null +++ b/hosts/ocaml/browser/compile-modules.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node +/** + * compile-modules.js — Pre-compile .sx files to bytecode JSON. + * + * Uses the js_of_ocaml kernel in Node.js to compile each .sx module, + * then serializes the bytecode as JSON for fast browser loading. + * + * Usage: node compile-modules.js [dist-dir] + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const distDir = process.argv[2] || path.join(__dirname, 'dist'); +const sxDir = path.join(distDir, 'sx'); + +if (!fs.existsSync(sxDir)) { + console.error('sx dir not found:', sxDir); + process.exit(1); +} + +// Load the js_of_ocaml kernel +const kernelPath = path.join(__dirname, '..', '_build', 'default', 'browser', 'sx_browser.bc.js'); +if (!fs.existsSync(kernelPath)) { + console.error('Kernel not found:', kernelPath); + process.exit(1); +} +require(kernelPath); +const K = globalThis.SxKernel; +if (!K) { console.error('SxKernel not initialized'); process.exit(1); } + +const FILES = [ + 'render.sx', 'core-signals.sx', 'signals.sx', 'deps.sx', 'router.sx', + 'page-helpers.sx', 'freeze.sx', 'bytecode.sx', 'compiler.sx', 'vm.sx', + 'dom.sx', 'browser.sx', 'adapter-html.sx', 'adapter-sx.sx', 'adapter-dom.sx', + 'boot-helpers.sx', 'hypersx.sx', 'harness.sx', 'harness-reactive.sx', + 'harness-web.sx', 'engine.sx', 'orchestration.sx', 'boot.sx', +]; + +// Load all files to build up the env (need compiler loaded) +console.log('Loading SX environment...'); +for (const file of FILES) { + const r = K.load(fs.readFileSync(path.join(sxDir, file), 'utf8')); + if (typeof r === 'string' && r.startsWith('Error')) { + console.error(' FAIL', file, r); + process.exit(1); + } +} +console.log(' ' + FILES.length + ' files loaded'); + +// Compile each file to bytecode +console.log('Compiling bytecode modules...'); +let compiled = 0, skipped = 0; + +for (const file of FILES) { + const srcPath = path.join(sxDir, file); + const src = fs.readFileSync(srcPath, 'utf8'); + const hash = crypto.createHash('sha256').update(src).digest('hex').slice(0, 16); + + try { + // Parse source to get expression list, then compile + const code = K.eval('(compile-module (sx-parse ' + JSON.stringify(src) + '))'); + + if (typeof code === 'string' && code.startsWith('Error')) { + console.error(' SKIP', file, '—', code); + skipped++; + continue; + } + + const json = { + magic: 'SXBC', + version: 1, + hash: hash, + module: serializeModule(code), + }; + + const outPath = srcPath.replace(/\.sx$/, '.sxbc.json'); + fs.writeFileSync(outPath, JSON.stringify(json)); + const size = fs.statSync(outPath).size; + console.log(' ok', file, '→', Math.round(size / 1024) + 'K'); + compiled++; + } catch (e) { + console.error(' SKIP', file, '—', e.message || e); + skipped++; + } +} + +console.log('Done:', compiled, 'compiled,', skipped, 'skipped'); + +// --- Serialization --- + +function serializeModule(code) { + return { + bytecode: extractList(code.bytecode), + constants: extractList(code.constants).map(serializeConstant), + }; +} + +function extractList(v) { + if (!v) return []; + if (Array.isArray(v)) return v; + if (v._type === 'list' && v.items) return v.items; + if (v.items) return v.items; + return []; +} + +function serializeConstant(c) { + if (c === null || c === undefined) return { t: 'nil' }; + if (typeof c === 'number') return { t: 'n', v: c }; + if (typeof c === 'string') return { t: 's', v: c }; + if (typeof c === 'boolean') return { t: 'b', v: c }; + if (c._type === 'symbol') return { t: 'sym', v: c.name }; + if (c._type === 'keyword') return { t: 'kw', v: c.name }; + if (c._type === 'list') return { t: 'list', v: extractList(c).map(serializeConstant) }; + // Code object (nested lambda bytecode) + if (c.bytecode) return { t: 'code', v: serializeModule(c) }; + if (c._type === 'dict') { + // Check if it's a code object stored as dict + const bc = c.get ? c.get('bytecode') : c.bytecode; + if (bc) return { t: 'code', v: serializeModule(c) }; + // Regular dict + const entries = {}; + if (c.forEach) c.forEach((v, k) => { entries[k] = serializeConstant(v); }); + return { t: 'dict', v: entries }; + } + return { t: 'nil' }; +} diff --git a/hosts/ocaml/browser/sx_browser.ml b/hosts/ocaml/browser/sx_browser.ml index e0a6e0e6..4f839628 100644 --- a/hosts/ocaml/browser/sx_browser.ml +++ b/hosts/ocaml/browser/sx_browser.ml @@ -231,6 +231,34 @@ let api_load src_js = | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) | Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) +let api_load_module module_js = + try + let code_val = js_to_value module_js in + let code = Sx_vm.code_from_value code_val in + let globals = Hashtbl.create 256 in + Hashtbl.iter (fun id v -> Hashtbl.replace globals (unintern id) v) global_env.bindings; + let _result = Sx_vm.execute_module code globals in + (* Copy all globals back into env — new defines + unchanged values *) + Hashtbl.iter (fun k v -> + Hashtbl.replace global_env.bindings (intern k) v + ) globals; + Js.Unsafe.inject (Hashtbl.length globals) + with + | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) + | exn -> Js.Unsafe.inject (Js.string ("Error: " ^ Printexc.to_string exn)) + +let api_compile_module src_js = + let src = Js.to_string src_js in + try + let exprs = Sx_parser.parse_all src in + let compile_fn = env_get global_env "compile-module" in + let code = Sx_ref.eval_expr (List [compile_fn; List exprs]) (Env global_env) in + return_via_side_channel (value_to_js code) + with + | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) + | Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) + | Not_found -> Js.Unsafe.inject (Js.string "Error: compile-module not loaded") + let api_render_to_html expr_js = let expr = js_to_value expr_js in let prev = !_sx_render_mode in @@ -624,6 +652,8 @@ let () = Js.Unsafe.set sx (Js.string "evalExpr") (wrap api_eval_expr); Js.Unsafe.set sx (Js.string "renderToHtml") (Js.wrap_callback api_render_to_html); Js.Unsafe.set sx (Js.string "load") (Js.wrap_callback api_load); + Js.Unsafe.set sx (Js.string "loadModule") (Js.wrap_callback api_load_module); + Js.Unsafe.set sx (Js.string "compileModule") (wrap api_compile_module); Js.Unsafe.set sx (Js.string "typeOf") (Js.wrap_callback api_type_of); Js.Unsafe.set sx (Js.string "inspect") (Js.wrap_callback api_inspect); Js.Unsafe.set sx (Js.string "engine") (Js.wrap_callback api_engine);