Merge branch 'fix_actors'
This commit is contained in:
File diff suppressed because it is too large
Load Diff
45464
boot/fold.cm.mcode
45464
boot/fold.cm.mcode
File diff suppressed because it is too large
Load Diff
57389
boot/mcode.cm.mcode
57389
boot/mcode.cm.mcode
File diff suppressed because it is too large
Load Diff
69076
boot/parse.cm.mcode
69076
boot/parse.cm.mcode
File diff suppressed because it is too large
Load Diff
21773
boot/streamline.cm.mcode
Normal file
21773
boot/streamline.cm.mcode
Normal file
File diff suppressed because it is too large
Load Diff
25210
boot/tokenize.cm.mcode
25210
boot/tokenize.cm.mcode
File diff suppressed because it is too large
Load Diff
179
docs/actors.md
179
docs/actors.md
@@ -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()
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
447
source/mach.c
447
source/mach.c
@@ -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
6
tests/actor_clock.ce
Normal 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()
|
||||
})
|
||||
9
tests/actor_connection.ce
Normal file
9
tests/actor_connection.ce
Normal 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
8
tests/actor_couple.ce
Normal 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')
|
||||
10
tests/actor_delay_cancel.ce
Normal file
10
tests/actor_delay_cancel.ce
Normal 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)
|
||||
5
tests/actor_helper_echo.ce
Normal file
5
tests/actor_helper_echo.ce
Normal 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)
|
||||
5
tests/actor_helper_report.ce
Normal file
5
tests/actor_helper_report.ce
Normal 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)
|
||||
2
tests/actor_helper_stop.ce
Normal file
2
tests/actor_helper_stop.ce
Normal 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
9
tests/actor_overling.ce
Normal 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
6
tests/actor_receiver.ce
Normal 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
30
tests/actor_requestors.ce
Normal 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
5
tests/actor_self.ce
Normal 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()
|
||||
9
tests/actor_send_reply.ce
Normal file
9
tests/actor_send_reply.ce
Normal 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
13
tests/actor_start.ce
Normal 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
10
tests/actor_time_limit.ce
Normal 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
2
tests/actor_unneeded.ce
Normal file
@@ -0,0 +1,2 @@
|
||||
// Test: $unneeded fires after the specified time
|
||||
$unneeded($stop, 1)
|
||||
@@ -163,3 +163,4 @@ if (failed > 0) {
|
||||
print(" FAIL " + error_names[_j] + ": " + error_reasons[_j])
|
||||
}
|
||||
}
|
||||
$stop()
|
||||
|
||||
@@ -37,3 +37,4 @@ function test_nested() {
|
||||
test_nested()
|
||||
|
||||
print("done")
|
||||
$stop()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
// tests/reply_actor.ce - Simple child that just logs
|
||||
log.console("reply_actor: alive!")
|
||||
$stop()
|
||||
|
||||
Reference in New Issue
Block a user