Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to EigenScript are documented here.
## [Unreleased]

### Added
- **`task_self` — a task can learn its own id (#526).** `task_self of null`
returns the running task's id in the same integer space `task_spawn`
returns (the main task is 0, including before any task has been spawned).
Surfaced by `lib/supervise` (#409): without it a worker has no reply
address to hand out, so message-link supervision — a worker delivering its
exit as an ordinary message to its supervisor's mailbox — was not
expressible. Deterministic (pure scheduler state, zero tape records).
- **`lib/supervise` — observer-native supervision (#409).** A supervisor over
the #408 cooperative task layer that, beyond BEAM-style crash-restart, also
catches the silently **wedged** worker — alive, not crashed, never timing
Expand Down
1 change: 1 addition & 0 deletions docs/BUILTINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ Requires full build. Transformer model inference and training.
| `channel_closed` | `channel_closed of channel` | Returns 1 if closed, 0 otherwise. |
| `task_spawn` | `task_spawn of fn` or `task_spawn of [fn, arg1, ...]` | Create a cooperative task (#408) running `fn` on the single OS thread — deterministic by construction, unlike `spawn`'s OS thread. Args are deep-COPIED (share-nothing, like channel sends), not shared by reference. Returns a numeric task id. (Increment 1a: the task is recorded and reported by `task_alive`; the copying-stack scheduler that runs and interleaves tasks — `task_yield`/`task_join` — lands in a later increment.) |
| `task_alive` | `task_alive of id` | Returns 1 while the task is runnable or suspended, 0 once it has finished (or for an unknown id). |
| `task_self` | `task_self of null` | The **running task's own id** (a number, in the same integer space `task_spawn` returns; the main task is 0, including before any task has been spawned). Lets a worker hand out its own id as a reply address — the message-link pattern a mailbox otherwise cannot express (#526). Deterministic — reads scheduler state, records no nondeterminism. |
| `task_yield` | `task_yield of null` | Cooperatively hand control to the next ready task; this task resumes round-robin. A no-op when no task has been spawned. Forbidden inside an `arena_mark`…`arena_reset` scope or a nested evaluation (raises `value`). |
| `must_not_yield` | `must_not_yield of fn` | Run `fn of null` as an **atomic** critical section, asserting it issues no scheduler yield (#488). Any *suspending* task builtin inside — `task_yield`, a blocking `task_recv`/`task_join`, `task_sleep` — raises `value` instead of suspending, so a yield introduced into a region that relies on cooperative atomicity fails loudly rather than corrupting under a rare interleaving. Non-suspending ops (`task_try_recv`, `task_send`, joining an already-finished task) are allowed. Returns `fn`'s result (or propagates its error); the region depth is balanced even if the body raises. Nestable. |
| `task_join` | `task_join of id` | Block until task `id` finishes, then return its deep-copied result — or re-raise its uncaught error (as the same `{kind, message, line}` it died with). Joining an already-finished task returns immediately; an unknown id (or self) returns null. All tasks blocked with none runnable is a `deadlock` error, not a hang — catchable by a `try`/`catch` around the join on the main task (`e.kind == "deadlock"`); terminal only if unhandled. |
Expand Down
7 changes: 7 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,13 @@ nothing, like channel sends); `task_recv of null` returns the next
message or blocks cooperatively until one arrives. `task_try_recv`
is the non-blocking form.

A task learns its **own** id with `task_self of null` — the same
integer space `task_spawn` returns; the main task is 0. That is what
makes a *reply address* expressible: a spawner passes `task_self of
null` down as an argument (or a worker sends its own `task_self` up),
and messages can then flow back to whoever asked — the link pattern
message-based supervision needs.

```eigenscript
worker_id is 0
define worker() as:
Expand Down
11 changes: 11 additions & 0 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -4929,6 +4929,16 @@ Value* builtin_task_now(Value *arg) {
return make_num(task_virtual_now());
}

/* task_self of null → the running task's id (a number, in the same integer
* space task_spawn returns; the main task is 0, including before any task has
* been spawned). Lets a worker hand out its own id as a reply address — the
* message-link supervision pattern (#526). Deterministic; reads scheduler
* state, so it records no tape nondet. */
Value* builtin_task_self(Value *arg) {
(void)arg;
return make_num((double)task_current_id());
}

/* task_sched_seed of n — install a scheduling seed. By default tasks run FIFO
* round-robin; with a seed the scheduler picks the next ready task
* pseudo-randomly from a deterministic PRNG. The interleaving stays
Expand Down Expand Up @@ -5993,6 +6003,7 @@ void register_builtins(Env *env) {
env_set_local_owned(env, "spawn", make_builtin(builtin_spawn));
env_set_local_owned(env, "task_spawn", make_builtin(builtin_task_spawn));
env_set_local_owned(env, "task_alive", make_builtin(builtin_task_alive));
env_set_local_owned(env, "task_self", make_builtin(builtin_task_self));
env_set_local_owned(env, "task_yield", make_builtin(builtin_task_yield));
env_set_local_owned(env, "must_not_yield", make_builtin(builtin_must_not_yield));
env_set_local_owned(env, "task_join", make_builtin(builtin_task_join));
Expand Down
8 changes: 8 additions & 0 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -5446,6 +5446,14 @@ double task_virtual_now(void) {
return s ? s->now : 0;
}

/* task_self (builtins.c): the running task's id, in the same integer space
* task_spawn returns — 0 for the main task, including before any scheduler
* exists. Pure scheduler state, so no tape participation. */
int task_current_id(void) {
TaskScheduler *s = sched_get();
return s ? s->current : 0;
}

/* When the ready queue is empty, advance the virtual clock to the earliest
* sleeper's wake time and make every task due at (or before) that instant
* runnable. Returns 1 if any sleeper was woken (the trampoline then loops),
Expand Down
1 change: 1 addition & 0 deletions src/vm.h
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ int task_do_kill(int tid); /* deterministic teardown of `tid`; 0 = bad
/* Inc 3 virtual time (builtins.c task_sleep/task_now). */
void task_request_sleep(double ticks); /* current task sleeps until virtual now + ticks */
double task_virtual_now(void); /* current virtual-clock value (0 with no scheduler) */
int task_current_id(void); /* running task id; 0 = main (incl. no scheduler) — task_self (#526) */
/* Inc 4 seeded scheduling strategy (builtins.c task_sched_seed). */
void task_sched_set_seed(double seed); /* install a seed → seeded pick; ensures the scheduler */

Expand Down
23 changes: 23 additions & 0 deletions tests/test_tasks.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,27 @@ define _mny_probe_recv() as:
_mny_w1 is task_spawn of _mny_probe_recv
assert_eq of [task_join of _mny_w1, 1, "blocking task_recv inside must_not_yield raises"]

# --- task_self (#526): a task can learn its own id -----------------------

# Main is task 0, even with the scheduler already active.
assert_eq of [task_self of null, 0, "main task's task_self is 0"]

# A worker's task_self is the same id its spawner got back from task_spawn.
define _self_worker() as:
return task_self of null
_self_w is task_spawn of _self_worker
assert_eq of [task_join of _self_w, _self_w, "worker's task_self equals its spawn id"]

# Reply-address pattern (the gap that motivated #526): the spawner hands its
# own id down; the worker messages its own task_self back to that address.
define _self_replier(sup) as:
local me is task_self of null
task_send of [sup, me]
return null
_self_sup is task_self of null
_self_r is task_spawn of [_self_replier, _self_sup]
task_join of _self_r
_self_msg is task_recv of null
assert_eq of [_self_msg, _self_r, "worker replied its own id to the supervisor's mailbox"]

test_summary of null
Loading