Merge branch 'fix_actors'

This commit is contained in:
2026-02-17 12:35:26 -06:00
30 changed files with 122225 additions and 100841 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

21773
boot/streamline.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -67,9 +67,9 @@ An actor is a script that **does not return a value**. It runs as an independent
// worker.ce
print("Worker started")
$receiver(function(msg, reply) {
$receiver(function(msg) {
print("Received:", msg)
// Process message...
send(msg, {status: "ok"})
})
```
@@ -83,106 +83,128 @@ $receiver(function(msg, reply) {
Actors have access to special functions prefixed with `$`:
### $me
### $self
Reference to the current actor.
Reference to the current actor. This is a stone (immutable) actor object.
```javascript
print($me) // actor reference
print($self) // actor reference
print(is_actor($self)) // true
```
### $overling
Reference to the parent actor that started this actor. `null` for the root actor. Child actors are automatically coupled to their overling — if the parent dies, the child dies too.
```javascript
if ($overling != null) {
send($overling, {status: "ready"})
}
```
### $stop()
Stop the current actor.
Stop the current actor. When called with an actor argument, stops that underling (child) instead.
```javascript
$stop()
$stop() // stop self
$stop(child) // stop a child actor
```
### $send(actor, message, callback)
Send a message to another actor.
```javascript
$send(other_actor, {type: "ping", data: 42}, function(reply) {
print("Got reply:", reply)
})
```
Messages are automatically **splatted** — flattened to plain data without prototypes.
### $start(callback, program)
Start a new actor from a script.
Start a new child actor from a script. The callback receives lifecycle events:
- `{type: "greet", actor: <ref>}` — child started successfully
- `{type: "stop"}` — child stopped cleanly
- `{type: "disrupt", reason: ...}` — child crashed
```javascript
$start(function(new_actor) {
print("Started:", new_actor)
$start(function(event) {
if (event.type == 'greet') {
print("Child started:", event.actor)
send(event.actor, {task: "work"})
}
if (event.type == 'stop') {
print("Child stopped")
}
if (event.type == 'disrupt') {
print("Child crashed:", event.reason)
}
}, "worker")
```
### $delay(callback, seconds)
Schedule a callback after a delay.
Schedule a callback after a delay. Returns a cancel function that can be called to prevent the callback from firing.
```javascript
$delay(function() {
var cancel = $delay(function() {
print("5 seconds later")
}, 5)
// To cancel before it fires:
cancel()
```
### $clock(callback)
Get called every frame/tick.
Get called every frame/tick. The callback receives the current time as a number.
```javascript
$clock(function(dt) {
// Called each tick with delta time
$clock(function(t) {
// called each tick with current time
})
```
### $receiver(callback)
Set up a message receiver.
Set up a message receiver. The callback is called with the incoming message whenever another actor sends a message to this actor.
To reply to a message, call `send(message, reply_data)` — the message object contains routing information that directs the reply back to the sender.
```javascript
$receiver(function(message, reply) {
// Handle incoming message
reply({status: "ok"})
$receiver(function(message) {
// handle incoming message
send(message, {status: "ok"})
})
```
### $portal(callback, port)
Open a network port.
Open a network port to receive connections from remote actors.
```javascript
$portal(function(connection) {
// Handle new connection
// handle new connection
}, 8080)
```
### $contact(callback, record)
Connect to a remote address.
Connect to a remote actor at a given address.
```javascript
$contact(function(connection) {
// Connected
// connected
}, {host: "example.com", port: 80})
```
### $time_limit(requestor, seconds)
Wrap a requestor with a timeout. See [Requestors](/docs/requestors/) for details.
Wrap a requestor with a timeout. Returns a new requestor that will cancel the original and call its callback with a failure if the time limit is exceeded. See [Requestors](/docs/requestors/) for details.
```javascript
$time_limit(my_requestor, 10) // 10 second timeout
var timed = $time_limit(my_requestor, 10)
timed(function(result, reason) {
// reason will explain timeout if it fires
}, initial_value)
```
### $couple(actor)
Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between an actor and its overling (parent).
Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between a child actor and its overling (parent).
```javascript
$couple(other_actor)
@@ -190,7 +212,7 @@ $couple(other_actor)
### $unneeded(callback, seconds)
Schedule the actor for removal after a specified time.
Schedule the actor for removal after a specified time. The callback fires when the time elapses.
```javascript
$unneeded(function() {
@@ -200,20 +222,76 @@ $unneeded(function() {
### $connection(callback, actor, config)
Get information about the connection to another actor, such as latency, bandwidth, and activity.
Get information about the connection to another actor. For local actors, returns `{type: "local"}`. For remote actors, returns connection details including latency, bandwidth, and activity.
```javascript
$connection(function(info) {
print(info.latency)
if (info.type == "local") {
print("same machine")
} else {
print(info.latency)
}
}, other_actor, {})
```
## Runtime Functions
These functions are available in actors without the `$` prefix:
### send(actor, message, callback)
Send a message to another actor. The message must be an object record.
The optional callback receives the reply when the recipient responds.
```javascript
send(other_actor, {type: "ping"}, function(reply) {
print("Got reply:", reply)
})
```
To reply to a received message, pass the message itself as the first argument — it contains routing information:
```javascript
$receiver(function(message) {
send(message, {result: 42})
})
```
Messages are automatically flattened to plain data.
### is_actor(value)
Returns `true` if the value is an actor reference.
```javascript
if (is_actor(some_value)) {
send(some_value, {ping: true})
}
```
### log
Logging functions: `log.console(msg)`, `log.error(msg)`, `log.system(msg)`.
### use(path)
Import a module. See [Module Resolution](#module-resolution) below.
### args
Array of command-line arguments passed to the actor.
### sequence(), parallel(), race(), fallback()
Requestor composition functions. See [Requestors](/docs/requestors/) for details.
## Module Resolution
When you call `use('name')`, ƿit searches:
1. **Current package** — files relative to package root
2. **Dependencies** — packages declared in `pit.toml`
2. **Dependencies** — packages declared in `cell.toml`
3. **Core** — built-in ƿit modules
```javascript
@@ -234,8 +312,14 @@ var config = use('config')
print("Starting application...")
$start(function(worker) {
$send(worker, {task: "process", data: [1, 2, 3]})
$start(function(event) {
if (event.type == 'greet') {
send(event.actor, {task: "process", data: [1, 2, 3]})
}
if (event.type == 'stop') {
print("Worker finished")
$stop()
}
}, "worker")
$delay(function() {
@@ -246,11 +330,12 @@ $delay(function() {
```javascript
// worker.ce - Worker actor
$receiver(function(msg, reply) {
$receiver(function(msg) {
if (msg.task == "process") {
var result = array(msg.data, x => x * 2)
reply({result: result})
var result = array(msg.data, function(x) { return x * 2 })
send(msg, {result: result})
}
$stop()
})
```

View File

@@ -31,7 +31,7 @@ The cancel function, when called, should abort the in-progress work.
```javascript
var fetch_data = function(callback, url) {
$contact(function(connection) {
$send(connection, {get: url}, function(response) {
send(connection, {get: url}, function(response) {
callback(response)
})
}, {host: url, port: 80})
@@ -154,11 +154,11 @@ If the requestor does not complete within the time limit, it is cancelled and th
## Requestors and Actors
Requestors are particularly useful with actor messaging. Since `$send` is callback-based, it fits naturally:
Requestors are particularly useful with actor messaging. Since `send` is callback-based, it fits naturally:
```javascript
var ask_worker = function(callback, task) {
$send(worker, task, function(reply) {
send(worker, task, function(reply) {
callback(reply)
})
}

View File

@@ -26,7 +26,7 @@ function ensure_build_dir() {
return dir
}
// Load seed pipeline from boot/ (tokenize, parse, mcode only)
// Load seed pipeline from boot/
function boot_load(name) {
var mcode_path = core_path + '/boot/' + name + '.cm.mcode'
var mcode_blob = null
@@ -44,6 +44,7 @@ var tokenize_mod = boot_load("tokenize")
var parse_mod = boot_load("parse")
var fold_mod = boot_load("fold")
var mcode_mod = boot_load("mcode")
var streamline_mod = boot_load("streamline")
function analyze(src, filename) {
var tok_result = tokenize_mod(src, filename)
@@ -77,7 +78,7 @@ function compile_and_cache(name, source_path) {
var mach_blob = null
if (cached && fd.is_file(cached)) return
ast = analyze(text(source_blob), source_path)
compiled = mcode_mod(ast)
compiled = streamline_mod(mcode_mod(ast))
mcode_json = json_mod.encode(compiled)
mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (cached) {

View File

@@ -566,6 +566,12 @@ var root = null
var receive_fn = null
var greeters = {} // Router functions for when messages are received for a specific actor
var peers = {}
var id_address = {}
var peer_queue = {}
var portal = null
var portal_fn = null
function peer_connection(peer) {
return {
latency: peer.rtt,
@@ -604,12 +610,6 @@ $_.connection = function(callback, actor, config) {
callback()
}
var peers = {}
var id_address = {}
var peer_queue = {}
var portal = null
var portal_fn = null
// takes a function input value that will eventually be called with the current time in number form.
$_.portal = function(fn, port) {
if (portal) {

View File

@@ -2254,318 +2254,6 @@ static int ml_int(cJSON *arr, int idx) {
return (int)cJSON_GetArrayItem(arr, idx)->valuedouble;
}
/* ---- Register compression ----
The mcode compiler allocates slots monotonically, producing register numbers
that can exceed 255. Since MachInstr32 uses 8-bit fields, we must compress
the register space via live-range analysis before lowering.
For each slot we record its first and last instruction reference, then do a
greedy linear-scan allocation to pack them into the fewest physical registers.
Slots referenced by child functions via get/put (parent_slot) are in the
PARENT frame and are not remapped here — only current-frame register
operands are touched. */
#define MAX_REG_ITEMS 32
/* Return cJSON pointers to all current-frame register operands in an
instruction. out[] must have room for MAX_REG_ITEMS entries. */
static int mcode_reg_items(cJSON *it, cJSON **out) {
int sz = cJSON_GetArraySize(it);
if (sz < 3) return 0;
const char *op = cJSON_GetArrayItem(it, 0)->valuestring;
int c = 0;
#define ADD(pos) do { \
cJSON *_r = cJSON_GetArrayItem(it, (pos)); \
if (_r && cJSON_IsNumber(_r) && c < MAX_REG_ITEMS) out[c++] = _r; \
} while (0)
/* get/put: only [1] is current-frame (dest/src); [2]=parent_slot, [3]=level */
if (!strcmp(op, "get") || !strcmp(op, "put")) { ADD(1); return c; }
/* dest-only */
if (!strcmp(op, "access") || !strcmp(op, "int") ||
!strcmp(op, "function") || !strcmp(op, "regexp") ||
!strcmp(op, "true") || !strcmp(op, "false") || !strcmp(op, "null"))
{ ADD(1); return c; }
/* invoke: [1]=frame, [2]=dest (result register) */
if (!strcmp(op, "invoke") || !strcmp(op, "tail_invoke")) { ADD(1); ADD(2); return c; }
/* goinvoke: [1]=frame only (no result) */
if (!strcmp(op, "goinvoke")) { ADD(1); return c; }
/* setarg: [1]=call, [2]=arg_idx(const), [3]=val */
if (!strcmp(op, "setarg")) { ADD(1); ADD(3); return c; }
/* frame/goframe: [1]=call, [2]=func, [3]=nr_args(const) */
if (!strcmp(op, "frame") || !strcmp(op, "goframe")) { ADD(1); ADD(2); return c; }
/* no regs */
if (!strcmp(op, "jump") || !strcmp(op, "disrupt")) return 0;
/* cond only */
if (!strcmp(op, "jump_true") || !strcmp(op, "jump_false") ||
!strcmp(op, "jump_not_null"))
{ ADD(1); return c; }
/* single reg */
if (!strcmp(op, "return")) { ADD(1); return c; }
/* delete: [1]=dest, [2]=obj, [3]=key (string or reg) */
if (!strcmp(op, "delete")) {
ADD(1); ADD(2);
cJSON *k = cJSON_GetArrayItem(it, 3);
if (k && cJSON_IsNumber(k)) out[c++] = k;
return c;
}
/* record: [1]=dest, [2]=0(const) — no line/col suffix */
if (!strcmp(op, "record")) { ADD(1); return c; }
/* array: [1]=dest, [2]=count(const) — elements added via separate push instrs */
if (!strcmp(op, "array")) {
ADD(1);
return c;
}
/* load_field: [1]=dest, [2]=obj, [3]=key (string or reg) */
if (!strcmp(op, "load_field")) {
ADD(1); ADD(2);
cJSON *key = cJSON_GetArrayItem(it, 3);
if (key && cJSON_IsNumber(key)) out[c++] = key;
return c;
}
/* store_field: [1]=obj, [2]=val, [3]=key (string or reg) */
if (!strcmp(op, "store_field")) {
ADD(1); ADD(2);
cJSON *key = cJSON_GetArrayItem(it, 3);
if (key && cJSON_IsNumber(key)) out[c++] = key;
return c;
}
/* Default: every numeric operand in [1..sz-3] is a register.
Covers move, arithmetic, comparisons, type checks, push, pop,
load_dynamic, store_dynamic, in, concat, logical, bitwise, etc. */
for (int j = 1; j < sz - 2; j++) {
cJSON *item = cJSON_GetArrayItem(it, j);
if (item && cJSON_IsNumber(item)) out[c++] = item;
}
return c;
#undef ADD
}
/* Compress register numbers in a single function's mcode JSON so they
fit in 8 bits. Modifies the cJSON instructions and nr_slots in place.
Returns a malloc'd remap table (caller must free), or NULL if no
compression was needed. *out_old_nr_slots is set to the original count. */
static int *mcode_compress_regs(cJSON *fobj, int *out_old_nr_slots,
int *captured_slots, int n_captured) {
cJSON *nr_slots_j = cJSON_GetObjectItemCaseSensitive(fobj, "nr_slots");
int nr_slots = (int)cJSON_GetNumberValue(nr_slots_j);
*out_old_nr_slots = nr_slots;
if (nr_slots <= 255) return NULL;
int nr_args = (int)cJSON_GetNumberValue(
cJSON_GetObjectItemCaseSensitive(fobj, "nr_args"));
cJSON *instrs = cJSON_GetObjectItemCaseSensitive(fobj, "instructions");
int n = instrs ? cJSON_GetArraySize(instrs) : 0;
/* Step 1: build live ranges (first_ref / last_ref per slot) */
int *first_ref = sys_malloc(nr_slots * sizeof(int));
int *last_ref = sys_malloc(nr_slots * sizeof(int));
for (int i = 0; i < nr_slots; i++) { first_ref[i] = -1; last_ref[i] = -1; }
/* this + args are live for the whole function */
int pinned = 1 + nr_args;
for (int i = 0; i < pinned; i++) { first_ref[i] = 0; last_ref[i] = n; }
{ cJSON *it = instrs ? instrs->child : NULL;
for (int i = 0; it; i++, it = it->next) {
if (!cJSON_IsArray(it)) continue;
cJSON *regs[MAX_REG_ITEMS];
int rc = mcode_reg_items(it, regs);
for (int j = 0; j < rc; j++) {
int s = (int)regs[j]->valuedouble;
if (s < 0 || s >= nr_slots) continue;
if (first_ref[s] < 0) first_ref[s] = i;
last_ref[s] = i;
}
} }
/* Step 1a: extend live ranges for closure-captured slots.
If a child function captures a parent slot via get/put, that slot must
remain live for the entire parent function (the closure can read it at
any time while the parent frame is on the stack). */
for (int ci = 0; ci < n_captured; ci++) {
int s = captured_slots[ci];
if (s >= 0 && s < nr_slots) {
if (first_ref[s] < 0) first_ref[s] = 0;
last_ref[s] = n;
}
}
/* Step 1b: extend live ranges for loops (backward jumps).
Build label→position map, then for each backward jump [target..jump],
extend all overlapping live ranges to cover the full loop body. */
{
/* Collect label positions */
typedef struct { const char *name; int pos; } LabelPos;
int lbl_cap = 32, lbl_n = 0;
LabelPos *lbls = sys_malloc(lbl_cap * sizeof(LabelPos));
{ cJSON *it = instrs ? instrs->child : NULL;
for (int i = 0; it; i++, it = it->next) {
if (cJSON_IsString(it)) {
if (lbl_n >= lbl_cap) {
lbl_cap *= 2;
lbls = sys_realloc(lbls, lbl_cap * sizeof(LabelPos));
}
lbls[lbl_n++] = (LabelPos){it->valuestring, i};
}
} }
/* Find backward jumps and extend live ranges */
int changed = 1;
while (changed) {
changed = 0;
cJSON *it = instrs ? instrs->child : NULL;
for (int i = 0; it; i++, it = it->next) {
if (!cJSON_IsArray(it)) continue;
int sz = cJSON_GetArraySize(it);
if (sz < 3) continue;
const char *op = it->child->valuestring;
const char *target = NULL;
if (!strcmp(op, "jump")) {
target = it->child->next->valuestring;
} else if (!strcmp(op, "jump_true") || !strcmp(op, "jump_false") ||
!strcmp(op, "jump_not_null")) {
target = it->child->next->next->valuestring;
}
if (!target) continue;
/* Find label position */
int tpos = -1;
for (int j = 0; j < lbl_n; j++) {
if (!strcmp(lbls[j].name, target)) { tpos = lbls[j].pos; break; }
}
if (tpos < 0 || tpos >= i) continue; /* forward jump or not found */
/* Backward jump: extend registers that are live INTO the loop
(first_ref < loop start but used inside). Temporaries born
inside the loop body don't need extension — they are per-iteration. */
for (int s = pinned; s < nr_slots; s++) {
if (first_ref[s] < 0) continue;
if (first_ref[s] >= tpos) continue; /* born inside loop — skip */
if (last_ref[s] < tpos) continue; /* dead before loop — skip */
/* Register is live into the loop body — extend to loop end */
if (last_ref[s] < i) { last_ref[s] = i; changed = 1; }
}
}
}
sys_free(lbls);
}
/* Step 2: linear-scan register allocation */
typedef struct { int slot, first, last; } SlotInfo;
int cnt = 0;
SlotInfo *sorted = sys_malloc(nr_slots * sizeof(SlotInfo));
for (int s = pinned; s < nr_slots; s++)
if (first_ref[s] >= 0)
sorted[cnt++] = (SlotInfo){s, first_ref[s], last_ref[s]};
/* Sort by first_ref, tie-break by original slot (keeps named vars first) */
for (int i = 1; i < cnt; i++) {
SlotInfo key = sorted[i];
int j = i - 1;
while (j >= 0 && (sorted[j].first > key.first ||
(sorted[j].first == key.first && sorted[j].slot > key.slot))) {
sorted[j + 1] = sorted[j];
j--;
}
sorted[j + 1] = key;
}
int *remap = sys_malloc(nr_slots * sizeof(int));
for (int i = 0; i < nr_slots; i++) remap[i] = i;
/* Free-register pool (min-heap would be ideal but a flat scan is fine) */
int *pool = sys_malloc(nr_slots * sizeof(int));
int pool_n = 0;
int next_phys = pinned;
typedef struct { int phys, last; } ActiveAlloc;
ActiveAlloc *active = sys_malloc(cnt * sizeof(ActiveAlloc));
int active_n = 0;
for (int i = 0; i < cnt; i++) {
int first = sorted[i].first;
/* Expire intervals whose last_ref < first */
for (int j = 0; j < active_n; ) {
if (active[j].last < first) {
pool[pool_n++] = active[j].phys;
active[j] = active[--active_n];
} else {
j++;
}
}
/* Pick lowest available physical register */
int phys;
if (pool_n > 0) {
int mi = 0;
for (int j = 1; j < pool_n; j++)
if (pool[j] < pool[mi]) mi = j;
phys = pool[mi];
pool[mi] = pool[--pool_n];
} else {
phys = next_phys++;
}
remap[sorted[i].slot] = phys;
active[active_n++] = (ActiveAlloc){phys, sorted[i].last};
}
/* Compute new nr_slots */
int new_max = pinned;
for (int s = 0; s < nr_slots; s++)
if (first_ref[s] >= 0 && remap[s] >= new_max)
new_max = remap[s] + 1;
if (new_max > 255)
fprintf(stderr, " WARNING: %d live regs still exceeds 255\n", new_max);
/* Verify: check no two registers with overlapping live ranges share phys */
for (int a = pinned; a < nr_slots; a++) {
if (first_ref[a] < 0) continue;
for (int b = a + 1; b < nr_slots; b++) {
if (first_ref[b] < 0) continue;
if (remap[a] != remap[b]) continue;
/* Same phys — ranges must NOT overlap */
if (first_ref[a] <= last_ref[b] && first_ref[b] <= last_ref[a]) {
fprintf(stderr, " OVERLAP: slot %d [%d,%d] and slot %d [%d,%d] -> phys %d\n",
a, first_ref[a], last_ref[a], b, first_ref[b], last_ref[b], remap[a]);
}
}
}
/* Step 3: apply remap to instructions */
{ cJSON *it = instrs ? instrs->child : NULL;
for (int i = 0; it; i++, it = it->next) {
if (!cJSON_IsArray(it)) continue;
cJSON *regs[MAX_REG_ITEMS];
int rc = mcode_reg_items(it, regs);
for (int j = 0; j < rc; j++) {
int old = (int)regs[j]->valuedouble;
if (old >= 0 && old < nr_slots) {
cJSON_SetNumberValue(regs[j], remap[old]);
}
}
} }
/* Update nr_slots in the JSON */
cJSON_SetNumberValue(nr_slots_j, new_max);
sys_free(first_ref); sys_free(last_ref);
sys_free(sorted);
sys_free(pool); sys_free(active);
return remap; /* caller must free */
}
/* Lower one function's mcode instructions to MachInstr32 */
static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
McodeLowerState s = {0};
@@ -2575,6 +2263,14 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
cJSON_GetObjectItemCaseSensitive(fobj, "nr_close_slots"));
s.nr_slots = (int)cJSON_GetNumberValue(
cJSON_GetObjectItemCaseSensitive(fobj, "nr_slots"));
if (s.nr_slots > 255) {
cJSON *nm_chk = cJSON_GetObjectItemCaseSensitive(fobj, "name");
const char *fn_name = nm_chk ? cJSON_GetStringValue(nm_chk) : "<anonymous>";
fprintf(stderr, "ERROR: function '%s' has %d slots (max 255). "
"Ensure the streamline optimizer ran before mach compilation.\n",
fn_name, s.nr_slots);
return NULL;
}
int dis_raw = (int)cJSON_GetNumberValue(
cJSON_GetObjectItemCaseSensitive(fobj, "disruption_pc"));
cJSON *nm = cJSON_GetObjectItemCaseSensitive(fobj, "name");
@@ -3007,131 +2703,8 @@ MachCode *mach_compile_mcode(cJSON *mcode_json) {
cJSON *main_obj = cJSON_GetObjectItemCaseSensitive(mcode_json, "main");
/* Build parent_of[]: for each function, which function index is its parent.
parent_of[i] = parent index, or func_count for main, or -1 if unknown.
Scan each function (and main) for "function" instructions. */
int *parent_of = sys_malloc(func_count * sizeof(int));
for (int i = 0; i < func_count; i++) parent_of[i] = -1;
/* Scan main's instructions */
{
cJSON *main_instrs = cJSON_GetObjectItemCaseSensitive(main_obj, "instructions");
cJSON *it = main_instrs ? main_instrs->child : NULL;
for (; it; it = it->next) {
if (!cJSON_IsArray(it) || cJSON_GetArraySize(it) < 3) continue;
const char *op = it->child->valuestring;
if (!strcmp(op, "function")) {
int child_idx = (int)it->child->next->next->valuedouble;
if (child_idx >= 0 && child_idx < func_count)
parent_of[child_idx] = func_count; /* main */
}
}
}
/* Scan each function's instructions */
{ cJSON *fobj = funcs_arr ? funcs_arr->child : NULL;
for (int fi = 0; fobj; fi++, fobj = fobj->next) {
cJSON *finstrs = cJSON_GetObjectItemCaseSensitive(fobj, "instructions");
cJSON *it = finstrs ? finstrs->child : NULL;
for (; it; it = it->next) {
if (!cJSON_IsArray(it) || cJSON_GetArraySize(it) < 3) continue;
const char *op = it->child->valuestring;
if (!strcmp(op, "function")) {
int child_idx = (int)it->child->next->next->valuedouble;
if (child_idx >= 0 && child_idx < func_count)
parent_of[child_idx] = fi;
}
}
} }
/* Build per-function capture sets: for each function F, which of its slots
are captured by descendant functions via get/put. Captured slots must
have extended live ranges during register compression. */
int **cap_slots = sys_malloc((func_count + 1) * sizeof(int *));
int *cap_counts = sys_malloc((func_count + 1) * sizeof(int));
memset(cap_slots, 0, (func_count + 1) * sizeof(int *));
memset(cap_counts, 0, (func_count + 1) * sizeof(int));
{ cJSON *fobj = funcs_arr ? funcs_arr->child : NULL;
for (int fi = 0; fobj; fi++, fobj = fobj->next) {
cJSON *finstrs = cJSON_GetObjectItemCaseSensitive(fobj, "instructions");
cJSON *it = finstrs ? finstrs->child : NULL;
for (; it; it = it->next) {
if (!cJSON_IsArray(it) || cJSON_GetArraySize(it) < 4) continue;
const char *op = it->child->valuestring;
if (strcmp(op, "get") && strcmp(op, "put")) continue;
int slot = (int)it->child->next->next->valuedouble;
int level = (int)it->child->next->next->next->valuedouble;
/* Walk up parent chain to find the ancestor whose slot is referenced */
int ancestor = fi;
for (int l = 0; l < level && ancestor >= 0; l++)
ancestor = parent_of[ancestor];
if (ancestor < 0) continue;
/* Add slot to ancestor's capture list (deduplicate) */
int found = 0;
for (int k = 0; k < cap_counts[ancestor]; k++)
if (cap_slots[ancestor][k] == slot) { found = 1; break; }
if (!found) {
cap_slots[ancestor] = sys_realloc(cap_slots[ancestor],
(cap_counts[ancestor] + 1) * sizeof(int));
cap_slots[ancestor][cap_counts[ancestor]++] = slot;
}
}
} }
/* Compress registers for functions that exceed 8-bit slot limits.
Save remap tables so we can fix get/put parent_slot references. */
int **remaps = sys_malloc((func_count + 1) * sizeof(int *));
int *remap_sizes = sys_malloc((func_count + 1) * sizeof(int));
memset(remaps, 0, (func_count + 1) * sizeof(int *));
{ cJSON *fobj = funcs_arr ? funcs_arr->child : NULL;
for (int i = 0; fobj; i++, fobj = fobj->next)
remaps[i] = mcode_compress_regs(fobj,
&remap_sizes[i], cap_slots[i], cap_counts[i]);
}
/* main is stored at index func_count in our arrays */
remaps[func_count] = mcode_compress_regs(main_obj,
&remap_sizes[func_count], cap_slots[func_count], cap_counts[func_count]);
/* Free capture lists */
for (int i = 0; i <= func_count; i++)
if (cap_slots[i]) sys_free(cap_slots[i]);
sys_free(cap_slots);
sys_free(cap_counts);
/* Fix up get/put parent_slot references using ancestor remap tables */
{ cJSON *fobj = funcs_arr ? funcs_arr->child : NULL;
for (int fi = 0; fobj; fi++, fobj = fobj->next) {
cJSON *finstrs = cJSON_GetObjectItemCaseSensitive(fobj, "instructions");
cJSON *it = finstrs ? finstrs->child : NULL;
for (; it; it = it->next) {
if (!cJSON_IsArray(it) || cJSON_GetArraySize(it) < 4) continue;
const char *op = it->child->valuestring;
if (strcmp(op, "get") && strcmp(op, "put")) continue;
int level = (int)it->child->next->next->next->valuedouble;
/* Walk up parent chain 'level' times to find ancestor */
int ancestor = fi;
for (int l = 0; l < level && ancestor >= 0; l++) {
ancestor = parent_of[ancestor];
}
if (ancestor < 0) continue; /* unknown parent — leave as is */
int *anc_remap = remaps[ancestor];
if (!anc_remap) continue; /* ancestor wasn't compressed */
cJSON *slot_item = it->child->next->next;
int old_slot = (int)slot_item->valuedouble;
if (old_slot >= 0 && old_slot < remap_sizes[ancestor]) {
int new_slot = anc_remap[old_slot];
cJSON_SetNumberValue(slot_item, new_slot);
}
}
} }
/* Free remap tables */
for (int i = 0; i <= func_count; i++)
if (remaps[i]) sys_free(remaps[i]);
sys_free(remaps);
sys_free(remap_sizes);
sys_free(parent_of);
/* Slot compression is handled by the streamline optimizer before mach
compilation. mcode_lower_func() asserts nr_slots <= 255. */
/* Compile all flat functions */
MachCode **compiled = NULL;

6
tests/actor_clock.ce Normal file
View File

@@ -0,0 +1,6 @@
// Test: $clock fires with a time number
$clock(function(t) {
if (!is_number(t)) disrupt
if (t <= 0) disrupt
$stop()
})

View File

@@ -0,0 +1,9 @@
// Test: $connection reports local for a child actor
$start(function(event) {
if (event.type == 'greet') {
$connection(function(info) {
if (info.type != "local") disrupt
$stop()
}, event.actor, {})
}
}, 'tests/actor_helper_echo')

8
tests/actor_couple.ce Normal file
View File

@@ -0,0 +1,8 @@
// Test: $couple($self) is a no-op, $couple on a child doesn't disrupt
$couple($self)
$start(function(event) {
if (event.type == 'greet') {
$couple(event.actor)
$delay($stop, 0.1)
}
}, 'tests/actor_helper_echo')

View File

@@ -0,0 +1,10 @@
// Test: $delay returns a cancel function that prevents the callback
var fired = false
var cancel = $delay(function() {
fired = true
}, 0.5)
cancel()
var _t = $delay(function() {
if (fired) disrupt
$stop()
}, 1)

View File

@@ -0,0 +1,5 @@
// Helper actor that echoes messages back with {pong: true}
$receiver(function(msg) {
send(msg, {pong: true})
})
var _t = $delay($stop, 5)

View File

@@ -0,0 +1,5 @@
// Helper actor that reports $overling status
$receiver(function(msg) {
send(msg, {has_overling: $overling != null})
})
var _t = $delay($stop, 5)

View File

@@ -0,0 +1,2 @@
// Helper actor that stops after 0.1 seconds
var _t = $delay($stop, 0.1)

9
tests/actor_overling.ce Normal file
View File

@@ -0,0 +1,9 @@
// Test: child actor reports $overling is not null
$start(function(event) {
if (event.type == 'greet') {
send(event.actor, {check: true}, function(reply) {
if (!reply.has_overling) disrupt
$stop()
})
}
}, 'tests/actor_helper_report')

6
tests/actor_receiver.ce Normal file
View File

@@ -0,0 +1,6 @@
// Test: $receiver fires when sending to self
$receiver(function(msg) {
if (!msg.test) disrupt
$stop()
})
send($self, {test: true})

30
tests/actor_requestors.ce Normal file
View File

@@ -0,0 +1,30 @@
// Test: sequence, parallel, and fallback requestor composition
var immediate = function(callback, value) {
callback(42)
}
var add_one = function(callback, value) {
callback(value + 1)
}
var broken = function(callback, value) {
callback(null, "broken")
}
var _t = sequence([immediate, add_one])(function(result, reason) {
if (reason != null) disrupt
if (result != 43) disrupt
parallel([immediate, immediate])(function(results, reason) {
if (reason != null) disrupt
if (length(results) != 2) disrupt
if (results[0] != 42) disrupt
if (results[1] != 42) disrupt
fallback([broken, immediate])(function(result, reason) {
if (reason != null) disrupt
if (result != 42) disrupt
$stop()
})
})
})

5
tests/actor_self.ce Normal file
View File

@@ -0,0 +1,5 @@
// Test: $self and is_actor
if ($self == null) disrupt
if (!is_actor($self)) disrupt
if (!is_stone($self)) disrupt
$stop()

View File

@@ -0,0 +1,9 @@
// Test: send with reply callback
$start(function(event) {
if (event.type == 'greet') {
send(event.actor, {ping: true}, function(reply) {
if (!reply.pong) disrupt
$stop()
})
}
}, 'tests/actor_helper_echo')

13
tests/actor_start.ce Normal file
View File

@@ -0,0 +1,13 @@
// Test: $start lifecycle events (greet and stop)
var got_greet = false
$start(function(event) {
if (event.type == 'greet') {
if (!event.actor) disrupt
if (!is_actor(event.actor)) disrupt
got_greet = true
}
if (event.type == 'stop') {
if (!got_greet) disrupt
$stop()
}
}, 'tests/actor_helper_stop')

10
tests/actor_time_limit.ce Normal file
View File

@@ -0,0 +1,10 @@
// Test: $time_limit fires timeout for a requestor that never completes
var never_complete = function(callback, value) {
return null
}
var timed = $time_limit(never_complete, 0.5)
var _t = timed(function(val, reason) {
if (val != null) disrupt
if (reason == null) disrupt
$stop()
})

2
tests/actor_unneeded.ce Normal file
View File

@@ -0,0 +1,2 @@
// Test: $unneeded fires after the specified time
$unneeded($stop, 1)

View File

@@ -163,3 +163,4 @@ if (failed > 0) {
print(" FAIL " + error_names[_j] + ": " + error_reasons[_j])
}
}
$stop()

View File

@@ -37,3 +37,4 @@ function test_nested() {
test_nested()
print("done")
$stop()

View File

@@ -8,7 +8,7 @@ $start(e => {
if (e.type == 'disrupt') {
log.console(`underling successfully killed.`)
send($parent, { type: "test_result", passed: true })
send($overling, {type: "test_result", passed: true})
$stop()
}
}, 'tests/hang_actor')

View File

@@ -1,2 +1,3 @@
// tests/reply_actor.ce - Simple child that just logs
log.console("reply_actor: alive!")
$stop()