Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a74231563a | ||
|
|
6799d90a7d | ||
|
|
1fee8f9f8b | ||
|
|
d18ea1b330 | ||
|
|
4de0659474 | ||
|
|
27a9b72b07 | ||
|
|
a3622bd5bd | ||
|
|
2f6700415e | ||
|
|
243d92f7f3 | ||
|
|
8f9d026b9b | ||
|
|
2c9ac8f7b6 | ||
|
|
80f24e131f | ||
|
|
a8f8af7662 | ||
|
|
f5b3494762 | ||
|
|
13a6f6c79d | ||
|
|
1a925371d3 | ||
|
|
08d2bacb1f | ||
|
|
7322153e57 | ||
|
|
cc72c4cb0f | ||
|
|
ae1f09a28f | ||
|
|
3c842912a1 | ||
|
|
7cacf32078 | ||
|
|
b740612761 | ||
|
|
6001c2b4bb | ||
|
|
98625fa15b | ||
|
|
87fafa44c8 | ||
|
|
45ce76aef7 | ||
|
|
32fb44857c | ||
|
|
31d67f6710 | ||
|
|
3621b1ef33 | ||
|
|
836227c8d3 | ||
|
|
0ae59705d4 | ||
|
|
8e2607b6ca | ||
|
|
dc73e86d8c | ||
|
|
555cceb9d6 | ||
|
|
fbb7933eb6 | ||
|
|
0287d6ada4 | ||
|
|
73cd6a255d |
@@ -382,13 +382,13 @@ static const JSCFunctionListEntry js_reader_funcs[] = {
|
||||
JSValue js_miniz_use(JSContext *js)
|
||||
{
|
||||
JS_NewClassID(&js_reader_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_reader_class_id, &js_reader_class);
|
||||
JS_NewClass(js, js_reader_class_id, &js_reader_class);
|
||||
JSValue reader_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, reader_proto, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_reader_class_id, reader_proto);
|
||||
|
||||
JS_NewClassID(&js_writer_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_writer_class_id, &js_writer_class);
|
||||
JS_NewClass(js, js_writer_class_id, &js_writer_class);
|
||||
JSValue writer_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, writer_proto, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_writer_class_id, writer_proto);
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
(function engine() {
|
||||
// Hidden vars (os, actorsym, init, core_path) come from env
|
||||
var ACTORDATA = actorsym
|
||||
var SYSYM = '__SYSTEM__'
|
||||
|
||||
var _cell = {}
|
||||
var need_stop = false
|
||||
|
||||
var dylib_ext
|
||||
|
||||
switch(os.platform()) {
|
||||
case 'Windows': dylib_ext = '.dll'; break;
|
||||
case 'macOS': dylib_ext = '.dylib'; break;
|
||||
case 'Linux': dylib_ext = '.so'; break;
|
||||
}
|
||||
var cases = {
|
||||
Windows: '.dll',
|
||||
macOS: '.dylib',
|
||||
Linux: '.so'
|
||||
}
|
||||
|
||||
print(os.platform())
|
||||
|
||||
dylib_ext = cases[os.platform()]
|
||||
|
||||
var MOD_EXT = '.cm'
|
||||
var ACTOR_EXT = '.ce'
|
||||
@@ -45,20 +49,26 @@ function ends_with(str, suffix) {
|
||||
return search(str, suffix, -length(suffix)) != null
|
||||
}
|
||||
|
||||
print("engine: loading js")
|
||||
var js = use_embed('js')
|
||||
print("engine: loading fd")
|
||||
var fd = use_embed('fd')
|
||||
print("engine: loaded fd")
|
||||
|
||||
print("engine: getting home")
|
||||
// Get the shop path from HOME environment
|
||||
var home = os.getenv('HOME') || os.getenv('USERPROFILE')
|
||||
if (!home) {
|
||||
throw Error('Could not determine home directory')
|
||||
os.print('Could not determine home directory\n')
|
||||
os.exit(1)
|
||||
}
|
||||
var shop_path = home + '/.cell'
|
||||
var packages_path = shop_path + '/packages'
|
||||
var core_path = packages_path + '/core'
|
||||
|
||||
if (!fd.is_dir(core_path)) {
|
||||
throw Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.')
|
||||
os.print('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.\n')
|
||||
os.exit(1)
|
||||
}
|
||||
|
||||
var use_cache = {}
|
||||
@@ -70,34 +80,49 @@ function use_core(path) {
|
||||
if (use_cache[cache_key])
|
||||
return use_cache[cache_key];
|
||||
|
||||
print(`engine: use_core('${path}') - embed`)
|
||||
var sym = use_embed(replace(path, '/', '_'))
|
||||
|
||||
// Core scripts are in packages/core/
|
||||
var file_path = core_path + '/' + path + MOD_EXT
|
||||
|
||||
if (fd.is_file(file_path)) {
|
||||
print(`engine: use_core('${path}') - loading script`)
|
||||
var script_blob = fd.slurp(file_path)
|
||||
var script = text(script_blob)
|
||||
var mod = `(function setup_module(use){${script}})`
|
||||
var fn = js.eval('core:' + path, mod)
|
||||
var fn = mach_eval('core:' + path, mod)
|
||||
if (fn == null) {
|
||||
print(`engine: use_core('${path}') - mach_eval returned null!`)
|
||||
return null
|
||||
}
|
||||
print(`engine: use_core('${path}') - calling (fn type: ${typeof(fn)})`)
|
||||
var result = call(fn,sym, [use_core])
|
||||
print(`engine: use_core('${path}') - done`)
|
||||
use_cache[cache_key] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
print(`engine: use_core('${path}') - using embed directly`)
|
||||
use_cache[cache_key] = sym;
|
||||
return sym;
|
||||
}
|
||||
|
||||
print("engine: loading blob")
|
||||
var blob = use_core('blob')
|
||||
print("engine: loaded blob")
|
||||
|
||||
function actor() {
|
||||
|
||||
}
|
||||
|
||||
print("engine: loading actor")
|
||||
var actor_mod = use_core('actor')
|
||||
print("engine: loading wota")
|
||||
var wota = use_core('wota')
|
||||
print("engine: loading nota")
|
||||
var nota = use_core('nota')
|
||||
print("engine: loaded nota")
|
||||
|
||||
function is_actor(value) {
|
||||
return is_object(value) && value[ACTORDATA]
|
||||
@@ -133,32 +158,27 @@ function log(name, args) {
|
||||
var caller = caller_data(1)
|
||||
var msg = args[0]
|
||||
|
||||
switch(name) {
|
||||
case 'console':
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
break
|
||||
case 'error':
|
||||
msg = msg ?? Error()
|
||||
if (is_proto(msg, Error))
|
||||
msg = msg.name + ": " + msg.message + "\n" + msg.stack
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
break
|
||||
case 'system':
|
||||
msg = "[SYSTEM] " + msg
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
break
|
||||
default:
|
||||
log.console(`unknown log type: ${name}`)
|
||||
break
|
||||
if (name == 'console') {
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
} else if (name == 'error') {
|
||||
if (msg == null) msg = Error()
|
||||
if (is_proto(msg, Error))
|
||||
msg = msg.name + ": " + msg.message + "\n" + msg.stack
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
} else if (name == 'system') {
|
||||
msg = "[SYSTEM] " + msg
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
} else {
|
||||
log.console(`unknown log type: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
function disrupt(err)
|
||||
function actor_die(err)
|
||||
{
|
||||
if (is_function(err.toString)) {
|
||||
os.print(err.toString())
|
||||
os.print("\n")
|
||||
os.print(err.stack)
|
||||
if (err && is_function(err.toString)) {
|
||||
os.print(err.toString())
|
||||
os.print("\n")
|
||||
if (err.stack) os.print(err.stack)
|
||||
}
|
||||
|
||||
if (overling) {
|
||||
@@ -185,15 +205,20 @@ function disrupt(err)
|
||||
log.console(err.stack)
|
||||
}
|
||||
|
||||
actor_mod.disrupt()
|
||||
actor_mod["disrupt"]()
|
||||
}
|
||||
|
||||
|
||||
|
||||
actor_mod.on_exception(disrupt)
|
||||
print("engine: setting up actor_die")
|
||||
actor_mod.on_exception(actor_die)
|
||||
|
||||
_cell.args = init ?? {}
|
||||
print("engine: setting cell args")
|
||||
print(`engine: init is ${init}`)
|
||||
_cell.args = init != null ? init : {}
|
||||
print("engine: args set")
|
||||
_cell.id = "newguy"
|
||||
print("engine: id set")
|
||||
|
||||
function create_actor(desc = {id:guid()}) {
|
||||
var actor = {}
|
||||
@@ -201,19 +226,25 @@ function create_actor(desc = {id:guid()}) {
|
||||
return actor
|
||||
}
|
||||
|
||||
print("engine: creating $_")
|
||||
var $_ = {}
|
||||
print("engine: creating self actor")
|
||||
$_.self = create_actor()
|
||||
|
||||
print("engine: setting os props")
|
||||
os.use_cache = use_cache
|
||||
os.global_shop_path = shop_path
|
||||
os.$_ = $_
|
||||
print("engine: os props set")
|
||||
|
||||
print("engine: loading shop")
|
||||
var shop = use_core('internal/shop')
|
||||
|
||||
print("engine: loaded shop, loading json")
|
||||
var json = use_core('json')
|
||||
var time = use_core('time')
|
||||
|
||||
print("engine: loading pronto")
|
||||
var pronto = use_core('pronto')
|
||||
print("engine: loaded pronto")
|
||||
var fallback = pronto.fallback
|
||||
var parallel = pronto.parallel
|
||||
var race = pronto.race
|
||||
@@ -241,11 +272,15 @@ os.runtime_env = runtime_env
|
||||
|
||||
$_.time_limit = function(requestor, seconds)
|
||||
{
|
||||
if (!pronto.is_requestor(requestor))
|
||||
throw Error('time_limit: first argument must be a requestor');
|
||||
if (!is_number(seconds) || seconds <= 0)
|
||||
throw Error('time_limit: seconds must be a positive number');
|
||||
|
||||
if (!pronto.is_requestor(requestor)) {
|
||||
log.error('time_limit: first argument must be a requestor')
|
||||
disrupt
|
||||
}
|
||||
if (!is_number(seconds) || seconds <= 0) {
|
||||
log.error('time_limit: seconds must be a positive number')
|
||||
disrupt
|
||||
}
|
||||
|
||||
return function time_limit_requestor(callback, value) {
|
||||
pronto.check_callback(callback, 'time_limit')
|
||||
var finished = false
|
||||
@@ -260,7 +295,14 @@ $_.time_limit = function(requestor, seconds)
|
||||
timer_cancel = null
|
||||
}
|
||||
if (requestor_cancel) {
|
||||
try { pronto.requestor_cancel(reason) } catch (_) {}
|
||||
requestor_cancel(reason)
|
||||
requestor_cancel = null
|
||||
}
|
||||
}
|
||||
|
||||
function safe_cancel_requestor(reason) {
|
||||
if (requestor_cancel) {
|
||||
requestor_cancel(reason)
|
||||
requestor_cancel = null
|
||||
}
|
||||
}
|
||||
@@ -268,15 +310,12 @@ $_.time_limit = function(requestor, seconds)
|
||||
timer_cancel = $_.delay(function() {
|
||||
if (finished) return
|
||||
def reason = make_reason(factory, 'Timeout.', seconds)
|
||||
if (requestor_cancel) {
|
||||
try { requestor_cancel(reason) } catch (_) {}
|
||||
requestor_cancel = null
|
||||
}
|
||||
safe_cancel_requestor(reason)
|
||||
finished = true
|
||||
callback(null, reason)
|
||||
}, seconds)
|
||||
|
||||
try {
|
||||
function do_request() {
|
||||
requestor_cancel = requestor(function(val, reason) {
|
||||
if (finished) return
|
||||
finished = true
|
||||
@@ -286,16 +325,14 @@ $_.time_limit = function(requestor, seconds)
|
||||
}
|
||||
callback(val, reason)
|
||||
}, value)
|
||||
} catch (ex) {
|
||||
cancel(ex)
|
||||
callback(null, ex)
|
||||
} disruption {
|
||||
cancel(Error('requestor failed'))
|
||||
callback(null, Error('requestor failed'))
|
||||
}
|
||||
do_request()
|
||||
|
||||
return function(reason) {
|
||||
if (requestor_cancel) {
|
||||
try { requestor_cancel(reason) } catch (_) {}
|
||||
requestor_cancel = null
|
||||
}
|
||||
safe_cancel_requestor(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,52 +445,54 @@ 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 Error(`Already started a portal listening on ${portal.port}`)
|
||||
if (!port) throw Error("Requires a valid port.")
|
||||
if (portal) {
|
||||
log.error(`Already started a portal listening on ${portal.port}`)
|
||||
disrupt
|
||||
}
|
||||
if (!port) {
|
||||
log.error("Requires a valid port.")
|
||||
disrupt
|
||||
}
|
||||
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) {
|
||||
arrfor(queue, (msg, index) => e.peer.send(nota.encode(msg)))
|
||||
log.system(`sent ${msg} out of queue`)
|
||||
peer_queue.delete(e.peer)
|
||||
}
|
||||
break
|
||||
case "disconnect":
|
||||
if (e.type == "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) {
|
||||
arrfor(queue, (msg, index) => e.peer.send(nota.encode(msg)))
|
||||
log.system(`sent ${msg} out of queue`)
|
||||
peer_queue.delete(e.peer)
|
||||
arrfor(array(peers), function(id, index) {
|
||||
if (peers[id] == e.peer) delete peers[id]
|
||||
}
|
||||
} else if (e.type == "disconnect") {
|
||||
peer_queue.delete(e.peer)
|
||||
arrfor(array(peers), function(id, index) {
|
||||
if (peers[id] == e.peer) delete peers[id]
|
||||
})
|
||||
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
|
||||
} else if (e.type == "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 (!is_object(obj)) return
|
||||
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
|
||||
obj[ACTORDATA].address = e.peer.address
|
||||
obj[ACTORDATA].port = e.peer.port
|
||||
}
|
||||
arrfor(array(obj), function(key, index) {
|
||||
if (key in obj)
|
||||
populate_actor_addresses(obj[key])
|
||||
})
|
||||
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 (!is_object(obj)) return
|
||||
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
|
||||
obj[ACTORDATA].address = e.peer.address
|
||||
obj[ACTORDATA].port = e.peer.port
|
||||
}
|
||||
arrfor(array(obj), function(key, index) {
|
||||
if (key in obj)
|
||||
populate_actor_addresses(obj[key])
|
||||
})
|
||||
}
|
||||
if (data.data) populate_actor_addresses(data.data)
|
||||
turn(data)
|
||||
break
|
||||
}
|
||||
if (data.data) populate_actor_addresses(data.data)
|
||||
turn(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,10 +526,14 @@ $_.stop = function stop(actor) {
|
||||
need_stop = true
|
||||
return
|
||||
}
|
||||
if (!is_actor(actor))
|
||||
throw Error('Can only call stop on an actor.')
|
||||
if (is_null(underlings[actor[ACTORDATA].id]))
|
||||
throw Error('Can only call stop on an underling or self.')
|
||||
if (!is_actor(actor)) {
|
||||
log.error('Can only call stop on an actor.')
|
||||
disrupt
|
||||
}
|
||||
if (is_null(underlings[actor[ACTORDATA].id])) {
|
||||
log.error('Can only call stop on an underling or self.')
|
||||
disrupt
|
||||
}
|
||||
|
||||
sys_msg(actor, {kind:"stop"})
|
||||
}
|
||||
@@ -527,20 +570,22 @@ function actor_prep(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);
|
||||
}
|
||||
actor_send(actor, send)
|
||||
}
|
||||
|
||||
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 Error(`Must send to an actor object. Attempted send to ${actor}`)
|
||||
|
||||
if (!is_object(message)) throw Error('Must send an object record.')
|
||||
if (!is_actor(actor) && !is_actor(actor.replycc)) {
|
||||
log.error(`Must send to an actor object. Attempted send to ${actor}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
if (!is_object(message)) {
|
||||
log.error('Must send an object record.')
|
||||
disrupt
|
||||
}
|
||||
|
||||
// message to self
|
||||
if (actor[ACTORDATA].id == _cell.id) {
|
||||
@@ -583,12 +628,10 @@ function actor_send(actor, message) {
|
||||
// Holds all messages queued during the current turn.
|
||||
var message_queue = []
|
||||
|
||||
var need_stop = false
|
||||
|
||||
function send_messages() {
|
||||
function send_messages() {
|
||||
// if we've been flagged to stop, bail out before doing anything
|
||||
if (need_stop) {
|
||||
disrupt()
|
||||
actor_die()
|
||||
message_queue = []
|
||||
return
|
||||
}
|
||||
@@ -608,19 +651,26 @@ var need_stop = false
|
||||
var replies = {}
|
||||
|
||||
function send(actor, message, reply) {
|
||||
if (!is_object(actor))
|
||||
throw Error(`Must send to an actor object. Provided: ${actor}`);
|
||||
if (!is_object(actor)) {
|
||||
log.error(`Must send to an actor object. Provided: ${actor}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
if (!is_object(message))
|
||||
throw Error('Message must be an object')
|
||||
if (!is_object(message)) {
|
||||
log.error('Message must be an object')
|
||||
disrupt
|
||||
}
|
||||
var send_msg = {type:"user", data: message}
|
||||
var target = actor
|
||||
|
||||
if (actor[HEADER] && actor[HEADER].replycc) {
|
||||
var header = actor[HEADER]
|
||||
if (!header.replycc || !is_actor(header.replycc))
|
||||
throw Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
|
||||
if (!header.replycc || !is_actor(header.replycc)) {
|
||||
log.error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
actor = header.replycc
|
||||
target = header.replycc
|
||||
send_msg.return = header.reply
|
||||
}
|
||||
|
||||
@@ -638,7 +688,7 @@ function send(actor, message, reply) {
|
||||
}
|
||||
|
||||
// Instead of sending immediately, queue it
|
||||
actor_prep(actor, send_msg);
|
||||
actor_prep(target, send_msg);
|
||||
}
|
||||
|
||||
stone(send)
|
||||
@@ -669,7 +719,7 @@ overling = _cell.args.overling
|
||||
$_.overling = overling
|
||||
|
||||
root = _cell.args.root
|
||||
root ??= $_.self
|
||||
if (root == null) root = $_.self
|
||||
|
||||
if (overling) {
|
||||
$_.couple(overling) // auto couple to overling
|
||||
@@ -705,36 +755,35 @@ function handle_actor_disconnect(id) {
|
||||
delete greeters[id]
|
||||
}
|
||||
log.system(`actor ${id} disconnected`)
|
||||
if (!is_null(couplings[id])) disrupt("coupled actor died") // couplings now disrupts instead of stop
|
||||
if (!is_null(couplings[id])) actor_die("coupled actor died") // couplings now disrupts instead of stop
|
||||
}
|
||||
|
||||
function handle_sysym(msg)
|
||||
{
|
||||
var from
|
||||
switch(msg.kind) {
|
||||
case 'stop':
|
||||
disrupt("got stop message")
|
||||
break
|
||||
case 'underling':
|
||||
from = msg.from
|
||||
var greeter = greeters[from[ACTORDATA].id]
|
||||
if (greeter) greeter(msg.message)
|
||||
if (msg.message.type == 'disrupt')
|
||||
delete underlings[from[ACTORDATA].id]
|
||||
break
|
||||
case 'contact':
|
||||
if (portal_fn) {
|
||||
var letter2 = msg.data
|
||||
letter2[HEADER] = msg
|
||||
delete msg.data
|
||||
portal_fn(letter2)
|
||||
} else throw Error('Got a contact message, but no portal is established.')
|
||||
break
|
||||
case 'couple': // from must be notified when we die
|
||||
from = msg.from
|
||||
underlings[from[ACTORDATA].id] = true
|
||||
log.system(`actor ${from} is coupled to me`)
|
||||
break
|
||||
if (msg.kind == 'stop') {
|
||||
actor_die("got stop message")
|
||||
} else if (msg.kind == 'underling') {
|
||||
from = msg.from
|
||||
var greeter = greeters[from[ACTORDATA].id]
|
||||
if (greeter) greeter(msg.message)
|
||||
if (msg.message.type == 'disrupt')
|
||||
delete underlings[from[ACTORDATA].id]
|
||||
} else if (msg.kind == 'contact') {
|
||||
if (portal_fn) {
|
||||
var letter2 = msg.data
|
||||
letter2[HEADER] = msg
|
||||
delete msg.data
|
||||
portal_fn(letter2)
|
||||
} else {
|
||||
log.error('Got a contact message, but no portal is established.')
|
||||
disrupt
|
||||
}
|
||||
} else if (msg.kind == 'couple') {
|
||||
// from must be notified when we die
|
||||
from = msg.from
|
||||
underlings[from[ACTORDATA].id] = true
|
||||
log.system(`actor ${from} is coupled to me`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,30 +793,27 @@ function handle_message(msg) {
|
||||
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)
|
||||
if (msg.type == "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
|
||||
case "stopped":
|
||||
handle_actor_disconnect(msg.id)
|
||||
break
|
||||
}
|
||||
|
||||
if (receive_fn) receive_fn(letter)
|
||||
} else if (msg.type == "stopped") {
|
||||
handle_actor_disconnect(msg.id)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function enet_check()
|
||||
{
|
||||
@@ -792,8 +838,10 @@ if (!locator) {
|
||||
locator = shop.resolve_locator(_cell.args.program + ".ce", pkg)
|
||||
}
|
||||
|
||||
if (!locator)
|
||||
throw Error(`Main program ${_cell.args.program} could not be found`)
|
||||
if (!locator) {
|
||||
os.print(`Main program ${_cell.args.program} could not be found\n`)
|
||||
os.exit(1)
|
||||
}
|
||||
|
||||
$_.clock(_ => {
|
||||
// Get capabilities for the main program
|
||||
@@ -818,7 +866,6 @@ $_.clock(_ => {
|
||||
var val = call(locator.symbol, null, [_cell.args.arg, use_fn, env])
|
||||
|
||||
if (val)
|
||||
throw Error('Program must not return anything');
|
||||
log.error('Program must not return anything')
|
||||
disrupt
|
||||
})
|
||||
|
||||
})()
|
||||
@@ -75,7 +75,11 @@ static const JSCFunctionListEntry js_kim_funcs[] = {
|
||||
|
||||
JSValue js_kim_use(JSContext *js)
|
||||
{
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod, js_kim_funcs, countof(js_kim_funcs));
|
||||
return mod;
|
||||
JSGCRef mod_ref;
|
||||
JS_PushGCRef(js, &mod_ref);
|
||||
mod_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod_ref.val, js_kim_funcs, countof(js_kim_funcs));
|
||||
JSValue result = mod_ref.val;
|
||||
JS_PopGCRef(js, &mod_ref);
|
||||
return result;
|
||||
}
|
||||
@@ -577,9 +577,13 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
|
||||
JSValue js_os_use(JSContext *js) {
|
||||
JS_NewClassID(&js_dylib_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_dylib_class_id, &js_dylib_class);
|
||||
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_os_funcs,countof(js_os_funcs));
|
||||
return mod;
|
||||
JS_NewClass(js, js_dylib_class_id, &js_dylib_class);
|
||||
|
||||
JSGCRef mod_ref;
|
||||
JS_PushGCRef(js, &mod_ref);
|
||||
mod_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod_ref.val, js_os_funcs, countof(js_os_funcs));
|
||||
JSValue result = mod_ref.val;
|
||||
JS_PopGCRef(js, &mod_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,11 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
};
|
||||
|
||||
JSValue js_os_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod, js_os_funcs, countof(js_os_funcs));
|
||||
return mod;
|
||||
JSGCRef mod_ref;
|
||||
JS_PushGCRef(js, &mod_ref);
|
||||
mod_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod_ref.val, js_os_funcs, countof(js_os_funcs));
|
||||
JSValue result = mod_ref.val;
|
||||
JS_PopGCRef(js, &mod_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,11 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
};
|
||||
|
||||
JSValue js_os_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod, js_os_funcs, countof(js_os_funcs));
|
||||
return mod;
|
||||
JSGCRef mod_ref;
|
||||
JS_PushGCRef(js, &mod_ref);
|
||||
mod_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod_ref.val, js_os_funcs, countof(js_os_funcs));
|
||||
JSValue result = mod_ref.val;
|
||||
JS_PopGCRef(js, &mod_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
116
internal/shop.cm
116
internal/shop.cm
@@ -1,5 +1,6 @@
|
||||
print("shop: starting")
|
||||
var toml = use('toml')
|
||||
|
||||
print("shop: loaded toml")
|
||||
var json = use('json')
|
||||
var fd = use('fd')
|
||||
var http = use('http')
|
||||
@@ -142,8 +143,10 @@ function package_in_shop(package) {
|
||||
|
||||
function abs_path_to_package(package_dir)
|
||||
{
|
||||
if (!fd.is_file(package_dir + '/cell.toml'))
|
||||
throw Error('Not a valid package directory (no cell.toml): ' + package_dir)
|
||||
if (!fd.is_file(package_dir + '/cell.toml')) {
|
||||
log.error('Not a valid package directory (no cell.toml): ' + package_dir)
|
||||
disrupt
|
||||
}
|
||||
|
||||
var packages_prefix = get_packages_dir() + '/'
|
||||
var core_dir = packages_prefix + core_package
|
||||
@@ -175,14 +178,15 @@ function abs_path_to_package(package_dir)
|
||||
return package_dir
|
||||
|
||||
// For local directories (e.g., linked targets), read the package name from cell.toml
|
||||
try {
|
||||
var toml_pkg = null
|
||||
var read_toml = function() {
|
||||
var content = text(fd.slurp(package_dir + '/cell.toml'))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.package)
|
||||
return cfg.package
|
||||
} catch (e) {
|
||||
// Fall through
|
||||
}
|
||||
toml_pkg = cfg.package
|
||||
} disruption {}
|
||||
read_toml()
|
||||
if (toml_pkg) return toml_pkg
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -299,12 +303,14 @@ Shop.resolve_package_info = function(pkg) {
|
||||
|
||||
// Verify if a package name is valid and return status
|
||||
Shop.verify_package_name = function(pkg) {
|
||||
if (!pkg) throw Error("Empty package name")
|
||||
if (pkg == 'local') throw Error("local is not a valid package name")
|
||||
if (pkg == 'core') throw Error("core is not a valid package name")
|
||||
|
||||
if (search(pkg, '://') != null)
|
||||
throw Error(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
|
||||
if (!pkg) { log.error("Empty package name"); disrupt }
|
||||
if (pkg == 'local') { log.error("local is not a valid package name"); disrupt }
|
||||
if (pkg == 'core') { log.error("core is not a valid package name"); disrupt }
|
||||
|
||||
if (search(pkg, '://') != null) {
|
||||
log.error(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
|
||||
disrupt
|
||||
}
|
||||
}
|
||||
|
||||
// Convert module package to download URL
|
||||
@@ -441,7 +447,7 @@ ${script}
|
||||
// Resolve module function, hashing it in the process
|
||||
// path is the exact path to the script file
|
||||
function resolve_mod_fn(path, pkg) {
|
||||
if (!fd.is_file(path)) throw Error(`path ${path} is not a file`)
|
||||
if (!fd.is_file(path)) { log.error(`path ${path} is not a file`); disrupt }
|
||||
|
||||
var file_info = Shop.file_info(path)
|
||||
var file_pkg = file_info.package
|
||||
@@ -576,32 +582,38 @@ Shop.open_package_dylib = function(pkg) {
|
||||
|
||||
var toml_path = pkg_dir + '/cell.toml'
|
||||
if (fd.is_file(toml_path)) {
|
||||
try {
|
||||
var read_toml_disrupted = false
|
||||
var do_read_toml = function() {
|
||||
var content = text(fd.slurp(toml_path))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.dependencies) {
|
||||
arrfor(array(cfg.dependencies), function(alias, i) {
|
||||
var dep_pkg = cfg.dependencies[alias]
|
||||
try {
|
||||
var open_dep = function() {
|
||||
Shop.open_package_dylib(dep_pkg)
|
||||
} catch (dep_e) {
|
||||
// Dependency dylib load failed, continue with others
|
||||
}
|
||||
} disruption {}
|
||||
open_dep()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Error reading toml, continue
|
||||
} disruption {
|
||||
read_toml_disrupted = true
|
||||
}
|
||||
do_read_toml()
|
||||
}
|
||||
|
||||
var dl_path = get_lib_path(pkg)
|
||||
if (fd.is_file(dl_path)) {
|
||||
if (!open_dls[dl_path]) {
|
||||
try {
|
||||
var open_disrupted = false
|
||||
var do_open = function() {
|
||||
open_dls[dl_path] = os.dylib_open(dl_path)
|
||||
} catch (e) {
|
||||
} disruption {
|
||||
open_disrupted = true
|
||||
}
|
||||
do_open()
|
||||
if (open_disrupted) {
|
||||
dylib_visited[pkg] = false
|
||||
throw e
|
||||
disrupt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,14 +848,14 @@ function execute_module(info)
|
||||
// C only
|
||||
used = call_c_module(c_resolve)
|
||||
} else {
|
||||
throw Error(`Module ${info.path} could not be found`)
|
||||
log.error(`Module ${info.path} could not be found`); disrupt
|
||||
}
|
||||
|
||||
// if (is_function(used))
|
||||
// throw Error('C module loader returned a function; did you forget to call it?')
|
||||
|
||||
if (!used)
|
||||
throw Error(`Module ${info} returned null`)
|
||||
log.error(`Module ${info} returned null`); disrupt
|
||||
|
||||
// stone(used)
|
||||
return used
|
||||
@@ -852,16 +864,20 @@ function execute_module(info)
|
||||
function get_module(path, package_context) {
|
||||
var info = resolve_module_info(path, package_context)
|
||||
|
||||
if (!info)
|
||||
throw Error(`Module ${path} could not be found in ${package_context}`)
|
||||
if (!info) {
|
||||
log.error(`Module ${path} could not be found in ${package_context}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
return execute_module(info)
|
||||
}
|
||||
|
||||
Shop.use = function use(path, package_context) {
|
||||
var info = resolve_module_info(path, package_context)
|
||||
if (!info)
|
||||
throw Error(`Module ${path} could not be found in ${package_context}`)
|
||||
if (!info) {
|
||||
log.error(`Module ${path} could not be found in ${package_context}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
if (use_cache[info.cache_key])
|
||||
return use_cache[info.cache_key]
|
||||
@@ -889,13 +905,20 @@ function fetch_remote_hash(pkg) {
|
||||
if (!api_url) return null
|
||||
|
||||
|
||||
try {
|
||||
var result = null
|
||||
var fetch_disrupted = false
|
||||
var do_fetch = function() {
|
||||
var resp = http.fetch(api_url)
|
||||
return Shop.extract_commit_hash(pkg, text(resp))
|
||||
} catch (e) {
|
||||
result = Shop.extract_commit_hash(pkg, text(resp))
|
||||
} disruption {
|
||||
fetch_disrupted = true
|
||||
}
|
||||
do_fetch()
|
||||
if (fetch_disrupted) {
|
||||
log.console("Warning: Could not check for updates for " + pkg)
|
||||
return null
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Download a zip for a package at a specific commit and cache it
|
||||
@@ -909,14 +932,20 @@ function download_zip(pkg, commit_hash) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
var zip_blob = http.fetch(download_url)
|
||||
var zip_blob = null
|
||||
var dl_disrupted = false
|
||||
var do_download = function() {
|
||||
zip_blob = http.fetch(download_url)
|
||||
fd.slurpwrite(cache_path, zip_blob)
|
||||
return zip_blob
|
||||
} catch (e) {
|
||||
log.error("Download failed for " + pkg + ": " + e)
|
||||
} disruption {
|
||||
dl_disrupted = true
|
||||
}
|
||||
do_download()
|
||||
if (dl_disrupted) {
|
||||
log.error("Download failed for " + pkg)
|
||||
return null
|
||||
}
|
||||
return zip_blob
|
||||
}
|
||||
|
||||
// Get zip from cache, returns null if not cached
|
||||
@@ -1027,8 +1056,7 @@ Shop.extract = function(pkg) {
|
||||
|
||||
var zip_blob = get_package_zip(pkg)
|
||||
|
||||
if (!zip_blob)
|
||||
throw Error("No zip blob available for " + pkg)
|
||||
if (!zip_blob) { log.error("No zip blob available for " + pkg); disrupt }
|
||||
|
||||
// Extract zip for remote package
|
||||
install_zip(zip_blob, target_dir)
|
||||
@@ -1113,7 +1141,7 @@ Shop.update = function(pkg) {
|
||||
|
||||
function install_zip(zip_blob, target_dir) {
|
||||
var zip = miniz.read(zip_blob)
|
||||
if (!zip) throw Error("Failed to read zip archive")
|
||||
if (!zip) { log.error("Failed to read zip archive"); disrupt }
|
||||
|
||||
if (fd.is_link(target_dir)) fd.unlink(target_dir)
|
||||
if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1)
|
||||
@@ -1165,14 +1193,14 @@ Shop.get = function(pkg) {
|
||||
if (!lock[pkg]) {
|
||||
var info = Shop.resolve_package_info(pkg)
|
||||
if (!info) {
|
||||
throw Error("Invalid package: " + pkg)
|
||||
log.error("Invalid package: " + pkg); disrupt
|
||||
}
|
||||
|
||||
var commit = null
|
||||
if (info != 'local') {
|
||||
commit = fetch_remote_hash(pkg)
|
||||
if (!commit) {
|
||||
throw Error("Could not resolve commit for " + pkg)
|
||||
log.error("Could not resolve commit for " + pkg); disrupt
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ function is_valid_package(dir) {
|
||||
// Get current package name from cell.toml or null
|
||||
function get_current_package_name() {
|
||||
if (!is_valid_package('.')) return null
|
||||
try {
|
||||
var pkg_name = 'local'
|
||||
var do_load = function() {
|
||||
var config = pkg.load_config(null)
|
||||
return config.package || 'local'
|
||||
} catch (e) {
|
||||
return 'local'
|
||||
}
|
||||
if (config.package) pkg_name = config.package
|
||||
} disruption {}
|
||||
do_load()
|
||||
return pkg_name
|
||||
}
|
||||
|
||||
// Get the directory for a package
|
||||
|
||||
81
link.cm
81
link.cm
@@ -65,13 +65,18 @@ Link.load = function() {
|
||||
link_cache = {}
|
||||
return link_cache
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
var load_disrupted = false
|
||||
var do_load = function() {
|
||||
var content = text(fd.slurp(path))
|
||||
var cfg = toml.decode(content)
|
||||
link_cache = cfg.links || {}
|
||||
} catch (e) {
|
||||
log.console("Warning: Failed to load link.toml: " + e)
|
||||
} disruption {
|
||||
load_disrupted = true
|
||||
}
|
||||
do_load()
|
||||
if (load_disrupted) {
|
||||
log.console("Warning: Failed to load link.toml")
|
||||
link_cache = {}
|
||||
}
|
||||
return link_cache
|
||||
@@ -90,14 +95,16 @@ Link.add = function(canonical, target, shop) {
|
||||
// Validate canonical package exists in shop
|
||||
var lock = shop.load_lock()
|
||||
if (!lock[canonical]) {
|
||||
throw Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
|
||||
log.error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Validate target is a valid package
|
||||
if (starts_with(target, '/')) {
|
||||
// Local path - must have cell.toml
|
||||
if (!fd.is_file(target + '/cell.toml')) {
|
||||
throw Error('Target ' + target + ' is not a valid package (no cell.toml)')
|
||||
log.error('Target ' + target + ' is not a valid package (no cell.toml)')
|
||||
disrupt
|
||||
}
|
||||
} else {
|
||||
// Remote package target - ensure it's installed
|
||||
@@ -116,7 +123,8 @@ Link.add = function(canonical, target, shop) {
|
||||
var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target)
|
||||
var toml_path = target_path + '/cell.toml'
|
||||
if (fd.is_file(toml_path)) {
|
||||
try {
|
||||
var read_deps_disrupted = false
|
||||
var do_read_deps = function() {
|
||||
var content = text(fd.slurp(toml_path))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.dependencies) {
|
||||
@@ -128,16 +136,24 @@ Link.add = function(canonical, target, shop) {
|
||||
return
|
||||
}
|
||||
// Install the dependency if not already in shop
|
||||
try {
|
||||
var install_disrupted = false
|
||||
var do_install = function() {
|
||||
shop.get(dep_locator)
|
||||
shop.extract(dep_locator)
|
||||
} catch (e) {
|
||||
log.console(` Warning: Could not install dependency ${dep_locator}: ${e.message}`)
|
||||
log.error(e)
|
||||
} disruption {
|
||||
install_disrupted = true
|
||||
}
|
||||
do_install()
|
||||
if (install_disrupted) {
|
||||
log.console(` Warning: Could not install dependency ${dep_locator}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
} disruption {
|
||||
read_deps_disrupted = true
|
||||
}
|
||||
do_read_deps()
|
||||
if (read_deps_disrupted) {
|
||||
log.console(` Warning: Could not read dependencies from ${toml_path}`)
|
||||
}
|
||||
}
|
||||
@@ -149,14 +165,14 @@ Link.add = function(canonical, target, shop) {
|
||||
Link.remove = function(canonical) {
|
||||
var links = Link.load()
|
||||
if (!links[canonical]) return false
|
||||
|
||||
|
||||
// Remove the symlink if it exists
|
||||
var target_dir = get_package_abs_dir(canonical)
|
||||
if (fd.is_link(target_dir)) {
|
||||
fd.unlink(target_dir)
|
||||
log.console("Removed symlink at " + target_dir)
|
||||
}
|
||||
|
||||
|
||||
delete links[canonical]
|
||||
Link.save(links)
|
||||
log.console("Unlinked " + canonical)
|
||||
@@ -172,7 +188,7 @@ Link.clear = function() {
|
||||
fd.unlink(target_dir)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Link.save({})
|
||||
log.console("Cleared all links")
|
||||
return true
|
||||
@@ -186,25 +202,25 @@ Link.sync_one = function(canonical, target, shop) {
|
||||
// Ensure parent directories exist
|
||||
var parent = fd.dirname(target_dir)
|
||||
ensure_dir(parent)
|
||||
|
||||
|
||||
// Check current state
|
||||
var current_link = null
|
||||
if (fd.is_link(target_dir)) {
|
||||
current_link = fd.readlink(target_dir)
|
||||
}
|
||||
|
||||
|
||||
// If already correctly linked, nothing to do
|
||||
if (current_link == link_target) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// Remove existing file/dir/link
|
||||
if (fd.is_link(target_dir)) {
|
||||
fd.unlink(target_dir)
|
||||
} else if (fd.is_dir(target_dir)) {
|
||||
fd.rmdir(target_dir, 1)
|
||||
}
|
||||
|
||||
|
||||
// Create symlink
|
||||
fd.symlink(link_target, target_dir)
|
||||
return true
|
||||
@@ -218,7 +234,9 @@ Link.sync_all = function(shop) {
|
||||
|
||||
arrfor(array(links), function(canonical) {
|
||||
var target = links[canonical]
|
||||
try {
|
||||
var sync_disrupted = false
|
||||
var sync_error_msg = ""
|
||||
var do_sync = function() {
|
||||
// Validate target exists
|
||||
var link_target = resolve_link_target(target)
|
||||
if (!fd.is_dir(link_target)) {
|
||||
@@ -234,7 +252,8 @@ Link.sync_all = function(shop) {
|
||||
|
||||
// Install dependencies of the linked package
|
||||
var toml_path = link_target + '/cell.toml'
|
||||
try {
|
||||
var read_deps_disrupted = false
|
||||
var do_read_deps = function() {
|
||||
var content = text(fd.slurp(toml_path))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.dependencies) {
|
||||
@@ -245,21 +264,25 @@ Link.sync_all = function(shop) {
|
||||
return
|
||||
}
|
||||
// Install the dependency if not already in shop
|
||||
try {
|
||||
var install_dep = function() {
|
||||
shop.get(dep_locator)
|
||||
shop.extract(dep_locator)
|
||||
} catch (e) {
|
||||
// Silently continue - dependency may already be installed
|
||||
}
|
||||
} disruption {}
|
||||
install_dep()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Could not read dependencies - continue anyway
|
||||
} disruption {
|
||||
read_deps_disrupted = true
|
||||
}
|
||||
do_read_deps()
|
||||
|
||||
count++
|
||||
} catch (e) {
|
||||
push(errors, canonical + ': ' + e.message)
|
||||
} disruption {
|
||||
sync_disrupted = true
|
||||
}
|
||||
do_sync()
|
||||
if (sync_disrupted) {
|
||||
push(errors, canonical + ': sync failed')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -571,13 +571,13 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
|
||||
JSValue js_enet_use(JSContext *ctx)
|
||||
{
|
||||
JS_NewClassID(&enet_host_id);
|
||||
JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host);
|
||||
JS_NewClass(ctx, enet_host_id, &enet_host);
|
||||
JSValue host_proto = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
|
||||
JS_SetClassProto(ctx, enet_host_id, host_proto);
|
||||
|
||||
JS_NewClassID(&enet_peer_class_id);
|
||||
JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class);
|
||||
JS_NewClass(ctx, enet_peer_class_id, &enet_peer_class);
|
||||
JSValue peer_proto = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
|
||||
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto);
|
||||
|
||||
16
package.cm
16
package.cm
@@ -51,7 +51,8 @@ package.load_config = function(name)
|
||||
return config_cache[config_path]
|
||||
|
||||
if (!fd.is_file(config_path)) {
|
||||
throw Error(`${config_path} does not exist`)
|
||||
log.error(`${config_path} does not exist`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
var content = text(fd.slurp(config_path))
|
||||
@@ -158,19 +159,20 @@ package.split_alias = function(name, path)
|
||||
var parts = array(path, '/')
|
||||
var first_part = parts[0]
|
||||
|
||||
try {
|
||||
var split_result = null
|
||||
var do_split = function() {
|
||||
var config = package.load_config(name)
|
||||
if (!config) return null
|
||||
if (!config) return
|
||||
|
||||
var deps = config.dependencies
|
||||
if (deps && deps[first_part]) {
|
||||
var dep_locator = deps[first_part]
|
||||
var remaining_path = text(array(parts, 1), '/')
|
||||
return { package: dep_locator, path: remaining_path }
|
||||
split_result = { package: dep_locator, path: remaining_path }
|
||||
}
|
||||
} catch (e) {
|
||||
// Config doesn't exist or couldn't be loaded
|
||||
}
|
||||
} disruption {}
|
||||
do_split()
|
||||
if (split_result) return split_result
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
95
pronto.cm
95
pronto.cm
@@ -3,6 +3,8 @@
|
||||
// Based on Douglas Crockford's parseq, adapted for Cell.
|
||||
// Time is in seconds.
|
||||
|
||||
function safe_call(fn, arg) { fn(arg) } disruption {}
|
||||
|
||||
function make_reason(factory, excuse, evidence) {
|
||||
def reason = Error(`pronto.${factory}${excuse ? ': ' + excuse : ''}`)
|
||||
reason.evidence = evidence
|
||||
@@ -15,12 +17,12 @@ function is_requestor(fn) {
|
||||
|
||||
function check_requestors(list, factory) {
|
||||
if (!is_array(list) || some(list, r => !is_requestor(r)))
|
||||
throw make_reason(factory, 'Bad requestor array.', list)
|
||||
disrupt
|
||||
}
|
||||
|
||||
function check_callback(cb, factory) {
|
||||
if (!is_function(cb) || length(cb) != 2)
|
||||
throw make_reason(factory, 'Not a callback.', cb)
|
||||
disrupt
|
||||
}
|
||||
|
||||
// fallback(requestor_array)
|
||||
@@ -28,7 +30,7 @@ function check_callback(cb, factory) {
|
||||
function fallback(requestor_array) {
|
||||
def factory = 'fallback'
|
||||
if (!is_array(requestor_array) || length(requestor_array) == 0)
|
||||
throw make_reason(factory, 'Empty requestor array.')
|
||||
disrupt
|
||||
check_requestors(requestor_array, factory)
|
||||
|
||||
return function fallback_requestor(callback, value) {
|
||||
@@ -40,7 +42,7 @@ function fallback(requestor_array) {
|
||||
function cancel(reason) {
|
||||
cancelled = true
|
||||
if (current_cancel) {
|
||||
try { current_cancel(reason) } catch (_) {}
|
||||
safe_call(current_cancel, reason)
|
||||
current_cancel = null
|
||||
}
|
||||
}
|
||||
@@ -55,7 +57,8 @@ function fallback(requestor_array) {
|
||||
def requestor = requestor_array[index]
|
||||
index += 1
|
||||
|
||||
try {
|
||||
var requestor_disrupted = false
|
||||
var start_requestor = function() {
|
||||
current_cancel = requestor(function(val, reason) {
|
||||
if (cancelled) return
|
||||
current_cancel = null
|
||||
@@ -65,7 +68,11 @@ function fallback(requestor_array) {
|
||||
try_next()
|
||||
}
|
||||
}, value)
|
||||
} catch (ex) {
|
||||
} disruption {
|
||||
requestor_disrupted = true
|
||||
}
|
||||
start_requestor()
|
||||
if (requestor_disrupted) {
|
||||
try_next()
|
||||
}
|
||||
}
|
||||
@@ -80,7 +87,7 @@ function fallback(requestor_array) {
|
||||
function parallel(requestor_array, throttle, need) {
|
||||
def factory = 'parallel'
|
||||
if (!is_array(requestor_array))
|
||||
throw make_reason(factory, 'Not an array.', requestor_array)
|
||||
disrupt
|
||||
check_requestors(requestor_array, factory)
|
||||
|
||||
def length = length(requestor_array)
|
||||
@@ -89,10 +96,10 @@ function parallel(requestor_array, throttle, need) {
|
||||
|
||||
if (need == null) need = length
|
||||
if (!is_number(need) || need < 0 || need > length)
|
||||
throw make_reason(factory, 'Bad need.', need)
|
||||
disrupt
|
||||
|
||||
if (throttle != null && (!is_number(throttle) || throttle < 1))
|
||||
throw make_reason(factory, 'Bad throttle.', throttle)
|
||||
disrupt
|
||||
|
||||
return function parallel_requestor(callback, value) {
|
||||
check_callback(callback, factory)
|
||||
@@ -107,7 +114,8 @@ function parallel(requestor_array, throttle, need) {
|
||||
if (finished) return
|
||||
finished = true
|
||||
arrfor(cancel_list, c => {
|
||||
try { if (is_function(c)) c(reason) } catch (_) {}
|
||||
var do_cancel = function() { if (is_function(c)) c(reason) } disruption {}
|
||||
do_cancel()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,7 +125,9 @@ function parallel(requestor_array, throttle, need) {
|
||||
next_index += 1
|
||||
def requestor = requestor_array[idx]
|
||||
|
||||
try {
|
||||
var requestor_disrupted = false
|
||||
var requestor_ex = null
|
||||
var run_requestor = function() {
|
||||
cancel_list[idx] = requestor(function(val, reason) {
|
||||
if (finished) return
|
||||
cancel_list[idx] = null
|
||||
@@ -142,11 +152,15 @@ function parallel(requestor_array, throttle, need) {
|
||||
|
||||
start_one()
|
||||
}, value)
|
||||
} catch (ex) {
|
||||
} disruption {
|
||||
requestor_disrupted = true
|
||||
}
|
||||
run_requestor()
|
||||
if (requestor_disrupted) {
|
||||
failures += 1
|
||||
if (failures > length - need) {
|
||||
cancel(ex)
|
||||
callback(null, ex)
|
||||
cancel(requestor_ex)
|
||||
callback(null, requestor_ex)
|
||||
return
|
||||
}
|
||||
start_one()
|
||||
@@ -166,16 +180,16 @@ function parallel(requestor_array, throttle, need) {
|
||||
function race(requestor_array, throttle, need) {
|
||||
def factory = 'race'
|
||||
if (!is_array(requestor_array) || length(requestor_array) == 0)
|
||||
throw make_reason(factory, 'Empty requestor array.')
|
||||
disrupt
|
||||
check_requestors(requestor_array, factory)
|
||||
|
||||
def length = length(requestor_array)
|
||||
if (need == null) need = 1
|
||||
if (!is_number(need) || need < 1 || need > length)
|
||||
throw make_reason(factory, 'Bad need.', need)
|
||||
disrupt
|
||||
|
||||
if (throttle != null && (!is_number(throttle) || throttle < 1))
|
||||
throw make_reason(factory, 'Bad throttle.', throttle)
|
||||
disrupt
|
||||
|
||||
return function race_requestor(callback, value) {
|
||||
check_callback(callback, factory)
|
||||
@@ -190,7 +204,8 @@ function race(requestor_array, throttle, need) {
|
||||
if (finished) return
|
||||
finished = true
|
||||
arrfor(cancel_list, c => {
|
||||
try { if (is_function(c)) c(reason) } catch (_) {}
|
||||
var do_cancel = function() { if (is_function(c)) c(reason) } disruption {}
|
||||
do_cancel()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,7 +215,9 @@ function race(requestor_array, throttle, need) {
|
||||
next_index += 1
|
||||
def requestor = requestor_array[idx]
|
||||
|
||||
try {
|
||||
var requestor_disrupted = false
|
||||
var requestor_ex = null
|
||||
var run_requestor = function() {
|
||||
cancel_list[idx] = requestor(function(val, reason) {
|
||||
if (finished) return
|
||||
cancel_list[idx] = null
|
||||
@@ -228,11 +245,15 @@ function race(requestor_array, throttle, need) {
|
||||
|
||||
start_one()
|
||||
}, value)
|
||||
} catch (ex) {
|
||||
} disruption {
|
||||
requestor_disrupted = true
|
||||
}
|
||||
run_requestor()
|
||||
if (requestor_disrupted) {
|
||||
failures += 1
|
||||
if (failures > length - need) {
|
||||
cancel(ex)
|
||||
callback(null, ex)
|
||||
cancel(requestor_ex)
|
||||
callback(null, requestor_ex)
|
||||
return
|
||||
}
|
||||
start_one()
|
||||
@@ -251,7 +272,7 @@ function race(requestor_array, throttle, need) {
|
||||
function sequence(requestor_array) {
|
||||
def factory = 'sequence'
|
||||
if (!is_array(requestor_array))
|
||||
throw make_reason(factory, 'Not an array.', requestor_array)
|
||||
disrupt
|
||||
check_requestors(requestor_array, factory)
|
||||
|
||||
if (length(requestor_array) == 0)
|
||||
@@ -266,7 +287,7 @@ function sequence(requestor_array) {
|
||||
function cancel(reason) {
|
||||
cancelled = true
|
||||
if (current_cancel) {
|
||||
try { current_cancel(reason) } catch (_) {}
|
||||
safe_call(current_cancel, reason)
|
||||
current_cancel = null
|
||||
}
|
||||
}
|
||||
@@ -281,7 +302,9 @@ function sequence(requestor_array) {
|
||||
def requestor = requestor_array[index]
|
||||
index += 1
|
||||
|
||||
try {
|
||||
var requestor_disrupted = false
|
||||
var requestor_ex = null
|
||||
var run_requestor = function() {
|
||||
current_cancel = requestor(function(result, reason) {
|
||||
if (cancelled) return
|
||||
current_cancel = null
|
||||
@@ -291,8 +314,12 @@ function sequence(requestor_array) {
|
||||
run_next(result)
|
||||
}
|
||||
}, val)
|
||||
} catch (ex) {
|
||||
callback(null, ex)
|
||||
} disruption {
|
||||
requestor_disrupted = true
|
||||
}
|
||||
run_requestor()
|
||||
if (requestor_disrupted) {
|
||||
callback(null, requestor_ex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,15 +333,21 @@ function sequence(requestor_array) {
|
||||
function requestorize(unary) {
|
||||
def factory = 'requestorize'
|
||||
if (!is_function(unary))
|
||||
throw make_reason(factory, 'Not a function.', unary)
|
||||
disrupt
|
||||
|
||||
return function requestorized(callback, value) {
|
||||
check_callback(callback, factory)
|
||||
try {
|
||||
var call_disrupted = false
|
||||
var call_ex = null
|
||||
var do_call = function() {
|
||||
def result = unary(value)
|
||||
callback(result == null ? true : result)
|
||||
} catch (ex) {
|
||||
callback(null, ex)
|
||||
} disruption {
|
||||
call_disrupted = true
|
||||
}
|
||||
do_call()
|
||||
if (call_disrupted) {
|
||||
callback(null, call_ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
qop.c
4
qop.c
@@ -458,13 +458,13 @@ static const JSCFunctionListEntry js_qop_funcs[] = {
|
||||
|
||||
JSValue js_qop_use(JSContext *js) {
|
||||
JS_NewClassID(&js_qop_archive_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_qop_archive_class_id, &js_qop_archive_class);
|
||||
JS_NewClass(js, js_qop_archive_class_id, &js_qop_archive_class);
|
||||
JSValue archive_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, archive_proto, js_qop_archive_funcs, countof(js_qop_archive_funcs));
|
||||
JS_SetClassProto(js, js_qop_archive_class_id, archive_proto);
|
||||
|
||||
JS_NewClassID(&js_qop_writer_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_qop_writer_class_id, &js_qop_writer_class);
|
||||
JS_NewClass(js, js_qop_writer_class_id, &js_qop_writer_class);
|
||||
JSValue writer_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, writer_proto, js_qop_writer_funcs, countof(js_qop_writer_funcs));
|
||||
JS_SetClassProto(js, js_qop_writer_class_id, writer_proto);
|
||||
|
||||
210
source/cell.c
210
source/cell.c
@@ -26,6 +26,7 @@ static int run_test_suite(size_t heap_size);
|
||||
|
||||
cell_rt *root_cell = NULL;
|
||||
static char *core_path = NULL;
|
||||
static JSRuntime *g_runtime = NULL;
|
||||
|
||||
// Get the home directory
|
||||
static const char* get_home_dir(void) {
|
||||
@@ -105,35 +106,46 @@ static char* load_core_file(const char *filename, size_t *out_size) {
|
||||
return data;
|
||||
}
|
||||
|
||||
static int print_json_errors(const char *json) {
|
||||
if (!json) return 0;
|
||||
cJSON *root = cJSON_Parse(json);
|
||||
static int print_tree_errors(cJSON *root) {
|
||||
if (!root) return 0;
|
||||
cJSON *errors = cJSON_GetObjectItemCaseSensitive(root, "errors");
|
||||
if (!cJSON_IsArray(errors) || cJSON_GetArraySize(errors) == 0) {
|
||||
cJSON_Delete(root);
|
||||
if (!cJSON_IsArray(errors) || cJSON_GetArraySize(errors) == 0)
|
||||
return 0;
|
||||
}
|
||||
const char *filename = "<unknown>";
|
||||
cJSON *fname = cJSON_GetObjectItemCaseSensitive(root, "filename");
|
||||
if (cJSON_IsString(fname))
|
||||
filename = fname->valuestring;
|
||||
int prev_line = -1;
|
||||
const char *prev_msg = NULL;
|
||||
cJSON *e;
|
||||
cJSON_ArrayForEach(e, errors) {
|
||||
const char *msg = cJSON_GetStringValue(
|
||||
cJSON_GetObjectItemCaseSensitive(e, "message"));
|
||||
cJSON *line = cJSON_GetObjectItemCaseSensitive(e, "line");
|
||||
cJSON *col = cJSON_GetObjectItemCaseSensitive(e, "column");
|
||||
int cur_line = cJSON_IsNumber(line) ? (int)line->valuedouble : -1;
|
||||
if (prev_msg && msg && cur_line == prev_line && strcmp(msg, prev_msg) == 0)
|
||||
continue;
|
||||
prev_line = cur_line;
|
||||
prev_msg = msg;
|
||||
if (msg && cJSON_IsNumber(line) && cJSON_IsNumber(col))
|
||||
fprintf(stderr, "%s:%d:%d: error: %s\n",
|
||||
filename, (int)line->valuedouble, (int)col->valuedouble, msg);
|
||||
else if (msg)
|
||||
fprintf(stderr, "%s: error: %s\n", filename, msg);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int print_json_errors(const char *json) {
|
||||
if (!json) return 0;
|
||||
cJSON *root = cJSON_Parse(json);
|
||||
if (!root) return 0;
|
||||
int result = print_tree_errors(root);
|
||||
cJSON_Delete(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get the core path for use by scripts
|
||||
const char* cell_get_core_path(void) {
|
||||
return core_path;
|
||||
@@ -154,17 +166,31 @@ JSValue js_wota_use(JSContext *js);
|
||||
|
||||
void script_startup(cell_rt *prt)
|
||||
{
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
JS_SetInterruptHandler(rt, (JSInterruptHandler *)actor_interrupt_cb, prt);
|
||||
JSContext *js = JS_NewContext(rt);
|
||||
if (!g_runtime) {
|
||||
g_runtime = JS_NewRuntime();
|
||||
}
|
||||
JSContext *js = JS_NewContext(g_runtime);
|
||||
JS_SetInterruptHandler(js, (JSInterruptHandler *)actor_interrupt_cb, prt);
|
||||
|
||||
JS_SetContextOpaque(js, prt);
|
||||
prt->context = js;
|
||||
|
||||
/* Register all GCRef fields so the Cheney GC can relocate them. */
|
||||
JS_AddGCRef(js, &prt->idx_buffer_ref);
|
||||
JS_AddGCRef(js, &prt->on_exception_ref);
|
||||
JS_AddGCRef(js, &prt->message_handle_ref);
|
||||
JS_AddGCRef(js, &prt->unneeded_ref);
|
||||
JS_AddGCRef(js, &prt->actor_sym_ref);
|
||||
prt->idx_buffer_ref.val = JS_NULL;
|
||||
prt->on_exception_ref.val = JS_NULL;
|
||||
prt->message_handle_ref.val = JS_NULL;
|
||||
prt->unneeded_ref.val = JS_NULL;
|
||||
prt->actor_sym_ref.val = JS_NULL;
|
||||
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
JS_FreeValue(js, js_blob_use(js));
|
||||
|
||||
// Load and compile engine.cm
|
||||
// Load and parse engine.cm to AST
|
||||
size_t engine_size;
|
||||
char *data = load_core_file(ENGINE, &engine_size);
|
||||
if (!data) {
|
||||
@@ -172,42 +198,71 @@ void script_startup(cell_rt *prt)
|
||||
return;
|
||||
}
|
||||
|
||||
JSValue bytecode = JS_Compile(js, data, engine_size, ENGINE);
|
||||
cJSON *ast = JS_ASTTree(data, engine_size, ENGINE);
|
||||
free(data);
|
||||
if (JS_IsException(bytecode)) {
|
||||
uncaught_exception(js, bytecode);
|
||||
if (!ast) {
|
||||
printf("ERROR: Failed to parse %s\n", ENGINE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create hidden environment
|
||||
JSValue hidden_env = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, hidden_env, "os", js_os_use(js));
|
||||
JS_SetPropertyStr(js, hidden_env, "json", js_json_use(js));
|
||||
JS_SetPropertyStr(js, hidden_env, "nota", js_nota_use(js));
|
||||
JS_SetPropertyStr(js, hidden_env, "wota", js_wota_use(js));
|
||||
if (print_tree_errors(ast)) {
|
||||
cJSON_Delete(ast);
|
||||
return;
|
||||
}
|
||||
|
||||
crt->actor_sym = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, hidden_env, "actorsym", JS_DupValue(js, crt->actor_sym));
|
||||
// Create hidden environment (GC-protected to survive allocations)
|
||||
JSGCRef env_ref;
|
||||
JS_PushGCRef(js, &env_ref);
|
||||
env_ref.val = JS_NewObject(js);
|
||||
|
||||
// Create modules with GC rooting
|
||||
JSGCRef os_ref, json_ref, nota_ref, wota_ref;
|
||||
JS_PushGCRef(js, &os_ref);
|
||||
JS_PushGCRef(js, &json_ref);
|
||||
JS_PushGCRef(js, ¬a_ref);
|
||||
JS_PushGCRef(js, &wota_ref);
|
||||
|
||||
os_ref.val = js_os_use(js);
|
||||
json_ref.val = js_json_use(js);
|
||||
nota_ref.val = js_nota_use(js);
|
||||
wota_ref.val = js_wota_use(js);
|
||||
|
||||
// Set properties on env (SetPropertyStr internally roots its args)
|
||||
JS_SetPropertyStr(js, env_ref.val, "os", os_ref.val);
|
||||
JS_SetPropertyStr(js, env_ref.val, "json", json_ref.val);
|
||||
JS_SetPropertyStr(js, env_ref.val, "nota", nota_ref.val);
|
||||
JS_SetPropertyStr(js, env_ref.val, "wota", wota_ref.val);
|
||||
|
||||
JS_PopGCRef(js, &wota_ref);
|
||||
JS_PopGCRef(js, ¬a_ref);
|
||||
JS_PopGCRef(js, &json_ref);
|
||||
JS_PopGCRef(js, &os_ref);
|
||||
|
||||
crt->actor_sym_ref.val = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, env_ref.val, "actorsym", JS_DupValue(js, crt->actor_sym_ref.val));
|
||||
|
||||
// Always set init (even if null)
|
||||
if (crt->init_wota) {
|
||||
JS_SetPropertyStr(js, hidden_env, "init", wota2value(js, crt->init_wota));
|
||||
JSValue init_val = wota2value(js, crt->init_wota);
|
||||
JS_SetPropertyStr(js, env_ref.val, "init", init_val);
|
||||
free(crt->init_wota);
|
||||
crt->init_wota = NULL;
|
||||
} else {
|
||||
JS_SetPropertyStr(js, hidden_env, "init", JS_NULL);
|
||||
JS_SetPropertyStr(js, env_ref.val, "init", JS_NULL);
|
||||
}
|
||||
|
||||
if (core_path) {
|
||||
JS_SetPropertyStr(js, hidden_env, "core_path", JS_NewString(js, core_path));
|
||||
JSValue path_val = JS_NewString(js, core_path);
|
||||
JS_SetPropertyStr(js, env_ref.val, "core_path", path_val);
|
||||
}
|
||||
|
||||
// Stone the environment
|
||||
hidden_env = JS_Stone(js, hidden_env);
|
||||
JSValue hidden_env = JS_Stone(js, env_ref.val);
|
||||
JS_PopGCRef(js, &env_ref);
|
||||
|
||||
// Integrate and run
|
||||
// Run through MACH VM
|
||||
crt->state = ACTOR_RUNNING;
|
||||
JSValue v = JS_Integrate(js, bytecode, hidden_env);
|
||||
JSValue v = JS_RunMachTree(js, ast, hidden_env);
|
||||
cJSON_Delete(ast);
|
||||
uncaught_exception(js, v);
|
||||
crt->state = ACTOR_IDLE;
|
||||
set_actor_state(crt);
|
||||
@@ -399,11 +454,13 @@ int cell_init(int argc, char **argv)
|
||||
script = (char *)script_or_file;
|
||||
}
|
||||
|
||||
char *json = JS_AST(script, strlen(script), filename);
|
||||
if (json) {
|
||||
int has_errors = print_json_errors(json);
|
||||
printf("%s\n", json);
|
||||
free(json);
|
||||
cJSON *ast = JS_ASTTree(script, strlen(script), filename);
|
||||
if (ast) {
|
||||
int has_errors = print_tree_errors(ast);
|
||||
char *pretty = cJSON_Print(ast);
|
||||
cJSON_Delete(ast);
|
||||
printf("%s\n", pretty);
|
||||
free(pretty);
|
||||
free(allocated_script);
|
||||
return has_errors ? 1 : 0;
|
||||
} else {
|
||||
@@ -445,8 +502,14 @@ int cell_init(int argc, char **argv)
|
||||
char *json = JS_Tokenize(script, strlen(script), filename);
|
||||
if (json) {
|
||||
int has_errors = print_json_errors(json);
|
||||
printf("%s\n", json);
|
||||
cJSON *root = cJSON_Parse(json);
|
||||
free(json);
|
||||
if (root) {
|
||||
char *pretty = cJSON_Print(root);
|
||||
cJSON_Delete(root);
|
||||
printf("%s\n", pretty);
|
||||
free(pretty);
|
||||
}
|
||||
free(allocated_script);
|
||||
return has_errors ? 1 : 0;
|
||||
} else {
|
||||
@@ -485,30 +548,32 @@ int cell_init(int argc, char **argv)
|
||||
script = (char *)script_or_file;
|
||||
}
|
||||
|
||||
char *ast_json = JS_AST(script, strlen(script), filename);
|
||||
if (!ast_json) {
|
||||
cJSON *ast = JS_ASTTree(script, strlen(script), filename);
|
||||
if (!ast) {
|
||||
printf("Failed to parse AST\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (print_json_errors(ast_json)) {
|
||||
free(ast_json);
|
||||
if (print_tree_errors(ast)) {
|
||||
cJSON_Delete(ast);
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *mcode_json = JS_Mcode(ast_json);
|
||||
free(ast_json);
|
||||
cJSON *mcode = JS_McodeTree(ast);
|
||||
cJSON_Delete(ast);
|
||||
|
||||
if (!mcode_json) {
|
||||
if (!mcode) {
|
||||
printf("Failed to generate MCODE\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("%s\n", mcode_json);
|
||||
free(mcode_json);
|
||||
char *pretty = cJSON_Print(mcode);
|
||||
cJSON_Delete(mcode);
|
||||
printf("%s\n", pretty);
|
||||
free(pretty);
|
||||
|
||||
free(allocated_script);
|
||||
return 0;
|
||||
@@ -536,40 +601,39 @@ int cell_init(int argc, char **argv)
|
||||
script = (char *)script_or_file;
|
||||
}
|
||||
|
||||
char *ast_json = JS_AST(script, strlen(script), filename);
|
||||
if (!ast_json) {
|
||||
cJSON *ast = JS_ASTTree(script, strlen(script), filename);
|
||||
if (!ast) {
|
||||
printf("Failed to parse AST\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (print_json_errors(ast_json)) {
|
||||
free(ast_json); free(allocated_script);
|
||||
if (print_tree_errors(ast)) {
|
||||
cJSON_Delete(ast); free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *mcode_json = JS_Mcode(ast_json);
|
||||
free(ast_json);
|
||||
cJSON *mcode = JS_McodeTree(ast);
|
||||
cJSON_Delete(ast);
|
||||
|
||||
if (!mcode_json) {
|
||||
if (!mcode) {
|
||||
printf("Failed to generate MCODE\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (print_json_errors(mcode_json)) {
|
||||
free(mcode_json); free(allocated_script);
|
||||
if (print_tree_errors(mcode)) {
|
||||
cJSON_Delete(mcode); free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Use a larger heap context for execution */
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
if (!rt) { printf("Failed to create JS runtime\n"); free(mcode_json); free(allocated_script); return 1; }
|
||||
if (!rt) { printf("Failed to create JS runtime\n"); cJSON_Delete(mcode); free(allocated_script); return 1; }
|
||||
JSContext *ctx = JS_NewContextWithHeapSize(rt, 64 * 1024);
|
||||
if (!ctx) { printf("Failed to create execution context\n"); free(mcode_json); JS_FreeRuntime(rt); free(allocated_script); return 1; }
|
||||
if (!ctx) { printf("Failed to create execution context\n"); cJSON_Delete(mcode); JS_FreeRuntime(rt); free(allocated_script); return 1; }
|
||||
|
||||
JSValue result = JS_CallMcode(ctx, mcode_json);
|
||||
free(mcode_json);
|
||||
JSValue result = JS_CallMcodeTree(ctx, mcode); /* takes ownership of mcode */
|
||||
|
||||
if (JS_IsException(result)) {
|
||||
JSValue exc = JS_GetException(ctx);
|
||||
@@ -629,15 +693,15 @@ int cell_init(int argc, char **argv)
|
||||
script = (char *)script_or_file;
|
||||
}
|
||||
|
||||
char *ast_json = JS_AST(script, strlen(script), filename);
|
||||
if (!ast_json) {
|
||||
cJSON *ast = JS_ASTTree(script, strlen(script), filename);
|
||||
if (!ast) {
|
||||
printf("Failed to parse AST\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (print_json_errors(ast_json)) {
|
||||
free(ast_json);
|
||||
if (print_tree_errors(ast)) {
|
||||
cJSON_Delete(ast);
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
@@ -645,18 +709,18 @@ int cell_init(int argc, char **argv)
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
if (!rt) {
|
||||
printf("Failed to create JS runtime\n");
|
||||
free(ast_json); free(allocated_script);
|
||||
cJSON_Delete(ast); free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
JSContext *ctx = JS_NewContext(rt);
|
||||
if (!ctx) {
|
||||
printf("Failed to create JS context\n");
|
||||
free(ast_json); JS_FreeRuntime(rt); free(allocated_script);
|
||||
cJSON_Delete(ast); JS_FreeRuntime(rt); free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
JS_DumpMach(ctx, ast_json, JS_NULL);
|
||||
free(ast_json);
|
||||
JS_DumpMachTree(ctx, ast, JS_NULL);
|
||||
cJSON_Delete(ast);
|
||||
|
||||
JS_FreeContext(ctx);
|
||||
JS_FreeRuntime(rt);
|
||||
@@ -693,15 +757,15 @@ int cell_init(int argc, char **argv)
|
||||
script = (char *)script_or_file;
|
||||
}
|
||||
|
||||
char *ast_json = JS_AST(script, strlen(script), filename);
|
||||
if (!ast_json) {
|
||||
cJSON *ast = JS_ASTTree(script, strlen(script), filename);
|
||||
if (!ast) {
|
||||
printf("Failed to parse AST\n");
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (print_json_errors(ast_json)) {
|
||||
free(ast_json);
|
||||
if (print_tree_errors(ast)) {
|
||||
cJSON_Delete(ast);
|
||||
free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
@@ -709,18 +773,18 @@ int cell_init(int argc, char **argv)
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
if (!rt) {
|
||||
printf("Failed to create JS runtime\n");
|
||||
free(ast_json); free(allocated_script);
|
||||
cJSON_Delete(ast); free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
JSContext *ctx = JS_NewContext(rt);
|
||||
if (!ctx) {
|
||||
printf("Failed to create JS context\n");
|
||||
free(ast_json); JS_FreeRuntime(rt); free(allocated_script);
|
||||
cJSON_Delete(ast); JS_FreeRuntime(rt); free(allocated_script);
|
||||
return 1;
|
||||
}
|
||||
|
||||
JSValue result = JS_RunMach(ctx, ast_json, JS_NULL);
|
||||
free(ast_json);
|
||||
JSValue result = JS_RunMachTree(ctx, ast, JS_NULL);
|
||||
cJSON_Delete(ast);
|
||||
|
||||
int exit_code = 0;
|
||||
if (JS_IsException(result)) {
|
||||
|
||||
@@ -144,7 +144,7 @@ JS_SetPropertyFunctionList(js, TYPE##_proto, js_##TYPE##_funcs, countof(js_##TYP
|
||||
|
||||
#define QJSCLASSPREP_NO_FUNCS(TYPE) \
|
||||
JS_NewClassID(&js_##TYPE##_id);\
|
||||
JS_NewClass(JS_GetRuntime(js), js_##TYPE##_id, &js_##TYPE##_class);\
|
||||
JS_NewClass(js, js_##TYPE##_id, &js_##TYPE##_class);\
|
||||
JSValue TYPE##_proto = JS_NewObject(js); \
|
||||
JS_SetClassProto(js, js_##TYPE##_id, TYPE##_proto); \
|
||||
|
||||
|
||||
@@ -24,21 +24,26 @@ typedef struct letter {
|
||||
|
||||
typedef struct cell_rt {
|
||||
JSContext *context;
|
||||
JSValue idx_buffer;
|
||||
JSValue on_exception;
|
||||
JSValue message_handle;
|
||||
|
||||
/* JSValues on the GC heap — each paired with a JSGCRef so the
|
||||
Cheney GC can relocate them during compaction. */
|
||||
JSGCRef idx_buffer_ref;
|
||||
JSGCRef on_exception_ref;
|
||||
JSGCRef message_handle_ref;
|
||||
JSGCRef unneeded_ref;
|
||||
JSGCRef actor_sym_ref;
|
||||
|
||||
void *init_wota;
|
||||
|
||||
/* Protects JSContext usage */
|
||||
pthread_mutex_t *mutex; /* for everything else */
|
||||
pthread_mutex_t *msg_mutex; /* For message queue and timers queue */
|
||||
|
||||
|
||||
char *id;
|
||||
|
||||
int idx_count;
|
||||
|
||||
/* The “mailbox” for incoming messages + a dedicated lock for it: */
|
||||
/* The "mailbox" for incoming messages + a dedicated lock for it: */
|
||||
letter *letters;
|
||||
|
||||
/* CHANGED FOR EVENTS: a separate lock for the actor->events queue */
|
||||
@@ -47,14 +52,11 @@ typedef struct cell_rt {
|
||||
int state;
|
||||
uint32_t ar; // timer for unneeded
|
||||
double ar_secs; // time for unneeded
|
||||
JSValue unneeded; // fn to call before unneeded
|
||||
|
||||
|
||||
int disrupt;
|
||||
int main_thread_only;
|
||||
int affinity;
|
||||
|
||||
JSValue actor_sym;
|
||||
|
||||
|
||||
const char *name; // human friendly name
|
||||
cell_hook trace_hook;
|
||||
} cell_rt;
|
||||
@@ -63,8 +65,6 @@ cell_rt *create_actor(void *wota);
|
||||
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
|
||||
void actor_disrupt(cell_rt *actor);
|
||||
|
||||
JSValue actor_sym(cell_rt *actor);
|
||||
|
||||
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);
|
||||
|
||||
@@ -74,7 +74,7 @@ JSC_CCALL(os_register_actor,
|
||||
JS_ToFloat64(js, &ar, argv[3]);
|
||||
const char *err = register_actor(id, rt, JS_ToBool(js, argv[2]), ar);
|
||||
if (err) return JS_ThrowInternalError(js, "Could not register actor: %s", err);
|
||||
rt->message_handle = JS_DupValue(js, argv[1]);
|
||||
rt->message_handle_ref.val = argv[1];
|
||||
rt->context = js;
|
||||
JS_FreeCString(js, id);
|
||||
)
|
||||
@@ -106,8 +106,7 @@ JSC_SCALL(actor_setname,
|
||||
|
||||
JSC_CCALL(actor_on_exception,
|
||||
cell_rt *rt = JS_GetContextOpaque(js);
|
||||
JS_FreeValue(js, rt->on_exception);
|
||||
rt->on_exception = JS_DupValue(js,argv[0]);
|
||||
rt->on_exception_ref.val = argv[0];
|
||||
)
|
||||
|
||||
JSC_CCALL(actor_clock,
|
||||
|
||||
4858
source/quickjs.c
4858
source/quickjs.c
File diff suppressed because it is too large
Load Diff
@@ -367,10 +367,10 @@ JSRuntime *JS_NewRuntime (void);
|
||||
void JS_SetRuntimeInfo (JSRuntime *rt, const char *info);
|
||||
void JS_SetMemoryLimit (JSRuntime *rt, size_t limit);
|
||||
/* use 0 to disable maximum stack size check */
|
||||
void JS_SetMaxStackSize (JSRuntime *rt, size_t stack_size);
|
||||
void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size);
|
||||
/* should be called when changing thread to update the stack top value
|
||||
used to check stack overflow. */
|
||||
void JS_UpdateStackTop (JSRuntime *rt);
|
||||
void JS_UpdateStackTop (JSContext *ctx);
|
||||
void JS_FreeRuntime (JSRuntime *rt);
|
||||
void *JS_GetRuntimeOpaque (JSRuntime *rt);
|
||||
void JS_SetRuntimeOpaque (JSRuntime *rt, void *opaque);
|
||||
@@ -429,9 +429,9 @@ JSClassID JS_NewClassID (JSClassID *pclass_id);
|
||||
/* Returns the class ID if `v` is an object, otherwise returns
|
||||
* JS_INVALID_CLASS_ID. */
|
||||
JSClassID JS_GetClassID (JSValue v);
|
||||
int JS_NewClass (JSRuntime *rt, JSClassID class_id,
|
||||
int JS_NewClass (JSContext *ctx, JSClassID class_id,
|
||||
const JSClassDef *class_def);
|
||||
int JS_IsRegisteredClass (JSRuntime *rt, JSClassID class_id);
|
||||
int JS_IsRegisteredClass (JSContext *ctx, JSClassID class_id);
|
||||
|
||||
extern JSClassID js_class_id_alloc;
|
||||
|
||||
@@ -740,7 +740,7 @@ JSValue JS_JSONStringify (JSContext *ctx, JSValue obj,
|
||||
|
||||
/* return != 0 if the JS code needs to be interrupted */
|
||||
typedef int JSInterruptHandler (JSRuntime *rt, void *opaque);
|
||||
void JS_SetInterruptHandler (JSRuntime *rt, JSInterruptHandler *cb,
|
||||
void JS_SetInterruptHandler (JSContext *ctx, JSInterruptHandler *cb,
|
||||
void *opaque);
|
||||
/* select which debug info is stripped from the compiled code */
|
||||
#define JS_STRIP_SOURCE (1 << 0) /* strip source code */
|
||||
@@ -1218,45 +1218,57 @@ CellModule *cell_module_from_bytecode (JSContext *ctx, JSFunctionBytecode *main_
|
||||
Returns allocated CellModule (caller must free with cell_module_free), or NULL on error. */
|
||||
CellModule *JS_CompileModule (JSContext *ctx, const char *input, size_t input_len, const char *filename);
|
||||
|
||||
/* Parse source code and return AST as cJSON tree.
|
||||
Caller must call cJSON_Delete() on result. */
|
||||
struct cJSON *JS_ASTTree (const char *source, size_t len, const char *filename);
|
||||
|
||||
/* Parse source code and return AST as JSON string.
|
||||
Returns malloc'd JSON string (caller must free), or NULL on error.
|
||||
No JSContext needed — pure string transformation. */
|
||||
Returns malloc'd JSON string (caller must free), or NULL on error. */
|
||||
char *JS_AST (const char *source, size_t len, const char *filename);
|
||||
|
||||
/* Tokenize source code and return token array as JSON string.
|
||||
Returns malloc'd JSON string (caller must free), or NULL on error.
|
||||
No JSContext needed — pure string transformation. */
|
||||
Returns malloc'd JSON string (caller must free), or NULL on error. */
|
||||
char *JS_Tokenize (const char *source, size_t len, const char *filename);
|
||||
|
||||
/* Compiled bytecode (context-free, serializable) */
|
||||
typedef struct MachCode MachCode;
|
||||
|
||||
/* Compile AST JSON to context-free MachCode.
|
||||
Returns MachCode* (caller must free with JS_FreeMachCode), or NULL on error. */
|
||||
/* Compile AST cJSON tree to context-free MachCode. */
|
||||
MachCode *JS_CompileMachTree(struct cJSON *ast);
|
||||
|
||||
/* Compile AST JSON string to context-free MachCode. */
|
||||
MachCode *JS_CompileMach(const char *ast_json);
|
||||
|
||||
/* Free a compiled MachCode tree. */
|
||||
void JS_FreeMachCode(MachCode *mc);
|
||||
|
||||
/* Load compiled MachCode into a JSContext, materializing JSValues.
|
||||
Returns JSCodeRegister* linked and ready for execution. */
|
||||
/* Load compiled MachCode into a JSContext, materializing JSValues. */
|
||||
struct JSCodeRegister *JS_LoadMachCode(JSContext *ctx, MachCode *mc, JSValue env);
|
||||
|
||||
/* Dump MACH bytecode to stdout for debugging. Takes AST JSON.
|
||||
Internally compiles, loads, and dumps binary bytecode. */
|
||||
/* Dump MACH bytecode to stdout. Takes AST cJSON tree. */
|
||||
void JS_DumpMachTree (JSContext *ctx, struct cJSON *ast, JSValue env);
|
||||
|
||||
/* Dump MACH bytecode to stdout. Takes AST JSON string. */
|
||||
void JS_DumpMach (JSContext *ctx, const char *ast_json, JSValue env);
|
||||
|
||||
/* Compile and execute MACH bytecode. Takes AST JSON.
|
||||
Internally compiles, loads, and executes.
|
||||
Returns result of execution, or JS_EXCEPTION on error. */
|
||||
/* Compile and execute MACH bytecode from AST cJSON tree. */
|
||||
JSValue JS_RunMachTree (JSContext *ctx, struct cJSON *ast, JSValue env);
|
||||
|
||||
/* Compile and execute MACH bytecode from AST JSON string. */
|
||||
JSValue JS_RunMach (JSContext *ctx, const char *ast_json, JSValue env);
|
||||
|
||||
/* Compile AST JSON to MCODE JSON (string-based IR).
|
||||
Returns malloc'd JSON string, or NULL on error. Caller must free.
|
||||
No JSContext needed — pure string transformation. */
|
||||
/* Compile AST cJSON tree to MCODE cJSON tree.
|
||||
Caller must call cJSON_Delete() on result. */
|
||||
struct cJSON *JS_McodeTree (struct cJSON *ast);
|
||||
|
||||
/* Compile AST JSON string to MCODE JSON string.
|
||||
Returns malloc'd JSON string, or NULL on error. Caller must free. */
|
||||
char *JS_Mcode (const char *ast_json);
|
||||
|
||||
/* Parse and execute MCODE JSON directly via the MCODE interpreter.
|
||||
/* Execute MCODE from cJSON tree. Takes ownership of root. */
|
||||
JSValue JS_CallMcodeTree (JSContext *ctx, struct cJSON *root);
|
||||
|
||||
/* Parse and execute MCODE JSON string.
|
||||
Returns result of execution, or JS_EXCEPTION on error. */
|
||||
JSValue JS_CallMcode (JSContext *ctx, const char *mcode_json);
|
||||
|
||||
|
||||
@@ -266,11 +266,11 @@ void actor_free(cell_rt *actor)
|
||||
|
||||
JSContext *js = actor->context;
|
||||
|
||||
JS_FreeValue(js, actor->idx_buffer);
|
||||
JS_FreeValue(js, actor->message_handle);
|
||||
JS_FreeValue(js, actor->on_exception);
|
||||
JS_FreeValue(js, actor->unneeded);
|
||||
JS_FreeValue(js, actor->actor_sym);
|
||||
JS_DeleteGCRef(js, &actor->idx_buffer_ref);
|
||||
JS_DeleteGCRef(js, &actor->on_exception_ref);
|
||||
JS_DeleteGCRef(js, &actor->message_handle_ref);
|
||||
JS_DeleteGCRef(js, &actor->unneeded_ref);
|
||||
JS_DeleteGCRef(js, &actor->actor_sym_ref);
|
||||
|
||||
for (int i = 0; i < hmlen(actor->timers); i++) {
|
||||
JS_FreeValue(js, actor->timers[i].value);
|
||||
@@ -289,10 +289,8 @@ void actor_free(cell_rt *actor)
|
||||
|
||||
arrfree(actor->letters);
|
||||
|
||||
JSRuntime *rt = JS_GetRuntime(js);
|
||||
JS_SetInterruptHandler(rt, NULL, NULL);
|
||||
JS_SetInterruptHandler(js, NULL, NULL);
|
||||
JS_FreeContext(js);
|
||||
JS_FreeRuntime(rt);
|
||||
free(actor->id);
|
||||
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
@@ -419,8 +417,8 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
|
||||
|
||||
actor->disrupt = 1;
|
||||
|
||||
if (!JS_IsNull(actor->unneeded)) {
|
||||
JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL);
|
||||
if (!JS_IsNull(actor->unneeded_ref.val)) {
|
||||
JSValue ret = JS_Call(actor->context, actor->unneeded_ref.val, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor->context, ret);
|
||||
}
|
||||
|
||||
@@ -434,14 +432,13 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
|
||||
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
|
||||
{
|
||||
if (actor->disrupt) return;
|
||||
JS_FreeValue(actor->context, actor->unneeded);
|
||||
|
||||
|
||||
if (!JS_IsFunction(fn)) {
|
||||
actor->unneeded = JS_NULL;
|
||||
actor->unneeded_ref.val = JS_NULL;
|
||||
goto END;
|
||||
}
|
||||
|
||||
actor->unneeded = JS_DupValue(actor->context, fn);
|
||||
|
||||
actor->unneeded_ref.val = fn;
|
||||
actor->ar_secs = seconds;
|
||||
|
||||
END:
|
||||
@@ -493,11 +490,10 @@ cell_rt *create_actor(void *wota)
|
||||
actor->heap = mi_heap_new();
|
||||
#endif
|
||||
actor->init_wota = wota;
|
||||
actor->idx_buffer = JS_NULL;
|
||||
actor->message_handle = JS_NULL;
|
||||
actor->unneeded = JS_NULL;
|
||||
actor->on_exception = JS_NULL;
|
||||
actor->actor_sym = JS_NULL;
|
||||
/* GCRef fields are registered after JSContext creation in script_startup.
|
||||
For now, zero-init from calloc is sufficient (val = 0 = JS_MKVAL(JS_TAG_INT,0),
|
||||
which is not a pointer so GC-safe). The actual JS_NULL assignment and
|
||||
JS_AddGCRef happen in script_startup. */
|
||||
|
||||
arrsetcap(actor->letters, 5);
|
||||
|
||||
@@ -587,7 +583,7 @@ void actor_turn(cell_rt *actor)
|
||||
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes
|
||||
JSValue arg = js_new_blob_stoned_copy(actor->context, (void*)blob_data(l.blob_data), size);
|
||||
blob_destroy(l.blob_data);
|
||||
result = JS_Call(actor->context, actor->message_handle, JS_NULL, 1, &arg);
|
||||
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg);
|
||||
uncaught_exception(actor->context, result);
|
||||
JS_FreeValue(actor->context, arg);
|
||||
} else if (l.type == LETTER_CALLBACK) {
|
||||
|
||||
@@ -118,11 +118,11 @@ void actor_free(cell_rt *actor)
|
||||
|
||||
JSContext *js = actor->context;
|
||||
|
||||
JS_FreeValue(js, actor->idx_buffer);
|
||||
JS_FreeValue(js, actor->message_handle);
|
||||
JS_FreeValue(js, actor->on_exception);
|
||||
JS_FreeValue(js, actor->unneeded);
|
||||
JS_FreeValue(js, actor->actor_sym);
|
||||
JS_DeleteGCRef(js, &actor->idx_buffer_ref);
|
||||
JS_DeleteGCRef(js, &actor->on_exception_ref);
|
||||
JS_DeleteGCRef(js, &actor->message_handle_ref);
|
||||
JS_DeleteGCRef(js, &actor->unneeded_ref);
|
||||
JS_DeleteGCRef(js, &actor->actor_sym_ref);
|
||||
|
||||
/* Free timer callbacks stored in actor */
|
||||
for (int i = 0; i < hmlen(actor->timers); i++) {
|
||||
@@ -142,10 +142,8 @@ void actor_free(cell_rt *actor)
|
||||
|
||||
arrfree(actor->letters);
|
||||
|
||||
JSRuntime *rt = JS_GetRuntime(js);
|
||||
JS_SetInterruptHandler(rt, NULL, NULL);
|
||||
JS_SetInterruptHandler(js, NULL, NULL);
|
||||
JS_FreeContext(js);
|
||||
JS_FreeRuntime(rt);
|
||||
free(actor->id);
|
||||
|
||||
free(actor);
|
||||
@@ -157,14 +155,13 @@ void actor_free(cell_rt *actor)
|
||||
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
|
||||
{
|
||||
if (actor->disrupt) return;
|
||||
JS_FreeValue(actor->context, actor->unneeded);
|
||||
|
||||
if (!JS_IsFunction(actor->context, fn)) {
|
||||
actor->unneeded = JS_NULL;
|
||||
|
||||
if (!JS_IsFunction(fn)) {
|
||||
actor->unneeded_ref.val = JS_NULL;
|
||||
goto END;
|
||||
}
|
||||
|
||||
actor->unneeded = JS_DupValue(actor->context, fn);
|
||||
|
||||
actor->unneeded_ref.val = fn;
|
||||
actor->ar_secs = seconds;
|
||||
|
||||
END:
|
||||
@@ -257,8 +254,8 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
|
||||
|
||||
actor->disrupt = 1;
|
||||
|
||||
if (!JS_IsNull(actor->unneeded)) {
|
||||
JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL);
|
||||
if (!JS_IsNull(actor->unneeded_ref.val)) {
|
||||
JSValue ret = JS_Call(actor->context, actor->unneeded_ref.val, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor->context, ret);
|
||||
}
|
||||
|
||||
@@ -328,11 +325,7 @@ cell_rt *create_actor(void *wota)
|
||||
{
|
||||
cell_rt *actor = calloc(sizeof(*actor), 1);
|
||||
actor->init_wota = wota;
|
||||
actor->idx_buffer = JS_NULL;
|
||||
actor->message_handle = JS_NULL;
|
||||
actor->unneeded = JS_NULL;
|
||||
actor->on_exception = JS_NULL;
|
||||
actor->actor_sym = JS_NULL;
|
||||
/* GCRef fields are registered after JSContext creation in script_startup. */
|
||||
|
||||
arrsetcap(actor->letters, 5);
|
||||
|
||||
@@ -408,7 +401,7 @@ void actor_turn(cell_rt *actor)
|
||||
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes
|
||||
JSValue arg = js_new_blob_stoned_copy(actor->context, (void *)blob_data(l.blob_data), size);
|
||||
blob_destroy(l.blob_data);
|
||||
result = JS_Call(actor->context, actor->message_handle, JS_NULL, 1, &arg);
|
||||
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg);
|
||||
uncaught_exception(actor->context, result);
|
||||
JS_FreeValue(actor->context, arg);
|
||||
} else if (l.type == LETTER_CALLBACK) {
|
||||
|
||||
@@ -2393,22 +2393,22 @@ TEST(ast_sem_nested_function_scope) {
|
||||
|
||||
TEST(mach_compile_basic) {
|
||||
const char *src = "var x = 1; x = x + 1";
|
||||
char *ast_json = JS_AST(src, strlen(src), "<test>");
|
||||
ASSERT_MSG(ast_json != NULL, "JS_AST returned NULL");
|
||||
MachCode *mc = JS_CompileMach(ast_json);
|
||||
free(ast_json);
|
||||
ASSERT_MSG(mc != NULL, "JS_CompileMach returned NULL");
|
||||
cJSON *ast = JS_ASTTree(src, strlen(src), "<test>");
|
||||
ASSERT_MSG(ast != NULL, "JS_ASTTree returned NULL");
|
||||
MachCode *mc = JS_CompileMachTree(ast);
|
||||
cJSON_Delete(ast);
|
||||
ASSERT_MSG(mc != NULL, "JS_CompileMachTree returned NULL");
|
||||
JS_FreeMachCode(mc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
TEST(mach_compile_function) {
|
||||
const char *src = "function f(x) { return x + 1 }";
|
||||
char *ast_json = JS_AST(src, strlen(src), "<test>");
|
||||
ASSERT_MSG(ast_json != NULL, "JS_AST returned NULL");
|
||||
MachCode *mc = JS_CompileMach(ast_json);
|
||||
free(ast_json);
|
||||
ASSERT_MSG(mc != NULL, "JS_CompileMach returned NULL");
|
||||
cJSON *ast = JS_ASTTree(src, strlen(src), "<test>");
|
||||
ASSERT_MSG(ast != NULL, "JS_ASTTree returned NULL");
|
||||
MachCode *mc = JS_CompileMachTree(ast);
|
||||
cJSON_Delete(ast);
|
||||
ASSERT_MSG(mc != NULL, "JS_CompileMachTree returned NULL");
|
||||
JS_FreeMachCode(mc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
657
syntax_suite.ce
Normal file
657
syntax_suite.ce
Normal file
@@ -0,0 +1,657 @@
|
||||
// Syntax suite: covers every syntactic feature of cell script
|
||||
// Run: ./cell --mach-run syntax_suite.ce
|
||||
|
||||
var passed = 0
|
||||
var failed = 0
|
||||
var error_names = []
|
||||
var error_reasons = []
|
||||
var fail_msg = ""
|
||||
|
||||
for (var _i = 0; _i < 100; _i++) {
|
||||
error_names[] = null
|
||||
error_reasons[] = null
|
||||
}
|
||||
|
||||
var fail = function(msg) {
|
||||
fail_msg = msg
|
||||
disrupt
|
||||
}
|
||||
|
||||
var assert_eq = function(actual, expected, msg) {
|
||||
if (actual != expected) fail(msg + " (got=" + text(actual) + " expected=" + text(expected) + ")")
|
||||
}
|
||||
|
||||
var run = function(name, fn) {
|
||||
fail_msg = ""
|
||||
fn()
|
||||
passed = passed + 1
|
||||
} disruption {
|
||||
error_names[failed] = name
|
||||
error_reasons[failed] = fail_msg == "" ? "disruption" : fail_msg
|
||||
failed = failed + 1
|
||||
}
|
||||
|
||||
var should_disrupt = function(fn) {
|
||||
var caught = false
|
||||
var wrapper = function() {
|
||||
fn()
|
||||
} disruption {
|
||||
caught = true
|
||||
}
|
||||
wrapper()
|
||||
return caught
|
||||
}
|
||||
|
||||
// === LITERALS ===
|
||||
|
||||
run("number literals", function() {
|
||||
assert_eq(42, 42, "integer")
|
||||
assert_eq(3.14 > 3, true, "float")
|
||||
assert_eq(-5, -5, "negative")
|
||||
assert_eq(0, 0, "zero")
|
||||
assert_eq(1e3, 1000, "scientific")
|
||||
})
|
||||
|
||||
run("string literals", function() {
|
||||
assert_eq("hello", "hello", "double quote")
|
||||
assert_eq("", "", "empty string")
|
||||
assert_eq("line1\nline2" != "line1line2", true, "escape sequence")
|
||||
})
|
||||
|
||||
run("template literals", function() {
|
||||
var x = "world"
|
||||
assert_eq(`hello ${x}`, "hello world", "interpolation")
|
||||
assert_eq(`${1 + 2}`, "3", "expression interpolation")
|
||||
assert_eq(`plain`, "plain", "no interpolation")
|
||||
})
|
||||
|
||||
run("boolean literals", function() {
|
||||
assert_eq(true, true, "true")
|
||||
assert_eq(false, false, "false")
|
||||
})
|
||||
|
||||
run("null literal", function() {
|
||||
assert_eq(null, null, "null")
|
||||
})
|
||||
|
||||
run("array literal", function() {
|
||||
var a = [1, 2, 3]
|
||||
assert_eq(length(a), 3, "array length")
|
||||
assert_eq(a[0], 1, "first element")
|
||||
var e = []
|
||||
assert_eq(length(e), 0, "empty array")
|
||||
})
|
||||
|
||||
run("object literal", function() {
|
||||
var o = {a: 1, b: "two"}
|
||||
assert_eq(o.a, 1, "object prop")
|
||||
var e = {}
|
||||
assert_eq(e.x, null, "empty object missing prop")
|
||||
})
|
||||
|
||||
run("regex literal", function() {
|
||||
var r = /\d+/
|
||||
var result = extract("abc123", r)
|
||||
assert_eq(result[0], "123", "regex match")
|
||||
var ri = /hello/i
|
||||
var result2 = extract("Hello", ri)
|
||||
assert_eq(result2[0], "Hello", "regex flags")
|
||||
})
|
||||
|
||||
// === DECLARATIONS ===
|
||||
|
||||
run("var declaration", function() {
|
||||
var x = 5
|
||||
assert_eq(x, 5, "var init")
|
||||
})
|
||||
|
||||
run("var uninitialized", function() {
|
||||
var x
|
||||
assert_eq(x, null, "var defaults to null")
|
||||
})
|
||||
|
||||
run("var multiple declaration", function() {
|
||||
var a = 1, b = 2, c = 3
|
||||
assert_eq(a + b + c, 6, "multi var")
|
||||
})
|
||||
|
||||
run("def declaration", function() {
|
||||
def x = 42
|
||||
assert_eq(x, 42, "def const")
|
||||
})
|
||||
|
||||
// === ARITHMETIC OPERATORS ===
|
||||
|
||||
run("arithmetic operators", function() {
|
||||
assert_eq(2 + 3, 5, "add")
|
||||
assert_eq(5 - 3, 2, "sub")
|
||||
assert_eq(3 * 4, 12, "mul")
|
||||
assert_eq(12 / 4, 3, "div")
|
||||
assert_eq(10 % 3, 1, "mod")
|
||||
assert_eq(2 ** 3, 8, "exp")
|
||||
})
|
||||
|
||||
// === COMPARISON OPERATORS ===
|
||||
|
||||
run("comparison operators", function() {
|
||||
assert_eq(5 == 5, true, "eq")
|
||||
assert_eq(5 != 6, true, "neq")
|
||||
assert_eq(3 < 5, true, "lt")
|
||||
assert_eq(5 > 3, true, "gt")
|
||||
assert_eq(3 <= 3, true, "lte")
|
||||
assert_eq(5 >= 5, true, "gte")
|
||||
})
|
||||
|
||||
// === LOGICAL OPERATORS ===
|
||||
|
||||
run("logical operators", function() {
|
||||
assert_eq(true && true, true, "and")
|
||||
assert_eq(true && false, false, "and false")
|
||||
assert_eq(false || true, true, "or")
|
||||
assert_eq(false || false, false, "or false")
|
||||
assert_eq(!true, false, "not")
|
||||
assert_eq(!false, true, "not false")
|
||||
})
|
||||
|
||||
run("short circuit", function() {
|
||||
var called = false
|
||||
var fn = function() { called = true; return true }
|
||||
var r = false && fn()
|
||||
assert_eq(called, false, "and short circuit")
|
||||
r = true || fn()
|
||||
assert_eq(called, false, "or short circuit")
|
||||
})
|
||||
|
||||
// === BITWISE OPERATORS ===
|
||||
|
||||
run("bitwise operators", function() {
|
||||
assert_eq(5 & 3, 1, "and")
|
||||
assert_eq(5 | 3, 7, "or")
|
||||
assert_eq(5 ^ 3, 6, "xor")
|
||||
assert_eq(~0, -1, "not")
|
||||
assert_eq(1 << 3, 8, "lshift")
|
||||
assert_eq(8 >> 3, 1, "rshift")
|
||||
assert_eq(-1 >>> 1, 2147483647, "unsigned rshift")
|
||||
})
|
||||
|
||||
// === UNARY OPERATORS ===
|
||||
|
||||
run("unary operators", function() {
|
||||
assert_eq(+5, 5, "unary plus")
|
||||
assert_eq(-5, -5, "unary minus")
|
||||
assert_eq(-(-5), 5, "double negate")
|
||||
})
|
||||
|
||||
run("increment decrement", function() {
|
||||
var x = 5
|
||||
assert_eq(x++, 5, "postfix inc returns old")
|
||||
assert_eq(x, 6, "postfix inc side effect")
|
||||
x = 5
|
||||
assert_eq(++x, 6, "prefix inc returns new")
|
||||
x = 5
|
||||
assert_eq(x--, 5, "postfix dec returns old")
|
||||
assert_eq(x, 4, "postfix dec side effect")
|
||||
x = 5
|
||||
assert_eq(--x, 4, "prefix dec returns new")
|
||||
})
|
||||
|
||||
// === COMPOUND ASSIGNMENT ===
|
||||
|
||||
run("compound assignment", function() {
|
||||
var x = 10
|
||||
x += 3; assert_eq(x, 13, "+=")
|
||||
x -= 3; assert_eq(x, 10, "-=")
|
||||
x *= 2; assert_eq(x, 20, "*=")
|
||||
x /= 4; assert_eq(x, 5, "/=")
|
||||
x %= 3; assert_eq(x, 2, "%=")
|
||||
})
|
||||
|
||||
// === TERNARY OPERATOR ===
|
||||
|
||||
run("ternary operator", function() {
|
||||
var a = true ? 1 : 2
|
||||
assert_eq(a, 1, "ternary true")
|
||||
var b = false ? 1 : 2
|
||||
assert_eq(b, 2, "ternary false")
|
||||
var c = true ? (false ? 1 : 2) : 3
|
||||
assert_eq(c, 2, "ternary nested")
|
||||
})
|
||||
|
||||
// === COMMA OPERATOR ===
|
||||
|
||||
run("comma operator", function() {
|
||||
var x = (1, 2, 3)
|
||||
assert_eq(x, 3, "comma returns last")
|
||||
})
|
||||
|
||||
// === IN OPERATOR ===
|
||||
|
||||
run("in operator", function() {
|
||||
var o = {a: 1}
|
||||
assert_eq("a" in o, true, "key exists")
|
||||
assert_eq("b" in o, false, "key missing")
|
||||
})
|
||||
|
||||
// === DELETE OPERATOR ===
|
||||
|
||||
run("delete operator", function() {
|
||||
var o = {a: 1, b: 2}
|
||||
delete o.a
|
||||
assert_eq("a" in o, false, "delete removes key")
|
||||
assert_eq(o.b, 2, "delete leaves others")
|
||||
})
|
||||
|
||||
// === PROPERTY ACCESS ===
|
||||
|
||||
run("dot access", function() {
|
||||
var o = {x: 10}
|
||||
assert_eq(o.x, 10, "dot read")
|
||||
o.x = 20
|
||||
assert_eq(o.x, 20, "dot write")
|
||||
})
|
||||
|
||||
run("bracket access", function() {
|
||||
var o = {x: 10}
|
||||
assert_eq(o["x"], 10, "bracket read")
|
||||
var key = "x"
|
||||
assert_eq(o[key], 10, "computed bracket")
|
||||
o["y"] = 20
|
||||
assert_eq(o.y, 20, "bracket write")
|
||||
})
|
||||
|
||||
run("object-as-key", function() {
|
||||
var k = {}
|
||||
var o = {}
|
||||
o[k] = 42
|
||||
assert_eq(o[k], 42, "object key set/get")
|
||||
assert_eq(o[{}], null, "new object is different key")
|
||||
assert_eq(k in o, true, "object key in")
|
||||
delete o[k]
|
||||
assert_eq(k in o, false, "object key delete")
|
||||
})
|
||||
|
||||
run("chained access", function() {
|
||||
var d = {a: {b: [1, {c: 99}]}}
|
||||
assert_eq(d.a.b[1].c, 99, "mixed chain")
|
||||
})
|
||||
|
||||
// === ARRAY PUSH/POP SYNTAX ===
|
||||
|
||||
run("array push pop", function() {
|
||||
var a = [1, 2]
|
||||
a[] = 3
|
||||
assert_eq(length(a), 3, "push length")
|
||||
assert_eq(a[2], 3, "push value")
|
||||
var v = a[]
|
||||
assert_eq(v, 3, "pop value")
|
||||
assert_eq(length(a), 2, "pop length")
|
||||
})
|
||||
|
||||
// === CONTROL FLOW: IF/ELSE ===
|
||||
|
||||
run("if else", function() {
|
||||
var x = 0
|
||||
if (true) x = 1
|
||||
assert_eq(x, 1, "if true")
|
||||
if (false) x = 2 else x = 3
|
||||
assert_eq(x, 3, "if else")
|
||||
if (false) x = 4
|
||||
else if (true) x = 5
|
||||
else x = 6
|
||||
assert_eq(x, 5, "else if")
|
||||
})
|
||||
|
||||
// === CONTROL FLOW: WHILE ===
|
||||
|
||||
run("while loop", function() {
|
||||
var i = 0
|
||||
while (i < 5) i++
|
||||
assert_eq(i, 5, "while basic")
|
||||
})
|
||||
|
||||
run("while break continue", function() {
|
||||
var i = 0
|
||||
while (true) {
|
||||
if (i >= 3) break
|
||||
i++
|
||||
}
|
||||
assert_eq(i, 3, "while break")
|
||||
var sum = 0
|
||||
i = 0
|
||||
while (i < 5) {
|
||||
i++
|
||||
if (i % 2 == 0) continue
|
||||
sum += i
|
||||
}
|
||||
assert_eq(sum, 9, "while continue")
|
||||
})
|
||||
|
||||
// === CONTROL FLOW: FOR ===
|
||||
|
||||
run("for loop", function() {
|
||||
var sum = 0
|
||||
for (var i = 0; i < 5; i++) sum += i
|
||||
assert_eq(sum, 10, "for basic")
|
||||
})
|
||||
|
||||
run("for break continue", function() {
|
||||
var sum = 0
|
||||
for (var i = 0; i < 10; i++) {
|
||||
if (i == 5) break
|
||||
sum += i
|
||||
}
|
||||
assert_eq(sum, 10, "for break")
|
||||
sum = 0
|
||||
for (var i = 0; i < 5; i++) {
|
||||
if (i % 2 == 0) continue
|
||||
sum += i
|
||||
}
|
||||
assert_eq(sum, 4, "for continue")
|
||||
})
|
||||
|
||||
run("nested for", function() {
|
||||
var sum = 0
|
||||
for (var i = 0; i < 3; i++)
|
||||
for (var j = 0; j < 3; j++)
|
||||
sum++
|
||||
assert_eq(sum, 9, "nested for")
|
||||
})
|
||||
|
||||
// === BLOCK SCOPING ===
|
||||
|
||||
run("block scoping", function() {
|
||||
var x = 1
|
||||
{
|
||||
var x = 2
|
||||
assert_eq(x, 2, "inner block")
|
||||
}
|
||||
assert_eq(x, 1, "outer preserved")
|
||||
})
|
||||
|
||||
run("for iterator scope", function() {
|
||||
for (var i = 0; i < 1; i++) {}
|
||||
assert_eq(should_disrupt(function() { var y = i }), true, "for var does not leak")
|
||||
})
|
||||
|
||||
// === FUNCTIONS ===
|
||||
|
||||
run("function expression", function() {
|
||||
var fn = function(a, b) { return a + b }
|
||||
assert_eq(fn(2, 3), 5, "basic call")
|
||||
})
|
||||
|
||||
run("arrow function", function() {
|
||||
var double = x => x * 2
|
||||
assert_eq(double(5), 10, "arrow single param")
|
||||
var add = (a, b) => a + b
|
||||
assert_eq(add(2, 3), 5, "arrow multi param")
|
||||
var block = x => {
|
||||
var y = x * 2
|
||||
return y + 1
|
||||
}
|
||||
assert_eq(block(5), 11, "arrow block body")
|
||||
})
|
||||
|
||||
run("function no return", function() {
|
||||
var fn = function() { var x = 1 }
|
||||
assert_eq(fn(), null, "no return gives null")
|
||||
})
|
||||
|
||||
run("function early return", function() {
|
||||
var fn = function() { return 1; return 2 }
|
||||
assert_eq(fn(), 1, "early return")
|
||||
})
|
||||
|
||||
run("extra and missing args", function() {
|
||||
var fn = function(a, b) { return a + b }
|
||||
assert_eq(fn(1, 2, 3), 3, "extra args ignored")
|
||||
var fn2 = function(a, b) { return a }
|
||||
assert_eq(fn2(1), 1, "missing args ok")
|
||||
})
|
||||
|
||||
run("iife", function() {
|
||||
var r = (function(x) { return x * 2 })(21)
|
||||
assert_eq(r, 42, "immediately invoked")
|
||||
})
|
||||
|
||||
// === CLOSURES ===
|
||||
|
||||
run("closure", function() {
|
||||
var make = function(x) {
|
||||
return function(y) { return x + y }
|
||||
}
|
||||
var add5 = make(5)
|
||||
assert_eq(add5(3), 8, "closure captures")
|
||||
})
|
||||
|
||||
run("closure mutation", function() {
|
||||
var counter = function() {
|
||||
var n = 0
|
||||
return function() { n = n + 1; return n }
|
||||
}
|
||||
var c = counter()
|
||||
assert_eq(c(), 1, "first")
|
||||
assert_eq(c(), 2, "second")
|
||||
})
|
||||
|
||||
// === RECURSION ===
|
||||
|
||||
run("recursion", function() {
|
||||
var fact = function(n) {
|
||||
if (n <= 1) return 1
|
||||
return n * fact(n - 1)
|
||||
}
|
||||
assert_eq(fact(5), 120, "factorial")
|
||||
})
|
||||
|
||||
// === THIS BINDING ===
|
||||
|
||||
run("this binding", function() {
|
||||
var obj = {
|
||||
val: 10,
|
||||
get: function() { return this.val }
|
||||
}
|
||||
assert_eq(obj.get(), 10, "method this")
|
||||
})
|
||||
|
||||
// === DISRUPTION ===
|
||||
|
||||
run("disrupt keyword", function() {
|
||||
assert_eq(should_disrupt(function() { disrupt }), true, "bare disrupt")
|
||||
})
|
||||
|
||||
run("disruption handler", function() {
|
||||
var x = 0
|
||||
var fn = function() { x = 1 } disruption { x = 2 }
|
||||
fn()
|
||||
assert_eq(x, 1, "no disruption path")
|
||||
var fn2 = function() { disrupt } disruption { x = 3 }
|
||||
fn2()
|
||||
assert_eq(x, 3, "disruption caught")
|
||||
})
|
||||
|
||||
run("disruption re-raise", function() {
|
||||
var outer_caught = false
|
||||
var outer = function() {
|
||||
var inner = function() { disrupt } disruption { disrupt }
|
||||
inner()
|
||||
} disruption {
|
||||
outer_caught = true
|
||||
}
|
||||
outer()
|
||||
assert_eq(outer_caught, true, "re-raise propagates")
|
||||
})
|
||||
|
||||
// === PROTOTYPAL INHERITANCE ===
|
||||
|
||||
run("meme and proto", function() {
|
||||
var parent = {x: 10}
|
||||
var child = meme(parent)
|
||||
assert_eq(child.x, 10, "inherited prop")
|
||||
assert_eq(proto(child), parent, "proto returns parent")
|
||||
child.x = 20
|
||||
assert_eq(parent.x, 10, "override does not mutate parent")
|
||||
})
|
||||
|
||||
run("meme with mixins", function() {
|
||||
var p = {a: 1}
|
||||
var m1 = {b: 2}
|
||||
var m2 = {c: 3}
|
||||
var child = meme(p, [m1, m2])
|
||||
assert_eq(child.a, 1, "parent prop")
|
||||
assert_eq(child.b, 2, "mixin1")
|
||||
assert_eq(child.c, 3, "mixin2")
|
||||
})
|
||||
|
||||
// === STONE (FREEZE) ===
|
||||
|
||||
run("stone", function() {
|
||||
var o = {x: 1}
|
||||
assert_eq(is_stone(o), false, "not frozen")
|
||||
stone(o)
|
||||
assert_eq(is_stone(o), true, "frozen")
|
||||
assert_eq(should_disrupt(function() { o.x = 2 }), true, "write disrupts")
|
||||
})
|
||||
|
||||
// === FUNCTION PROXY ===
|
||||
|
||||
run("function proxy", function() {
|
||||
var proxy = function(name, args) {
|
||||
return `${name}:${length(args)}`
|
||||
}
|
||||
assert_eq(proxy.hello(), "hello:0", "proxy dot call")
|
||||
assert_eq(proxy.add(1, 2), "add:2", "proxy with args")
|
||||
assert_eq(proxy["method"](), "method:0", "proxy bracket call")
|
||||
var m = "dynamic"
|
||||
assert_eq(proxy[m](), "dynamic:0", "proxy computed name")
|
||||
})
|
||||
|
||||
run("non-proxy function prop access disrupts", function() {
|
||||
var fn = function() { return 1 }
|
||||
assert_eq(should_disrupt(function() { var x = fn.foo }), true, "prop read disrupts")
|
||||
assert_eq(should_disrupt(function() { fn.foo = 1 }), true, "prop write disrupts")
|
||||
})
|
||||
|
||||
// === TYPE CHECKING ===
|
||||
|
||||
run("is_* functions", function() {
|
||||
assert_eq(is_number(42), true, "is_number")
|
||||
assert_eq(is_text("hi"), true, "is_text")
|
||||
assert_eq(is_logical(true), true, "is_logical")
|
||||
assert_eq(is_object({}), true, "is_object")
|
||||
assert_eq(is_array([]), true, "is_array")
|
||||
assert_eq(is_function(function(){}), true, "is_function")
|
||||
assert_eq(is_null(null), true, "is_null")
|
||||
assert_eq(is_object([]), false, "array not object")
|
||||
assert_eq(is_array({}), false, "object not array")
|
||||
})
|
||||
|
||||
// === TRUTHINESS / FALSINESS ===
|
||||
|
||||
run("falsy values", function() {
|
||||
if (false) fail("false")
|
||||
if (0) fail("0")
|
||||
if ("") fail("empty string")
|
||||
if (null) fail("null")
|
||||
assert_eq(true, true, "all falsy passed")
|
||||
})
|
||||
|
||||
run("truthy values", function() {
|
||||
if (!1) fail("1")
|
||||
if (!"hi") fail("string")
|
||||
if (!{}) fail("object")
|
||||
if (![]) fail("array")
|
||||
if (!true) fail("true")
|
||||
assert_eq(true, true, "all truthy passed")
|
||||
})
|
||||
|
||||
// === VARIABLE SHADOWING ===
|
||||
|
||||
run("variable shadowing", function() {
|
||||
var x = 10
|
||||
var fn = function() {
|
||||
var x = 20
|
||||
return x
|
||||
}
|
||||
assert_eq(fn(), 20, "inner shadows")
|
||||
assert_eq(x, 10, "outer unchanged")
|
||||
})
|
||||
|
||||
// === OPERATOR PRECEDENCE ===
|
||||
|
||||
run("precedence", function() {
|
||||
assert_eq(2 + 3 * 4, 14, "mul before add")
|
||||
assert_eq((2 + 3) * 4, 20, "parens override")
|
||||
assert_eq(-2 * 3, -6, "unary before mul")
|
||||
})
|
||||
|
||||
// === CURRYING / HIGHER-ORDER ===
|
||||
|
||||
run("curried function", function() {
|
||||
var f = function(a) {
|
||||
return function(b) {
|
||||
return function(c) { return a + b + c }
|
||||
}
|
||||
}
|
||||
assert_eq(f(1)(2)(3), 6, "triple curry")
|
||||
})
|
||||
|
||||
// === SELF-REFERENCING STRUCTURES ===
|
||||
|
||||
run("self-referencing object", function() {
|
||||
var o = {name: "root"}
|
||||
o.self = o
|
||||
assert_eq(o.self.self.name, "root", "cycle access")
|
||||
})
|
||||
|
||||
// === IDENTIFIER ? AND ! ===
|
||||
|
||||
run("question mark in identifier", function() {
|
||||
var nil? = (x) => x == null
|
||||
assert_eq(nil?(null), true, "nil? null")
|
||||
assert_eq(nil?(42), false, "nil? 42")
|
||||
})
|
||||
|
||||
run("bang in identifier", function() {
|
||||
var set! = (x) => x + 1
|
||||
assert_eq(set!(5), 6, "set! call")
|
||||
})
|
||||
|
||||
run("question mark mid identifier", function() {
|
||||
var is?valid = (x) => x > 0
|
||||
assert_eq(is?valid(3), true, "is?valid true")
|
||||
assert_eq(is?valid(-1), false, "is?valid false")
|
||||
})
|
||||
|
||||
run("bang mid identifier", function() {
|
||||
var do!stuff = () => 42
|
||||
assert_eq(do!stuff(), 42, "do!stuff call")
|
||||
})
|
||||
|
||||
run("ternary after question ident", function() {
|
||||
var nil? = (x) => x == null
|
||||
var a = nil?(null) ? "yes" : "no"
|
||||
assert_eq(a, "yes", "ternary true branch")
|
||||
var b = nil?(42) ? "yes" : "no"
|
||||
assert_eq(b, "no", "ternary false branch")
|
||||
})
|
||||
|
||||
run("bang not confused with logical not", function() {
|
||||
assert_eq(!true, false, "logical not true")
|
||||
assert_eq(!false, true, "logical not false")
|
||||
})
|
||||
|
||||
run("inequality not confused with bang ident", function() {
|
||||
assert_eq(1 != 2, true, "inequality true")
|
||||
assert_eq(1 != 1, false, "inequality false")
|
||||
})
|
||||
|
||||
// === SUMMARY ===
|
||||
|
||||
print(text(passed) + " passed, " + text(failed) + " failed out of " + text(passed + failed))
|
||||
if (failed > 0) {
|
||||
print("")
|
||||
for (var _j = 0; _j < failed; _j++) {
|
||||
print(" FAIL " + error_names[_j] + ": " + error_reasons[_j])
|
||||
}
|
||||
}
|
||||
92
test.ce
92
test.ce
@@ -29,12 +29,13 @@ function is_valid_package(dir) {
|
||||
// Get current package name from cell.toml or null
|
||||
function get_current_package_name() {
|
||||
if (!is_valid_package('.')) return null
|
||||
try {
|
||||
var pkg_name = 'local'
|
||||
var do_load = function() {
|
||||
var config = pkg.load_config(null)
|
||||
return config.package || 'local'
|
||||
} catch (e) {
|
||||
return 'local'
|
||||
}
|
||||
if (config.package) pkg_name = config.package
|
||||
} disruption {}
|
||||
do_load()
|
||||
return pkg_name
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
@@ -229,21 +230,48 @@ function spawn_actor_test(test_info) {
|
||||
actor: null
|
||||
}
|
||||
|
||||
try {
|
||||
var spawn_disrupted = false
|
||||
var do_spawn = function() {
|
||||
// Spawn the actor test - it should send back results
|
||||
var actor_path = text(test_info.path, 0, -3) // remove .ce
|
||||
entry.actor = $start(actor_path)
|
||||
push(pending_actor_tests, entry)
|
||||
} catch (e) {
|
||||
} disruption {
|
||||
spawn_disrupted = true
|
||||
}
|
||||
do_spawn()
|
||||
if (spawn_disrupted) {
|
||||
entry.status = "failed"
|
||||
entry.error = { message: `Failed to spawn actor: ${e}` }
|
||||
entry.error = { message: `Failed to spawn actor: ${test_name}` }
|
||||
entry.duration_ns = 0
|
||||
push(actor_test_results, entry)
|
||||
log.console(` FAIL ${test_name}: `)
|
||||
log.error(e)
|
||||
log.console(` FAIL ${test_name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Test runner with disruption support
|
||||
var test_passed = true
|
||||
var test_error_msg = ""
|
||||
var test_error_stack = ""
|
||||
|
||||
var run_test = function(fn) {
|
||||
test_passed = true
|
||||
test_error_msg = ""
|
||||
test_error_stack = ""
|
||||
var ret = fn()
|
||||
if (is_text(ret)) {
|
||||
test_passed = false
|
||||
test_error_msg = ret
|
||||
} else if (ret && (is_text(ret.message) || is_proto(ret, Error))) {
|
||||
test_passed = false
|
||||
test_error_msg = ret.message || text(ret)
|
||||
if (ret.stack) test_error_stack = ret.stack
|
||||
}
|
||||
} disruption {
|
||||
test_passed = false
|
||||
if (test_error_msg == "") test_error_msg = "test disrupted"
|
||||
}
|
||||
|
||||
function run_tests(package_name, specific_test) {
|
||||
var prefix = get_pkg_dir(package_name)
|
||||
var tests_dir = prefix + '/tests'
|
||||
@@ -293,7 +321,9 @@ function run_tests(package_name, specific_test) {
|
||||
failed: 0
|
||||
}
|
||||
|
||||
try {
|
||||
var load_disrupted = false
|
||||
var load_error_msg = ""
|
||||
var do_load = function() {
|
||||
var test_mod
|
||||
// For local packages (null), use the current directory as package context
|
||||
var use_pkg = package_name ? package_name : fd.realpath('.')
|
||||
@@ -322,34 +352,23 @@ function run_tests(package_name, specific_test) {
|
||||
}
|
||||
|
||||
var start_time = time.number()
|
||||
try {
|
||||
var ret = t.fn()
|
||||
|
||||
if (is_text(ret)) {
|
||||
throw Error(ret)
|
||||
} else if (ret && (is_text(ret.message) || is_proto(ret, Error))) {
|
||||
throw ret
|
||||
}
|
||||
run_test(t.fn)
|
||||
|
||||
if (test_passed) {
|
||||
test_entry.status = "passed"
|
||||
log.console(` PASS ${t.name}`)
|
||||
pkg_result.passed++
|
||||
file_result.passed++
|
||||
} catch (e) {
|
||||
} else {
|
||||
test_entry.status = "failed"
|
||||
test_entry.error = {
|
||||
message: e,
|
||||
stack: e.stack || ""
|
||||
message: test_error_msg
|
||||
}
|
||||
if (e.name) test_entry.error.name = e.name
|
||||
if (test_error_stack) test_entry.error.stack = test_error_stack
|
||||
|
||||
if (is_object(e) && e.message) {
|
||||
test_entry.error.message = e.message
|
||||
}
|
||||
|
||||
log.console(` FAIL ${t.name} ${test_entry.error.message}`)
|
||||
if (test_entry.error.stack) {
|
||||
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
|
||||
log.console(` FAIL ${t.name} ${test_error_msg}`)
|
||||
if (test_error_stack) {
|
||||
log.console(` ${text(array(test_error_stack, '\n'), '\n ')}`)
|
||||
}
|
||||
|
||||
pkg_result.failed++
|
||||
@@ -365,15 +384,18 @@ function run_tests(package_name, specific_test) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log.console(` Error loading ${f}: ${e}`)
|
||||
var test_entry = {
|
||||
} disruption {
|
||||
load_disrupted = true
|
||||
}
|
||||
do_load()
|
||||
if (load_disrupted) {
|
||||
log.console(` Error loading ${f}`)
|
||||
var test_entry = {
|
||||
package: pkg_result.package,
|
||||
test: "load_module",
|
||||
status: "failed",
|
||||
duration_ns: 0,
|
||||
error: { message: `Error loading module: ${e}` }
|
||||
error: { message: `Error loading module: ${f}` }
|
||||
}
|
||||
push(file_result.tests, test_entry)
|
||||
pkg_result.failed++
|
||||
|
||||
352
tests/blob.cm
352
tests/blob.cm
@@ -4,64 +4,65 @@ var os = use('os');
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw Error(message || "Assertion failed");
|
||||
return message || "Assertion failed"
|
||||
}
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
if (actual != expected) {
|
||||
throw Error(message || "Expected " + expected + ", got " + actual);
|
||||
return message || "Expected " + expected + ", got " + actual
|
||||
}
|
||||
}
|
||||
|
||||
function should_disrupt(fn) {
|
||||
var caught = false
|
||||
var wrapper = function() { fn() } disruption { caught = true }
|
||||
wrapper()
|
||||
return caught
|
||||
}
|
||||
|
||||
return {
|
||||
test_create_empty_blob: function() {
|
||||
var b = Blob();
|
||||
assertEqual(length(b), 0, "Empty blob should have length 0");
|
||||
return assertEqual(length(b), 0, "Empty blob should have length 0")
|
||||
},
|
||||
|
||||
|
||||
test_create_blob_with_capacity: function() {
|
||||
var b = Blob(100);
|
||||
assertEqual(length(b), 0, "New blob with capacity should still have length 0");
|
||||
return assertEqual(length(b), 0, "New blob with capacity should still have length 0")
|
||||
},
|
||||
|
||||
|
||||
test_write_and_read_single_bit: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(true);
|
||||
b.write_bit(false);
|
||||
b.write_bit(1);
|
||||
b.write_bit(0);
|
||||
assertEqual(length(b), 4, "Should have 4 bits after writing");
|
||||
|
||||
var r = assertEqual(length(b), 4, "Should have 4 bits after writing")
|
||||
if (r) return r
|
||||
|
||||
stone(b);
|
||||
assertEqual(b.read_logical(0), true, "First bit should be true");
|
||||
assertEqual(b.read_logical(1), false, "Second bit should be false");
|
||||
assertEqual(b.read_logical(2), true, "Third bit should be true (1)");
|
||||
assertEqual(b.read_logical(3), false, "Fourth bit should be false (0)");
|
||||
r = assertEqual(b.read_logical(0), true, "First bit should be true")
|
||||
if (r) return r
|
||||
r = assertEqual(b.read_logical(1), false, "Second bit should be false")
|
||||
if (r) return r
|
||||
r = assertEqual(b.read_logical(2), true, "Third bit should be true (1)")
|
||||
if (r) return r
|
||||
return assertEqual(b.read_logical(3), false, "Fourth bit should be false (0)")
|
||||
},
|
||||
|
||||
|
||||
test_out_of_range_read_throws_error: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(true);
|
||||
stone(b);
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
b.read_logical(100);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "Out of range read should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
b.read_logical(-1);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "Negative index read should throw");
|
||||
|
||||
if (!should_disrupt(function() { b.read_logical(100) }))
|
||||
return "Out of range read should disrupt"
|
||||
|
||||
if (!should_disrupt(function() { b.read_logical(-1) }))
|
||||
return "Negative index read should disrupt"
|
||||
},
|
||||
|
||||
|
||||
test_write_and_read_numbers: function() {
|
||||
var b = Blob();
|
||||
b.write_number(3.14159);
|
||||
@@ -69,41 +70,48 @@ return {
|
||||
b.write_number(0);
|
||||
b.write_number(1e20);
|
||||
stone(b);
|
||||
|
||||
assertEqual(b.read_number(0), 3.14159, "First number should match");
|
||||
assertEqual(b.read_number(64), -42, "Second number should match");
|
||||
assertEqual(b.read_number(128), 0, "Third number should match");
|
||||
assertEqual(b.read_number(192), 1e20, "Fourth number should match");
|
||||
|
||||
var r = assertEqual(b.read_number(0), 3.14159, "First number should match")
|
||||
if (r) return r
|
||||
r = assertEqual(b.read_number(64), -42, "Second number should match")
|
||||
if (r) return r
|
||||
r = assertEqual(b.read_number(128), 0, "Third number should match")
|
||||
if (r) return r
|
||||
return assertEqual(b.read_number(192), 1e20, "Fourth number should match")
|
||||
},
|
||||
|
||||
|
||||
test_write_and_read_text: function() {
|
||||
var b = Blob();
|
||||
b.write_text("Hello");
|
||||
b.write_text("World");
|
||||
b.write_text("🎉");
|
||||
stone(b);
|
||||
|
||||
assertEqual(b.read_text(0), "Hello", "First text should match");
|
||||
|
||||
return assertEqual(b.read_text(0), "Hello", "First text should match")
|
||||
},
|
||||
|
||||
|
||||
test_write_and_read_blobs: function() {
|
||||
var b1 = Blob();
|
||||
b1.write_bit(true);
|
||||
b1.write_bit(false);
|
||||
b1.write_bit(true);
|
||||
|
||||
|
||||
var b2 = Blob(10);
|
||||
b2.write_blob(b1);
|
||||
b2.write_bit(false);
|
||||
assertEqual(length(b2), 4, "Combined blob should have 4 bits");
|
||||
|
||||
var r = assertEqual(length(b2), 4, "Combined blob should have 4 bits")
|
||||
if (r) return r
|
||||
|
||||
stone(b2);
|
||||
assertEqual(b2.read_logical(0), true);
|
||||
assertEqual(b2.read_logical(1), false);
|
||||
assertEqual(b2.read_logical(2), true);
|
||||
assertEqual(b2.read_logical(3), false);
|
||||
r = assertEqual(b2.read_logical(0), true)
|
||||
if (r) return r
|
||||
r = assertEqual(b2.read_logical(1), false)
|
||||
if (r) return r
|
||||
r = assertEqual(b2.read_logical(2), true)
|
||||
if (r) return r
|
||||
return assertEqual(b2.read_logical(3), false)
|
||||
},
|
||||
|
||||
|
||||
test_blob_copy_constructor: function() {
|
||||
var b1 = Blob();
|
||||
b1.write_bit(true);
|
||||
@@ -111,249 +119,219 @@ return {
|
||||
b1.write_bit(true);
|
||||
b1.write_bit(true);
|
||||
stone(b1);
|
||||
|
||||
|
||||
var b2 = Blob(b1);
|
||||
stone(b2);
|
||||
assertEqual(length(b2), 4, "Copied blob should have same length");
|
||||
assertEqual(b2.read_logical(0), true);
|
||||
assertEqual(b2.read_logical(3), true);
|
||||
var r = assertEqual(length(b2), 4, "Copied blob should have same length")
|
||||
if (r) return r
|
||||
r = assertEqual(b2.read_logical(0), true)
|
||||
if (r) return r
|
||||
return assertEqual(b2.read_logical(3), true)
|
||||
},
|
||||
|
||||
|
||||
test_blob_partial_copy_constructor: function() {
|
||||
var b1 = Blob();
|
||||
for (var i = 0; i < 10; i++) {
|
||||
b1.write_bit(i % 2 == 0);
|
||||
}
|
||||
stone(b1);
|
||||
|
||||
|
||||
var b2 = Blob(b1, 2, 7);
|
||||
stone(b2);
|
||||
assertEqual(length(b2), 5, "Partial copy should have 5 bits");
|
||||
assertEqual(b2.read_logical(0), true);
|
||||
assertEqual(b2.read_logical(2), true);
|
||||
var r = assertEqual(length(b2), 5, "Partial copy should have 5 bits")
|
||||
if (r) return r
|
||||
r = assertEqual(b2.read_logical(0), true)
|
||||
if (r) return r
|
||||
return assertEqual(b2.read_logical(2), true)
|
||||
},
|
||||
|
||||
|
||||
test_create_blob_with_fill: function() {
|
||||
var b1 = Blob(8, true);
|
||||
var b2 = Blob(8, false);
|
||||
|
||||
|
||||
stone(b1);
|
||||
stone(b2);
|
||||
|
||||
|
||||
for (var i = 0; i < 8; i++) {
|
||||
assertEqual(b1.read_logical(i), true, "Bit " + i + " should be true");
|
||||
assertEqual(b2.read_logical(i), false, "Bit " + i + " should be false");
|
||||
var r = assertEqual(b1.read_logical(i), true, "Bit " + i + " should be true")
|
||||
if (r) return r
|
||||
r = assertEqual(b2.read_logical(i), false, "Bit " + i + " should be false")
|
||||
if (r) return r
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
test_create_blob_with_random_function: function() {
|
||||
var sequence = [true, false, true, true, false];
|
||||
var index = 0;
|
||||
|
||||
|
||||
var b = Blob(5, function() {
|
||||
return sequence[index++] ? 1 : 0;
|
||||
});
|
||||
|
||||
|
||||
stone(b);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
assertEqual(b.read_logical(i), sequence[i], "Bit " + i + " should match sequence");
|
||||
var r = assertEqual(b.read_logical(i), sequence[i], "Bit " + i + " should match sequence")
|
||||
if (r) return r
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
test_write_pad_and_check_padding: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(true);
|
||||
b.write_bit(false);
|
||||
b.write_bit(true);
|
||||
b.write_pad(8);
|
||||
|
||||
assertEqual(length(b), 8, "Should be padded to 8 bits");
|
||||
|
||||
var r = assertEqual(length(b), 8, "Should be padded to 8 bits")
|
||||
if (r) return r
|
||||
stone(b);
|
||||
|
||||
assert(b['pad?'](3, 8), "Should detect valid padding at position 3");
|
||||
assert(!b['pad?'](2, 8), "Should detect invalid padding at position 2");
|
||||
|
||||
r = assert(b['pad?'](3, 8), "Should detect valid padding at position 3")
|
||||
if (r) return r
|
||||
return assert(!b['pad?'](2, 8), "Should detect invalid padding at position 2")
|
||||
},
|
||||
|
||||
|
||||
test_read_blob_from_stone_blob: function() {
|
||||
var b1 = Blob();
|
||||
for (var i = 0; i < 16; i++) {
|
||||
b1.write_bit(i % 3 == 0);
|
||||
}
|
||||
stone(b1);
|
||||
|
||||
|
||||
var b2 = b1.read_blob(4, 12);
|
||||
stone(b2);
|
||||
assertEqual(length(b2), 8, "Read blob should have 8 bits");
|
||||
|
||||
assertEqual(b2.read_logical(2), true);
|
||||
assertEqual(b2.read_logical(5), true);
|
||||
var r = assertEqual(length(b2), 8, "Read blob should have 8 bits")
|
||||
if (r) return r
|
||||
|
||||
r = assertEqual(b2.read_logical(2), true)
|
||||
if (r) return r
|
||||
return assertEqual(b2.read_logical(5), true)
|
||||
},
|
||||
|
||||
|
||||
test_stone_blob_is_immutable: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(true);
|
||||
stone(b);
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
b.write_bit(false);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "Writing to stone blob should throw error");
|
||||
|
||||
if (!should_disrupt(function() { b.write_bit(false) }))
|
||||
return "Writing to stone blob should disrupt"
|
||||
},
|
||||
|
||||
|
||||
test_multiple_stone_calls_are_safe: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(true);
|
||||
assert(!stone.p(b), "Blob should not be a stone before stone() call");
|
||||
var r = assert(!stone.p(b), "Blob should not be a stone before stone() call")
|
||||
if (r) return r
|
||||
stone(b);
|
||||
assert(stone.p(b), "Blob should be a stone after stone() call");
|
||||
r = assert(stone.p(b), "Blob should be a stone after stone() call")
|
||||
if (r) return r
|
||||
stone(b);
|
||||
assertEqual(b.read_logical(0), true, "Blob data should remain intact");
|
||||
|
||||
assert(b.stone == null, "blob.stone should not be available as a method");
|
||||
r = assertEqual(b.read_logical(0), true, "Blob data should remain intact")
|
||||
if (r) return r
|
||||
|
||||
return assert(b.stone == null, "blob.stone should not be available as a method")
|
||||
},
|
||||
|
||||
|
||||
test_invalid_constructor_arguments: function() {
|
||||
var threw = false;
|
||||
try {
|
||||
var b = Blob("invalid");
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "Invalid constructor arguments should throw");
|
||||
if (!should_disrupt(function() { Blob("invalid") }))
|
||||
return "Invalid constructor arguments should disrupt"
|
||||
},
|
||||
|
||||
|
||||
test_write_bit_validation: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(0);
|
||||
b.write_bit(1);
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
b.write_bit(2);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "write_bit with value 2 should throw");
|
||||
|
||||
if (!should_disrupt(function() { b.write_bit(2) }))
|
||||
return "write_bit with value 2 should disrupt"
|
||||
},
|
||||
|
||||
|
||||
test_complex_data_round_trip: function() {
|
||||
var b = Blob();
|
||||
|
||||
|
||||
b.write_text("Test");
|
||||
b.write_number(123.456);
|
||||
b.write_bit(true);
|
||||
b.write_bit(false);
|
||||
b.write_number(-999.999);
|
||||
|
||||
|
||||
var originalLength = length(b);
|
||||
stone(b);
|
||||
|
||||
|
||||
var b2 = Blob(b);
|
||||
stone(b2);
|
||||
assertEqual(length(b2), originalLength, "Copy should have same length");
|
||||
assertEqual(b2.read_text(0), "Test", "First text should match");
|
||||
var r = assertEqual(length(b2), originalLength, "Copy should have same length")
|
||||
if (r) return r
|
||||
return assertEqual(b2.read_text(0), "Test", "First text should match")
|
||||
},
|
||||
|
||||
|
||||
test_zero_capacity_blob: function() {
|
||||
var b = Blob(0);
|
||||
assertEqual(length(b), 0, "Zero capacity blob should have length 0");
|
||||
var r = assertEqual(length(b), 0, "Zero capacity blob should have length 0")
|
||||
if (r) return r
|
||||
b.write_bit(true);
|
||||
assertEqual(length(b), 1, "Should expand when writing");
|
||||
return assertEqual(length(b), 1, "Should expand when writing")
|
||||
},
|
||||
|
||||
|
||||
test_large_blob_handling: function() {
|
||||
var b = Blob();
|
||||
var testSize = 1000;
|
||||
|
||||
|
||||
for (var i = 0; i < testSize; i++) {
|
||||
b.write_bit(i % 7 == 0);
|
||||
}
|
||||
|
||||
assertEqual(length(b), testSize, "Should have " + testSize + " bits");
|
||||
|
||||
var r = assertEqual(length(b), testSize, "Should have " + testSize + " bits")
|
||||
if (r) return r
|
||||
stone(b);
|
||||
|
||||
assertEqual(b.read_logical(0), true, "Bit 0 should be true");
|
||||
assertEqual(b.read_logical(7), true, "Bit 7 should be true");
|
||||
assertEqual(b.read_logical(14), true, "Bit 14 should be true");
|
||||
assertEqual(b.read_logical(15), false, "Bit 15 should be false");
|
||||
|
||||
r = assertEqual(b.read_logical(0), true, "Bit 0 should be true")
|
||||
if (r) return r
|
||||
r = assertEqual(b.read_logical(7), true, "Bit 7 should be true")
|
||||
if (r) return r
|
||||
r = assertEqual(b.read_logical(14), true, "Bit 14 should be true")
|
||||
if (r) return r
|
||||
return assertEqual(b.read_logical(15), false, "Bit 15 should be false")
|
||||
},
|
||||
|
||||
|
||||
test_non_stone_blob_read_methods_should_throw: function() {
|
||||
var b = Blob();
|
||||
b.write_bit(true);
|
||||
b.write_number(42);
|
||||
b.write_text("test");
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
b.read_logical(0);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "read_logical on non-stone blob should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
b.read_number(0);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "read_number on non-stone blob should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
b.read_text(0);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "read_text on non-stone blob should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
b.read_blob(0, 10);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "read_blob on non-stone blob should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
b['pad?'](0, 8);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "pad? on non-stone blob should throw");
|
||||
|
||||
if (!should_disrupt(function() { b.read_logical(0) }))
|
||||
return "read_logical on non-stone blob should disrupt"
|
||||
|
||||
if (!should_disrupt(function() { b.read_number(0) }))
|
||||
return "read_number on non-stone blob should disrupt"
|
||||
|
||||
if (!should_disrupt(function() { b.read_text(0) }))
|
||||
return "read_text on non-stone blob should disrupt"
|
||||
|
||||
if (!should_disrupt(function() { b.read_blob(0, 10) }))
|
||||
return "read_blob on non-stone blob should disrupt"
|
||||
|
||||
if (!should_disrupt(function() { b['pad?'](0, 8) }))
|
||||
return "pad? on non-stone blob should disrupt"
|
||||
},
|
||||
|
||||
|
||||
test_empty_text_write_and_read: function() {
|
||||
var b = Blob();
|
||||
b.write_text("");
|
||||
stone(b);
|
||||
assertEqual(b.read_text(0), "", "Empty string should round-trip");
|
||||
return assertEqual(b.read_text(0), "", "Empty string should round-trip")
|
||||
},
|
||||
|
||||
|
||||
test_invalid_read_positions: function() {
|
||||
var b = Blob();
|
||||
b.write_number(42);
|
||||
stone(b);
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
b.read_number(-10);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "Negative position should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
b.read_number(1000);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "Position beyond length should throw");
|
||||
|
||||
if (!should_disrupt(function() { b.read_number(-10) }))
|
||||
return "Negative position should disrupt"
|
||||
|
||||
if (!should_disrupt(function() { b.read_number(1000) }))
|
||||
return "Position beyond length should disrupt"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
return {
|
||||
test_disrupt: function() {
|
||||
throw 1
|
||||
disrupt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,85 +2,101 @@ var fd = use("fd")
|
||||
var miniz = use("miniz")
|
||||
var utf8 = use("utf8")
|
||||
|
||||
function safe_unlink(p) { fd.unlink(p) } disruption {}
|
||||
|
||||
return {
|
||||
create_and_read_zip: function() {
|
||||
var ZIP_PATH = "miniz_test.zip"
|
||||
var SOURCE_PATH = "miniz_source.txt"
|
||||
var ENTRY_PATH = "sample/hello.txt"
|
||||
var PAYLOAD = "Miniz integration test payload."
|
||||
|
||||
|
||||
function write_text_file(path, text) {
|
||||
var handle = fd.open(path, "w")
|
||||
fd.write(handle, text)
|
||||
fd.close(handle)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
var error_msg = null
|
||||
var do_test = function() {
|
||||
write_text_file(SOURCE_PATH, PAYLOAD)
|
||||
var source_blob = fd.slurp(SOURCE_PATH)
|
||||
var writer = miniz.write(ZIP_PATH)
|
||||
writer.add_file(ENTRY_PATH, source_blob)
|
||||
writer = null
|
||||
|
||||
|
||||
var zip_blob = fd.slurp(ZIP_PATH)
|
||||
var reader = miniz.read(zip_blob)
|
||||
|
||||
|
||||
if (!reader.exists(ENTRY_PATH))
|
||||
throw "entry missing in archive"
|
||||
|
||||
var extracted_blob = reader.slurp(ENTRY_PATH)
|
||||
var extracted_text = utf8.decode(extracted_blob)
|
||||
|
||||
if (extracted_text != PAYLOAD)
|
||||
throw "extracted text mismatch"
|
||||
} finally {
|
||||
try { fd.unlink(ZIP_PATH) } catch(e) {}
|
||||
try { fd.unlink(SOURCE_PATH) } catch(e) {}
|
||||
error_msg = "entry missing in archive"
|
||||
|
||||
if (!error_msg) {
|
||||
var extracted_blob = reader.slurp(ENTRY_PATH)
|
||||
var extracted_text = utf8.decode(extracted_blob)
|
||||
|
||||
if (extracted_text != PAYLOAD)
|
||||
error_msg = "extracted text mismatch"
|
||||
}
|
||||
} disruption {
|
||||
if (!error_msg) error_msg = "test disrupted"
|
||||
}
|
||||
do_test()
|
||||
safe_unlink(ZIP_PATH)
|
||||
safe_unlink(SOURCE_PATH)
|
||||
if (error_msg) return error_msg
|
||||
},
|
||||
|
||||
|
||||
list_and_count: function() {
|
||||
var ZIP_PATH = "miniz_list_test.zip"
|
||||
var ENTRY1 = "file1.txt"
|
||||
var ENTRY2 = "dir/file2.txt"
|
||||
|
||||
try {
|
||||
|
||||
var error_msg = null
|
||||
var do_test = function() {
|
||||
var writer = miniz.write(ZIP_PATH)
|
||||
writer.add_file(ENTRY1, utf8.encode("content1"))
|
||||
writer.add_file(ENTRY2, utf8.encode("content2"))
|
||||
writer = null
|
||||
|
||||
|
||||
var zip_blob = fd.slurp(ZIP_PATH)
|
||||
var reader = miniz.read(zip_blob)
|
||||
|
||||
|
||||
var listed = reader.list()
|
||||
if (length(listed) != reader.count())
|
||||
throw "list/count mismatch"
|
||||
if (length(listed) != 2)
|
||||
throw "unexpected entry count"
|
||||
} finally {
|
||||
try { fd.unlink(ZIP_PATH) } catch(e) {}
|
||||
error_msg = "list/count mismatch"
|
||||
if (!error_msg && length(listed) != 2)
|
||||
error_msg = "unexpected entry count"
|
||||
} disruption {
|
||||
if (!error_msg) error_msg = "test disrupted"
|
||||
}
|
||||
do_test()
|
||||
safe_unlink(ZIP_PATH)
|
||||
if (error_msg) return error_msg
|
||||
},
|
||||
|
||||
|
||||
exists_check: function() {
|
||||
var ZIP_PATH = "miniz_exists_test.zip"
|
||||
var ENTRY_PATH = "existing.txt"
|
||||
|
||||
try {
|
||||
|
||||
var error_msg = null
|
||||
var do_test = function() {
|
||||
var writer = miniz.write(ZIP_PATH)
|
||||
writer.add_file(ENTRY_PATH, utf8.encode("data"))
|
||||
writer = null
|
||||
|
||||
|
||||
var zip_blob = fd.slurp(ZIP_PATH)
|
||||
var reader = miniz.read(zip_blob)
|
||||
|
||||
|
||||
if (!reader.exists(ENTRY_PATH))
|
||||
throw "existing entry not found"
|
||||
if (reader.exists("nonexistent.txt"))
|
||||
throw "nonexistent entry reported as existing"
|
||||
} finally {
|
||||
try { fd.unlink(ZIP_PATH) } catch(e) {}
|
||||
error_msg = "existing entry not found"
|
||||
if (!error_msg && reader.exists("nonexistent.txt"))
|
||||
error_msg = "nonexistent entry reported as existing"
|
||||
} disruption {
|
||||
if (!error_msg) error_msg = "test disrupted"
|
||||
}
|
||||
do_test()
|
||||
safe_unlink(ZIP_PATH)
|
||||
if (error_msg) return error_msg
|
||||
}
|
||||
}
|
||||
|
||||
1820
tests/suite.cm
1820
tests/suite.cm
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
var cmds = {
|
||||
stop: $stop,
|
||||
disrupt: _ => {
|
||||
$delay(_ => { throw Error() }, 0.5)
|
||||
$delay(_ => { disrupt }, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
tests/use.cm
11
tests/use.cm
@@ -1,5 +1,10 @@
|
||||
try {
|
||||
var load_disrupted = false
|
||||
var do_load = function() {
|
||||
var u = use('tests/use')
|
||||
} catch(e) {
|
||||
log.console(e)
|
||||
} disruption {
|
||||
load_disrupted = true
|
||||
}
|
||||
do_load()
|
||||
if (load_disrupted) {
|
||||
log.console("use self-load disrupted")
|
||||
}
|
||||
|
||||
202
vm_suite.ce
202
vm_suite.ce
@@ -4,27 +4,33 @@
|
||||
|
||||
var passed = 0
|
||||
var failed = 0
|
||||
var errors = []
|
||||
var error_names = []
|
||||
var error_reasons = []
|
||||
var fail_msg = ""
|
||||
|
||||
// pre-allocate 500 slots to avoid array growth during disruption handlers
|
||||
for (var _i = 0; _i < 5; _i++) {
|
||||
error_names[] = null
|
||||
error_reasons[] = null
|
||||
}
|
||||
|
||||
var fail = function(msg) {
|
||||
fail_msg = msg
|
||||
print("failed: " + msg)
|
||||
disrupt
|
||||
}
|
||||
|
||||
var assert_eq = function(actual, expected, msg) {
|
||||
if (actual != expected) fail(msg + " (expected=" + text(expected) + " got=" + text(actual) + ")")
|
||||
if (actual != expected) fail(msg + " (got=" + text(actual) + ")")
|
||||
}
|
||||
|
||||
var run = function(name, fn) {
|
||||
fail_msg = ""
|
||||
fn()
|
||||
passed = passed + 1
|
||||
print("passed " + name)
|
||||
} disruption {
|
||||
error_names[failed] = name
|
||||
error_reasons[failed] = fail_msg == "" ? "disruption" : fail_msg
|
||||
failed = failed + 1
|
||||
errors[] = name
|
||||
}
|
||||
|
||||
var should_disrupt = function(fn) {
|
||||
@@ -3363,14 +3369,190 @@ run("gc object from keys function under pressure", function() {
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// DEFAULT PARAMETER TESTS
|
||||
// ============================================================================
|
||||
|
||||
run("default param constant", function() {
|
||||
var f = function(a, b=10) { return a + b }
|
||||
assert_eq(f(1), 11, "default param constant")
|
||||
})
|
||||
|
||||
run("default param overridden", function() {
|
||||
var f = function(a, b=10) { return a + b }
|
||||
assert_eq(f(1, 2), 3, "default param overridden")
|
||||
})
|
||||
|
||||
run("default param uses earlier param", function() {
|
||||
var f = function(a, b=a+1) { return b }
|
||||
assert_eq(f(5), 6, "default param uses earlier param")
|
||||
})
|
||||
|
||||
run("default param uses earlier param overridden", function() {
|
||||
var f = function(a, b=a+1) { return b }
|
||||
assert_eq(f(5, 20), 20, "default param uses earlier param overridden")
|
||||
})
|
||||
|
||||
run("multiple default params", function() {
|
||||
var f = function(a, b=10, c=a+1) { return a + b + c }
|
||||
assert_eq(f(1), 13, "multiple defaults f(1)")
|
||||
assert_eq(f(1, 2), 5, "multiple defaults f(1,2)")
|
||||
assert_eq(f(1, 2, 3), 6, "multiple defaults f(1,2,3)")
|
||||
})
|
||||
|
||||
run("arrow function default param", function() {
|
||||
var g = (x, y=100) => x + y
|
||||
assert_eq(g(5), 105, "arrow default param")
|
||||
assert_eq(g(5, 20), 25, "arrow default param overridden")
|
||||
})
|
||||
|
||||
run("default param null passed explicitly", function() {
|
||||
var f = function(a, b=10) { return b }
|
||||
assert_eq(f(1, null), 10, "explicit null triggers default")
|
||||
})
|
||||
|
||||
run("default param with string", function() {
|
||||
var f = function(a, b="hello") { return b }
|
||||
assert_eq(f(1), "hello", "default string param")
|
||||
assert_eq(f(1, "world"), "world", "default string overridden")
|
||||
})
|
||||
|
||||
run("default param first param has no default", function() {
|
||||
var f = function(a, b=42) { return a }
|
||||
assert_eq(f(7), 7, "first param no default")
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// FUNCTINO TESTS
|
||||
// ============================================================================
|
||||
|
||||
run("functino +! addition", function() {
|
||||
assert_eq(+!(3, 4), 7, "+! addition")
|
||||
})
|
||||
|
||||
run("functino -! subtraction", function() {
|
||||
assert_eq(-!(10, 3), 7, "-! subtraction")
|
||||
})
|
||||
|
||||
run("functino *! multiplication", function() {
|
||||
assert_eq(*!(5, 6), 30, "*! multiplication")
|
||||
})
|
||||
|
||||
run("functino /! division", function() {
|
||||
assert_eq(/!(10, 2), 5, "/! division")
|
||||
})
|
||||
|
||||
run("functino %! modulo", function() {
|
||||
assert_eq(%!(10, 3), 1, "%! modulo")
|
||||
})
|
||||
|
||||
run("functino **! power", function() {
|
||||
assert_eq(**!(2, 10), 1024, "**! power")
|
||||
})
|
||||
|
||||
run("functino <! less than", function() {
|
||||
assert_eq(<!(3, 5), true, "<! true")
|
||||
assert_eq(<!(5, 3), false, "<! false")
|
||||
assert_eq(<!(3, 3), false, "<! equal")
|
||||
})
|
||||
|
||||
run("functino >! greater than", function() {
|
||||
assert_eq(>!(5, 3), true, ">! true")
|
||||
assert_eq(>!(3, 5), false, ">! false")
|
||||
assert_eq(>!(3, 3), false, ">! equal")
|
||||
})
|
||||
|
||||
run("functino <=! less or equal", function() {
|
||||
assert_eq(<=!(3, 5), true, "<=! less")
|
||||
assert_eq(<=!(3, 3), true, "<=! equal")
|
||||
assert_eq(<=!(5, 3), false, "<=! greater")
|
||||
})
|
||||
|
||||
run("functino >=! greater or equal", function() {
|
||||
assert_eq(>=!(5, 3), true, ">=! greater")
|
||||
assert_eq(>=!(3, 3), true, ">=! equal")
|
||||
assert_eq(>=!(3, 5), false, ">=! less")
|
||||
})
|
||||
|
||||
run("functino =! equality", function() {
|
||||
assert_eq(=!(5, 5), true, "=! true")
|
||||
assert_eq(=!(5, 3), false, "=! false")
|
||||
})
|
||||
|
||||
run("functino !=! inequality", function() {
|
||||
assert_eq(!=!(5, 3), true, "!=! true")
|
||||
assert_eq(!=!(5, 5), false, "!=! false")
|
||||
})
|
||||
|
||||
run("functino =! with tolerance", function() {
|
||||
assert_eq(=!(1.0, 1.0001, 0.001), true, "=! within tolerance")
|
||||
assert_eq(=!(1.0, 1.01, 0.001), false, "=! outside tolerance")
|
||||
})
|
||||
|
||||
run("functino !=! with tolerance", function() {
|
||||
assert_eq(!=!(1.0, 1.01, 0.001), true, "!=! outside tolerance")
|
||||
assert_eq(!=!(1.0, 1.0001, 0.001), false, "!=! within tolerance")
|
||||
})
|
||||
|
||||
run("functino &! bitwise and", function() {
|
||||
assert_eq(&!(0xff, 0x0f), 0x0f, "&! bitwise and")
|
||||
})
|
||||
|
||||
run("functino |! bitwise or", function() {
|
||||
assert_eq(|!(0xf0, 0x0f), 0xff, "|! bitwise or")
|
||||
})
|
||||
|
||||
run("functino ^! bitwise xor", function() {
|
||||
assert_eq(^!(0xff, 0x0f), 0xf0, "^! bitwise xor")
|
||||
})
|
||||
|
||||
run("functino <<! shift left", function() {
|
||||
assert_eq(<<!(1, 4), 16, "<<! shift left")
|
||||
})
|
||||
|
||||
run("functino >>! shift right", function() {
|
||||
assert_eq(>>!(16, 4), 1, ">>! shift right")
|
||||
})
|
||||
|
||||
run("functino ~! bitwise not", function() {
|
||||
assert_eq(~!(0), -1, "~! bitwise not 0")
|
||||
})
|
||||
|
||||
run("functino []! array index", function() {
|
||||
var arr = [10, 20, 30]
|
||||
assert_eq([]!(arr, 0), 10, "[]! index 0")
|
||||
assert_eq([]!(arr, 1), 20, "[]! index 1")
|
||||
assert_eq([]!(arr, 2), 30, "[]! index 2")
|
||||
})
|
||||
|
||||
run("functino &&! logical and", function() {
|
||||
assert_eq(&&!(true, true), true, "&&! true true")
|
||||
assert_eq(&&!(true, false), false, "&&! true false")
|
||||
assert_eq(&&!(false, true), false, "&&! false true")
|
||||
assert_eq(&&!(1, 2), 2, "&&! truthy returns right")
|
||||
assert_eq(&&!(0, 2), 0, "&&! falsy returns left")
|
||||
})
|
||||
|
||||
run("functino ||! logical or", function() {
|
||||
assert_eq(||!(false, true), true, "||! false true")
|
||||
assert_eq(||!(true, false), true, "||! true false")
|
||||
assert_eq(||!(false, false), false, "||! false false")
|
||||
assert_eq(||!(0, 5), 5, "||! falsy returns right")
|
||||
assert_eq(||!(3, 5), 3, "||! truthy returns left")
|
||||
})
|
||||
|
||||
run("functino >>>! unsigned shift right", function() {
|
||||
assert_eq(>>>!(-1, 28), 15, ">>>! unsigned shift right")
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// SUMMARY
|
||||
// ============================================================================
|
||||
|
||||
print(`\nResults: ${passed} passed, ${failed} failed out of ${passed + failed}`)
|
||||
print(text(passed) + " passed, " + text(failed) + " failed out of " + text(passed + failed))
|
||||
if (failed > 0) {
|
||||
print("Failed tests:")
|
||||
arrfor(errors, function(name) {
|
||||
print(` - ${name}`)
|
||||
})
|
||||
print("")
|
||||
for (var _j = 0; _j < failed; _j++) {
|
||||
print(" FAIL " + error_names[_j] + ": " + error_reasons[_j])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user