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};
34use std::sync::Arc;
35
36use tatara_lisp_eval::ffi::Arity;
37use tatara_lisp_eval::{ErrorObj, Interpreter, Value};
38
39use crate::Pid;
40
41/// The host state a blue process tree runs against.
42///
43/// Mailboxes live here rather than on the [`crate::Supervisor`] on purpose:
44/// the supervisor owns the processes and hands `&mut H` to `Vm::step`, so a
45/// supervisor-owned mailbox map would be borrowed twice at once.
46#[derive(Debug, Default)]
47pub struct System {
48 boxes: HashMap<Pid, VecDeque<Value>>,
49 /// Whose turn it is. The scheduler sets this before each slice so
50 /// `receive` and `self` know which process is asking.
51 current: Pid,
52 /// Sends to a pid with no mailbox. Kept rather than dropped so a
53 /// misdirected message is diagnosable instead of invisible.
54 pub undeliverable: Vec<(Pid, Value)>,
55}
56
57impl crate::Readiness for System {
58 fn is_ready(&self, pid: Pid) -> bool {
59 self.has_mail(pid)
60 }
61
62 fn any_pending(&self) -> bool {
63 self.any_mail()
64 }
65
66 fn set_current(&mut self, pid: Pid) {
67 self.current = pid;
68 }
69
70 fn on_restart(&mut self, pid: Pid) {
71 self.clear(pid);
72 }
73}
74
75impl System {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 /// Give `pid` a mailbox. Called by the supervisor on spawn.
81 pub fn register(&mut self, pid: Pid) {
82 self.boxes.entry(pid).or_default();
83 }
84
85 pub fn current(&self) -> Pid {
86 self.current
87 }
88
89 /// Enqueue a **deep copy** of `msg`.
90 ///
91 /// `Value` is `Arc`-based, so pushing it as-is would share structure
92 /// between two processes and quietly reintroduce exactly the coupling that
93 /// separate interpreters removed. Copying at the boundary is what makes
94 /// "processes share nothing" true rather than nearly true.
95 pub fn send(&mut self, to: Pid, msg: Value) {
96 let msg = deep_copy(&msg);
97 match self.boxes.get_mut(&to) {
98 Some(q) => q.push_back(msg),
99 None => self.undeliverable.push((to, msg)),
100 }
101 }
102
103 pub fn has_mail(&self, pid: Pid) -> bool {
104 self.boxes.get(&pid).is_some_and(|q| !q.is_empty())
105 }
106
107 pub fn mail_count(&self, pid: Pid) -> usize {
108 self.boxes.get(&pid).map_or(0, VecDeque::len)
109 }
110
111 /// Empty a mailbox. A restarted process starts with a clean one: the
112 /// messages that were in flight when it crashed were addressed to the
113 /// *incarnation that died*, and replaying them into the fresh one is how
114 /// a poison message takes down a process forever.
115 pub fn clear(&mut self, pid: Pid) {
116 if let Some(q) = self.boxes.get_mut(&pid) {
117 q.clear();
118 }
119 }
120
121 /// Is any mailbox non-empty? Used to tell a live wait from a deadlock.
122 pub fn any_mail(&self) -> bool {
123 self.boxes.values().any(|q| !q.is_empty())
124 }
125}
126
127/// Copy a value so nothing is shared with the sender.
128///
129/// Scalars are already values. Containers are rebuilt, so the receiver holds
130/// its own `Arc`s. **Non-data values are refused rather than shared**: a
131/// closure captures its defining environment, so sending one would hand a
132/// second process a live reference into the first's globals — the exact leak
133/// this function exists to prevent. Refusing yields `Nil` at the boundary
134/// rather than silently sharing; [`copy_data`] is the strict form that names
135/// the refusal instead.
136pub fn deep_copy(v: &Value) -> Value {
137 match v {
138 Value::Nil
139 | Value::Bool(_)
140 | Value::Int(_)
141 | Value::Float(_)
142 | Value::Str(_)
143 | Value::Symbol(_)
144 | Value::Keyword(_) => v.clone(),
145 Value::List(items) => Value::list(items.iter().map(deep_copy)),
146 // Maps and error objects are data. Before tatara-lisp 0.4 gave blue a
147 // persistent `Map`, this arm did not exist and a sent `{a: 1}` arrived
148 // as nil.
149 Value::Map(m) => Value::Map(Arc::new(
150 m.iter().map(|(k, v)| (k.clone(), deep_copy(v))).collect(),
151 )),
152 Value::Error(e) => Value::Error(Arc::new(ErrorObj {
153 tag: e.tag.clone(),
154 message: e.message.clone(),
155 data: e
156 .data
157 .iter()
158 .map(|(k, v)| (deep_copy(k), deep_copy(v)))
159 .collect(),
160 })),
161 Value::Sexp(s, span) => Value::Sexp(s.clone(), *span),
162 // A closure, a promise, a foreign handle: each either captures an
163 // environment or wraps host state. Not copyable, and not safe to share.
164 _ => Value::Nil,
165 }
166}
167
168/// [`deep_copy`] that refuses non-data instead of turning it into nil.
169///
170/// What the blue-level words use: a `send` or `spawn` that silently delivered
171/// nil in place of a function would move the failure to the receiver, far from
172/// the line that caused it. The error names the kind of value refused.
173///
174/// # Errors
175///
176/// The type name of the first value that is not data.
177pub fn copy_data(v: &Value) -> Result<Value, &'static str> {
178 Ok(match v {
179 Value::Nil
180 | Value::Bool(_)
181 | Value::Int(_)
182 | Value::Float(_)
183 | Value::Str(_)
184 | Value::Symbol(_)
185 | Value::Keyword(_) => v.clone(),
186 Value::List(items) => {
187 Value::list(items.iter().map(copy_data).collect::<Result<Vec<_>, _>>()?)
188 }
189 Value::Map(m) => Value::Map(Arc::new(
190 m.iter()
191 .map(|(k, v)| copy_data(v).map(|v| (k.clone(), v)))
192 .collect::<Result<_, _>>()?,
193 )),
194 Value::Error(e) => Value::Error(Arc::new(ErrorObj {
195 tag: e.tag.clone(),
196 message: e.message.clone(),
197 data: e
198 .data
199 .iter()
200 .map(|(k, v)| Ok((copy_data(k)?, copy_data(v)?)))
201 .collect::<Result<_, &'static str>>()?,
202 })),
203 Value::Sexp(s, span) => Value::Sexp(s.clone(), *span),
204 other => return Err(kind_of(other)),
205 })
206}
207
208/// What a non-data value is, in words a refusal can use. A function is a
209/// function whichever executor made it: the VM's closures are `Foreign`
210/// values, and naming them "foreign" would make one refusal read two ways.
211fn kind_of(v: &Value) -> &'static str {
212 match v {
213 Value::Closure(_) | Value::NativeFn(_) => "function",
214 Value::Foreign(any) if any.is::<tatara_lisp_eval::vm::run::CompiledClosure>() => "function",
215 Value::Foreign(_) => "host handle",
216 other => other.type_name(),
217 }
218}
219
220/// Install `send`, `receive` and `self` into an interpreter whose host is a
221/// [`System`].
222///
223/// Separate from `blue_lang_runtime::interpreter` because these three are the
224/// only primitives that require a scheduler: called from `Vm::run`, `receive`
225/// on an empty mailbox is a named deadlock rather than a wait.
226pub fn install_process_primitives(interp: &mut Interpreter<System>) {
227 // (send pid msg) -> msg
228 //
229 // Returns the message, as Erlang's `!` does, so `send` composes in an
230 // expression rather than only as a statement.
231 interp.register_fn(
232 "send",
233 Arity::Exact(2),
234 |args: &[Value], sys: &mut System, span| {
235 let to = match &args[0] {
236 Value::Int(n) => Pid(*n as u64),
237 // A typed rejection, not a silent no-op: sending to a
238 // non-pid is a program error and must read as one.
239 other => {
240 return Err(tatara_lisp_eval::EvalError::type_mismatch(
241 "pid (int)",
242 other.type_name(),
243 span,
244 )
245 .into())
246 }
247 };
248 sys.send(to, args[1].clone());
249 Ok(args[1].clone())
250 },
251 );
252
253 // (receive) -> the oldest message, parking while the mailbox is empty.
254 //
255 // Two-phase: `ready` may only LOOK at the mailbox (it holds `&System`),
256 // and `take` runs only once something is there. The compiler, not a
257 // comment, is what stops a future selective receive from popping a
258 // message and then parking.
259 interp.register_awaitable_fn(
260 "receive",
261 Arity::Exact(0),
262 |_args: &[Value], sys: &System| sys.has_mail(sys.current()),
263 |_args: &[Value], sys: &mut System, _span| {
264 let me = sys.current();
265 Ok(sys
266 .boxes
267 .get_mut(&me)
268 .and_then(VecDeque::pop_front)
269 .unwrap_or(Value::Nil))
270 },
271 );
272
273 // (self) -> my pid
274 interp.register_fn(
275 "self",
276 Arity::Exact(0),
277 |_args: &[Value], sys: &mut System, _span| Ok(Value::Int(sys.current().0 as i64)),
278 );
279}