Skip to main content

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}