Files
cell/internal/engine.cm
2025-12-17 02:53:01 -06:00

994 lines
25 KiB
Plaintext

(function engine() {
var ACTORDATA = cell.hidden.actorsym
var SYSYM = '__SYSTEM__'
var hidden = cell.hidden
var os = hidden.os;
cell.os = null
var dylib_ext
cell.id ??= "newguy"
switch(os.platform()) {
case 'Windows': dylib_ext = '.dll'; break;
case 'macOS': dylib_ext = '.dylib'; break;
case 'Linux': dylib_ext = '.so'; break;
}
var MOD_EXT = '.cm'
var ACTOR_EXT = '.ce'
var load_internal = os.load_internal
function use_embed(name) {
return load_internal(`js_${name}_use`)
}
globalThis.use = use_embed
globalThis.meme = function(obj) {
return {
__proto__: obj
}
}
globalThis.logical = function(val1)
{
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
return false;
if (val1 == 1 || val1 == true || val1 == "true")
return true;
return null;
}
var utf8 = use_embed('utf8')
var js = use_embed('js')
var fd = use_embed('fd')
// Get the shop path from HOME environment
var home = os.getenv('HOME') || os.getenv('USERPROFILE')
if (!home) {
throw new Error('Could not determine home directory')
}
var shop_path = home + '/.cell'
var packages_path = shop_path + '/packages'
var core_path = packages_path + '/core'
if (!fd.is_dir(core_path)) {
throw new Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.')
}
var use_cache = {}
use_cache['core/os'] = os
var _Symbol = Symbol
// Load a core module from the file system
function use_core(path) {
var cache_key = 'core/' + path
if (use_cache[cache_key])
return use_cache[cache_key];
var sym = use_embed(path.replace('/','_'))
// Core scripts are in packages/core/
var file_path = core_path + '/' + path + MOD_EXT
if (fd.is_file(file_path)) {
var script_blob = fd.slurp(file_path)
var script = utf8.decode(script_blob)
var mod = `(function setup_module($_){${script}})`
var fn = js.eval('core:' + path, mod)
var result = fn.call(sym);
use_cache[cache_key] = result;
return result;
}
use_cache[cache_key] = sym;
return sym;
}
globalThis.use = use_core
var blob = use('blob')
var blob_stone = blob.prototype.stone
var blob_stonep = blob.prototype.stonep;
delete blob.prototype.stone;
delete blob.prototype.stonep;
// Capture Object and Array methods before they're deleted
var _Object = Object
var _ObjectKeys = Object.keys
var _ObjectFreeze = Object.freeze
var _ObjectIsFrozen = Object.isFrozen
var _ObjectDefineProperty = Object.defineProperty
var _ObjectGetPrototypeOf = Object.getPrototypeOf
var _ObjectCreate = meme
var _ArrayIsArray = Array.isArray
function deepFreeze(object) {
if (object instanceof blob)
blob_stone.call(object);
var propNames = _ObjectKeys(object);
for (var name of propNames) {
var value = object[name];
if ((value && typeof value == "object") || typeof value == "function")
deepFreeze(value);
}
return _ObjectFreeze(object);
}
globalThis.stone = deepFreeze
stone.p = function(object)
{
if (object instanceof blob)
return blob_stonep.call(object)
return _ObjectIsFrozen(object)
}
var actor_mod = use('actor')
var wota = use('wota')
var nota = use('nota')
// Load internal modules for global functions
globalThis.text = use('internal/text')
globalThis.number = use('internal/number')
globalThis.array = use('internal/array')
globalThis.object = use('internal/object')
globalThis.fn = use('internal/fn')
// Global utility functions (use already-captured references)
var _isArray = _ArrayIsArray
var _keys = _ObjectKeys
var _getPrototypeOf = _ObjectGetPrototypeOf
var _create = _ObjectCreate
globalThis.length = function(value) {
if (value == null) return null
// For functions, return arity
if (typeof value == 'function') return value.length
// For strings, return codepoint count
if (typeof value == 'string') return value.length
// For arrays, return element count
if (_isArray(value)) return value.length
// For blobs, return bit count
if (value instanceof blob && typeof value.length == 'number') return value.length
// For records with length field
if (typeof value == 'object' && value != null) {
if ('length' in value) {
var len = value.length
if (typeof len == 'function') return len.call(value)
if (typeof len == 'number') return len
}
}
return null
}
globalThis.reverse = function(value) {
if (_isArray(value)) {
var result = []
for (var i = value.length - 1; i >= 0; i--) {
result.push(value[i])
}
return result
}
// For blobs, would need blob module support
if (isa(value, blob)) {
// Simplified: return null for now, would need proper blob reversal
return null
}
return null
}
var keyCounter = 0
globalThis.key = function() {
return _Symbol('key_' + (keyCounter++))
}
globalThis.isa = function(value, master) {
if (master == null) return false
// isa(value, array) - check if object has all keys
if (_isArray(master)) {
if (typeof value != 'object' || value == null) return false
for (var i = 0; i < master.length; i++) {
if (!(master[i] in value)) return false
}
return true
}
// isa(value, function) - check if function.prototype is in chain
if (typeof master == 'function') {
// Special type checks
if (master == stone) return _ObjectIsFrozen(value) || typeof value != 'object'
if (master == number) return typeof value == 'number'
if (master == text) return typeof value == 'string'
if (master == logical) return typeof value == 'boolean'
if (master == array) return _isArray(value)
if (master == object) return typeof value == 'object' && value != null && !_isArray(value)
if (master == fn) return typeof value == 'function'
// Check prototype chain
if (master.prototype) {
var proto = _getPrototypeOf(value)
while (proto != null) {
if (proto == master.prototype) return true
proto = _getPrototypeOf(proto)
}
}
return false
}
// isa(object, master_object) - check prototype chain
if (typeof master == 'object') {
var proto = _getPrototypeOf(value)
while (proto != null) {
if (proto == master) return true
proto = _getPrototypeOf(proto)
}
return false
}
return false
}
globalThis.proto = function(obj) {
if (!isa(obj, object)) return null
var p = _getPrototypeOf(obj)
if (p == _Object.prototype) return null
return p
}
globalThis.splat = function(obj) {
if (typeof obj != 'object' || obj == null) return null
var result = {}
var current = obj
// Walk prototype chain and collect text keys
while (current != null) {
var keys = _keys(current)
for (var i = 0; i < keys.length; i++) {
var k = keys[i]
if (!(k in result)) {
var val = current[k]
// Only include serializable types
if (typeof val == 'object' || typeof val == 'number' ||
typeof val == 'string' || typeof val == 'boolean') {
result[k] = val
}
}
}
current = _getPrototypeOf(current)
}
// Call to_data if present
if (typeof obj.to_data == 'function') {
var extra = obj.to_data(result)
if (typeof extra == 'object' && extra != null) {
var extraKeys = _keys(extra)
for (var i = 0; i < extraKeys.length; i++) {
result[extraKeys[i]] = extra[extraKeys[i]]
}
}
}
return result
}
var ENETSERVICE = 0.1
var REPLYTIMEOUT = 60 // seconds before replies are ignored
globalThis.pi = 3.14159265358979323846264338327950288419716939937510
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"
function create_actor(desc = {id:guid()}) {
var actor = {}
actor[ACTORDATA] = desc
return actor
}
var $_ = create_actor()
os.use_cache = use_cache
os.global_shop_path = shop_path
os.$_ = $_
var shop = use('shop')
globalThis.use = shop.use
globalThis.json = use('json')
var time = use('time')
var pronto = use('pronto')
globalThis.fallback = pronto.fallback
globalThis.parallel = pronto.parallel
globalThis.race = pronto.race
globalThis.sequence = pronto.sequence
globalThis.time_limit = pronto.time_limit
globalThis.requestorize = pronto.requestorize
globalThis.objectify = pronto.objectify
var config = {
ar_timer: 60,
actor_memory:0,
net_service:0.1,
reply_timeout:60,
main: true
}
cell.config = config
ENETSERVICE = config.net_service
REPLYTIMEOUT = config.reply_timeout
/*
When handling a message, the message appears like this:
{
type: type of message
- contact: used for contact messages
- stop: used to issue stop command
- etc
reply: ID this message will respond to (callback saved on the actor)
replycc: the actor that is waiting for the reply
target: ID of the actor that's supposed to receive the message. Only added to non direct sends (out of portals)
return: reply ID so the replycc actor can know what callback to send the message to
data: the actual content of the message
}
actors look like
{
id: the GUID of this actor
address: the IP this actor can be found at
port: the port of the IP the actor can be found at
}
*/
function guid(bits = 256)
{
var guid = new blob(bits, os.random)
stone(guid)
return text(guid,'h')
}
var HEADER = key()
// returns a number between 0 and 1. There is a 50% chance that the result is less than 0.5.
$_.random = function() {
return (os.random() / 9007199254740992)
}
$_.random_fit = os.random
// takes a function input value that will eventually be called with the current time in number form.
$_.clock = function(fn) {
actor_mod.clock(_ => {
fn(time.number())
send_messages()
})
}
var underlings = new Set() // this is more like "all actors that are notified when we die"
var overling = null
var root = null
var receive_fn = null
var greeters = {} // Router functions for when messages are received for a specific actor
globalThis.is_actor = function is_actor(actor) {
return actor[ACTORDATA]
}
function peer_connection(peer) {
return {
latency: peer.rtt,
bandwidth: {
incoming: peer.incoming_bandwidth,
outgoing: peer.outgoing_bandwidth
},
activity: {
last_sent: peer.last_send_time,
last_received: peer.last_receive_time
},
mtu: peer.mtu,
data: {
incoming_total: peer.incoming_data_total,
outgoing_total: peer.outgoing_data_total,
reliable_in_transit: peer.reliable_data_in_transit
},
latency_variance: peer.rtt_variance,
packet_loss: peer.packet_loss,
state: peer.state
}
}
// takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated. This can include latency, bandwidth, activity, congestion, cost, partitions. The callback is given a record containing the requested information.
$_.connection = function(callback, actor, config) {
var peer = peers[actor[ACTORDATA].id]
if (peer) {
callback(peer_connection(peer))
return
}
if (actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
callback({type:"local"})
return
}
callback()
}
var peers = {}
var id_address = {}
var peer_queue = {}
var portal = null
var portal_fn = null
// takes a function input value that will eventually be called with the current time in number form.
$_.portal = function(fn, port) {
if (portal) throw new Error(`Already started a portal listening on ${portal.port}`)
if (!port) throw new Error("Requires a valid port.")
log.system(`starting a portal on port ${port}`)
portal = enet.create_host({address: "any", port})
portal_fn = fn
}
function handle_host(e) {
switch (e.type) {
case "connect":
log.system(`connected a new peer: ${e.peer.address}:${e.peer.port}`)
peers[`${e.peer.address}:${e.peer.port}`] = e.peer
var queue = peer_queue.get(e.peer)
if (queue) {
for (var msg of queue) e.peer.send(nota.encode(msg))
log.system(`sent ${json.encode(msg)} out of queue`)
peer_queue.delete(e.peer)
}
break
case "disconnect":
peer_queue.delete(e.peer)
for (var id in peers) if (peers[id] == e.peer) delete peers[id]
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
break
case "receive":
var data = nota.decode(e.data)
if (data.replycc && !data.replycc.address) {
data.replycc[ACTORDATA].address = e.peer.address
data.replycc[ACTORDATA].port = e.peer.port
}
function populate_actor_addresses(obj) {
if (!isa(obj, object)) return
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
obj[ACTORDATA].address = e.peer.address
obj[ACTORDATA].port = e.peer.port
}
for (var key in obj) {
if (object.has(obj, key)) {
populate_actor_addresses(obj[key])
}
}
}
if (data.data) populate_actor_addresses(data.data)
turn(data)
break
}
}
// takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated. This can include latency, bandwidth, activity, congestion, cost, partitions. The callback is given a record containing the requested information.
$_.contact = function(callback, record) {
send(create_actor(record), record, callback)
}
// registers a function that will receive all messages...
$_.receiver = function receiver(fn) {
receive_fn = fn
}
$_.start = function start(cb, program, ...args) {
if (!program) return
var id = guid()
if (args.length == 1 && Array.isArray(args[0])) args = args[0]
var startup = {
id,
overling: $_,
root,
arg: args,
program,
}
greeters[id] = cb
message_queue.push({ startup })
}
// stops an underling or self.
$_.stop = function stop(actor) {
if (!actor) {
need_stop = true
return
}
if (!is_actor(actor))
throw new Error('Can only call stop on an actor.')
if (!underlings.has(actor[ACTORDATA].id))
throw new Error('Can only call stop on an underling or self.')
sys_msg(actor, {kind:"stop"})
}
// schedules the removal of an actor after a specified amount of time.
$_.unneeded = function unneeded(fn, seconds) {
actor_mod.unneeded(fn, seconds)
}
// schedules the invocation of a function after a specified amount of time.
$_.delay = function delay(fn, seconds = 0) {
if (seconds <= 0) {
$_.clock(fn)
return
}
function delay_turn() {
fn()
send_messages()
}
var id = actor_mod.delay(delay_turn, seconds)
return function() { actor_mod.removetimer(id) }
}
var enet = use('enet')
// causes this actor to stop when another actor stops.
var couplings = new Set()
$_.couple = function couple(actor) {
if (actor == $_) return // can't couple to self
couplings.add(actor[ACTORDATA].id)
sys_msg(actor, {kind:'couple', from: $_})
log.system(`coupled to ${actor}`)
}
function actor_prep(actor, send) {
message_queue.push({actor,send});
}
// Send a message immediately without queuing
function actor_send_immediate(actor, send) {
try {
actor_send(actor, send);
} catch (err) {
log.error("Failed to send immediate message:", err);
}
}
function actor_send(actor, message) {
if (actor[HEADER] && !actor[HEADER].replycc) // attempting to respond to a message but sender is not expecting; silently drop
return
if (!is_actor(actor) && !is_actor(actor.replycc)) throw new Error(`Must send to an actor object. Attempted send to ${json.encode(actor)}`)
if (typeof message != 'object') throw new Error('Must send an object record.')
// message to self
if (actor[ACTORDATA].id == cell.id) {
if (receive_fn) receive_fn(message.data)
return
}
// message to actor in same flock
if (actor[ACTORDATA].id && actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
var wota_blob = wota.encode(message)
// log.console(`sending wota blob of ${wota_blob.length/8} bytes`)
actor_mod.mailbox_push(actor[ACTORDATA].id, wota_blob)
return
}
if (actor[ACTORDATA].address) {
if (actor[ACTORDATA].id)
message.target = actor[ACTORDATA].id
else
message.type = "contact"
var peer = peers[actor[ACTORDATA].address + ":" + actor[ACTORDATA].port]
if (!peer) {
if (!portal) {
log.system(`creating a contactor ...`)
portal = enet.create_host({address:"any"})
log.system(`allowing contact to port ${portal.port}`)
}
log.system(`no peer! connecting to ${actor[ACTORDATA].address}:${actor[ACTORDATA].port}`)
peer = portal.connect(actor[ACTORDATA].address, actor[ACTORDATA].port)
peer_queue.set(peer, [message])
} else {
peer.send(nota.encode(message))
}
return
}
log.system(`Unable to send message to actor ${json.encode(actor[ACTORDATA])}`)
}
// Holds all messages queued during the current turn.
var message_queue = []
var need_stop = false
function send_messages() {
// if we've been flagged to stop, bail out before doing anything
if (need_stop) {
disrupt()
message_queue.length = 0
return
}
for (var msg of message_queue) {
if (msg.startup) {
// now is the time to actually spin up the actor
actor_mod.createactor(msg.startup)
} else {
actor_send(msg.actor, msg.send)
}
}
message_queue.length = 0
}
var replies = {}
globalThis.send = function send(actor, message, reply) {
if (typeof actor != 'object')
throw new Error(`Must send to an actor object. Provided: ${json.encode(actor)}`);
if (typeof message != 'object')
throw new Error('Message must be an object')
var send = {type:"user", data: message}
if (actor[HEADER] && actor[HEADER].replycc) {
var header = actor[HEADER]
if (!header.replycc || !is_actor(header.replycc))
throw new Error(`Supplied actor had a return, but it's not a valid actor! ${json.encode(actor[HEADER])}`)
actor = header.replycc
send.return = header.reply
}
if (reply) {
var id = guid()
replies[id] = reply
$_.delay(_ => {
if (replies[id]) {
replies[id](null, "timeout")
delete replies[id]
}
}, REPLYTIMEOUT)
send.reply = id
send.replycc = $_
}
// Instead of sending immediately, queue it
actor_prep(actor,send);
}
stone(send)
if (!cell.args.id) cell.id = guid()
else cell.id = cell.args.id
$_[ACTORDATA].id = cell.id
// Actor's timeslice for processing a single message
function turn(msg)
{
var mes = wota.decode(msg)
handle_message(mes)
send_messages()
}
//log.console(`FIXME: need to get main from config, not just set to true`)
//log.console(`FIXME: remove global access (ie globalThis.use)`)
//log.console(`FIXME: add freeze/unfreeze at this level, so we can do it (but scripts cannot)`)
actor_mod.register_actor(cell.id, turn, true, config.ar_timer)
if (config.actor_memory)
js.mem_limit(config.actor_memory)
if (config.stack_max)
js.max_stacksize(config.system.stack_max);
overling = cell.args.overling
root = cell.args.root
root ??= $_
if (overling) {
$_.couple(overling) // auto couple to overling
report_to_overling({type:'greet', actor: $_})
}
// sys messages are always dispatched immediately
function sys_msg(actor, msg)
{
actor_send(actor, {[SYSYM]:msg})
}
// messages sent to here get put into the cb provided to start
function report_to_overling(msg)
{
if (!overling) return
sys_msg(overling, {kind:'underling', message:msg, from: $_})
}
// Determine the program to run from command line
var program = cell.args.program
if (!program) {
log.error('No program specified. Usage: cell <program.ce> [args...]')
os.exit(1)
}
function handle_actor_disconnect(id) {
var greeter = greeters[id]
if (greeter) {
greeter({type: "stopped", id})
delete greeters[id]
}
log.system(`actor ${id} disconnected`)
if (couplings.has(id)) disrupt("coupled actor died") // couplings now disrupts instead of stop
}
function handle_sysym(msg)
{
switch(msg.kind) {
case 'stop':
disrupt("got stop message")
break
case 'underling':
var from = msg.from
var greeter = greeters[from[ACTORDATA].id]
if (greeter) greeter(msg.message)
if (msg.message.type == 'disrupt')
underlings.delete(from[ACTORDATA].id)
break
case 'contact':
if (portal_fn) {
var letter2 = msg.data
letter2[HEADER] = msg
delete msg.data
portal_fn(letter2)
} else throw new Error('Got a contact message, but no portal is established.')
break
case 'couple': // from must be notified when we die
var from = msg.from
underlings.add(from[ACTORDATA].id)
log.system(`actor ${from} is coupled to me`)
break
}
}
function handle_message(msg) {
if (msg[SYSYM]) {
handle_sysym(msg[SYSYM], msg.from)
return
}
switch (msg.type) {
case "user":
var letter = msg.data // what the sender really sent
_ObjectDefineProperty(letter, HEADER, {
value: msg, enumerable: false
})
_ObjectDefineProperty(letter, ACTORDATA, { // this is so is_actor == true
value: { reply: msg.reply }, enumerable: false
})
if (msg.return) {
var fn = replies[msg.return]
if (fn) fn(letter)
delete replies[msg.return]
return
}
if (receive_fn) receive_fn(letter)
return
case "stopped":
handle_actor_disconnect(msg.id)
break
}
};
function enet_check()
{
if (portal) portal.service(handle_host)
$_.delay(enet_check, ENETSERVICE);
}
// enet_check();
var init_end = time.number()
var load_program_start = time.number()
// Finally, run the program
actor_mod.setname(cell.args.program)
var prog = cell.args.program
var package = use('package')
var locator = shop.resolve_locator(cell.args.program + ".ce", null)
if (!locator) {
var pkg = package.find_package_dir(cell.args.program + ".ce")
locator = shop.resolve_locator(cell.args.program + ".ce", pkg)
}
if (!locator)
throw new Error(`Main program ${cell.args.program} could not be found`)
// Hide JavaScript built-ins - make them inaccessible
// Store references we need internally before deleting
var _Object = Object
var _Array = Array
var _String = String
var _Number = Number
var _Boolean = Boolean
var _Math = Math
var _Function = Function
var _Error = Error
var _JSON = JSON
// juicing these before Math is gone
use('math/radians')
use('math/cycles')
use('math/degrees')
// Delete from globalThis
delete globalThis.Object
delete globalThis.Math
delete globalThis.Number
delete globalThis.String
delete globalThis.Array
delete globalThis.Boolean
delete globalThis.Date
delete globalThis.Function
delete globalThis.Reflect
delete globalThis.Proxy
delete globalThis.WeakMap
delete globalThis.WeakSet
delete globalThis.WeakRef
delete globalThis.BigInt
delete globalThis.Symbol
delete globalThis.Map
delete globalThis.Set
delete globalThis.Promise
delete globalThis.ArrayBuffer
delete globalThis.DataView
delete globalThis.Int8Array
delete globalThis.Uint8Array
delete globalThis.Uint8ClampedArray
delete globalThis.Int16Array
delete globalThis.Uint16Array
delete globalThis.Int32Array
delete globalThis.Uint32Array
delete globalThis.Float32Array
delete globalThis.Float64Array
delete globalThis.BigInt64Array
delete globalThis.BigUint64Array
delete globalThis.eval
delete globalThis.parseInt
delete globalThis.parseFloat
delete globalThis.isNaN
delete globalThis.isFinite
delete globalThis.decodeURI
delete globalThis.decodeURIComponent
delete globalThis.encodeURI
delete globalThis.encodeURIComponent
delete globalThis.escape
delete globalThis.unescape
delete globalThis.Intl
delete globalThis.RegExp
// TODO: delete globalThis
var mem = js.calc_mem()
log.console(json.encode(mem))
log.console("total memory usage: " + text(mem.memory_used_size/1024) + " KB");
$_.clock(_ => {
var val = locator.symbol.call(null, $_, cell.args.arg);
if (val)
throw new Error('Program must not return anything');
})
})()