From 48bc6365520f46c35604c1e8343c339807c29463 Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Tue, 16 Jun 2026 18:33:32 +0000 Subject: [PATCH] =?UTF-8?q?mailbox:=20Phase=201=20=E2=80=94=20typed-addres?= =?UTF-8?q?s=20tokens=20(RFC=20014)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the value-token layer for typed addressing, ahead of the handle table: - Addressable { type Msg }: minimal actor-type -> message-type hook (decision (a)). Raw actors are closures over channels; GenServer is multi-message and stays addressed via ServerRef. This gives the single-message actors a key. - Addr: direct, identity-bound address (RFC's Pid, renamed to avoid the collision with the non-generic Pid identity). Plain Pid + PhantomDataA>. - Name: durable, re-resolving address; const-constructible (Name::new). Both tokens are Copy + Send + Sync regardless of key (phantom is fn()->T), with hand-written trait impls so no spurious T: Copy/Eq bounds leak in. No behavior yet; send/resolve/storage land in later phases. Addr::new is #[cfg(test)] until Phase 2 has a real (non-test) caller. Unit tests cover identity/copy/eq/debug and Send+Sync. --- src/lib.rs | 2 +- src/pid.rs | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index fcd5ae1..80076cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ pub use gen_server::{CallError, CallTimeoutError, CastError, GenServer, ServerBu pub use link::{link, trap_exit, unlink, ExitSignal}; pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId}; pub use mutex::{LockTimeout, Mutex, MutexGuard}; -pub use pid::Pid; +pub use pid::{Addr, Addressable, Name, Pid}; pub use pg::{join, leave, members, pick, Incarnation, Member, NodeId}; pub use registry::{name_of, register, unregister, whereis, RegisterError}; pub use runtime::{init, Config, Runtime}; diff --git a/src/pid.rs b/src/pid.rs index 2c1a7d9..1352b52 100644 --- a/src/pid.rs +++ b/src/pid.rs @@ -36,3 +36,188 @@ impl std::fmt::Display for Pid { write!(f, "<{}.{}>", self.index, self.generation) } } + +// --------------------------------------------------------------------------- +// Typed addresses (RFC 014 — typed addressable mailboxes). Phase 1: types only. +// --------------------------------------------------------------------------- +// +// Two addressing modes layered over the plain `Pid` identity, distinguished by +// their liveness semantics (RFC 014 §4.2): +// +// - `Addr` — direct, identity-bound. Keyed by the *actor type* `A`; its +// message type is `A::Msg` (see [`Addressable`]). Bound to one specific actor +// incarnation: once that actor dies the send fails (generation mismatch) and +// is *not* redirected. This is the RFC's `Pid`, renamed — a generic +// `Pid` cannot coexist with the non-generic `Pid` identity the rest of the +// runtime is built on. +// - `Name` — durable, re-resolving, location-transparent. A phantom-typed +// key over the registry, re-resolved on *every* send, so it always reaches +// whoever currently holds the name (surviving takeover; later, locating an +// actor on another node). The clustering / BEAM-interop priority mode. +// +// Both are pure value tokens: an identity (or a name) plus a zero-sized phantom +// that re-imposes the message type at the call site. The phantom is +// `fn() -> T`, so a token is unconditionally `Copy + Send + Sync` and borrows +// nothing from `T` — `T` is only ever a compile-time key. (This is also why the +// trait impls below are hand-written: a `#[derive]` would wrongly demand +// `T: Copy`/`Eq`/… on the phantom parameter.) + +use std::marker::PhantomData; + +/// An actor type with a single associated message type, so it can key an +/// [`Addr`]. The raw channel layer has no such trait (actors are closures over +/// channels), and `GenServer` is intrinsically multi-message and addressed via +/// its own `ServerRef`; this is the minimal hook that gives a typed, +/// identity-bound address to the actors that *do* have one message type. +/// (RFC 014 §4.2, decision (a).) +pub trait Addressable: 'static { + /// The message this actor receives. An `Addr` delivers `Self::Msg`. + type Msg: Send + 'static; +} + +/// A direct, identity-bound address to a specific actor of type `A` +/// (RFC 014's `Pid`): the plain [`Pid`] identity plus a phantom `A`, whose +/// message type is `A::Msg`. A send through an `Addr` targets the exact +/// incarnation the pid names; when that actor dies the send fails and is *not* +/// redirected. For a durable, re-resolving address use [`Name`]. +pub struct Addr { + pid: Pid, + _marker: PhantomData A>, +} + +impl Addr { + /// Wrap a plain identity as a typed address. Crate-internal on purpose: a + /// public bare-`Pid` → typed-`Addr` mint is exactly the unchecked step the + /// typed API exists to avoid. The sanctioned bare-pid path is the fallible + /// `send_dyn` escape hatch (RFC 014 §4.6); typed addresses reach users via + /// spawn / resolution. + /// + /// Phase 1 has no non-test constructor (this is a types-only commit); the + /// `#[cfg(test)]` comes off in Phase 2 when the handle table / spawn mints + /// these for real. + #[cfg(test)] + #[inline] + pub(crate) const fn new(pid: Pid) -> Self { + Self { pid, _marker: PhantomData } + } + + /// The plain runtime identity this address points at. + #[inline] + pub const fn pid(self) -> Pid { + self.pid + } +} + +impl Copy for Addr {} +impl Clone for Addr { + fn clone(&self) -> Self { *self } +} +impl PartialEq for Addr { + fn eq(&self, other: &Self) -> bool { self.pid == other.pid } +} +impl Eq for Addr {} +impl std::hash::Hash for Addr { + fn hash(&self, state: &mut H) { self.pid.hash(state); } +} +impl std::fmt::Debug for Addr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Addr<{}>({})", std::any::type_name::(), self.pid) + } +} + +/// A durable, re-resolving address: a static name plus a phantom message type +/// `M` (RFC 014's `Name`). Declared as a constant and shared freely: +/// +/// ```ignore +/// const COUNTER: Name = Name::new("counter"); +/// ``` +/// +/// Unlike [`Addr`], a `Name` is resolved through the registry on *every* send, +/// so it always reaches whoever currently holds the name. +pub struct Name { + name: &'static str, + _marker: PhantomData M>, +} + +impl Name { + /// Bind a static string as a typed name. `const`, so names live as + /// associated constants at call sites. + #[inline] + pub const fn new(name: &'static str) -> Self { + Self { name, _marker: PhantomData } + } + + /// The underlying registry key. + #[inline] + pub const fn as_str(self) -> &'static str { + self.name + } +} + +impl Copy for Name {} +impl Clone for Name { + fn clone(&self) -> Self { *self } +} +impl PartialEq for Name { + fn eq(&self, other: &Self) -> bool { self.name == other.name } +} +impl Eq for Name {} +impl std::hash::Hash for Name { + fn hash(&self, state: &mut H) { self.name.hash(state); } +} +impl std::fmt::Debug for Name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Name<{}>({:?})", std::any::type_name::(), self.name) + } +} + +#[cfg(test)] +mod typed_addr_tests { + use super::*; + + // A stand-in actor type with one message type, exercising `Addressable`. + struct Counter; + enum CounterMsg { Inc, Get } + impl Addressable for Counter { + type Msg = CounterMsg; + } + + fn msg_type_name() -> &'static str { + std::any::type_name::() + } + + #[test] + fn addr_is_a_copyable_identity_token() { + let a = Addr::::new(Pid::new(3, 1)); + let b = a; // Copy, not move + assert_eq!(a.pid(), Pid::new(3, 1)); + assert_eq!(a, b); + // Same index, different generation = different actor incarnation. + assert_ne!(a, Addr::::new(Pid::new(3, 2))); + assert!(format!("{a:?}").starts_with("Addr<")); + } + + #[test] + fn name_is_a_copyable_string_token() { + const COUNTER: Name = Name::new("counter"); + let n = COUNTER; // Copy + assert_eq!(n.as_str(), "counter"); + assert_eq!(n, COUNTER); + assert_ne!(n, Name::::new("other")); + assert!(format!("{n:?}").contains("\"counter\"")); + } + + #[test] + fn addressable_exposes_the_message_type() { + assert!(msg_type_name::().ends_with("CounterMsg")); + } + + // Tokens must be usable across threads (they key cross-node addressing + // later): assert Send + Sync regardless of whether the keys are. + #[test] + fn tokens_are_send_sync_without_key_bounds() { + fn assert_send_sync() {} + assert_send_sync::>(); + assert_send_sync::>(); + } +}