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::>();
+ }
+}