Compare commits
58 Commits
8cf98d8a9e
...
internals
| 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 | ||
|
|
83ea67c01b | ||
|
|
16059cca4e | ||
|
|
9ffe60ebef | ||
|
|
2beafec5d9 | ||
|
|
aba8eb66bd | ||
|
|
1abcaa92c7 | ||
|
|
168f7c71d5 | ||
|
|
56ed895b6e | ||
|
|
1e4646999d | ||
|
|
68d6c907fe | ||
|
|
8150c64c7d | ||
|
|
024d796ca4 | ||
|
|
ea185dbffd | ||
|
|
6571262af0 | ||
|
|
77ae133747 | ||
|
|
142a2d518b | ||
|
|
5b65c64fe5 | ||
|
|
e985fa5fe1 | ||
|
|
160ade2410 | ||
|
|
e2bc5948c1 |
@@ -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);
|
||||
|
||||
287
docs/pitcode.md
Normal file
287
docs/pitcode.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Pitmachine
|
||||
A pitmachine is an abstract register machine for executing pitcode.
|
||||
|
||||
The pitmachine assembly language is JSON. The instructions field contains an array of labels and instructions. Labels are the targets of branch instructions. They are simple text. Slots are elements in an activation frame. They are designated by a small positive integer. Slots are the general registers of the pitmachine. Slots hold the arguments, variables, and temporaries of a function invocation.
|
||||
|
||||
{
|
||||
"name": "program_name",
|
||||
"data": {⸳⸳⸳},
|
||||
"source": "...",
|
||||
The labels record associates labels with tokens for use by debuggers.
|
||||
|
||||
"labels": {
|
||||
"entry": token,
|
||||
"beyond": token,
|
||||
...
|
||||
},
|
||||
The instructions array is a list of instructions and labels.
|
||||
|
||||
instructions: [
|
||||
A statement label:
|
||||
|
||||
"entry",
|
||||
go to beyond:
|
||||
|
||||
["jump", "beyond"],
|
||||
assign slot 8: pi / 2
|
||||
|
||||
["access", 13, {"kind": "name", "name": "pi", "make": "intrinsic", ⸳⸳⸳}],
|
||||
["int", 14, 2],
|
||||
["divide", 8, 13, 14],
|
||||
⸳⸳⸳
|
||||
"beyond"
|
||||
⸳⸳⸳
|
||||
]
|
||||
}
|
||||
|
||||
## Pitcode instructions
|
||||
This is a register based machine.
|
||||
|
||||
### Arithmetic
|
||||
Arithmetic instructions perform operations on numbers. All other cases disrupt (except add, which is polymorphic).
|
||||
|
||||
"add", dest, left, right // dest, left, and right are numbers designating slots in the current activation frame. Also works on texts (concatenation).
|
||||
|
||||
"subtract", dest, left, right
|
||||
|
||||
"multiply", dest, left, right
|
||||
|
||||
"divide", dest, left, right
|
||||
|
||||
"integer_divide", dest, left, right
|
||||
|
||||
"modulo", dest, left, right
|
||||
|
||||
"remainder", dest, left, right
|
||||
|
||||
"max", dest, left, right
|
||||
|
||||
"min", dest, left, right
|
||||
|
||||
"abs", dest, right
|
||||
|
||||
"neg", dest, right
|
||||
|
||||
"sign", dest, right
|
||||
|
||||
"fraction", dest, right
|
||||
|
||||
"integer", dest, right
|
||||
|
||||
"ceiling", dest, right, place
|
||||
|
||||
"floor", dest, right, place
|
||||
|
||||
"round", dest, right, place
|
||||
|
||||
"trunc", dest, right, place
|
||||
|
||||
### Text
|
||||
Text instructions perform operations on texts. All other cases disrupt.
|
||||
|
||||
"concat", dest, left, right // concat two texts
|
||||
|
||||
"concat_space", dest, left, right
|
||||
|
||||
"character", dest, right
|
||||
|
||||
"codepoint", dest, right
|
||||
|
||||
"length", dest, right
|
||||
|
||||
"lower", dest, right
|
||||
|
||||
"upper", dest, right
|
||||
|
||||
"append", pretext, right
|
||||
|
||||
Append the right text to the pretext, forwarding and growing its capacity if necessary.
|
||||
|
||||
### Comparison
|
||||
Comparison instructions perform operations on texts or numbers. All other cases disrupt.
|
||||
|
||||
"eq", dest, left, right
|
||||
|
||||
"ne", dest, left, right
|
||||
|
||||
"lt", dest, left, right
|
||||
|
||||
"le", dest, left, right
|
||||
|
||||
"gt", dest, left, right
|
||||
|
||||
"ge", dest, left, right
|
||||
|
||||
### Logical
|
||||
|
||||
"not", dest, right
|
||||
|
||||
### Bitwise
|
||||
Bitwise instructions convert operands to 32-bit integers. Non-numbers disrupt.
|
||||
|
||||
"bitand", dest, left, right
|
||||
|
||||
"bitor", dest, left, right
|
||||
|
||||
"bitxor", dest, left, right
|
||||
|
||||
"bitnot", dest, right
|
||||
|
||||
"shl", dest, left, right
|
||||
|
||||
"shr", dest, left, right
|
||||
|
||||
"ushr", dest, left, right
|
||||
|
||||
### Function
|
||||
|
||||
"frame", dest, func, nr_args
|
||||
|
||||
Prepare to invoke the func object. If the nr_args is too large, disrupt. Allocate the new activation frame. Put the current frame pointer into it.
|
||||
|
||||
"goframe", dest, func, nr_args
|
||||
|
||||
Same as frame, except that the current frame is reused if it is large enough.
|
||||
|
||||
"invoke", frame
|
||||
|
||||
Store the next instruction address in the current frame. Make the new frame the current frame. Jump to the entry point.
|
||||
|
||||
"goinvoke", frame
|
||||
|
||||
"apply", func, array
|
||||
|
||||
"return", value
|
||||
|
||||
"return_value", dest
|
||||
|
||||
"setarg", frame, slot, value // set the slot of frame to value
|
||||
|
||||
### Branching
|
||||
|
||||
"jump", label
|
||||
|
||||
"jump_true", slot, label
|
||||
|
||||
If the value in the slot is true, jump to the label. Otherwise, continue with the next instruction.
|
||||
|
||||
"jump_false": slot, label
|
||||
|
||||
If the value in the slot is false, jump to the label. Otherwise, continue with the next instruction.
|
||||
|
||||
"jump_null": slot, label
|
||||
|
||||
"jump_empty": slot, label
|
||||
|
||||
"wary_true", slot, label
|
||||
|
||||
If the value in the slot is true, jump to the label. If the value is false, continue with the next instruction. Otherwise disrupt because of a type error.
|
||||
|
||||
"wary_false": slot, label
|
||||
|
||||
If the value in the slot is false, jump to the label. If the value is true, continue with the next instruction. Otherwise disrupt because of a type error.
|
||||
|
||||
### Sensory
|
||||
|
||||
Does the right slot contain a value of the indicated type?
|
||||
|
||||
"array?", dest, right
|
||||
|
||||
"blob?", dest, right
|
||||
|
||||
"character?", dest, right
|
||||
|
||||
"data?", dest, right
|
||||
|
||||
"digit?", dest, right
|
||||
|
||||
"false?", dest, right
|
||||
|
||||
"fit?", dest, right
|
||||
|
||||
"function?", dest, right
|
||||
|
||||
"integer?", dest, right
|
||||
|
||||
"letter?", dest, right
|
||||
|
||||
"logical?", dest, right
|
||||
|
||||
"null?", dest, right
|
||||
|
||||
"pattern?", dest, right
|
||||
|
||||
"record?", dest, right
|
||||
|
||||
"stone?", dest, right
|
||||
|
||||
"text?", dest, right
|
||||
|
||||
"true?", dest, right
|
||||
|
||||
"upper?", dest, right
|
||||
|
||||
"whitespace?", dest, right
|
||||
|
||||
### Potpourri
|
||||
|
||||
"stone", dest, right // stone an object
|
||||
|
||||
"true", dest
|
||||
|
||||
"false", dest
|
||||
|
||||
"null", dest
|
||||
|
||||
"move", dest, right
|
||||
|
||||
"int", dest, small_int
|
||||
|
||||
"access", dest, literal
|
||||
|
||||
This is used to access values (numbers, texts) from the program's immutable memory. The literal is a number or text.
|
||||
|
||||
"load", dest, object, subscript
|
||||
|
||||
This is used to load values from records and arrays.
|
||||
|
||||
"store", dest, object, subscript
|
||||
|
||||
This is used to store values into records and arrays.
|
||||
|
||||
"delete", object, subscript
|
||||
|
||||
used to delete a field from a record.
|
||||
|
||||
"get", dest, slot, level
|
||||
|
||||
This is used to get values from slots in outer frames.
|
||||
|
||||
"put", value, slot, level
|
||||
|
||||
This is used to store values into slots in outer frames.
|
||||
|
||||
"push", array, slot
|
||||
|
||||
Append to a mutable array.
|
||||
|
||||
"pop", dest, array
|
||||
|
||||
Remove the last element of a mutable array, putting the element into dest.
|
||||
|
||||
"disrupt"
|
||||
|
||||
### Make
|
||||
|
||||
"array", dest, nr_elements
|
||||
|
||||
"blob", dest, nr_bits
|
||||
|
||||
"function", dest, code_name_text
|
||||
|
||||
"pretext", dest, nr_characters
|
||||
|
||||
A pretext must be converted to text by stone before it can leave the function scope.
|
||||
|
||||
"record", dest, nr_elements
|
||||
|
||||
@@ -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);
|
||||
|
||||
240
source/cell.c
240
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,45 +601,58 @@ 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);
|
||||
const char *str = JS_ToCString(ctx, exc);
|
||||
if (str) { printf("Exception: %s\n", str); JS_FreeCString(ctx, str); }
|
||||
if (str) { printf("Error: %s\n", str); JS_FreeCString(ctx, str); }
|
||||
cJSON *stack = JS_GetStack(ctx);
|
||||
if (stack) {
|
||||
int n = cJSON_GetArraySize(stack);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *fr = cJSON_GetArrayItem(stack, i);
|
||||
const char *fn = cJSON_GetStringValue(cJSON_GetObjectItem(fr, "function"));
|
||||
const char *file = cJSON_GetStringValue(cJSON_GetObjectItem(fr, "file"));
|
||||
int line = (int)cJSON_GetNumberValue(cJSON_GetObjectItem(fr, "line"));
|
||||
int col = (int)cJSON_GetNumberValue(cJSON_GetObjectItem(fr, "column"));
|
||||
printf(" at %s (%s:%d:%d)\n", fn ? fn : "<anonymous>", file ? file : "<unknown>", line, col);
|
||||
}
|
||||
cJSON_Delete(stack);
|
||||
}
|
||||
JS_FreeValue(ctx, exc);
|
||||
} else if (!JS_IsNull(result)) {
|
||||
const char *str = JS_ToCString(ctx, result);
|
||||
if (str) { printf("%s\n", str); JS_FreeCString(ctx, str); }
|
||||
@@ -615,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;
|
||||
}
|
||||
@@ -631,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);
|
||||
@@ -679,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;
|
||||
}
|
||||
@@ -695,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)) {
|
||||
@@ -716,6 +794,20 @@ int cell_init(int argc, char **argv)
|
||||
printf("Error: %s\n", err_str);
|
||||
JS_FreeCString(ctx, err_str);
|
||||
}
|
||||
cJSON *stack = JS_GetStack(ctx);
|
||||
if (stack) {
|
||||
int n = cJSON_GetArraySize(stack);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *fr = cJSON_GetArrayItem(stack, i);
|
||||
const char *fn = cJSON_GetStringValue(cJSON_GetObjectItem(fr, "function"));
|
||||
const char *file = cJSON_GetStringValue(cJSON_GetObjectItem(fr, "file"));
|
||||
int line = (int)cJSON_GetNumberValue(cJSON_GetObjectItem(fr, "line"));
|
||||
int col = (int)cJSON_GetNumberValue(cJSON_GetObjectItem(fr, "column"));
|
||||
printf(" at %s (%s:%d:%d)\n", fn ? fn : "<anonymous>", file ? file : "<unknown>", line, col);
|
||||
}
|
||||
cJSON_Delete(stack);
|
||||
}
|
||||
JS_FreeValue(ctx, exc);
|
||||
exit_code = 1;
|
||||
} else if (!JS_IsNull(result)) {
|
||||
const char *str = JS_ToCString(ctx, 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,
|
||||
|
||||
7386
source/quickjs.c
7386
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,48 +1218,65 @@ 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);
|
||||
|
||||
/* Get stack trace as cJSON array of frame objects.
|
||||
Returns NULL if no register VM frame is active.
|
||||
Caller must call cJSON_Delete() on the result. */
|
||||
struct cJSON *JS_GetStack (JSContext *ctx);
|
||||
|
||||
/* Integrate a CellModule with an environment and execute.
|
||||
Returns callable function value, or JS_EXCEPTION on error. */
|
||||
JSValue cell_module_integrate (JSContext *ctx, CellModule *mod, JSValue env);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
39
tests/demo.ce
Normal file
39
tests/demo.ce
Normal file
@@ -0,0 +1,39 @@
|
||||
function safe_add(a, b) {
|
||||
return a + b
|
||||
} disruption {
|
||||
print("disruption caught in safe_add")
|
||||
}
|
||||
|
||||
function inner() {
|
||||
disrupt
|
||||
}
|
||||
|
||||
function outer() {
|
||||
inner()
|
||||
} disruption {
|
||||
print("disruption caught in outer — from inner()")
|
||||
}
|
||||
|
||||
// Test 1: explicit disrupt with handler
|
||||
function test_explicit() {
|
||||
disrupt
|
||||
} disruption {
|
||||
print("test 1: explicit disrupt handled")
|
||||
}
|
||||
test_explicit()
|
||||
|
||||
// Test 2: type error disrupt (number + function)
|
||||
safe_add(1, print)
|
||||
|
||||
// Test 3: unwinding — inner disrupts, outer catches
|
||||
outer()
|
||||
|
||||
// Test 4: disrupt from inside disruption clause
|
||||
function test_nested() {
|
||||
disrupt
|
||||
} disruption {
|
||||
print("test 4: first disruption")
|
||||
}
|
||||
test_nested()
|
||||
|
||||
print("done")
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
3558
vm_suite.ce
Normal file
3558
vm_suite.ce
Normal file
File diff suppressed because it is too large
Load Diff
@@ -252,29 +252,21 @@ loop: for (var m = 0; m < 3; m++) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- try/catch/finally ---
|
||||
var tc
|
||||
try {
|
||||
throw "error"
|
||||
} catch (e) {
|
||||
tc = e
|
||||
// --- disrupt and disruption ---
|
||||
function disrupt_test() {
|
||||
disrupt
|
||||
}
|
||||
|
||||
var tcf = 0
|
||||
try {
|
||||
throw "err"
|
||||
} catch (e) {
|
||||
tcf = 1
|
||||
} finally {
|
||||
tcf += 10
|
||||
function disruption_test() {
|
||||
var x = 1
|
||||
} disruption {
|
||||
var y = 2
|
||||
}
|
||||
|
||||
// --- try/finally (no catch) ---
|
||||
var tf = 0
|
||||
try {
|
||||
tf = 1
|
||||
} finally {
|
||||
tf += 1
|
||||
function disrupt_with_disruption() {
|
||||
disrupt
|
||||
} disruption {
|
||||
var handled = true
|
||||
}
|
||||
|
||||
// --- delete operator ---
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
try { 1 } catch(e) { 2 }
|
||||
@@ -1 +0,0 @@
|
||||
var x = 1; try { throw 0 } catch(e) { x = 2 } finally { x = x + 1 }; x
|
||||
@@ -1 +0,0 @@
|
||||
var x = 1; try { x = 2 } finally { x = 3 }; x
|
||||
@@ -1 +0,0 @@
|
||||
try { throw "err" } catch(e) { e }
|
||||
Reference in New Issue
Block a user