Bytecode module compiler: loadModule/compileModule APIs + build script

- sx_browser.ml: add api_load_module (execute pre-compiled bytecode on VM,
  copy defines back to env) and api_compile_module (compile SX source to
  bytecode via compile-module function)
- compile-modules.js: Node.js build tool that loads the js_of_ocaml kernel,
  compiles all 23 .sx platform files to bytecode, writes .sxbc.json files
- Serialization format: type-tagged JSON constants (s/n/b/nil/sym/kw/list/code)
  with nested code objects for lambda closures

All 23 files compile successfully (430K total bytecode JSON).
Next: wire up sx-platform.js to load bytecode instead of source.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 17:37:26 +00:00
parent 2802dd99e2
commit cb7bbc9557
2 changed files with 158 additions and 0 deletions

View File

@@ -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' };
}

View File

@@ -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);