From 53cb85b7a7b8c605e4adceef021006579b57e172 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Thu, 9 Jul 2026 18:12:11 -0500 Subject: [PATCH] =?UTF-8?q?feat(tasks):=20task=5Fself=20=E2=80=94=20a=20ta?= =?UTF-8?q?sk=20can=20learn=20its=20own=20id=20(#526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Deterministic (pure scheduler state, zero tape records). Unblocks the message-link supervision pattern surfaced by lib/supervise (#409): a worker can now hand out its own id as a reply address. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 7 +++++++ docs/BUILTINS.md | 1 + docs/SPEC.md | 7 +++++++ src/builtins.c | 11 +++++++++++ src/vm.c | 8 ++++++++ src/vm.h | 1 + tests/test_tasks.eigs | 23 +++++++++++++++++++++++ 7 files changed, 58 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a5d8b..5be3319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md index 1359bfe..ea3b637 100644 --- a/docs/BUILTINS.md +++ b/docs/BUILTINS.md @@ -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. | diff --git a/docs/SPEC.md b/docs/SPEC.md index ae38889..caad473 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -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: diff --git a/src/builtins.c b/src/builtins.c index 9848c34..d1cd1c9 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -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 @@ -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)); diff --git a/src/vm.c b/src/vm.c index 05fcdff..9fca437 100644 --- a/src/vm.c +++ b/src/vm.c @@ -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), diff --git a/src/vm.h b/src/vm.h index d5da70b..f55142e 100644 --- a/src/vm.h +++ b/src/vm.h @@ -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 */ diff --git a/tests/test_tasks.eigs b/tests/test_tasks.eigs index 16722a2..8b44b9f 100644 --- a/tests/test_tasks.eigs +++ b/tests/test_tasks.eigs @@ -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