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:
@@ -69,10 +69,66 @@ for (const file of FILES) {
|
||||
script += src + '\n';
|
||||
}
|
||||
|
||||
// Compile each module
|
||||
// ---------------------------------------------------------------------------
|
||||
// Strip define-library/import wrappers for bytecode compilation.
|
||||
//
|
||||
// The VM's execute_module doesn't handle define-library or import — they're
|
||||
// CEK special forms. So the compiled bytecode should contain just the body
|
||||
// defines. The eval-blob phase (above) already handled library registration
|
||||
// via CEK. The JS loader pre-resolves deps, so no registry needed at runtime.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function stripLibraryWrapper(source) {
|
||||
// Line-based stripping: remove (import ...), unwrap (define-library ... (begin BODY)).
|
||||
// Works with both pre-existing and newly-wrapped formats.
|
||||
const lines = source.split('\n');
|
||||
const result = [];
|
||||
let skip = false; // inside header region (define-library, export)
|
||||
let importDepth = 0; // tracking multi-line import paren depth
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip (import ...) — may be single or multi-line
|
||||
if (importDepth > 0) {
|
||||
for (const ch of trimmed) { if (ch === '(') importDepth++; else if (ch === ')') importDepth--; }
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith('(import ')) {
|
||||
importDepth = 0;
|
||||
for (const ch of trimmed) { if (ch === '(') importDepth++; else if (ch === ')') importDepth--; }
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip (define-library ...) header lines until (begin
|
||||
if (trimmed.startsWith('(define-library ')) { skip = true; continue; }
|
||||
if (skip && trimmed.startsWith('(export')) { continue; }
|
||||
if (skip && trimmed.match(/^\(begin/)) { skip = false; continue; }
|
||||
if (skip) continue;
|
||||
|
||||
// Skip closing )) of define-library — line is just ) or )) optionally with comments
|
||||
if (trimmed.match(/^\)+(\s*;.*)?$/)) {
|
||||
// Check if this is the end-of-define-library closer (only `)` chars + optional comment)
|
||||
// vs a regular body closer like ` )` inside a nested form
|
||||
// Only skip if at column 0 (not indented = top-level closer)
|
||||
if (line.match(/^\)/)) continue;
|
||||
}
|
||||
|
||||
// Skip standalone comments that are just structural markers
|
||||
if (trimmed.match(/^;;\s*(end define-library|Re-export)/)) continue;
|
||||
|
||||
result.push(line);
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
// Compile each module (stripped of define-library/import wrappers)
|
||||
const compileEpochs = {};
|
||||
for (const file of FILES) {
|
||||
const src = fs.readFileSync(path.join(sxDir, file), 'utf8');
|
||||
const rawSrc = fs.readFileSync(path.join(sxDir, file), 'utf8');
|
||||
const src = stripLibraryWrapper(rawSrc);
|
||||
const buf = Buffer.from(src, 'utf8');
|
||||
const ep = epoch++;
|
||||
compileEpochs[ep] = file;
|
||||
@@ -208,6 +264,86 @@ if (fs.existsSync(staticSxDir)) {
|
||||
console.log('Copied', copied, 'files to', staticSxDir);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generate module-manifest.json — dependency graph for lazy loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
console.log('Generating module manifest...');
|
||||
|
||||
// Extract library name from (define-library (namespace name) ...) in source
|
||||
function extractLibraryName(source) {
|
||||
const m = source.match(/\(define-library\s+(\([^)]+\))/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// Extract top-level (import (namespace name)) deps from source
|
||||
// Only matches imports BEFORE define-library (dependency declarations)
|
||||
function extractImportDeps(source) {
|
||||
const deps = [];
|
||||
const lines = source.split('\n');
|
||||
for (const line of lines) {
|
||||
// Stop at define-library — imports after that are self-imports
|
||||
if (line.startsWith('(define-library')) break;
|
||||
const m = line.match(/^\(import\s+(\([^)]+\))\)/);
|
||||
if (m) deps.push(m[1]);
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
// Flatten library spec: "(sx dom)" → "sx dom"
|
||||
function libKey(spec) {
|
||||
return spec.replace(/^\(/, '').replace(/\)$/, '');
|
||||
}
|
||||
|
||||
const manifest = {};
|
||||
let entryFile = null;
|
||||
|
||||
for (const file of FILES) {
|
||||
const srcPath = path.join(sxDir, file);
|
||||
if (!fs.existsSync(srcPath)) continue;
|
||||
const src = fs.readFileSync(srcPath, 'utf8');
|
||||
const libName = extractLibraryName(src);
|
||||
const deps = extractImportDeps(src);
|
||||
const sxbcFile = file.replace(/\.sx$/, '.sxbc');
|
||||
|
||||
if (libName) {
|
||||
manifest[libKey(libName)] = {
|
||||
file: sxbcFile,
|
||||
deps: deps.map(libKey),
|
||||
};
|
||||
} else if (deps.length > 0) {
|
||||
// Entry point (no define-library, has imports)
|
||||
entryFile = { file: sxbcFile, deps: deps.map(libKey) };
|
||||
}
|
||||
}
|
||||
|
||||
if (entryFile) {
|
||||
// Partition entry deps into eager (needed at boot) and lazy (loaded on demand).
|
||||
// Lazy deps are fetched by the suspension handler when the kernel requests them.
|
||||
const LAZY_ENTRY_DEPS = new Set([
|
||||
'sx bytecode', // JIT-only — enable-jit! runs after boot
|
||||
]);
|
||||
const eagerDeps = entryFile.deps.filter(d => !LAZY_ENTRY_DEPS.has(d));
|
||||
const lazyDeps = entryFile.deps.filter(d => LAZY_ENTRY_DEPS.has(d));
|
||||
manifest['_entry'] = {
|
||||
file: entryFile.file,
|
||||
deps: eagerDeps,
|
||||
};
|
||||
if (lazyDeps.length > 0) {
|
||||
manifest['_entry'].lazy_deps = lazyDeps;
|
||||
}
|
||||
}
|
||||
|
||||
const manifestPath = path.join(sxDir, 'module-manifest.json');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
||||
console.log(' Wrote', manifestPath, '(' + Object.keys(manifest).length + ' modules)');
|
||||
|
||||
// Copy manifest to static dir
|
||||
if (fs.existsSync(staticSxDir)) {
|
||||
fs.copyFileSync(manifestPath, path.join(staticSxDir, 'module-manifest.json'));
|
||||
console.log(' Copied manifest to', staticSxDir);
|
||||
}
|
||||
|
||||
const total = Date.now() - t0;
|
||||
console.log('Done:', compiled, 'compiled,', skipped, 'skipped in', Math.round(total / 1000) + 's');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user