Transparent lazy module loading — code loads like data
When the VM or CEK hits an undefined symbol, it checks a symbol→library index (built from manifest exports at boot), loads the library that exports it, and returns the value. Execution continues as if the module was always loaded. No import statements, no load-library! calls, no Suspense boundaries — just call the function. This is the same mechanism as IO suspension for data fetching. The programmer doesn't distinguish between calling a local function and calling one that needs its module fetched first. The runtime treats code as just another resource. Implementation: - _symbol_resolve_hook in sx_types.ml — called by env_get_id (CEK path) and vm_global_get (VM path) when a symbol isn't found - Symbol→library index built from manifest exports in sx-platform.js - __resolve-symbol native calls __sxLoadLibrary, module loads, symbol appears in globals, execution resumes - compile-modules.js extracts export lists into module-manifest.json - Playground page demonstrates: (freeze-scope) triggers freeze.sxbc download transparently on first use 2650/2650 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -279,6 +279,20 @@ function extractImportDeps(source) {
|
||||
return deps;
|
||||
}
|
||||
|
||||
// Extract exported symbol names from (export name1 name2 ...) clause
|
||||
function extractExports(source) {
|
||||
const exports = [];
|
||||
const m = source.match(/\(export\s+([\s\S]*?)\)\s*\(/);
|
||||
if (!m) return exports;
|
||||
// Parse symbol names from the export list (skip keywords, nested forms)
|
||||
const tokens = m[1].split(/\s+/).filter(t => t && !t.startsWith(':') && !t.startsWith('(') && !t.startsWith(')'));
|
||||
for (const t of tokens) {
|
||||
const clean = t.replace(/[()]/g, '');
|
||||
if (clean && !clean.startsWith(':')) exports.push(clean);
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
|
||||
// Flatten library spec: "(sx dom)" → "sx dom"
|
||||
function libKey(spec) {
|
||||
return spec.replace(/^\(/, '').replace(/\)$/, '');
|
||||
@@ -296,9 +310,11 @@ for (const file of FILES) {
|
||||
const sxbcFile = file.replace(/\.sx$/, '.sxbc');
|
||||
|
||||
if (libName) {
|
||||
const exports = extractExports(src);
|
||||
manifest[libKey(libName)] = {
|
||||
file: sxbcFile,
|
||||
deps: deps.map(libKey),
|
||||
exports: exports,
|
||||
};
|
||||
} else if (deps.length > 0) {
|
||||
// Entry point (no define-library, has imports)
|
||||
|
||||
@@ -550,6 +550,47 @@
|
||||
return ok;
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// Transparent lazy loading — symbol → library index
|
||||
//
|
||||
// When the VM hits an undefined symbol, the resolve hook checks this
|
||||
// index, loads the library that exports it, and returns the value.
|
||||
// The programmer just calls the function — loading is invisible.
|
||||
// ================================================================
|
||||
|
||||
var _symbolIndex = null; // symbol name → library key
|
||||
|
||||
function buildSymbolIndex() {
|
||||
if (_symbolIndex) return _symbolIndex;
|
||||
if (!_manifest) loadManifest();
|
||||
if (!_manifest) return null;
|
||||
_symbolIndex = {};
|
||||
for (var key in _manifest) {
|
||||
if (key.startsWith('_')) continue;
|
||||
var entry = _manifest[key];
|
||||
if (entry.exports) {
|
||||
for (var i = 0; i < entry.exports.length; i++) {
|
||||
_symbolIndex[entry.exports[i]] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _symbolIndex;
|
||||
}
|
||||
|
||||
// Register the resolve hook — called by the VM when GLOBAL_GET fails
|
||||
K.registerNative("__resolve-symbol", function(args) {
|
||||
var name = args[0];
|
||||
if (!name) return null;
|
||||
var idx = buildSymbolIndex();
|
||||
if (!idx || !idx[name]) return null;
|
||||
var lib = idx[name];
|
||||
if (_loadedLibs[lib]) return null; // already loaded but symbol still missing — real error
|
||||
// Load the library
|
||||
__sxLoadLibrary(lib);
|
||||
// Return null — the VM will re-lookup in globals after the hook loads the module
|
||||
return null;
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// Compatibility shim — expose Sx global matching current JS API
|
||||
// ================================================================
|
||||
|
||||
@@ -229,6 +229,21 @@ let () =
|
||||
Sx_types._vm_global_set_hook := Some (fun name v ->
|
||||
Hashtbl.replace global_env.bindings (Sx_types.intern name) v)
|
||||
|
||||
(* Symbol resolve hook: transparent lazy module loading.
|
||||
When GLOBAL_GET can't find a symbol, this calls the JS __resolve-symbol
|
||||
native which checks the manifest's symbol→library index and loads the
|
||||
library that exports it. After loading, the symbol is in _vm_globals. *)
|
||||
let () =
|
||||
Sx_types._symbol_resolve_hook := Some (fun name ->
|
||||
match Hashtbl.find_opt Sx_primitives.primitives "__resolve-symbol" with
|
||||
| None -> None
|
||||
| Some resolve_fn ->
|
||||
(try ignore (resolve_fn [String name]) with _ -> ());
|
||||
(* Check if the symbol appeared in globals after the load *)
|
||||
match Hashtbl.find_opt _vm_globals name with
|
||||
| Some v -> Some v
|
||||
| None -> None)
|
||||
|
||||
(* ================================================================== *)
|
||||
(* Core API *)
|
||||
(* ================================================================== *)
|
||||
|
||||
Reference in New Issue
Block a user