diff --git a/scripts/engine.cm b/scripts/engine.cm index 3a096911..1fc054f6 100644 --- a/scripts/engine.cm +++ b/scripts/engine.cm @@ -9,619 +9,58 @@ cell.os = null var dylib_ext +function mod_scriptor(name, script) +{ + return `(function setup_${name}_module($_){${script}})` +} + switch(os.platform()) { case 'Windows': dylib_ext = '.dll'; break; case 'macOS': dylib_ext = '.dylib'; break; case 'Linux': dylib_ext = '.so'; break; } -var load_internal = os.load_internal - -function use_embed(name) { - return load_internal(`js_${name}_use`) -} - -var actor_mod = use_embed('actor') -var wota = use_embed('wota') -var nota = use_embed('nota') -var enet = use_embed('enet') -var fd = use_embed('fd') - -var ENETSERVICE = 0.1 -var REPLYTIMEOUT = 60 // seconds before replies are ignored - var MOD_EXT = '.cm' var ACTOR_EXT = '.ce' - -globalThis.pi = 3.1415926535897932 -var open_dl = {} - -function get_import_package(name) { - var parts = name.split('/') - if (parts.length > 1) - return parts[0] - - return null -} - -function get_import_dl(name) { - var pkg = get_import_package(name) - if (!pkg) return null - if (open_dl[pkg]) return open_dl[pkg] - var dlpath = `.cell/modules/${pkg}/${pkg}${dylib_ext}` - var dl = os.dylib_open(dlpath) - if (dl) { - open_dl[pkg] = dl - return dl - } - return null -} - -function get_c_symbol(name) { - var dl = get_import_dl(name) - var symname = `js_${name.replace('/', '_')}_use` - - if (dl) - return os.dylib_symbol(dl, symname) - else - return load_internal(symname) -} - -function caller_data(depth = 0) -{ - var file = "nofile" - var line = 0 - - var caller = new Error().stack.split("\n")[1+depth] - if (caller) { - var md = caller.match(/\((.*)\:/) - var m = md ? md[1] : "SCRIPT" - if (m) file = m - md = caller.match(/\:(\d*)\)/) - m = md ? md[1] : 0 - if (m) line = m - } - - return {file,line} -} -cell.args = cell.hidden.init -cell.args ??= {} -cell.id ??= "newguy" - -function console_rec(line, file, msg) { - return `[${cell.id.slice(0,5)}] [${file}:${line}]: ${msg}\n` - // time: [${time.text("mb d yyyy h:nn:ss")}] -} - -globalThis.log = {} -log.console = function(msg) -{ - var caller = caller_data(1) - os.print(console_rec(caller.line, caller.file, msg)) -} - -log.error = function(msg = new Error()) -{ - var caller = caller_data(1) - - if (msg instanceof Error) - msg = msg.name + ": " + msg.message + "\n" + msg.stack - - os.print(console_rec(caller.line,caller.file,msg)) -} - -log.system = function(msg) { - msg = "[SYSTEM] " + msg - log.console(msg) -} - -var shop_path = '.cell' - -if (!fd.stat('.cell').isDirectory) { - log.console("No cell directory found. Make one.\n"); - os.exit(1); -} - -function write_file(path, blob) { - var fd_handle = fd.open(path, 'w') - fd.write(fd_handle, blob) - fd.close(fd_handle) -} - -function mkdir_p(dir) { - if (dir == '' || dir == '.') return - var st = null - try { st = fd.stat(dir) } catch {} - - if (!st || !st.isDirectory) { - mkdir_p(dir.substring(0, dir.lastIndexOf('/'))) - try { fd.mkdir(dir) } catch {} - } +var load_internal = os.load_internal +function use_embed(name) { + return load_internal(`js_${name}_use`) } var qop = use_embed('qop') var core_qop = qop.open(hidden.core_qop_blob) var utf8 = use_embed('utf8') - -function disrupt(err) -{ - if (overling) { - if (err) { - // with an err, this is a forceful disrupt - var reason = (err instanceof Error) ? err.stack : err - report_to_overling({type:'disrupt', reason}) - } else - report_to_overling({type:'stop'}) - } - - if (underlings) { - for (var id of underlings) { - log.console(`calling on ${id} to disrupt too`) - $_.stop(create_actor({id})) - } - } - - if (err) { - log.console(err); - if (err.stack) - log.console(err.stack) - } - - actor_mod.disrupt() -} - -actor_mod.on_exception(disrupt) - var js = use_embed('js') var use_cache = {} -var BASEPATH = 'base' + MOD_EXT -var script = utf8.decode(core_qop.read(BASEPATH)) -var fnname = "base" -script = `(function ${fnname}() { ${script}; })` -js.eval(BASEPATH, script)() +// Three domains: +// core +// local +// module scope -var inProgress = {} -var loadingStack = [] - -// Track current package context for nested use() calls -var current_package = null - -// Get package name from a resolved path -function get_package_from_path(path) { - if (!path) return null - var modules_prefix = '.cell/modules/' - if (path.startsWith(modules_prefix)) { - var rest = path.substring(modules_prefix.length) - var slash_idx = rest.indexOf('/') - if (slash_idx > 0) { - return rest.substring(0, slash_idx) - } +function use_core(path) { + var cache_path = `2:${path}`; + if (use_cache[cache_path]) + return use_cache[cache_path]; + + var sym = use_embed(path) + var script = core_qop.read(path + MOD_EXT); + if (script) { + script = utf8.decode(script) + var mod = mod_scriptor(path, script) + var fn = js.eval(path, mod) + var result = fn.call(sym); + use_cache[cache_path] = result; + return result; } - return null + + use_cache[cache_path] = sym; + return sym; } -var config = null - - -// Scope definitions -var SCOPE_LOCAL = 0 -var SCOPE_DEPENDENCY = 1 -var SCOPE_CORE = 2 - -function resolve_path(requested, pkg_context, ext) { - if (requested.endsWith(ext)) ext = '' - - // Check for underscore prefix in imports - not allowed except for local access - if (requested.includes('/')) { - var parts = requested.split('/') - if (parts.some(part => part.startsWith('_'))) { - throw new Error(`Cannot import modules with underscore prefix: ${requested}`) - } - } else if (requested.startsWith('_')) { - throw new Error(`Cannot import modules with underscore prefix: ${requested}`) - } - - var dependencies = (config && config.dependencies) ? config.dependencies : {} - - // Helper to check file existence and return result object - function check(path, pkg, isCore, scope) { - if (isCore) { - try { - var blob = core_qop.read(path) - if (blob) { - return { path: path, package_name: null, isCore: true, scope: scope } - } - return null - } catch (e) { return null } - } - - // Check for private files if accessing from a different package - if (pkg && current_package && pkg != current_package) { - var filename = path.substring(path.lastIndexOf('/') + 1) - if (filename.startsWith('_')) return null - } - - if (fd.is_file(path)) { - return { path: path, package_name: pkg, isCore: false, scope: scope } - } - return null - } - - // Step 1: current package (Local) - if (pkg_context) { - var pkg_path = '.cell/modules/' + pkg_context + '/' + requested + ext - var underscore_pkg_path = '.cell/modules/' + pkg_context + '/_' + requested + ext - - var res = check(pkg_path, pkg_context, false, SCOPE_LOCAL) - var underscore_res = check(underscore_pkg_path, pkg_context, false, SCOPE_LOCAL) - - // If both regular and underscore versions exist, throw error - if (res && underscore_res) { - throw new Error(`Module conflict: both '${requested}' and '_${requested}' exist in package '${pkg_context}'`) - } - - // Prefer regular version, but allow underscore version if no regular exists - if (res) return res - if (underscore_res) return underscore_res - - // Check if package is locally replaced - if (config && config.replace && config.replace[pkg_context]) { - var replace_path = config.replace[pkg_context] - var full_path = replace_path + '/' + requested + ext - var underscore_full_path = replace_path + '/_' + requested + ext - - res = check(full_path, pkg_context, false, SCOPE_LOCAL) - underscore_res = check(underscore_full_path, pkg_context, false, SCOPE_LOCAL) - - if (res && underscore_res) { - throw new Error(`Module conflict: both '${requested}' and '_${requested}' exist in replaced package '${pkg_context}'`) - } - - if (res) return res - if (underscore_res) return underscore_res - } - } else { - // Top-level local - var project_path = requested + ext - var underscore_project_path = '_' + requested + ext - - var res = check(project_path, null, false, SCOPE_LOCAL) - var underscore_res = check(underscore_project_path, null, false, SCOPE_LOCAL) - - // If both regular and underscore versions exist, throw error - if (res && underscore_res) { - throw new Error(`Module conflict: both '${requested}' and '_${requested}' exist locally`) - } - - // Prefer regular version, but allow underscore version if no regular exists - if (res) return res - if (underscore_res) return underscore_res - } - - // Step 2: dependencies (explicit alias first) and replace directives - if (requested.includes('/')) { - var parts = requested.split('/') - var pkg_alias = parts[0] - var sub_path = parts.slice(1).join('/') - - // Check for replace directive first - if (config && config.replace && config.replace[pkg_alias]) { - var replace_path = config.replace[pkg_alias] - var full_path = replace_path + '/' + (sub_path || pkg_alias) + ext - var res = check(full_path, pkg_alias, false, SCOPE_DEPENDENCY) - if (res) return res - } else if (dependencies[pkg_alias]) { - var dep_path = '.cell/modules/' + pkg_alias + '/' + sub_path + ext - var res = check(dep_path, pkg_alias, false, SCOPE_DEPENDENCY) - if (res) return res - } - - // Also check if it's just a module in .cell/modules even if not in dependencies (implicit) - var implicit_path = '.cell/modules/' + requested + ext - var res = check(implicit_path, pkg_alias, false, SCOPE_DEPENDENCY) - if (res) return res - - } else { - // Check replace directives for simple names - if (config && config.replace && config.replace[requested]) { - var replace_path = config.replace[requested] - var full_path = replace_path + '/' + requested + ext - var res = check(full_path, requested, false, SCOPE_DEPENDENCY) - if (res) return res - } - // Check dependencies for simple names - for (var alias in dependencies) { - if (alias == requested) { - var dep_simple = '.cell/modules/' + alias + '/' + requested + ext - var res = check(dep_simple, alias, false, SCOPE_DEPENDENCY) - if (res) return res - } - } - - // Implicit check - var implicit_path = '.cell/modules/' + requested + '/' + requested + ext - var res = check(implicit_path, requested, false, SCOPE_DEPENDENCY) - if (res) return res - } - - // Step 3: core - var core_res = check(requested + ext, null, true, SCOPE_CORE) - return core_res -} - -function get_compiled_path(resolved) { - var build_base = '.cell/build/' - if (resolved.isCore) { - return build_base + 'core/' + resolved.path + '.o' - } else if (resolved.package_name) { - // If it's in .cell/modules//... - var prefix = '.cell/modules/' + resolved.package_name + '/' - if (resolved.path.startsWith(prefix)) { - return build_base + 'modules/' + resolved.package_name + '/' + resolved.path.substring(prefix.length) + '.o' - } - - // This seems correct for the "modules" case. - log.console(json.encode(resolved)) - return build_base + 'modules/' + resolved.package_name + '/' + resolved.path.substring(resolved.path.lastIndexOf('/') + 1) + '.o' - } else { - // Local project file - return build_base + 'local/' + resolved.path + '.o' - } -} - -var open_dl = {} -function get_c_symbol(requested, pkg_context) { - // Construct symbol name: js_x_y_z_use - var symname = `js_${requested.replace(/\//g, '_')}_use` - - // 1. Check Local - if (!pkg_context) { - // Check local dylib: .cell/local/local.dylib - var local_dl_path = '.cell/local/local' + dylib_ext - if (fd.is_file(local_dl_path)) { - var dl = open_dl['local'] - if (!dl) { - try { - dl = os.dylib_open(local_dl_path) - open_dl['local'] = dl - } catch(e) {} - } - if (dl) { - try { - // For local symbols, use js_local__use - var local_symname = `js_local_${requested.replace(/\//g, '_')}_use` - var sym = os.dylib_symbol(dl, local_symname) - if (sym) { - return { symbol: sym, scope: SCOPE_LOCAL } - } else { - } - } catch(e) { - } - } - } - } - - // 2. Check Modules - // Determine package name from requested path - var pkg_name = null - if (pkg_context) pkg_name = pkg_context - else if (requested.includes('/')) pkg_name = requested.split('/')[0] - else pkg_name = requested - - if (pkg_name) { - var mod_dl_path = `.cell/modules/${pkg_name}/${pkg_name}${dylib_ext}` - if (fd.is_file(mod_dl_path)) { - var dl = open_dl[pkg_name] - if (!dl) { - try { - dl = os.dylib_open(mod_dl_path) - open_dl[pkg_name] = dl - } catch(e) {} - } - if (dl) { - try { - var sym = os.dylib_symbol(dl, symname) - if (sym) return { symbol: sym, scope: SCOPE_DEPENDENCY } - } catch(e) {} - } - } - } - - // 3. Check Core (Internal) - var internal_sym = load_internal(symname) - if (internal_sym) return { symbol: internal_sym, scope: SCOPE_CORE } - - return null -} - -function get_module(name, pkg_context) { - return resolve_path(name, pkg_context, MOD_EXT) -} - -function get_actor_script(name, pkg_context) { - return resolve_path(name, pkg_context, ACTOR_EXT) -} - -globalThis.use = function use(file, ...args) { - var requested = file - - // Find C symbol - var c_res = get_c_symbol(requested, current_package) - - // Find Module - var mod_res = get_module(requested, current_package) - - var c_mod = null - var resolved = null - - // Decision logic - if (c_res && mod_res) { - if (c_res.scope < mod_res.scope) { - // C symbol is more specific - c_mod = c_res.symbol - resolved = null - } else if (mod_res.scope < c_res.scope) { - // Module is more specific - c_mod = null - resolved = mod_res - } else { - // Same scope - use both (C symbol as context) - c_mod = c_res.symbol - resolved = mod_res - } - } else if (c_res) { - c_mod = c_res.symbol - } else if (mod_res) { - resolved = mod_res - } else { - // Try embed as fallback for core - var embed_mod = use_embed(requested) - if (embed_mod) c_mod = embed_mod - } - - if (!c_mod && !resolved) - throw new Error(`Module ${file} could not be found (package context: ${current_package || 'none'})`) - - // Generate cache key - var cache_key = resolved - ? (resolved.isCore ? 'core:' + resolved.path : resolved.path) - : (c_mod ? 'c:' + requested : requested) - - if (use_cache[cache_key]) return use_cache[cache_key] - - // log.console(`cache miss: ${cache_key}`) - - // If we have a C module, use it as context - var context = {} - if (c_mod) { - context = c_mod - } - - // If we have a script, run it - var ret = c_mod - - if (resolved) { - var path = resolved.path - var isCore = resolved.isCore - var module_package = resolved.package_name - - // Check for circular dependencies - if (path && loadingStack.includes(path)) { - let cycleIndex = loadingStack.indexOf(path) - let cyclePath = loadingStack.slice(cycleIndex).concat(path) - throw new Error(`Circular dependency: ${cyclePath.join(" -> ")}`) - } - - inProgress[path] = true - loadingStack.push(path) - - var prev_package = current_package - current_package = module_package - - var compiledPath = get_compiled_path(resolved) - mkdir_p(compiledPath.substring(0, compiledPath.lastIndexOf('/'))) - - var useCompiled = false - var srcStat = fd.stat(path) - var compiledStat = fd.stat(compiledPath) - - // Always compile from source - never use precompiled for regular modules - // if (srcStat && srcStat.isFile && compiledStat && compiledStat.isFile && compiledStat.mtime > srcStat.mtime) { - // useCompiled = true - // } - - var fn - var mod_name = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.')) - - if (useCompiled) { - var compiledBlob = fd.slurp(compiledPath) - fn = js.compile_unblob(compiledBlob) - fn = js.eval_compile(fn) - log.console("use: using compiled version " + compiledPath) - } else { - if (isCore) { - var script = utf8.decode(core_qop.read(path)) - var mod_script = `(function setup_${requested.replace(/[^a-zA-Z0-9_]/g, '_')}_module(arg, $_){${script};})` - fn = js.compile(path, mod_script) - - // Save compiled version - mkdir_p(compiledPath.substring(0, compiledPath.lastIndexOf('/'))) - var compiled = js.compile_blob(fn) - write_file(compiledPath, compiled) - - fn = js.eval_compile(fn) - } else { - var script = utf8.decode(fd.slurp(path)) - var mod_script = `(function setup_${mod_name}_module(arg, $_){${script};})` - fn = js.compile(path, mod_script) - - // Save compiled version to .cell directory - var compiled = js.compile_blob(fn) - write_file(compiledPath, compiled) - - fn = js.eval_compile(fn) - } - } - - ret = fn.call(context, args, $_) - - current_package = prev_package - loadingStack.pop() - delete inProgress[path] - } - - if (!ret && c_mod) { - log.console("use: script returned nothing, using c_mod") - ret = c_mod - } - else if (!ret) throw new Error(`Use must be used with a module, but ${file} doesn't return a value`) - - use_cache[cache_key] = ret - return ret -} - -globalThis.json = use('json') -var time = use('time') -var st_now = time.number() - -var shop = use('shop') -config = shop.load_config() -var default_config = { - ar_timer: 60, - actor_memory:0, - net_service:0.1, - reply_timeout:60, - main: false, -} - -config ??= {} -config.system ??= {} -config.system.__proto__ = default_config - -cell.config = config - -ENETSERVICE = config.system.net_service -REPLYTIMEOUT = config.system.reply_timeout - -globalThis.text = use('text') - -// Load actor-specific configuration -function load_actor_config(program) { - // Extract actor name from program path - // e.g., "prosperon/_sdl_video" or "extramath/spline" - var actor_name = program - if (program.includes('/')) { - actor_name = program - } - - if (config.actors && config.actors[actor_name]) { - for (var key in config.actors[actor_name]) - cell.args[key] = config.actors[actor_name][key] - } -} +globalThis.use = use_core var blob = use('blob') var blob_stone = blob.prototype.stone @@ -657,6 +96,140 @@ stone.p = function(object) return Object.isFrozen(object) } +var actor_mod = use_embed('actor') +var wota = use_embed('wota') +var nota = use_embed('nota') +var fd = use_embed('fd') + +globalThis.text = use('text') + +var ENETSERVICE = 0.1 +var REPLYTIMEOUT = 60 // seconds before replies are ignored + +globalThis.pi = 3.1415926535897932 + +function caller_data(depth = 0) +{ + var file = "nofile" + var line = 0 + + var caller = new Error().stack.split("\n")[1+depth] + if (caller) { + var md = caller.match(/\((.*)\:/) + var m = md ? md[1] : "SCRIPT" + if (m) file = m + md = caller.match(/\:(\d*)\)/) + m = md ? md[1] : 0 + if (m) line = m + } + + return {file,line} +} + +function console_rec(line, file, msg) { + return `[${cell.id.slice(0,5)}] [${file}:${line}]: ${msg}\n` + // time: [${time.text("mb d yyyy h:nn:ss")}] +} + +globalThis.log = {} +log.console = function(msg) +{ + var caller = caller_data(1) + os.print(console_rec(caller.line, caller.file, msg)) +} + +log.error = function(msg = new Error()) +{ + var caller = caller_data(1) + + if (msg instanceof Error) + msg = msg.name + ": " + msg.message + "\n" + msg.stack + + os.print(console_rec(caller.line,caller.file,msg)) +} + +log.system = function(msg) { + msg = "[SYSTEM] " + msg + log.console(msg) +} + +function disrupt(err) +{ + if (overling) { + if (err) { + // with an err, this is a forceful disrupt + var reason = (err instanceof Error) ? err.stack : err + report_to_overling({type:'disrupt', reason}) + } else + report_to_overling({type:'stop'}) + } + + if (underlings) { + for (var id of underlings) { + log.console(`calling on ${id} to disrupt too`) + $_.stop(create_actor({id})) + } + } + + if (err) { + log.console(err); + if (err.stack) + log.console(err.stack) + } + + actor_mod.disrupt() +} + +actor_mod.on_exception(disrupt) + +cell.args = cell.hidden.init +cell.args ??= {} +cell.id ??= "newguy" + +var shop = use('shop') +os.core_qop = core_qop +os.use_cache = use_cache +shop.set_os(os) + +globalThis.use = shop.use + +var config = shop.load_config() + +// Get package name from a resolved path +function get_package_from_path(path) { + if (!path) return null + var modules_prefix = '.cell/modules/' + if (path.startsWith(modules_prefix)) { + var rest = path.substring(modules_prefix.length) + var slash_idx = rest.indexOf('/') + if (slash_idx > 0) { + return rest.substring(0, slash_idx) + } + } + return null +} + +globalThis.json = use('json') +var time = use('time') +var st_now = time.number() + +var default_config = { + ar_timer: 60, + actor_memory:0, + net_service:0.1, + reply_timeout:60, + main: false, +} + +config ??= {} +config.system ??= {} +config.system.__proto__ = default_config + +cell.config = config + +ENETSERVICE = config.system.net_service +REPLYTIMEOUT = config.system.reply_timeout + /* When handling a message, the message appears like this: { @@ -830,13 +403,7 @@ $_.receiver = function receiver(fn) { $_.start = function start(cb, program, ...args) { if (!program) return - - // Resolve the actor program path with package awareness - var resolved_program = resolve_path(program, current_package, ACTOR_EXT) - if (!resolved_program) { - throw new Error(`Actor program ${program} could not be found (package context: ${current_package || 'none'})`) - } - + var id = guid() if (args.length == 1 && Array.isArray(args[0])) args = args[0] var startup = { @@ -844,8 +411,7 @@ $_.start = function start(cb, program, ...args) { overling: $_, root, arg: args, - program: resolved_program.path, - package_context: resolved_program.package_name // Pass package context to new actor + program, } greeters[id] = cb message_queue.push({ startup }) @@ -885,6 +451,8 @@ $_.delay = function delay(fn, seconds = 0) { return function() { actor_mod.removetimer(id) } } +var enet = use_embed('enet') + // causes this actor to stop when another actor stops. var couplings = new Set() $_.couple = function couple(actor) { @@ -1029,8 +597,6 @@ function turn(msg) send_messages() } -load_actor_config(cell.args.program) - actor_mod.register_actor(cell.id, turn, cell.args.main, config.system.ar_timer) if (config.system.actor_memory) @@ -1043,18 +609,6 @@ overling = cell.args.overling root = cell.args.root root ??= $_ -// Set package context from parent actor (if spawned from a package) -if (cell.args.package_context) { - current_package = cell.args.package_context - log.console(`Actor initialized with package context: ${current_package}`) -} else { - // Infer package context from program path - current_package = get_package_from_path(cell.args.program) - if (current_package) { - log.console(`Actor inferred package context from path: ${current_package}`) - } -} - if (overling) { $_.couple(overling) // auto couple to overling @@ -1166,58 +720,17 @@ actor_mod.setname(cell.args.program) var prog = cell.args.program // Resolve the main program path -var resolved_prog = resolve_path(cell.args.program, current_package, ACTOR_EXT) +var resolved_prog = shop.resolve_locator(cell.args.program, ACTOR_EXT, null) if (!resolved_prog) { throw new Error(`Main program ${cell.args.program} could not be found`) } prog = resolved_prog.path - -var startfn -var compiledPath = get_compiled_path(resolved_prog) -var useCompiled = false - -// Always compile from source - never use precompiled for main program -// if (resolved_prog.isCore) { -// // For core, we check if we have a compiled version, else we compile it -// if (fd.is_file(compiledPath)) { -// useCompiled = true -// } -// } else { -// // For local/modules, check timestamps -// var srcStat = fd.stat(prog) -// var compiledStat = fd.stat(compiledPath) -// if (srcStat && srcStat.isFile && compiledStat && compiledStat.isFile && compiledStat.mtime > srcStat.mtime) { -// useCompiled = true -// } -// } - -if (useCompiled) { - var compiledBlob = fd.slurp(compiledPath) - var fn = js.compile_unblob(compiledBlob) - startfn = js.eval_compile(fn) - log.console("main: using compiled version " + compiledPath) -} else { - var progContent - if (resolved_prog.isCore) { - progContent = utf8.decode(core_qop.read(prog)) - } else { - progContent = utf8.decode(fd.slurp(prog)) - } - - var prog_script = `(function ${cell.args.program.name()}_start($_, arg) { var args = arg; ${progContent} })` - - // Compile and save - var fn = js.compile(cell.args.program, prog_script) - mkdir_p(compiledPath.substring(0, compiledPath.lastIndexOf('/'))) - var compiled = js.compile_blob(fn) - write_file(compiledPath, compiled) - - startfn = js.eval_compile(fn) -} +var prog_script = `(function start($_, arg) { var args = arg; ${resolved_prog.script} ; })` +var fn = js.eval(cell.args.program, prog_script) $_.clock(_ => { - var val = startfn($_, cell.args.arg); + var val = fn($_, cell.args.arg); if (val) throw new Error('Program must not return anything'); diff --git a/scripts/fd.c b/scripts/fd.c index 28f3a6a4..d368de7b 100644 --- a/scripts/fd.c +++ b/scripts/fd.c @@ -444,16 +444,20 @@ JSC_CCALL(fd_is_dir, ) JSC_SCALL(fd_slurpwrite, - int fd = js2fd(js, argv[0]); - if (fd < 0) return JS_EXCEPTION; - size_t len; const char *data = js_get_blob_data(js, &len, argv[1]); if (!data) return JS_ThrowTypeError(js, "blob expected"); - if (write(fd, data, len) != len) + int fd = open(str, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return JS_ThrowInternalError(js, "open failed: %s", strerror(errno)); + + ssize_t written = write(fd, data, len); + close(fd); + + if (written != (ssize_t)len) return JS_ThrowInternalError(js, "write failed: %s", strerror(errno)); return JS_NULL; diff --git a/scripts/json.cm b/scripts/json.cm index 412dfe9d..57673a74 100644 --- a/scripts/json.cm +++ b/scripts/json.cm @@ -1,26 +1,20 @@ var json = {} -var utf8 = use('utf8') - -var JS = JSON -JSON = null // Produce a JSON text from a Javascript object. If a record value, at any level, contains a json() method, it will be called, and the value it returns (usually a simpler record) will be JSONified. // If the record does not have a json() method, and if whitelist is a record, then only the keys that are associated with true in the whitelist are included. // If the space input is true, then line breaks and extra whitespace will be included in the text. json.encode = function encode(val,replacer,space = 1,whitelist) { - return JS.stringify(val, replacer, space) + return JSON.stringify(val, replacer, space) } // The text text is parsed, and the resulting value (usually a record or an array) is returned. // The optional reviver input is a method that will be called for every key and value at every level of the result. Each value will be replaced by the result of the reviver function. This can be used to reform data-only records into method-bearing records, or to transform date strings into seconds. json.decode = function decode(text,reviver) { - if (typeof text != 'string') { - text = utf8.decode(text) - if (!text) throw new Error("couldn't parse text: not a string") - } - return JS.parse(text,reviver) + if (typeof json_in != 'string') + throw new Error("couldn't parse text: not a string") + return JSON.parse(text,reviver) } diff --git a/scripts/shop.cm b/scripts/shop.cm index 73f7547e..8d5bd874 100644 --- a/scripts/shop.cm +++ b/scripts/shop.cm @@ -3,16 +3,107 @@ var toml = use('toml') var json = use('json') var fd = use('fd') -var utf8 = use('utf8') var http = use('http') var miniz = use('miniz') var time = use('time') +var js = use('js') +var qop +var core_qop + +// a locator is what is used to import a module, like prosperon/sprite +// in prosperon/sprite, sprite is the module, and prosperon is the package (usually, an alias) +// a canonical package name relates prosperon to its source, like gitea.pockle.world/john/prosperon var Shop = {} +var SCOPE_LOCAL = 0 +var SCOPE_PACKAGE = 1 +var SCOPE_CORE = 2 + +var MOD_EXT = '.cm' +var ACTOR_EXT = '.ce' + +var os +var use_cache +Shop.set_os = function(o) +{ + os = o + qop = os.load_internal('js_qop_use') + core_qop = os.core_qop + use_cache = os.use_cache +} + +var config = null + var shop_path = '.cell/cell.toml' var lock_path = '.cell/lock.toml' +var open_dl = {} + +function get_import_package(name) { + var parts = name.split('/') + if (parts.length > 1) + return parts[0] + + return null +} + +function get_import_name(path) +{ + var parts = path.split('/') + if (parts.length < 2) return null + return parts.slice(1).join('/') +} + +// given a path, get a full package import +// ie, 'prosperon/sprite' would return 'gitea.pockle.world/john/prosperon/sprite' +// if prosperon were a dependency +function get_package_from_path(path, ctx) +{ + var pkg = get_import_package(path) + var locator = get_import_name(path) + if (!pkg) return null + + var pck = get_normalized_module(pkg, ctx) + return pck + "/" + locator +} + +function get_normalized_package(path, ctx) +{ + var pkg = get_import_package(path) + if (!pkg) return null + return get_normalized_module(pkg, ctx) +} + +// taking the package into account, find the canonical name +function get_normalized_module(mod, ctx) { + var deps = Shop.load_config(ctx); + return deps.dependencies[mod] +} + +function get_import_dl(name) { + var pkg = get_import_package(name) + if (!pkg) return null + if (open_dl[pkg]) return open_dl[pkg] + var dlpath = `.cell/modules/${pkg}/${pkg}${dylib_ext}` + var dl = os.dylib_open(dlpath) + if (dl) { + open_dl[pkg] = dl + return dl + } + return null +} + +Shop.get_c_symbol = function get_c_symbol(name) { + var dl = get_import_dl(name) + var symname = `js_${name.replace('/', '_')}_use` + + if (dl) + return os.dylib_symbol(dl, symname) + else + return os.load_internal(symname) +} + function slurpwrite(path, content) { var f = fd.open(path) fd.write(f, content) @@ -35,12 +126,25 @@ function ensure_dir(path) { } // Load cell.toml configuration -Shop.load_config = function() { - if (!fd.stat(shop_path).isFile) - return null - - var content = utf8.decode(fd.slurp(shop_path)) - return toml.decode(content) +// module given in canonical format (e.g., "gitea.pockle.world/john/prosperon") +// If module is null, loads the root cell.toml +// If module is provided, loads module/cell.toml +Shop.load_config = function(module) { + var content + if (!module) { + if (!fd.is_file(shop_path)) + return null + + content = fd.slurp(shop_path) + } else { + var module_path = `.cell/modules/${module}/cell.toml` + if (!fd.stat(module_path).isFile) + return null + + content = fd.slurp(module_path) + } + + return toml.decode(text(content)) } // Save cell.toml configuration @@ -53,7 +157,7 @@ Shop.load_lock = function() { if (!fd.stat(lock_path).isFile) return {} - var content = utf8.decode(fd.slurp(lock_path)) + var content = text(fd.slurp(lock_path)) return toml.decode(content) || {} } @@ -285,7 +389,7 @@ Shop.install = function(alias) { try { log.console("Fetching info from " + api_url) var resp = http.fetch(api_url) - var resp_text = utf8.decode(resp) + var resp_text = text(resp) commit_hash = Shop.extract_commit_hash(locator, resp_text) log.console("Resolved commit: " + commit_hash) } catch (e) { @@ -376,6 +480,135 @@ Shop.verify = function() { return all_ok } + + +var open_dls = {} + +function get_locator_module_path(locator) +{ + var canon_pkg = get_package_from_path(locator); + var path = `.cell/modules/${canon_pkg}`; + if (fd.is_directory(path)) + return path; + return null +} + +function resolve_locator(path, ext, ctx) +{ + var deps = Shop.load_config(ctx).dependencies || {} + + var local_path + if (ctx) + local_path = `.cell/modules/${ctx}/${path}${ext}` + else + local_path = path + ext + + if (fd.is_file(local_path)) + return {path: local_path, scope: SCOPE_LOCAL, script: text(fd.slurp(local_path))}; + + var mod_path = `.cell/modules/${get_package_from_path(path, ctx)}${ext}` + + if (fd.is_file(mod_path)) + return {path: mod_path, scope: SCOPE_PACKAGE, script: text(fd.slurp(mod_path))}; + + var core = core_qop.read(path + ext) + if (core) + return {path, scope: SCOPE_CORE, script: text(core)}; + + return null; +} + +function resolve_c_symbol(path, package_ctx) +{ + var local_path = package_ctx ? package_ctx : 'local' + var local = `js_${local_path}_${path.replace('/', '_')}_use` + var local_dl_name = `.cell/build/${local_path}/cellmod.dylib` + + if (fd.is_file(local_dl_name)) { + if (!open_dls[local_dl_name]) + open_dls[local_dl_name] = os.dylib_open(local_dl_name); + + if (open_dls[local_dl_name]) { + var local_addr = os.dylib_symbol(open_dls[local_dl_name], local); + if (local_addr) return {symbol: local_addr, scope: SCOPE_LOCAL}; + } + } + + var static_local_addr = os.load_internal(`js_${local_path}_${path.replace('/', '_')}_use`); + if (static_local_addr) return {symbol: static_local_addr, scope: SCOPE_LOCAL}; + + var pkg = get_package_from_path(path, package_ctx) + if (pkg) { + var package_sym = `js_${pkg.replace('/', '_')}_use` + var canon_pkg = get_normalized_package(path, package_ctx) + var package_dl_name = `.cell/build/${canon_pkg}/cellmod.dylib`; + + if (canon_pkg && fd.is_file(package_dl_name)) { + if (!open_dls[package_dl_name]) + open_dls[package_dl_name] = os.dylib_open(package_dl_name); + + if (open_dls[package_dl_name]) { + var package_addr = os.dylib_symbol(open_dls[package_dl_name], package_sym); + if (package_addr) + return {symbol:package_addr, scope: SCOPE_PACKAGE, package: canon_pkg}; + } + } + + var static_package_addr = os.load_internal(`js_${pkg.replace('/', '_')}_use`); + if (static_package_addr) + return {symbol:static_package_addr, scope: SCOPE_PACKAGE, package: canon_pkg}; + } + + var core_addr = os.load_internal(`js_${path.replace('/', '_')}_use`); + if (core_addr) + return {symbol:core_addr, scope: SCOPE_CORE}; + + return null +} + +function mod_scriptor(name, script) +{ + return `(function setup_${name}_module($_){${script}})` +} + +function eval_mod(path, script, c_sym) +{ + var content = mod_scriptor(path, script) + var fn = js.eval(path, content) + return fn.call(c_sym) +} + +// first looks in local +// then in dependencies +// then in core +// package_context: optional package context to resolve relative paths within +Shop.use = function(path, package_context) { + var c_resolve = resolve_c_symbol(path, package_context) || {scope:999} + var mod_resolve = resolve_locator(path, '.cm', package_context) || {scope:999} + + var min_scope = Math.min(c_resolve.scope, mod_resolve.scope) + + if (min_scope == 999) { + throw new Error(`Module ${path} could not be found`) + } + + var cache_key = `${text(min_scope)}::${path}` + + if (use_cache[cache_key]) + return use_cache[cache_key] + + if (c_resolve.scope < mod_resolve.scope) + use_cache[cache_key] = c_resolve.symbol + else if (mod_resolve.scope < c_resolve.scope) + use_cache[cache_key] = eval_mod(mod_resolve.path, mod_resolve.script) + else + use_cache[cache_key] = eval_mod(mod_resolve.path, mod_resolve.script, c_resolve.symbol) + + return use_cache[cache_key] +} + +Shop.resolve_locator = resolve_locator + // Check for updates Shop.update = function() { var config = Shop.load_config() @@ -390,7 +623,7 @@ Shop.update = function() { if (api_url) { try { var resp = http.fetch(api_url) - var resp_text = utf8.decode(resp) + var resp_text = text(resp) var remote_hash = Shop.extract_commit_hash(locator, resp_text) var local_hash = lock[alias] ? lock[alias].commit : null @@ -508,28 +741,4 @@ Shop.resolve_module = function(module_name, package_name, is_file_fn) { return null } -// Get the package name from a file path -// e.g., '.cell/modules/extramath/spline.cm' -> 'extramath' -// e.g., 'myfile.cm' -> null -Shop.get_package_from_path = function(path) { - if (!path) return null - var modules_prefix = '.cell/modules/' - if (path.startsWith(modules_prefix)) { - var rest = path.substring(modules_prefix.length) - // This logic is tricky with nested paths like gitea.pockle.world/john/prosperon - // We probably need to reverse map from path to alias using config - var config = Shop.load_config() - if (config && config.dependencies) { - for (var alias in config.dependencies) { - var locator = config.dependencies[alias] - var parsed = Shop.parse_locator(locator) - if (rest.startsWith(parsed.path + '/')) { - return alias - } - } - } - } - return null -} - return Shop \ No newline at end of file diff --git a/scripts/text.cm b/scripts/text.cm index e3580b91..28b964f3 100644 --- a/scripts/text.cm +++ b/scripts/text.cm @@ -408,10 +408,4 @@ function format_number(num, format) { return null; } -text.base32_to_blob = that.base32_to_blob -text.base64_to_blob = that.base64_to_blob -text.base64url_to_blob = that.base64url_to_blob -text.blob_to_base64 = that.blob_to_base64 -text.blob_to_base64url = that.blob_to_base64url - return text; \ No newline at end of file diff --git a/scripts/time.cm b/scripts/time.cm index d9ed4153..08353129 100644 --- a/scripts/time.cm +++ b/scripts/time.cm @@ -2,7 +2,6 @@ var time = this; /* -------- host helpers -------------------------------------------------- */ - var now = time.now; // seconds since Misty epoch (C stub) var computer_zone = time.computer_zone; // integral hours, no DST var computer_dst = time.computer_dst; // true ↔ DST in effect diff --git a/source/cell_internal.h b/source/cell_internal.h index 49ec397a..b0710f91 100644 --- a/source/cell_internal.h +++ b/source/cell_internal.h @@ -74,7 +74,6 @@ const char *send_message(const char *id, void *msg); const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar); void actor_unneeded(cell_rt *actor, JSValue fn, double seconds); void script_startup(cell_rt *rt); -cell_rt *create_actor(void *wota); int uncaught_exception(JSContext *js, JSValue v); int actor_exists(const char *id); diff --git a/tests/delay.ce b/tests/delay.ce index a33ba2bd..67d6bc5f 100644 --- a/tests/delay.ce +++ b/tests/delay.ce @@ -1,4 +1,4 @@ - +log.console('hello') var count = 0 function loop() {