Commit Graph
5 Commits
Author SHA1 Message Date
smarm-agent a866e34b52 mailbox: typed-path producers, by-name gen_servers, root-exit teardown (RFC 014)
Implements the new addressing surface the examples in examples/ specify:

- spawn_addr::<A>(FnOnce(Receiver<A::Msg>)) -> Pid<A>: the typed-path
  producer. Makes the inbox, spawns the body with its receiver, and publishes
  the sender from the PARENT side (registry::install_for) before returning the
  pid, so an immediate send_to always resolves (no race on the body installing
  itself). Detached, like ServerBuilder::start.

- lookup_as / pick_as / members_as: re-type an erased pid as Pid<A> over the
  heterogeneous registry/pg stores, sharing one helper (pid::assert_type).
  Unchecked but sound: routing is by message TypeId, so a wrong A degrades to
  SendError::NoChannel on the next send, never a misdelivery.

- dispatch::<A>(group, msg) -> Result<Pid<A>, SendError<A::Msg>>: pick_as +
  send_to, with the message handed back on failure. New SendError::NoMember
  variant for the empty/all-dead group case.

- gen_server naming: ServerName<G> backed internally by the registry's existing
  typed-channel store keyed by TypeId::of::<Envelope<G>>() — Envelope stays
  private, no separate directory. Type-state NamedServerBuilder<G> keeps the
  current infallible ServerBuilder::start() untouched; its start() is fallible
  (parent-side register, NameTaken). Free call/cast/whereis_server resolve per
  use. ServerRef::shutdown (+ free shutdown) is the sys-style synchronous stop.

- root-exit teardown: the run's initial actor is recorded as root; when it
  finalizes it flags root_exited, and the scheduler's idle verdict then stops
  the parked-forever remainder (a one-shot RootDrain sweep). Deferred to the
  idle point on purpose: the run queue drains first, so actors with queued work
  finish rather than unwinding on the stop. This lets a named-server daemon (or
  any pinned actor) wind the run down instead of hanging on live_actors > 0.

request_stop is refactored to a request_stop_inner core so the sweep can drive
it from inside the runtime without re-borrowing the thread-local.

Lib + tests + examples build warning-free; full suite green.
2026-06-18 11:02:27 +00:00
smarm-agent 4c56938f0b mailbox: Phase 4b — send_dyn bare-pid escape hatch (RFC 014 §4.6)
The explicit fallible fallback for when only an untyped Pid is in hand (off a
Down, out of a future members()), so the typed send_to / send-by-name stay the
default and callers don't grow workarounds around a missing primitive.

- send_dyn::<M>(Pid, msg): one line over the shared send_to_pid core. Same
  identity-bound, no-redirect liveness as send_to (Dead once the incarnation is
  gone); the difference is the explicit M, so NoChannel is a real runtime
  outcome here rather than the debug_assert it collapses to on the typed paths.
- Takes Pid<Erased> specifically: a typed Pid<A> must .erase() to reach it, so
  opting into the fallible downcast stays visible at the call site.

tests/registry.rs: send_dyn_delivers_and_reports_wrong_type (right type
delivers; live actor with no channel for the asked type -> NoChannel) and
send_dyn_to_dead_pid_is_dead. Full suite + order-checker green, warning-free.

This completes the RFC 014 send surface: Name<M> (re-resolving), Pid<A>
(typed direct), send_dyn (bare-pid). send_after (§6) can now target any of
them — its Dest question is answered.
2026-06-17 11:45:48 +00:00
smarm-agent 3cec3ba1a1 mailbox: Phase 4a — direct identity-bound send to Pid<A> (RFC 014 §4.2)
Make a Pid<A> messageable: install + send_to, the direct addressing mode that
dies with the actor and never redirects (the counterpart to the re-resolving
Name).

- install::<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A>: opt-in, lazy,
  nameless publish (RFC 014 §5). Files the actor's inbox into the by_index
  mailbox table under TypeId::of::<A::Msg>() and re-types self's identity as
  Pid<A>. Infallible: no name to collide on, self is always live in run().
- register and install now share publish_channel (the insert-or-extend-mailbox
  step factored out); register additionally binds the name. Byte-identical
  storage, so name- and pid-addressing populate the same table.
- send_to::<A>(Pid<A>, A::Msg): resolve by raw pid, deliver with NO redirect.
  The stored mailbox must be this exact incarnation (generation included) and
  live, else SendError::Dead — even when the slot now holds a different live
  actor, which is left untouched. Resolution clones the Sender under the Leaf
  lock, releases, then sends (Leaf -> Channel), as name send / pg / finalize.
- send_to_pid is the shared raw-pid core; send_dyn (§4.6) lands on it next.
- SendError gains Dead(M), the pid-path counterpart to name-path Unresolved;
  into_inner / Display / Debug updated. Name send never yields Dead, pid send
  never yields Unresolved — disjoint by construction, documented on the enum.

tests/registry.rs: install_then_send_to_pid_delivers; and the load-bearing
send_to_does_not_redirect_after_takeover (slot reused by a new incarnation, the
stale Pid<A> send returns Dead and the new occupant gets only its own message).
Full suite + order-checker green, warning-free across all targets.
2026-06-17 11:43:16 +00:00
smarm-agent 48bdbada2b mailbox: Phase 2 — registry resolves name/pid to a messageable actor (RFC 014)
Rip out the old name<->pid bimap; rebuild registry.rs as the live mailbox
directory. A name resolves to a SINGLE actor (many-per-label is pg's job); that
actor owns a SET of typed channels (channels are typed, so no single mailbox),
keyed by message TypeId. Resolution: name -> pid (one actor) -> Mailbox ->
channel for type M. Same name + different type parameter selects different
channels of the one actor, so capability separation (§4.7) needs no new type.

- register<M>(Name<M>, Sender<M>): captures the current actor's channel under a
  name (one step, per decision). Adds channels cumulatively; NameTaken only vs a
  different LIVE holder; stale/dangling bindings pruned on contact.
- send<M>(Name<M>, msg): the payoff — resolve, clone the Sender UNDER the Leaf
  lock, release, then send (Leaf -> Channel; a send can unpark). Returns the msg
  on Unresolved / NoChannel / Closed.
- whereis -> single live pid; unregister frees only the name (mailbox/other
  names survive). name_of (reverse lookup) dropped — deferred to introspection.
- Contained erasure: each channel is Box<dyn Any+Send> filed under TypeId::of M
  and downcast to its own keying type, so the downcast can't fail on good data
  (debug-asserted via the stored type_name). Phantom M on Name re-imposes type.
- One Leaf RawMutex (the fold), so a name send stays on a single Leaf — two
  Leaves at once is a hard panic. runtime.rs field/init unchanged (same Registry
  name + new()).

tests/registry.rs rewritten for the new semantics: send-by-name delivery,
many typed channels on one actor, same-name/different-type routing, NameTaken,
takeover across slot reuse, Unresolved/NoChannel. Also fold in a Phase 1 fixup:
the phantom test key was an enum whose variants tripped dead-code under the test
build (only caught now that I run -D warnings on --tests). Full suite +
order-checker green, warning-free across all targets.
2026-06-16 19:17:45 +00:00
smarm 2c7cf0b811 feat(registry): named pid registry as a bimap
register/whereis/unregister plus the inverse lookup name_of. Erlang
semantics: one name per pid, one pid per name, registering over a live
binding errors; same-binding re-register is an idempotent Ok.

Bimap = two HashMaps held in exact inverse under one Leaf RawMutex in
RuntimeInner; the invariant is debug-asserted on every mutation. The
inverse direction costs one extra String per binding and buys O(1)
pid->name for supervisors/tracing/diagnostics.

Cleanup is lazy: no finalize_actor hook, no Slot field (which would buy
into the reset-in-three-places invariant). Liveness rides the
generation-checked slot word - pids are never reused, so a stale binding
is detectable, never misdirected - and bindings to dead actors behave as
absent, pruned on contact by whereis/name_of/register/unregister.
Liveness checks under the registry lock read only the atomic slot word,
so the leaf rule holds trivially.

send_by_name is deliberately absent: smarm has no per-pid mailbox, so
there is nothing generic to send *to* - the registry yields a Pid,
usable with monitor/link/request_stop and as routing for user channels.

Tests: roundtrip both directions, idempotence, NameTaken on live holder,
one-name-per-pid, NoProc, death-evaporates-binding (via lookup pruning
and via register's own eviction path), unregister, cross-actor lookup
wired into request_stop, and a 4-scheduler name-contention churn test.
2026-06-09 22:53:43 +00:00