Skip to main content

sv_sim_sim/sched/
mailbox.rs

1//! Classes plan (`docs/superpowers/plans/2026-10-03-classes.md`) Task 18: the
2//! scheduler-side state of the built-in `mailbox` / `mailbox #(T)` class (LRM
3//! 1800-2023 15.4).
4//!
5//! A mailbox is an ordinary heap object ([`crate::objects`]); its message queue
6//! and the processes blocked on it live here, in one [`Mailbox`] per object
7//! handle, because a message is a variable-sized run of words (the object's
8//! fixed-size field array cannot hold it) and a blocked process must be found
9//! again by the operation that releases it and by the engine when it is killed.
10//!
11//! # Messages
12//!
13//! A [`Message`] is the stored words of one value, a type fingerprint and a flag
14//! saying the value is a class handle (word 0), in which case the message owns
15//! one reference to the object (the object heap's usual rule for a holder: see
16//! `sv-sim-codegen`'s `collections`). Whoever takes a message out of the mailbox
17//! takes that reference over; a `peek` takes a new one (`objects::retain`).
18//! The fingerprint is how an untyped mailbox does the run-time type check of
19//! 15.4.5-15.4.8: a `get`/`peek` whose variable is not of an equivalent type to
20//! the message at the head of the queue is an error and takes nothing; the
21//! `try_` forms report it as a negative result.
22//!
23//! # Blocking
24//!
25//! `get`/`peek` on an empty mailbox and `put` on a full one block the caller.
26//! This module only decides who waits and who wakes; the scheduler blocks and
27//! wakes through the `emit` callback each function takes (`SyncBlock` for the
28//! caller that must wait, `SyncWake` per process released), the same protocol as
29//! [`super::semaphore`]. Waiters queue in arrival order ("The mailbox waiting
30//! queue is FIFO", 15.4.5).
31//!
32//! A blocked `get`/`peek` cannot hand its message to the caller directly (the
33//! call has already returned to compiled code, which must store the message
34//! into the destination variable so that the store is an observable value
35//! change). So [`get`] is a two-phase call, issued twice by the compiled
36//! statement (the CFG walker follows the statement with a suspend point and
37//! emits it again): the first call either completes at once or queues the
38//! process; the message handed to a woken waiter waits in a per-process slot
39//! (`pending`) until the second call, after the process resumed, collects it.
40//! A call that completed at once leaves a marker so the second call is a no-op.
41//! A woken waiter's message is chosen when it is woken (a `get` takes it out of
42//! the queue then), so a waiter killed after being woken loses its message --
43//! the same reading as the keys of a semaphore waiter killed after its grant.
44//!
45//! A waiter retains its mailbox object so it cannot be freed (and its slot
46//! reused) while a process is blocked on it. Messages still queued when the
47//! mailbox object itself is freed are not released (a documented leak when
48//! they are class handles: the object heap has no destructor hook for the
49//! side table).
50
51use std::cell::RefCell;
52use std::collections::{HashMap, VecDeque};
53
54use crate::objects;
55use crate::sched::{ProcessId, ProcessRequest};
56
57/// One queued message.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct Message {
60    /// The value's stored words.
61    pub words: Box<[u64]>,
62    /// The type fingerprint (equal exactly for equivalent types, 6.22.1).
63    pub fingerprint: u64,
64    /// The value is a class handle (word 0) and the message owns one reference
65    /// to the object.
66    pub obj: bool,
67    /// The value is a queue, dynamic array or associative array (word 0 is its
68    /// collection handle) and the message owns that collection: a `peek` hands
69    /// out a copy-on-write clone, a delivering `get` hands the handle itself
70    /// over, a dropped message frees it.
71    pub coll: bool,
72}
73
74impl Message {
75    /// A copy of a message that stays in the queue (`peek`): the copy owns its
76    /// own object reference.
77    fn peeked(&self) -> Message {
78        if self.obj {
79            objects::retain(self.words[0]);
80        }
81        let mut m = self.clone();
82        if self.coll {
83            m.words[0] = crate::collections::clone(self.words[0]);
84        }
85        m
86    }
87
88    /// Drops the reference an undelivered message owns.
89    pub fn discard(self) {
90        if self.obj {
91            objects::release(self.words[0]);
92        }
93        if self.coll {
94            crate::collections::release(self.words[0]);
95        }
96    }
97}
98
99/// What a woken `get`/`peek` finds when it collects its result.
100#[derive(Debug, PartialEq, Eq)]
101enum Delivery {
102    Message(Message),
103    /// The message at the head of the queue was not of the waiter's type.
104    Mismatch,
105}
106
107#[derive(Debug)]
108enum Pending {
109    /// The first phase completed at once: the second one has nothing to do.
110    Completed,
111    /// The process is blocked in a `get`/`peek` on this mailbox, or in a `put`.
112    Waiting(u64),
113    /// The process was woken; its result waits for the second phase.
114    Delivered(Delivery),
115}
116
117struct Getter {
118    proc: ProcessId,
119    peek: bool,
120    fingerprint: u64,
121}
122
123#[derive(Default)]
124struct Mailbox {
125    /// `0`: unbounded.
126    bound: usize,
127    messages: VecDeque<Message>,
128    getters: VecDeque<Getter>,
129    putters: VecDeque<(ProcessId, Message)>,
130}
131
132#[derive(Default)]
133struct Table {
134    mailboxes: HashMap<u64, Mailbox>,
135    pending: HashMap<ProcessId, Pending>,
136    /// Words a blocking `get`/`peek` statement evaluated in its first phase
137    /// (a side-effecting receiver index or destination selector) for the
138    /// second phase to read back, keyed by process and slot.
139    stash: HashMap<(ProcessId, u32), u64>,
140}
141
142// One table per OS thread, like the semaphore table and every other piece of
143// per-run scheduler state.
144thread_local! {
145    static TABLE: RefCell<Table> = RefCell::new(Table::default());
146}
147
148fn with_table<R>(f: impl FnOnce(&mut Table) -> R) -> R {
149    TABLE.with(|t| f(&mut t.borrow_mut()))
150}
151
152/// Object references to drop once the table is no longer borrowed.
153#[derive(Default)]
154struct Releases {
155    handles: Vec<u64>,
156    /// Collection handles owned by dropped messages.
157    colls: Vec<u64>,
158}
159
160impl Releases {
161    /// Drops what the undelivered message `m` owns.
162    fn owned(&mut self, m: &Message) {
163        if m.obj {
164            self.handles.push(m.words[0]);
165        }
166        if m.coll {
167            self.colls.push(m.words[0]);
168        }
169    }
170
171    /// Every mailbox operation ends here with the table unborrowed, so this is
172    /// also where a queue change is reported: `wait (mb.num() > 0)` sleeps on
173    /// object state, which no signal change reports.
174    fn run(self) {
175        for h in self.handles {
176            objects::release(h);
177        }
178        for h in self.colls {
179            crate::collections::release(h);
180        }
181        super::notify_object_state_change();
182    }
183}
184
185/// `new(bound)` (15.4.1). A negative bound is illegal (indeterminate, 15.4.1);
186/// it is read as `0`, unbounded. `false` when `h` is not a live object.
187pub fn init(h: u64, bound: i64) -> bool {
188    if !objects::is_live(h) {
189        return false;
190    }
191    let bound = usize::try_from(bound).unwrap_or(0);
192    with_table(|t| {
193        t.mailboxes.insert(
194            h,
195            Mailbox {
196                bound,
197                ..Mailbox::default()
198            },
199        );
200    });
201    true
202}
203
204/// `num()` (15.4.2).
205pub fn num(h: u64) -> usize {
206    with_table(|t| t.mailboxes.get(&h).map_or(0, |m| m.messages.len()))
207}
208
209/// How many processes are blocked on `h`.
210pub fn waiters(h: u64) -> usize {
211    with_table(|t| {
212        t.mailboxes
213            .get(&h)
214            .map_or(0, |m| m.getters.len() + m.putters.len())
215    })
216}
217
218fn is_full(m: &Mailbox) -> bool {
219    m.bound != 0 && m.messages.len() >= m.bound
220}
221
222/// Wakes `proc` (its waiter reference on the mailbox goes back to `rel`). A
223/// woken `get`/`peek` has a `delivery` to collect; a woken `put` is finished
224/// (its message is queued) and leaves no marker.
225fn wake(
226    t: &mut Table,
227    proc: ProcessId,
228    h: u64,
229    delivery: Option<Delivery>,
230    emit: &mut dyn FnMut(ProcessRequest),
231    rel: &mut Releases,
232) {
233    match delivery {
234        Some(d) => {
235            t.pending.insert(proc, Pending::Delivered(d));
236        }
237        None => {
238            t.pending.remove(&proc);
239        }
240    }
241    emit(ProcessRequest::SyncWake(proc));
242    rel.handles.push(h);
243}
244
245/// Moves blocked putters into the queue while there is room.
246fn admit_putters(t: &mut Table, h: u64, emit: &mut dyn FnMut(ProcessRequest), rel: &mut Releases) {
247    loop {
248        let Some(m) = t.mailboxes.get_mut(&h) else {
249            return;
250        };
251        if is_full(m) {
252            return;
253        }
254        let Some((proc, msg)) = m.putters.pop_front() else {
255            return;
256        };
257        m.messages.push_back(msg);
258        wake(t, proc, h, None, emit, rel);
259    }
260}
261
262/// Serves the blocked `get`/`peek`s while a message is queued, oldest first: a
263/// `peek` copies the head, a `get` removes it (which may admit a blocked
264/// putter), and the next waiter then sees the new head. "As long as a message
265/// remains in the mailbox queue, any process blocked in either a peek() or
266/// get() operation shall become unblocked" (15.4.7).
267fn serve_getters(t: &mut Table, h: u64, emit: &mut dyn FnMut(ProcessRequest), rel: &mut Releases) {
268    loop {
269        let Some(m) = t.mailboxes.get_mut(&h) else {
270            return;
271        };
272        if m.messages.is_empty() {
273            return;
274        }
275        let Some(g) = m.getters.pop_front() else {
276            return;
277        };
278        let head_fp = m.messages[0].fingerprint;
279        let delivery = if head_fp != g.fingerprint {
280            Delivery::Mismatch
281        } else if g.peek {
282            Delivery::Message(m.messages[0].peeked())
283        } else {
284            let msg = m.messages.pop_front().expect("checked non-empty");
285            Delivery::Message(msg)
286        };
287        let removed = matches!(&delivery, Delivery::Message(_)) && !g.peek;
288        wake(t, g.proc, h, Some(delivery), emit, rel);
289        if removed {
290            admit_putters(t, h, emit, rel);
291        }
292    }
293}
294
295/// The result of [`put`].
296#[derive(Debug, PartialEq, Eq)]
297pub enum PutResult {
298    /// The message is in the queue.
299    Stored,
300    /// The mailbox is full: the caller was queued (`emit` got `SyncBlock`) and
301    /// the message is held for it until there is room.
302    Blocked,
303}
304
305/// `put(message)` (15.4.3) for process `proc`. `Err(message)` when `h` is not a
306/// live mailbox (the message is handed back so the caller can drop it).
307pub fn put(
308    h: u64,
309    proc: ProcessId,
310    msg: Message,
311    emit: &mut dyn FnMut(ProcessRequest),
312) -> Result<PutResult, Message> {
313    let mut rel = Releases::default();
314    let result = with_table(|t| {
315        let Some(m) = t.mailboxes.get_mut(&h) else {
316            return Err(msg);
317        };
318        if !is_full(m) {
319            m.messages.push_back(msg);
320            serve_getters(t, h, emit, &mut rel);
321            return Ok(PutResult::Stored);
322        }
323        assert!(
324            !t.pending.contains_key(&proc),
325            "mailbox: process {proc} called put() while already blocked in a mailbox call"
326        );
327        objects::retain(h);
328        t.mailboxes
329            .get_mut(&h)
330            .expect("looked up above")
331            .putters
332            .push_back((proc, msg));
333        t.pending.insert(proc, Pending::Waiting(h));
334        emit(ProcessRequest::SyncBlock);
335        Ok(PutResult::Blocked)
336    });
337    rel.run();
338    result
339}
340
341/// `try_put(message)` (15.4.4): `Ok(())` when stored, `Err(message)` when the
342/// mailbox is full (or `h` is no live mailbox) -- the caller still owns the
343/// message and what it holds.
344pub fn try_put(h: u64, msg: Message, emit: &mut dyn FnMut(ProcessRequest)) -> Result<(), Message> {
345    let mut rel = Releases::default();
346    let result = with_table(|t| {
347        let Some(m) = t.mailboxes.get_mut(&h) else {
348            return Err(msg);
349        };
350        if is_full(m) {
351            return Err(msg);
352        }
353        m.messages.push_back(msg);
354        serve_getters(t, h, emit, &mut rel);
355        Ok(())
356    });
357    rel.run();
358    result
359}
360
361/// The result of a `try_get` / `try_peek`.
362#[derive(Debug, PartialEq, Eq)]
363pub enum Taken {
364    Empty,
365    /// The message is not of the requested type (nothing was taken).
366    Mismatch,
367    Got(Message),
368}
369
370fn take_front(
371    t: &mut Table,
372    h: u64,
373    fingerprint: u64,
374    peek: bool,
375    emit: &mut dyn FnMut(ProcessRequest),
376    rel: &mut Releases,
377) -> Taken {
378    let Some(m) = t.mailboxes.get_mut(&h) else {
379        return Taken::Empty;
380    };
381    let Some(head) = m.messages.front() else {
382        return Taken::Empty;
383    };
384    if head.fingerprint != fingerprint {
385        return Taken::Mismatch;
386    }
387    if peek {
388        return Taken::Got(head.peeked());
389    }
390    let msg = m.messages.pop_front().expect("checked non-empty");
391    admit_putters(t, h, emit, rel);
392    Taken::Got(msg)
393}
394
395/// `try_get` (`peek = false`, 15.4.6) / `try_peek` (`peek = true`, 15.4.8).
396pub fn try_take(
397    h: u64,
398    fingerprint: u64,
399    peek: bool,
400    emit: &mut dyn FnMut(ProcessRequest),
401) -> Taken {
402    let mut rel = Releases::default();
403    let taken = with_table(|t| take_front(t, h, fingerprint, peek, emit, &mut rel));
404    rel.run();
405    taken
406}
407
408/// What one phase of a blocking `get`/`peek` produced.
409#[derive(Debug, PartialEq, Eq)]
410pub enum Got {
411    /// Nothing to do: the first phase already finished the call.
412    Nothing,
413    /// The first phase found no such mailbox (a null handle): the call is
414    /// finished, and the second phase will report [`Got::Nothing`].
415    NoMailbox,
416    Message(Message),
417    /// The queued message is not of the caller's type (a run-time error).
418    Mismatch,
419    /// The caller was queued (`emit` got `SyncBlock`).
420    Blocked,
421}
422
423/// `get` / `peek` (15.4.5, 15.4.7) for process `proc`, in its two phases (see
424/// the module comment): the first call takes the head of the queue or queues
425/// the process; the second, issued after the process resumed, collects what a
426/// waking operation delivered -- or does nothing when the first completed.
427pub fn get(
428    h: u64,
429    proc: ProcessId,
430    fingerprint: u64,
431    peek: bool,
432    emit: &mut dyn FnMut(ProcessRequest),
433) -> Got {
434    let mut rel = Releases::default();
435    let got = with_table(|t| {
436        match t.pending.remove(&proc) {
437            Some(Pending::Completed) => return Got::Nothing,
438            Some(Pending::Delivered(Delivery::Message(m))) => return Got::Message(m),
439            Some(Pending::Delivered(Delivery::Mismatch)) => return Got::Mismatch,
440            Some(Pending::Waiting(on)) => {
441                panic!("mailbox: process {proc} resumed while still blocked on mailbox {on:#x}")
442            }
443            None => {}
444        }
445        if !t.mailboxes.contains_key(&h) {
446            t.pending.insert(proc, Pending::Completed);
447            return Got::NoMailbox;
448        }
449        match take_front(t, h, fingerprint, peek, emit, &mut rel) {
450            Taken::Got(m) => {
451                t.pending.insert(proc, Pending::Completed);
452                Got::Message(m)
453            }
454            Taken::Mismatch => {
455                t.pending.insert(proc, Pending::Completed);
456                Got::Mismatch
457            }
458            Taken::Empty => {
459                objects::retain(h);
460                t.mailboxes
461                    .get_mut(&h)
462                    .expect("looked up above")
463                    .getters
464                    .push_back(Getter {
465                        proc,
466                        peek,
467                        fingerprint,
468                    });
469                t.pending.insert(proc, Pending::Waiting(h));
470                emit(ProcessRequest::SyncBlock);
471                Got::Blocked
472            }
473        }
474    });
475    rel.run();
476    got
477}
478
479/// Whether the blocking `get`/`peek` statement `proc` is running is in its
480/// SECOND phase: the first phase always leaves a `pending` entry (a
481/// completed marker, a wait or a delivery) that the second one consumes, so a
482/// process with none is starting the statement. Compiled code evaluates a
483/// side-effecting receiver or destination in the first phase only.
484pub fn second_phase(proc: ProcessId) -> bool {
485    with_table(|t| t.pending.contains_key(&proc))
486}
487
488/// Saves `value` in `slot` of `proc`'s stash (see [`Table::stash`]).
489pub fn stash_set(proc: ProcessId, slot: u32, value: u64) {
490    with_table(|t| {
491        t.stash.insert((proc, slot), value);
492    });
493}
494
495/// The word last saved by [`stash_set`] for `slot` (`0` when none).
496pub fn stash_get(proc: ProcessId, slot: u32) -> u64 {
497    with_table(|t| t.stash.get(&(proc, slot)).copied().unwrap_or(0))
498}
499
500/// A blocked process ended (killed) before the call finished: drops it from
501/// its queue and releases what it held -- a blocked putter's message, a woken
502/// waiter's undelivered message. A no-op for a process in no mailbox call.
503pub fn cancel(proc: ProcessId) {
504    let mut rel = Releases::default();
505    with_table(|t| t.stash.retain(|(p, _), _| *p != proc));
506    with_table(|t| match t.pending.remove(&proc) {
507        None | Some(Pending::Completed) => {}
508        Some(Pending::Delivered(Delivery::Mismatch)) => {}
509        Some(Pending::Delivered(Delivery::Message(m))) => {
510            rel.owned(&m);
511        }
512        Some(Pending::Waiting(h)) => {
513            if let Some(m) = t.mailboxes.get_mut(&h) {
514                m.getters.retain(|g| g.proc != proc);
515                let mut kept = VecDeque::new();
516                for (p, msg) in std::mem::take(&mut m.putters) {
517                    if p == proc {
518                        rel.owned(&msg);
519                    } else {
520                        kept.push_back((p, msg));
521                    }
522                }
523                m.putters = kept;
524            }
525            rel.handles.push(h);
526        }
527    });
528    rel.run();
529}
530
531/// Forgets every mailbox call in flight (a new run starts with no process
532/// blocked; ids are reused from zero) and drops the references they held.
533pub fn reset_for_new_run() {
534    let old = with_table(std::mem::take);
535    let mut rel = Releases::default();
536    for (_, p) in old.pending {
537        if let Pending::Delivered(Delivery::Message(m)) = p {
538            rel.owned(&m);
539        }
540    }
541    for (h, m) in old.mailboxes {
542        for _ in 0..(m.getters.len() + m.putters.len()) {
543            rel.handles.push(h);
544        }
545        for (_, msg) in m.putters {
546            rel.owned(&msg);
547        }
548        for msg in m.messages {
549            msg.discard();
550        }
551    }
552    rel.run();
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use crate::netlist::ClassTemplateId;
559
560    // The object heap and this table are process-wide and every test shares
561    // them: each test allocates its own mailbox and a disjoint range of process
562    // ids, and asserts only on those.
563
564    const FP: u64 = 0x11;
565    const OTHER_FP: u64 = 0x22;
566
567    fn mbx(bound: i64, template: u32) -> u64 {
568        let h = objects::alloc(ClassTemplateId(template), 1);
569        assert!(init(h, bound));
570        h
571    }
572
573    fn msg(v: u64) -> Message {
574        Message {
575            words: vec![v].into_boxed_slice(),
576            fingerprint: FP,
577            obj: false,
578            coll: false,
579        }
580    }
581
582    fn value(m: &Message) -> u64 {
583        m.words[0]
584    }
585
586    #[derive(Default)]
587    struct Log(Vec<ProcessRequest>);
588    impl Log {
589        fn sink(&mut self) -> impl FnMut(ProcessRequest) + '_ {
590            |r| self.0.push(r)
591        }
592    }
593
594    #[test]
595    fn messages_come_out_in_fifo_order() {
596        let h = mbx(0, 18_000);
597        let mut log = Log::default();
598        for v in [10, 20, 30] {
599            assert_eq!(
600                put(h, 2_000, msg(v), &mut log.sink()),
601                Ok(PutResult::Stored)
602            );
603        }
604        assert_eq!(num(h), 3);
605        for want in [10, 20, 30] {
606            let Taken::Got(m) = try_take(h, FP, false, &mut log.sink()) else {
607                panic!("a message was queued");
608            };
609            assert_eq!(value(&m), want);
610        }
611        assert_eq!(try_take(h, FP, false, &mut log.sink()), Taken::Empty);
612        assert!(log.0.is_empty());
613    }
614
615    #[test]
616    fn try_put_fails_on_a_full_bounded_mailbox_and_hands_the_message_back() {
617        let h = mbx(2, 18_001);
618        let mut log = Log::default();
619        assert!(try_put(h, msg(1), &mut log.sink()).is_ok());
620        assert!(try_put(h, msg(2), &mut log.sink()).is_ok());
621        let back = try_put(h, msg(3), &mut log.sink()).unwrap_err();
622        assert_eq!(value(&back), 3);
623        assert_eq!(num(h), 2);
624    }
625
626    #[test]
627    fn an_unbounded_mailbox_is_never_full() {
628        let h = mbx(0, 18_002);
629        let mut log = Log::default();
630        for v in 0..1000 {
631            assert!(try_put(h, msg(v), &mut log.sink()).is_ok());
632        }
633        assert_eq!(num(h), 1000);
634        // A negative bound is illegal (15.4.1); it is read as unbounded.
635        let n = mbx(-3, 18_003);
636        assert!(try_put(n, msg(1), &mut log.sink()).is_ok());
637        assert!(try_put(n, msg(2), &mut log.sink()).is_ok());
638    }
639
640    #[test]
641    fn a_get_on_an_empty_mailbox_blocks_and_a_put_delivers_to_it() {
642        let h = mbx(0, 18_004);
643        let mut log = Log::default();
644        assert_eq!(get(h, 2_010, FP, false, &mut log.sink()), Got::Blocked);
645        assert_eq!(log.0, [ProcessRequest::SyncBlock]);
646        assert_eq!(waiters(h), 1);
647        log.0.clear();
648        assert_eq!(
649            put(h, 2_011, msg(7), &mut log.sink()),
650            Ok(PutResult::Stored)
651        );
652        assert_eq!(log.0, [ProcessRequest::SyncWake(2_010)]);
653        assert_eq!(num(h), 0, "the get took the message");
654        match get(h, 2_010, FP, false, &mut log.sink()) {
655            Got::Message(m) => assert_eq!(value(&m), 7),
656            other => panic!("second phase must deliver: {other:?}"),
657        }
658        // The collected call is over: the next get starts afresh.
659        assert_eq!(get(h, 2_010, FP, false, &mut log.sink()), Got::Blocked);
660    }
661
662    #[test]
663    fn a_get_that_finds_a_message_completes_and_its_second_phase_is_a_no_op() {
664        let h = mbx(0, 18_005);
665        let mut log = Log::default();
666        put(h, 2_020, msg(5), &mut log.sink()).unwrap();
667        match get(h, 2_021, FP, false, &mut log.sink()) {
668            Got::Message(m) => assert_eq!(value(&m), 5),
669            other => panic!("{other:?}"),
670        }
671        assert_eq!(get(h, 2_021, FP, false, &mut log.sink()), Got::Nothing);
672        assert!(log.0.is_empty());
673        assert_eq!(num(h), 0);
674    }
675
676    #[test]
677    fn waiters_are_served_in_arrival_order() {
678        let h = mbx(0, 18_006);
679        let mut log = Log::default();
680        for p in [2_030, 2_031, 2_032] {
681            assert_eq!(get(h, p, FP, false, &mut log.sink()), Got::Blocked);
682        }
683        log.0.clear();
684        put(h, 2_039, msg(1), &mut log.sink()).unwrap();
685        assert_eq!(log.0, [ProcessRequest::SyncWake(2_030)]);
686        put(h, 2_039, msg(2), &mut log.sink()).unwrap();
687        assert_eq!(
688            log.0,
689            [
690                ProcessRequest::SyncWake(2_030),
691                ProcessRequest::SyncWake(2_031)
692            ]
693        );
694        assert_eq!(waiters(h), 1);
695    }
696
697    #[test]
698    fn one_message_wakes_every_peeker_and_the_first_getter() {
699        let h = mbx(0, 18_007);
700        let mut log = Log::default();
701        assert_eq!(get(h, 2_040, FP, true, &mut log.sink()), Got::Blocked);
702        assert_eq!(get(h, 2_041, FP, true, &mut log.sink()), Got::Blocked);
703        assert_eq!(get(h, 2_042, FP, false, &mut log.sink()), Got::Blocked);
704        assert_eq!(get(h, 2_043, FP, true, &mut log.sink()), Got::Blocked);
705        log.0.clear();
706        put(h, 2_049, msg(9), &mut log.sink()).unwrap();
707        assert_eq!(
708            log.0,
709            [
710                ProcessRequest::SyncWake(2_040),
711                ProcessRequest::SyncWake(2_041),
712                ProcessRequest::SyncWake(2_042)
713            ],
714            "two peeks copy, the get removes it, the later peek stays blocked"
715        );
716        assert_eq!(num(h), 0);
717        assert_eq!(waiters(h), 1);
718        for p in [2_040, 2_041, 2_042] {
719            match get(h, p, FP, p == 2_040 || p == 2_041, &mut log.sink()) {
720                Got::Message(m) => assert_eq!(value(&m), 9),
721                other => panic!("process {p}: {other:?}"),
722            }
723        }
724    }
725
726    #[test]
727    fn peek_leaves_the_message_in_the_queue() {
728        let h = mbx(0, 18_008);
729        let mut log = Log::default();
730        put(h, 2_050, msg(4), &mut log.sink()).unwrap();
731        let Taken::Got(m) = try_take(h, FP, true, &mut log.sink()) else {
732            panic!("a message was queued");
733        };
734        assert_eq!(value(&m), 4);
735        assert_eq!(num(h), 1);
736    }
737
738    #[test]
739    fn a_put_on_a_full_mailbox_blocks_until_a_get_makes_room() {
740        let h = mbx(1, 18_009);
741        let mut log = Log::default();
742        put(h, 2_060, msg(1), &mut log.sink()).unwrap();
743        assert_eq!(
744            put(h, 2_061, msg(2), &mut log.sink()),
745            Ok(PutResult::Blocked)
746        );
747        assert_eq!(log.0, [ProcessRequest::SyncBlock]);
748        assert_eq!(num(h), 1, "the blocked message is not queued yet");
749        log.0.clear();
750        let Taken::Got(m) = try_take(h, FP, false, &mut log.sink()) else {
751            panic!("a message was queued");
752        };
753        assert_eq!(value(&m), 1);
754        assert_eq!(log.0, [ProcessRequest::SyncWake(2_061)]);
755        assert_eq!(num(h), 1, "the blocked putter's message moved in");
756        let Taken::Got(m) = try_take(h, FP, false, &mut log.sink()) else {
757            panic!("a message was queued");
758        };
759        assert_eq!(value(&m), 2);
760    }
761
762    #[test]
763    fn a_blocked_putter_is_not_overtaken_by_try_put() {
764        let h = mbx(1, 18_010);
765        let mut log = Log::default();
766        put(h, 2_070, msg(1), &mut log.sink()).unwrap();
767        put(h, 2_071, msg(2), &mut log.sink()).unwrap();
768        try_take(h, FP, false, &mut log.sink());
769        assert!(
770            try_put(h, msg(3), &mut log.sink()).is_err(),
771            "the freed slot went to the blocked putter"
772        );
773    }
774
775    #[test]
776    fn a_type_mismatch_takes_nothing() {
777        let h = mbx(0, 18_011);
778        let mut log = Log::default();
779        put(h, 2_080, msg(1), &mut log.sink()).unwrap();
780        assert_eq!(
781            try_take(h, OTHER_FP, false, &mut log.sink()),
782            Taken::Mismatch
783        );
784        assert_eq!(
785            try_take(h, OTHER_FP, true, &mut log.sink()),
786            Taken::Mismatch
787        );
788        assert_eq!(
789            get(h, 2_081, OTHER_FP, false, &mut log.sink()),
790            Got::Mismatch
791        );
792        assert_eq!(num(h), 1, "the message is still there");
793        // A waiter whose type is wrong is woken with the error, the message
794        // stays for the next one.
795        let e = mbx(0, 18_012);
796        assert_eq!(
797            get(e, 2_082, OTHER_FP, false, &mut log.sink()),
798            Got::Blocked
799        );
800        assert_eq!(get(e, 2_083, FP, false, &mut log.sink()), Got::Blocked);
801        log.0.clear();
802        put(e, 2_084, msg(8), &mut log.sink()).unwrap();
803        assert_eq!(
804            log.0,
805            [
806                ProcessRequest::SyncWake(2_082),
807                ProcessRequest::SyncWake(2_083)
808            ]
809        );
810        assert_eq!(
811            get(e, 2_082, OTHER_FP, false, &mut log.sink()),
812            Got::Mismatch
813        );
814        assert!(matches!(
815            get(e, 2_083, FP, false, &mut log.sink()),
816            Got::Message(_)
817        ));
818    }
819
820    fn obj_msg(h: u64) -> Message {
821        Message {
822            words: vec![h].into_boxed_slice(),
823            fingerprint: FP,
824            obj: true,
825            coll: false,
826        }
827    }
828
829    #[test]
830    fn a_class_message_owns_one_reference_until_it_is_taken() {
831        let h = mbx(0, 18_013);
832        let payload = objects::alloc(ClassTemplateId(18_014), 1);
833        let mut log = Log::default();
834        put(h, 2_090, obj_msg(payload), &mut log.sink()).unwrap();
835        assert_eq!(objects::refcount(payload), Some(1), "the message holds it");
836        let Taken::Got(copy) = try_take(h, FP, true, &mut log.sink()) else {
837            panic!("a message was queued");
838        };
839        assert_eq!(
840            objects::refcount(payload),
841            Some(2),
842            "a peeked copy owns its own"
843        );
844        copy.discard();
845        let Taken::Got(taken) = try_take(h, FP, false, &mut log.sink()) else {
846            panic!("a message was queued");
847        };
848        assert_eq!(
849            objects::refcount(payload),
850            Some(1),
851            "taking transfers the queue's reference"
852        );
853        taken.discard();
854        assert_eq!(objects::refcount(payload), None, "the last holder freed it");
855    }
856
857    #[test]
858    fn cancelling_a_blocked_putter_releases_its_message_and_the_mailbox() {
859        let h = mbx(1, 18_015);
860        let payload = objects::alloc(ClassTemplateId(18_016), 1);
861        let mut log = Log::default();
862        put(h, 2_100, msg(1), &mut log.sink()).unwrap();
863        assert_eq!(
864            put(h, 2_101, obj_msg(payload), &mut log.sink()),
865            Ok(PutResult::Blocked)
866        );
867        assert_eq!(objects::refcount(h), Some(2));
868        cancel(2_101);
869        assert_eq!(objects::refcount(payload), None, "its message was dropped");
870        assert_eq!(objects::refcount(h), Some(1));
871        assert_eq!(waiters(h), 0);
872        // Cancelling a process in no mailbox call is a no-op.
873        cancel(2_101);
874    }
875
876    #[test]
877    fn cancelling_a_blocked_getter_removes_it_and_a_woken_one_loses_its_message() {
878        let h = mbx(0, 18_017);
879        let mut log = Log::default();
880        assert_eq!(get(h, 2_110, FP, false, &mut log.sink()), Got::Blocked);
881        cancel(2_110);
882        assert_eq!(waiters(h), 0);
883        assert_eq!(objects::refcount(h), Some(1));
884        put(h, 2_111, msg(1), &mut log.sink()).unwrap();
885        assert_eq!(num(h), 1, "nobody is waiting any more");
886
887        let payload = objects::alloc(ClassTemplateId(18_018), 1);
888        let e = mbx(0, 18_019);
889        assert_eq!(get(e, 2_112, FP, false, &mut log.sink()), Got::Blocked);
890        put(e, 2_113, obj_msg(payload), &mut log.sink()).unwrap();
891        cancel(2_112);
892        assert_eq!(
893            objects::refcount(payload),
894            None,
895            "the lost message was freed"
896        );
897    }
898
899    #[test]
900    fn a_waiter_keeps_its_mailbox_alive_until_it_leaves() {
901        let h = mbx(0, 18_020);
902        let mut log = Log::default();
903        assert_eq!(get(h, 2_120, FP, false, &mut log.sink()), Got::Blocked);
904        assert_eq!(objects::refcount(h), Some(2));
905        put(h, 2_121, msg(1), &mut log.sink()).unwrap();
906        assert_eq!(objects::refcount(h), Some(1), "the woken waiter let go");
907    }
908
909    #[test]
910    fn a_non_mailbox_handle_stores_and_delivers_nothing() {
911        let mut log = Log::default();
912        assert!(!init(0, 0));
913        assert_eq!(num(0), 0);
914        assert_eq!(put(0, 2_130, msg(1), &mut log.sink()), Err(msg(1)));
915        assert!(try_put(0, msg(1), &mut log.sink()).is_err());
916        assert_eq!(try_take(0, FP, false, &mut log.sink()), Taken::Empty);
917        // Phase one names the missing mailbox (the caller reports it once);
918        // phase two of the finished call does nothing.
919        assert_eq!(get(0, 2_131, FP, false, &mut log.sink()), Got::NoMailbox);
920        assert_eq!(get(0, 2_131, FP, false, &mut log.sink()), Got::Nothing);
921        assert!(log.0.is_empty());
922    }
923}