#!/usr/bin/env node /** * compile-modules.js — Pre-compile .sx files to bytecode s-expressions. * * Uses the js_of_ocaml kernel in Node.js to compile each .sx module, * then serializes the bytecode as .sxbc (s-expression format) for 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 { 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 sx = serializeModuleToSx(code, hash); // Write .sxbc (s-expression format) const outPath = srcPath.replace(/\.sx$/, '.sxbc'); fs.writeFileSync(outPath, sx); // Also write .sxbc.json for backwards compatibility during transition const json = { magic: 'SXBC', version: 1, hash: hash, module: serializeModuleToJson(code), }; const jsonPath = srcPath.replace(/\.sx$/, '.sxbc.json'); fs.writeFileSync(jsonPath, 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'); // --- S-expression serialization --- function serializeModuleToSx(code, hash) { return '(sxbc 1 "' + hash + '"\n ' + serializeCodeToSx(code, 2) + ')\n'; } function serializeCodeToSx(code, indent) { const pad = ' '.repeat(indent); const bc = extractList(code.bytecode); const consts = extractList(code.constants); const arity = code.arity || code['arity'] || 0; const uvc = code['upvalue-count'] || 0; let parts = ['(code']; if (arity) parts.push(' :arity ' + arity); if (uvc) parts.push(' :upvalue-count ' + uvc); parts.push('\n' + pad + ' :bytecode (' + bc.join(' ') + ')'); parts.push('\n' + pad + ' :constants ('); const constStrs = consts.map(c => serializeConstToSx(c, indent + 4)); if (constStrs.length > 0) { parts.push('\n' + constStrs.map(s => pad + ' ' + s).join('\n')); parts.push(')'); } else { parts[parts.length - 1] += ')'; } parts.push(')'); return parts.join(''); } function serializeConstToSx(c, indent) { if (c === null || c === undefined) return 'nil'; if (typeof c === 'number') return String(c); if (typeof c === 'string') return '"' + c.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; if (typeof c === 'boolean') return c ? 'true' : 'false'; if (c._type === 'symbol') return "'" + c.name; if (c._type === 'keyword') return ':' + c.name; if (c._type === 'list') { const items = extractList(c).map(x => serializeConstToSx(x, indent)); return '(list ' + items.join(' ') + ')'; } // Code object (nested lambda bytecode) if (c.bytecode) return serializeCodeToSx(c, indent); if (c._type === 'dict') { const bc = c.get ? c.get('bytecode') : c.bytecode; if (bc) return serializeCodeToSx(c, indent); // Regular dict — serialize as {:key val ...} const entries = []; if (c.forEach) c.forEach((v, k) => { entries.push(':' + k + ' ' + serializeConstToSx(v, indent)); }); return '{' + entries.join(' ') + '}'; } return 'nil'; } // --- JSON serialization (backwards compat) --- function serializeModuleToJson(code) { const result = { bytecode: extractList(code.bytecode), constants: extractList(code.constants).map(serializeConstantJson), }; const arity = code.arity || code['arity']; const uvc = code['upvalue-count']; const locals = code.locals || code['locals']; if (arity) result.arity = typeof arity === 'number' ? arity : 0; if (uvc) result['upvalue-count'] = typeof uvc === 'number' ? uvc : 0; if (locals) result.locals = typeof locals === 'number' ? locals : 0; return result; } function serializeConstantJson(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(serializeConstantJson) }; if (c.bytecode) return { t: 'code', v: serializeModuleToJson(c) }; if (c._type === 'dict') { const bc = c.get ? c.get('bytecode') : c.bytecode; if (bc) return { t: 'code', v: serializeModuleToJson(c) }; const entries = {}; if (c.forEach) c.forEach((v, k) => { entries[k] = serializeConstantJson(v); }); return { t: 'dict', v: entries }; } return { t: 'nil' }; } 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 []; }