blue_lang_proc/mailbox.rs
1//! Mailboxes, `send`, and `receive`.
2//!
3//! Supervision without message passing is just restart. A telephony switch or
4//! an order router is a *mesh* of processes exchanging messages; the
5//! supervisor is what keeps the mesh up, not what makes it work.
6//!
7//! ## Blocking is free, and that is the whole point
8//!
9//! `receive` on an empty mailbox parks the process via
10//! [`tatara_lisp_eval::vm::Vm::park`]: the VM restores the operand stack,
11//! rewinds to the call, and reports `Progress::Blocked`. The scheduler then
12//! sets the process aside until a message arrives.
13//!
14//! The alternative — polling in a loop — is not merely wasteful. It **burns
15//! the waiting process's fuel**, so a process waiting on a message that will
16//! certainly arrive can die of a budget it never needed. A telephony process
17//! idle between calls would be the first casualty.
18//!
19//! ## `receive` cannot lose a message, structurally
20//!
21//! A parking primitive must consume nothing, because its call is
22//! re-executed — and `receive` is registered through
23//! [`Interpreter::register_awaitable_fn`], whose readiness phase holds an
24//! **immutable** `&System`. Dequeue-then-park is therefore not discouraged
25//! here; it does not typecheck (`register_awaitable_fn`'s `compile_fail`
26//! doctest asserts exactly that).
27//!
28//! This matters most for the case that does not exist yet. A *selective*
29//! receive takes a message, finds it does not match the pattern, and must
30//! wait — the shape that loses the message on every non-match. The split
31//! removes the possibility before the feature that would have hit it.
32
33use std::collections::{HashMap, VecDeque};
34
35use tatara_lisp_eval::ffi::Arity;
36use tatara_lisp_eval::{Interpreter, Value};
37
38use crate::Pid;
39
40/// The host state a blue process tree runs against.
41///
42/// Mailboxes live here rather than on the [`crate::Supervisor`] on purpose:
43/// the supervisor owns the processes and hands `&mut H` to `Vm::step`, so a
44/// supervisor-owned mailbox map would be borrowed twice at once.
45#[derive(Debug, Default)]
46pub struct System {
47 boxes: HashMap<Pid, VecDeque<Value>>,
48 /// Whose turn it is. The scheduler sets this before each slice so
49 /// `receive` and `self` know which process is asking.
50 current: Pid,
51 /// Sends to a pid with no mailbox. Kept rather than dropped so a
52 /// misdirected message is diagnosable instead of invisible.
53 pub undeliverable: Vec<(Pid, Value)>,
54}
55
56impl crate::Readiness for System {
57 fn is_ready(&self, pid: Pid) -> bool {
58 self.has_mail(pid)
59 }
60
61 fn any_pending(&self) -> bool {
62 self.any_mail()
63 }
64
65 fn set_current(&mut self, pid: Pid) {
66 self.current = pid;
67 }
68
69 fn on_restart(&mut self, pid: Pid) {
70 self.clear(pid);
71 }
72}
73
74impl System {
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 /// Give `pid` a mailbox. Called by the supervisor on spawn.
80 pub fn register(&mut self, pid: Pid) {
81 self.boxes.entry(pid).or_default();
82 }
83
84 pub fn current(&self) -> Pid {
85 self.current
86 }
87
88 /// Enqueue a **deep copy** of `msg`.
89 ///
90 /// `Value` is `Arc`-based, so pushing it as-is would share structure
91 /// between two processes and quietly reintroduce exactly the coupling that
92 /// separate interpreters removed. Copying at the boundary is what makes
93 /// "processes share nothing" true rather than nearly true.
94 pub fn send(&mut self, to: Pid, msg: Value) {
95 let msg = deep_copy(&msg);
96 match self.boxes.get_mut(&to) {
97 Some(q) => q.push_back(msg),
98 None => self.undeliverable.push((to, msg)),
99 }
100 }
101
102 pub fn has_mail(&self, pid: Pid) -> bool {
103 self.boxes.get(&pid).is_some_and(|q| !q.is_empty())
104 }
105
106 pub fn mail_count(&self, pid: Pid) -> usize {
107 self.boxes.get(&pid).map_or(0, VecDeque::len)
108 }
109
110 /// Empty a mailbox. A restarted process starts with a clean one: the
111 /// messages that were in flight when it crashed were addressed to the
112 /// *incarnation that died*, and replaying them into the fresh one is how
113 /// a poison message takes down a process forever.
114 pub fn clear(&mut self, pid: Pid) {
115 if let Some(q) = self.boxes.get_mut(&pid) {
116 q.clear();
117 }
118 }
119
120 /// Is any mailbox non-empty? Used to tell a live wait from a deadlock.
121 pub fn any_mail(&self) -> bool {
122 self.boxes.values().any(|q| !q.is_empty())
123 }
124}
125
126/// Copy a value so nothing is shared with the sender.
127///
128/// Scalars are already values. Containers are rebuilt, so the receiver holds
129/// its own `Arc`s. **Non-data values are refused rather than shared**: a
130/// closure captures its defining environment, so sending one would hand a
131/// second process a live reference into the first's globals — the exact leak
132/// this function exists to prevent. Refusing yields `Nil` at the boundary
133/// rather than silently sharing.
134pub fn deep_copy(v: &Value) -> Value {
135 match v {
136 Value::Nil
137 | Value::Bool(_)
138 | Value::Int(_)
139 | Value::Float(_)
140 | Value::Str(_)
141 | Value::Symbol(_)
142 | Value::Keyword(_) => v.clone(),
143 Value::List(items) => Value::List(std::sync::Arc::new(
144 items.iter().map(deep_copy).collect::<Vec<_>>(),
145 )),
146 Value::Sexp(s, span) => Value::Sexp(s.clone(), *span),
147 // A closure, a promise, a foreign handle, an error object: each either
148 // captures an environment or wraps host state. Not copyable, and not
149 // safe to share.
150 _ => Value::Nil,
151 }
152}
153
154/// Install `send`, `receive` and `self` into an interpreter whose host is a
155/// [`System`].
156///
157/// Separate from `blue_lang_runtime::interpreter` because these three are the
158/// only primitives that require a scheduler: called from `Vm::run`, `receive`
159/// on an empty mailbox is a named deadlock rather than a wait.
160pub fn install_process_primitives(interp: &mut Interpreter<System>) {
161 // (send pid msg) -> msg
162 //
163 // Returns the message, as Erlang's `!` does, so `send` composes in an
164 // expression rather than only as a statement.
165 interp.register_fn(
166 "send",
167 Arity::Exact(2),
168 |args: &[Value], sys: &mut System, span| {
169 let to = match &args[0] {
170 Value::Int(n) => Pid(*n as u64),
171 // A typed rejection, not a silent no-op: sending to a
172 // non-pid is a program error and must read as one.
173 other => {
174 return Err(tatara_lisp_eval::EvalError::type_mismatch(
175 "pid (int)",
176 other.type_name(),
177 span,
178 )
179 .into())
180 }
181 };
182 sys.send(to, args[1].clone());
183 Ok(args[1].clone())
184 },
185 );
186
187 // (receive) -> the oldest message, parking while the mailbox is empty.
188 //
189 // Two-phase: `ready` may only LOOK at the mailbox (it holds `&System`),
190 // and `take` runs only once something is there. The compiler, not a
191 // comment, is what stops a future selective receive from popping a
192 // message and then parking.
193 interp.register_awaitable_fn(
194 "receive",
195 Arity::Exact(0),
196 |_args: &[Value], sys: &System| sys.has_mail(sys.current()),
197 |_args: &[Value], sys: &mut System, _span| {
198 let me = sys.current();
199 Ok(sys
200 .boxes
201 .get_mut(&me)
202 .and_then(VecDeque::pop_front)
203 .unwrap_or(Value::Nil))
204 },
205 );
206
207 // (self) -> my pid
208 interp.register_fn(
209 "self",
210 Arity::Exact(0),
211 |_args: &[Value], sys: &mut System, _span| Ok(Value::Int(sys.current().0 as i64)),
212 );
213}