Step 5p6 lazy loading + Step 6b VM transpilation prep
Lazy module loading (Step 5 piece 6 completion): - Add define-library wrappers + import declarations to 13 source .sx files - compile-modules.js generates module-manifest.json with dependency graph - compile-modules.js strips define-library/import before bytecode compilation (VM doesn't handle these as special forms) - sx-platform.js replaces hardcoded 24-file loadWebStack() with manifest-driven recursive loader — only downloads modules the page needs - Result: 12 modules loaded (was 24), zero errors, zero warnings - Fallback to full load if manifest missing VM transpilation prep (Step 6b): - Refactor lib/vm.sx: 20 accessor functions replace raw dict access - Factor out collect-n-from-stack, collect-n-pairs, pad-n-nils helpers - bootstrap_vm.py: transpiles 9 VM logic functions to OCaml - sx_vm_ref.ml: proof that vm.sx transpiles (preamble has stubs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
200
hosts/ocaml/browser/wrap-modules.js
Normal file
200
hosts/ocaml/browser/wrap-modules.js
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* wrap-modules.js — Add define-library wrappers and import declarations
|
||||
* to browser .sx SOURCE files for lazy loading support.
|
||||
*
|
||||
* Targets the real source locations (spec/, web/, lib/), NOT dist/.
|
||||
* Run bundle.sh after to copy to dist/, then compile-modules.js.
|
||||
*
|
||||
* - 8 unwrapped files get define-library + export + begin wrappers
|
||||
* - 4 already-wrapped files get dependency import declarations
|
||||
* - boot.sx gets imports (stays unwrapped — entry point)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
// Source file → library name (null = entry point)
|
||||
const MODULES = {
|
||||
// Spec modules
|
||||
'spec/render.sx': { lib: '(sx render)', deps: [] },
|
||||
'spec/signals.sx': { lib: '(sx signals)', deps: [] },
|
||||
'web/web-signals.sx': { lib: '(sx signals-web)', deps: ['(sx dom)', '(sx browser)'] },
|
||||
'web/deps.sx': { lib: '(web deps)', deps: [] },
|
||||
'web/router.sx': { lib: '(web router)', deps: [] },
|
||||
'web/page-helpers.sx': { lib: '(web page-helpers)', deps: [] },
|
||||
// Lib modules
|
||||
'lib/freeze.sx': { lib: '(sx freeze)', deps: [] },
|
||||
'lib/highlight.sx': { lib: '(sx highlight)', deps: [] },
|
||||
'lib/bytecode.sx': { lib: '(sx bytecode)', deps: [] },
|
||||
'lib/compiler.sx': { lib: '(sx compiler)', deps: [] },
|
||||
'lib/vm.sx': { lib: '(sx vm)', deps: [] },
|
||||
// Web FFI
|
||||
'web/lib/dom.sx': { lib: '(sx dom)', deps: [] },
|
||||
'web/lib/browser.sx': { lib: '(sx browser)', deps: [] },
|
||||
// Web adapters
|
||||
'web/adapter-html.sx': { lib: '(web adapter-html)', deps: ['(sx render)'] },
|
||||
'web/adapter-sx.sx': { lib: '(web adapter-sx)', deps: ['(web boot-helpers)'] },
|
||||
'web/adapter-dom.sx': { lib: '(web adapter-dom)', deps: ['(sx dom)', '(sx render)'] },
|
||||
// Web framework
|
||||
'web/lib/boot-helpers.sx': { lib: '(web boot-helpers)', deps: ['(sx dom)', '(sx browser)', '(web adapter-dom)'] },
|
||||
'web/lib/hypersx.sx': { lib: '(sx hypersx)', deps: [] },
|
||||
'web/engine.sx': { lib: '(web engine)', deps: ['(web boot-helpers)', '(sx dom)', '(sx browser)'] },
|
||||
'web/orchestration.sx': { lib: '(web orchestration)', deps: ['(web boot-helpers)', '(sx dom)', '(sx browser)', '(web adapter-dom)', '(web engine)'] },
|
||||
'web/boot.sx': { lib: null, deps: ['(sx dom)', '(sx browser)', '(web boot-helpers)', '(web adapter-dom)',
|
||||
'(sx signals)', '(sx signals-web)', '(web router)', '(web page-helpers)',
|
||||
'(web orchestration)', '(sx render)',
|
||||
'(sx bytecode)', '(sx compiler)', '(sx vm)'] },
|
||||
// Test harness
|
||||
'spec/harness.sx': { lib: '(sx harness)', deps: [] },
|
||||
'web/harness-reactive.sx': { lib: '(sx harness-reactive)', deps: [] },
|
||||
'web/harness-web.sx': { lib: '(sx harness-web)', deps: [] },
|
||||
};
|
||||
|
||||
// Extract top-level define names from source.
|
||||
// Handles both `(define name ...)` and `(define\n name ...)` formats.
|
||||
function extractDefineNames(source) {
|
||||
const names = [];
|
||||
const lines = source.split('\n');
|
||||
let depth = 0;
|
||||
let expectName = false;
|
||||
for (const line of lines) {
|
||||
if (depth === 0) {
|
||||
const m = line.match(/^\(define\s+\(?(\S+)/);
|
||||
if (m) {
|
||||
names.push(m[1]);
|
||||
expectName = false;
|
||||
} else if (line.match(/^\(define\s*$/)) {
|
||||
expectName = true;
|
||||
}
|
||||
} else if (depth === 1 && expectName) {
|
||||
const m = line.match(/^\s+(\S+)/);
|
||||
if (m) {
|
||||
names.push(m[1]);
|
||||
expectName = false;
|
||||
}
|
||||
}
|
||||
for (const ch of line) {
|
||||
if (ch === '(') depth++;
|
||||
else if (ch === ')') depth--;
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function processFile(relPath, info) {
|
||||
const filePath = path.join(ROOT, relPath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(' SKIP', relPath, '(not found)');
|
||||
return;
|
||||
}
|
||||
|
||||
let source = fs.readFileSync(filePath, 'utf8');
|
||||
const { lib, deps } = info;
|
||||
const hasWrapper = source.includes('(define-library');
|
||||
const hasDepImports = deps.length > 0 && source.match(/^\(import\s+\(/m) &&
|
||||
!source.match(/^\(import\s+\(\w+ \w+\)\)\s*$/m); // more than just self-import
|
||||
|
||||
// Skip files with no deps and already wrapped (or no wrapper needed)
|
||||
if (deps.length === 0 && (hasWrapper || !lib)) {
|
||||
console.log(' ok', relPath, '(no changes needed)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build import lines for deps
|
||||
const importLines = deps.map(d => `(import ${d})`).join('\n');
|
||||
|
||||
// CASE 1: Entry point (boot.sx) — just add imports at top
|
||||
if (!lib) {
|
||||
if (deps.length > 0 && !source.startsWith('(import')) {
|
||||
source = importLines + '\n\n' + source;
|
||||
fs.writeFileSync(filePath, source);
|
||||
console.log(' +imports', relPath, `(${deps.length} deps, entry point)`);
|
||||
} else {
|
||||
console.log(' ok', relPath, '(entry point, already has imports)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// CASE 2: Already wrapped — add imports before define-library
|
||||
if (hasWrapper) {
|
||||
if (deps.length > 0) {
|
||||
// Check if imports already present
|
||||
const firstImportCheck = deps[0].replace(/[()]/g, '\\$&');
|
||||
if (source.match(new RegExp('\\(import ' + firstImportCheck))) {
|
||||
console.log(' ok', relPath, '(already has dep imports)');
|
||||
return;
|
||||
}
|
||||
const dlIdx = source.indexOf('(define-library');
|
||||
source = source.slice(0, dlIdx) + importLines + '\n\n' + source.slice(dlIdx);
|
||||
fs.writeFileSync(filePath, source);
|
||||
console.log(' +imports', relPath, `(${deps.length} deps)`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// CASE 3: Needs full wrapping
|
||||
if (deps.length === 0 && !hasWrapper) {
|
||||
// Wrap with no deps
|
||||
const names = extractDefineNames(source);
|
||||
if (names.length === 0) {
|
||||
console.log(' WARN', relPath, '— no defines found, skipping');
|
||||
return;
|
||||
}
|
||||
const wrapped = buildWrapped(lib, names, source, '');
|
||||
fs.writeFileSync(filePath, wrapped);
|
||||
console.log(' wrapped', relPath, `as ${lib} (${names.length} exports)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap with deps
|
||||
const names = extractDefineNames(source);
|
||||
if (names.length === 0) {
|
||||
console.log(' WARN', relPath, '— no defines found, skipping');
|
||||
return;
|
||||
}
|
||||
const wrapped = buildWrapped(lib, names, source, importLines);
|
||||
fs.writeFileSync(filePath, wrapped);
|
||||
console.log(' wrapped', relPath, `as ${lib} (${names.length} exports, ${deps.length} deps)`);
|
||||
}
|
||||
|
||||
function buildWrapped(libName, exportNames, bodySource, importSection) {
|
||||
const parts = [];
|
||||
|
||||
// Dependency imports (top-level, before define-library)
|
||||
if (importSection) {
|
||||
parts.push(importSection);
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
// define-library header
|
||||
parts.push(`(define-library ${libName}`);
|
||||
parts.push(` (export ${exportNames.join(' ')})`);
|
||||
parts.push(' (begin');
|
||||
parts.push('');
|
||||
|
||||
// Body (original source, indented)
|
||||
parts.push(bodySource);
|
||||
parts.push('');
|
||||
|
||||
// Close begin + define-library
|
||||
parts.push('))');
|
||||
parts.push('');
|
||||
|
||||
// Self-import for backward compat
|
||||
parts.push(`;; Re-export to global env`);
|
||||
parts.push(`(import ${libName})`);
|
||||
parts.push('');
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
console.log('Processing source .sx files...\n');
|
||||
for (const [relPath, info] of Object.entries(MODULES)) {
|
||||
processFile(relPath, info);
|
||||
}
|
||||
console.log('\nDone! Now run:');
|
||||
console.log(' bash hosts/ocaml/browser/bundle.sh');
|
||||
console.log(' node hosts/ocaml/browser/compile-modules.js');
|
||||
Reference in New Issue
Block a user