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};
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}