From 4c56938f0bd6dcdb65b861ec178bd52c3ac224c6 Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Wed, 17 Jun 2026 11:45:48 +0000 Subject: [PATCH] =?UTF-8?q?mailbox:=20Phase=204b=20=E2=80=94=20send=5Fdyn?= =?UTF-8?q?=20bare-pid=20escape=20hatch=20(RFC=20014=20=C2=A74.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::(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 specifically: a typed Pid 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 (re-resolving), Pid (typed direct), send_dyn (bare-pid). send_after (§6) can now target any of them — its Dest question is answered. --- src/lib.rs | 4 +++- src/registry.rs | 18 ++++++++++++++++++ tests/registry.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f723bf7..49a6ad6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,9 @@ pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId}; pub use mutex::{LockTimeout, Mutex, MutexGuard}; pub use pid::{Addressable, Erased, Name, Pid, RawPid}; pub use pg::{join, leave, members, pick, Incarnation, Member, NodeId}; -pub use registry::{install, register, send, send_to, unregister, whereis, RegisterError, SendError}; +pub use registry::{ + install, register, send, send_dyn, send_to, unregister, whereis, RegisterError, SendError, +}; pub use runtime::{init, Config, Runtime}; pub use scheduler::{ block_on_io, request_stop, run, self_pid, sleep, spawn, spawn_under, wait_readable, diff --git a/src/registry.rs b/src/registry.rs index f26d716..1ecaf47 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -398,3 +398,21 @@ fn send_to_pid( pub fn send_to(pid: Pid, msg: A::Msg) -> Result<(), SendError> { with_runtime(|inner| send_to_pid::(inner, pid.erase(), msg)) } + +/// The explicit bare-pid escape hatch (RFC 014 §4.6): deliver `msg` of type `M` +/// to `pid` when all you hold is an untyped [`Pid`] — a pid off a [`Down`], or +/// out of a future `members()` — so the typed [`send_to`] is unavailable. +/// +/// This is the one send whose message type can genuinely be wrong: the actor +/// may be live yet expose no channel for `M`, returning [`SendError::NoChannel`] +/// (on the typed paths that downcast collapses to a `debug_assert`). It is +/// named and documented as the fallible fallback so the typed `Pid` / +/// `Name` paths stay the obvious default and an agentic caller reaches for a +/// present primitive instead of inventing a workaround. Liveness is identical +/// to [`send_to`]: identity-bound, no redirect, [`SendError::Dead`] once the +/// addressed incarnation is gone. Panics if called outside `Runtime::run()`. +/// +/// [`Down`]: crate::Down +pub fn send_dyn(pid: Pid, msg: M) -> Result<(), SendError> { + with_runtime(|inner| send_to_pid::(inner, pid, msg)) +} diff --git a/tests/registry.rs b/tests/registry.rs index 41e7308..908af90 100644 --- a/tests/registry.rs +++ b/tests/registry.rs @@ -6,8 +6,8 @@ //! register-then-send race without busy-waiting on `whereis`. use smarm::{ - channel, install, register, run, send, send_to, spawn, unregister, whereis, Addressable, Name, - Pid, RegisterError, SendError, + channel, install, register, run, send, send_dyn, send_to, spawn, unregister, whereis, + Addressable, Name, Pid, RegisterError, SendError, }; const SVC: Name = Name::new("svc"); @@ -209,3 +209,43 @@ fn send_to_does_not_redirect_after_takeover() { b.join().unwrap(); }); } + +// --- RFC 014 §4.6: explicit bare-pid escape hatch --------------------------- + +#[test] +fn send_dyn_delivers_and_reports_wrong_type() { + run(|| { + let (ready_tx, ready_rx) = channel::<()>(); + let (done_tx, done_rx) = channel::<()>(); + let (tx, rx) = channel::(); + let h = spawn(move || { + register(Name::::new("dyn"), tx).unwrap(); // publishes a u64 channel + ready_tx.send(()).unwrap(); + assert_eq!(rx.recv().unwrap(), 3); + done_rx.recv().unwrap(); // stay alive for the wrong-type probe + }); + ready_rx.recv().unwrap(); + let p = h.pid(); // a bare Pid, as if recovered off a Down + send_dyn::(p, 3u64).unwrap(); // right type: delivered + // Live actor, but it has no channel for &str — the genuinely-fallible case. + assert!(matches!(send_dyn::<&'static str>(p, "nope"), Err(SendError::NoChannel(_)))); + done_tx.send(()).unwrap(); + h.join().unwrap(); + }); +} + +#[test] +fn send_dyn_to_dead_pid_is_dead() { + run(|| { + let (ready_tx, ready_rx) = channel::<()>(); + let (tx, _rx) = channel::(); + let h = spawn(move || { + register(Name::::new("dyn2"), tx).unwrap(); + ready_tx.send(()).unwrap(); // then return -> die + }); + ready_rx.recv().unwrap(); + let p = h.pid(); + h.join().unwrap(); // dead; the bare pid now names a dead incarnation + assert!(matches!(send_dyn::(p, 1u64), Err(SendError::Dead(_)))); + }); +}