docs(roadmap,readme): v0.6 complete — record outcomes and deviations

Marks the three v0.6 items done with their hashes and the load-bearing
deviations: two-class lock order replacing the strict leaf rule, the
registry as a bimap with name_of and without send_by_name (no per-pid
mailbox to send to), and call_timeout built on recv_timeout instead of
monitor machinery. Adds the Leaf -> Channel order to the standing
invariants; README module table picks up recv_timeout, call_timeout,
and the registry row.
This commit is contained in:
smarm
2026-06-09 22:58:26 +00:00
parent 90b7040504
commit 088280f3c9
2 changed files with 48 additions and 23 deletions
+45 -21
View File
@@ -104,31 +104,51 @@ precondition.
---
## v0.6 — Up next
## v0.6 — Actor ergonomics ✅ DONE
### 1. Channel mutex migration (first change)
`channel::Inner<T>` holds a `std::sync::Mutex`. Phase 2 surfaced a sharp
correctness issue: the `MutexGuard` is held with preemption **enabled**, so a
timeslice switch inside a channel critical section migrates the actor and
releases the pthread mutex from a different OS thread — UB (Linux futexes happen
to tolerate it, but it's not guaranteed). The Phase-1 `check_cancelled` gating
already removes the unwind source, so poison is safe today; this is about the
cross-thread release.
### 1. Channel mutex migration ✅ (`8ff6cf4`)
`channel::Inner<T>` migrated from `std::sync::Mutex` to `RawMutex`, closing
the cross-thread pthread-release hole (guard held with preemption enabled
timeslice switch → unlock from a different OS thread, UB) and removing poison.
Migrate to `RawMutex` (which disables preemption and is cross-thread-release
sound by construction). `recv_match` runs a user predicate under this lock —
keep it cheap/pure.
The strict leaf rule did not survive contact: finalize clones the
supervisor/trap senders and `monitor()` clones the Down sender, all under a
cold lock, and those senders *live in the slot* — the nesting is structural.
The check generalized to two classes: **Leaf → Channel**, at most one of
each, both violation directions debug-asserted at the acquisition site.
Channel critical sections call only the lock-free unpark protocol, so the
order is acyclic. `recv_match`'s user predicate still runs under the channel
lock — cheap/pure/channel-free, as documented.
### 2. Named registry
`register(name, pid)` / `whereis(name) -> Option<Pid>` / `send_by_name`.
`HashMap<String, Pid>` in `SharedState`, guarded by the shared lock. No
architecture changes; can land immediately after #1.
### 2. Named registry ✅ (`2c7cf0b`)
`register` / `whereis` / `unregister` + the inverse `name_of(pid)`: a
**bimap** (two HashMaps in exact inverse, debug-asserted) under one Leaf
`RawMutex` in `RuntimeInner` — the planned "HashMap in SharedState" predated
the v0.5 decomposition. Erlang semantics: one name per pid, one pid per
name, NameTaken only against a *live* holder. Cleanup is lazy: liveness
rides the generation-checked slot word (pids are never reused), dead
bindings behave as absent and are pruned on contact — no `finalize_actor`
hook, no Slot field, no reset-in-three-places exposure.
### 3. gen_server: call timeout
`call` with a deadline — monitor the server, wait reply-or-Down-or-deadline,
then `demonitor`+drop so a timed-out call leaks no registration. Requires a
per-`recv` deadline / `Signal::Timeout` primitive that doesn't exist yet.
(`handle_info` deferred — needs cross-channel mailbox merge; see Later.)
`send_by_name` was dropped from scope deliberately: smarm has no per-pid
mailbox, so there is nothing generic to send *to*. The registry yields a
`Pid` for monitor/link/request_stop and for routing user-owned channels.
### 3. gen_server: call timeout ✅ (`134ff52`, `90b7040`)
Landed as two primitives, *without* the sketched monitor machinery:
- `Receiver::recv_timeout(Duration)` — the per-`recv` deadline, on the
existing `timer::Reason::WaitTimeout`/`TimerTarget` machinery exactly as
`Mutex::lock_timeout` uses it (per-wait seq; stale heap entries no-op on
seq mismatch; message-first race resolution; `Disconnected` distinct from
`Timeout`).
- `ServerRef::call_timeout` = send + `recv_timeout` on the reply channel.
Server death is already observable there (reply sender drops as the server
unwinds), so monitor + demonitor-on-timeout had nothing left to guarantee:
no registration exists, so nothing can leak. Timed-out requests are still
handled; the late reply's send fails harmlessly (Erlang semantics).
(`handle_info` still deferred — needs cross-channel mailbox merge; see Later.)
---
@@ -189,6 +209,10 @@ hardware. Build + run full test suite on an aarch64 device; check
cross-channel merge is the still-unmade decision (see v0.6 #3).
- **Cooperative-only.** Preemption and cancellation both depend on the actor
reaching `check!()`/yield/alloc/blocking points.
- **Lock order is Leaf → Channel, one of each at most** (debug-asserted in
`raw_mutex.rs`). Leaf = cold locks / free list / stack pool / registry,
mutual leaves. A channel lock may be taken under a Leaf (finalize/monitor
clone senders living in slots); nothing may be locked under a channel lock.
- **Queue ops require preemption disabled.** A producer suspended mid-publish
stalls every consumer — livelock. `with_runtime`, `with_shared`, and
`RawMutex` guards all disable preemption for their span.