mailbox: Phase 3 — typed Pid<A> over a raw inner identity (RFC 014)

Fold the made-up Addr<A> back into the pid, where it belonged. The typed
handle IS the pid now.

- RawPid { index, generation }: today's Pid, renamed — the raw numbers, the key
  for identity-only plumbing and the heterogeneous monitor/link/pg tables.
- Pid<A = Erased>: RawPid + phantom actor type. Both an identity and a direct,
  identity-bound address; when A: Addressable a send delivers A::Msg to exactly
  this incarnation (next phase). Hand-written impls (eq/hash/fmt on the raw
  only, no A bounds); fn()->A phantom keeps it Copy+Send+Sync for any A.
- Erased: uninhabited, deliberately NOT Addressable, so a typed send to a
  Pid<Erased> won't compile — that's what send_dyn (section 4.6) will be for. It
  is the default type arg, so every plain "Pid" written today still compiles as
  Pid<Erased>; that, plus the fact that nothing destructures pid fields, is why
  an identity change touching 18 files cascaded to zero internal edits.
- Addr<A> deleted; Name<M> / the Phase-2 registry unchanged (they key on raw).

Per the call to make the consumers take typed pids: the user-facing identity
APIs — monitor, link, unlink, request_stop, spawn_under(supervisor), pg::join,
pg::leave — are now generic over A and erase at the boundary; their bodies are
byte-for-byte unchanged. Pure plumbing (unpark, set_current_pid,
register_supervisor_channel) stays erased — it only ever sees internal identity.

Phase 1's Addr tests become Pid<A> tests (identity/copy/erase/Debug, Send+Sync).
Full suite + order-checker green, warning-free across all targets.
This commit is contained in:
smarm-agent
2026-06-16 22:09:27 +00:00
parent 48bdbada2b
commit f5fbb5b144
6 changed files with 163 additions and 110 deletions
+4 -2
View File
@@ -175,7 +175,8 @@ pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle {
spawn_under(parent, f)
}
pub fn spawn_under(supervisor: Pid, f: impl FnOnce() + Send + 'static) -> JoinHandle {
pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) -> JoinHandle {
let supervisor = supervisor.erase();
// Stack + closure boxing happen before ANY runtime lock is taken: no
// syscall and no allocation ever stalls another scheduler thread.
let stack = with_runtime(|inner| inner.stack_pool.lock().pop())
@@ -287,7 +288,8 @@ pub(crate) fn retire_wait() {
/// observation point — a tight loop with no `check!()`, no allocation, and no
/// blocking op — cannot be stopped, exactly as it cannot be preempted. A no-op
/// if `pid` is already gone.
pub fn request_stop(pid: Pid) {
pub fn request_stop<A>(pid: Pid<A>) {
let pid = pid.erase();
let _ = try_with_runtime(|inner| {
if let Some(slot) = inner.slot_at(pid) {
{