narju/
sched.rs

1use std::any::Any;
2use std::collections::VecDeque;
3use std::fmt;
4
5use crate::floor::host::{Atom, HostType, Registry};
6use crate::floor::machine::{Fault, Machine, Mail, Step, Store};
7use crate::floor::{rc_val, Val};
8
9pub type Id = usize;
10
11/// Repeated from [`crate::world`] because an address names one and is below it.
12pub type ConnId = u64;
13
14fn sym(s: &str) -> Val {
15    Val::Sym(s.into())
16}
17
18fn pair(a: Val, b: Val) -> Val {
19    Val::Pair(rc_val(a), rc_val(b))
20}
21
22/// Where a task is: in this heap, or on the far side of one of this node's
23/// links. A host peer is not here - `('host name)` is an ordinary cons, a name
24/// the program writes, while a task address is a capability the runtime mints.
25///
26/// Opaque because crossing a link `Task(3)` must arrive as `Remote(conn, 3)`
27/// ([`crate::link::across`]), and a tagged cons is a shape a program can build
28/// as data, so the rewrite could not tell an address from something like one.
29#[derive(Debug, Clone, PartialEq)]
30pub enum Addr {
31    Task(Id),
32    Remote(ConnId, Id),
33}
34
35impl Addr {
36    pub const NAME: &'static str = "addr";
37
38    pub fn val(self) -> Val {
39        Val::Atom(Atom::new(self))
40    }
41
42    pub fn of(v: &Val) -> Option<&Addr> {
43        match v {
44            Val::Atom(a) => a.downcast::<Addr>(),
45            _ => None,
46        }
47    }
48
49    /// The conn is written even though the receiver discards it: what a link
50    /// does with an address is the link's business, not the codec's.
51    fn decode(bytes: &[u8]) -> Option<Atom> {
52        fn be(b: &[u8]) -> Option<u64> {
53            b.try_into().ok().map(u64::from_be_bytes)
54        }
55        fn id(b: &[u8]) -> Option<Id> {
56            be(b).and_then(|n| Id::try_from(n).ok())
57        }
58        match bytes.split_first()? {
59            (0, rest) => Some(Atom::new(Addr::Task(id(rest)?))),
60            (1, rest) if rest.len() == 16 => {
61                Some(Atom::new(Addr::Remote(be(&rest[..8])?, id(&rest[8..])?)))
62            }
63            _ => None,
64        }
65    }
66}
67
68impl fmt::Display for Addr {
69    /// Not a form the reader accepts, deliberately: an address cannot be
70    /// written down, so its printed form does not pretend to be source.
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Addr::Task(id) => write!(f, "#<task {id}>"),
74            Addr::Remote(conn, id) => write!(f, "#<task {id} on {conn}>"),
75        }
76    }
77}
78
79impl HostType for Addr {
80    fn type_name(&self) -> &'static str {
81        Addr::NAME
82    }
83
84    fn eq_atom(&self, other: &dyn HostType) -> bool {
85        other.as_any().downcast_ref::<Addr>() == Some(self)
86    }
87
88    fn encode(&self, out: &mut Vec<u8>) {
89        match self {
90            Addr::Task(id) => {
91                out.push(0);
92                out.extend_from_slice(&(*id as u64).to_be_bytes());
93            }
94            Addr::Remote(conn, id) => {
95                out.push(1);
96                out.extend_from_slice(&conn.to_be_bytes());
97                out.extend_from_slice(&(*id as u64).to_be_bytes());
98            }
99        }
100    }
101
102    fn as_any(&self) -> &dyn Any {
103        self
104    }
105}
106
107pub fn extend(r: Registry) -> Registry {
108    r.ty(Addr::NAME, Addr::decode)
109}
110
111pub fn addr(id: Id) -> Val {
112    Addr::Task(id).val()
113}
114
115enum Route {
116    Local(Id),
117    /// Not for anything here. The heap does not interpret it; that is the
118    /// executor's job, and why a host capability needs no floor verb of its own.
119    Away,
120    Bad,
121}
122
123fn route(v: &Val) -> Route {
124    match Addr::of(v) {
125        Some(Addr::Task(id)) => Route::Local(*id),
126        Some(Addr::Remote(_, _)) => Route::Away,
127        // A host peer is `('host name)`, and any other tagged cons is some
128        // executor's business too - the heap does not know what exists above it.
129        None => match v {
130            Val::Pair(tag, _) if matches!(&**tag, Val::Sym(_)) => Route::Away,
131            _ => Route::Bad,
132        },
133    }
134}
135
136/// `('task-down addr . result)`, where result is `(ok . v)`, `(throw . v)`,
137/// `(throw . noproc)` for a task already gone when the monitor was set, or
138/// `(throw . noconnection)` for one behind a link that died.
139pub fn down(to: Val, result: Val) -> Val {
140    pair(sym("task-down"), pair(to, result))
141}
142
143fn task_down(id: Id, result: Val) -> Val {
144    down(addr(id), result)
145}
146
147/// Something a running task asked for that only its heap can do.
148enum Request {
149    Send {
150        to: Id,
151        msg: Val,
152    },
153    Away {
154        to: Val,
155        msg: Val,
156    },
157    Spawn {
158        id: Id,
159        thunk: Val,
160    },
161    Watch {
162        watcher: Id,
163        target: Id,
164    },
165    /// A watch on what is not in this heap, passed out for the executor to keep.
166    /// Undecoded, because the heap does not know what kinds of address there are.
167    WatchAway {
168        watcher: Id,
169        target: Val,
170    },
171    Limit {
172        target: Id,
173        turns: usize,
174    },
175}
176
177/// Nothing here reaches the heap, because while a task runs the heap is its
178/// caller rather than something it can borrow. Sends and spawns accumulate as
179/// requests the heap drains when the turn ends.
180pub struct Post {
181    me: Id,
182    inbox: VecDeque<Val>,
183    out: Vec<Request>,
184    reached: bool,
185    /// The id the heap will hand the next task it creates. Lent for the turn so
186    /// `spawn` can answer at once; sound only because one task runs at a time.
187    next: Id,
188}
189
190impl Mail for Post {
191    fn spawn(&mut self, thunk: Val) -> Result<Val, Val> {
192        let id = self.next;
193        self.next += 1;
194        self.out.push(Request::Spawn { id, thunk });
195        Ok(addr(id))
196    }
197
198    fn send(&mut self, to: &Val, msg: Val) -> Result<Val, Val> {
199        match route(to) {
200            // A send to oneself lands now. Deferred, it would let a task
201            // park on a mailbox holding a message it has itself posted.
202            Route::Local(id) if id == self.me => self.inbox.push_back(msg),
203            Route::Local(id) => self.out.push(Request::Send { to: id, msg }),
204            Route::Away => self.out.push(Request::Away {
205                to: to.clone(),
206                msg,
207            }),
208            Route::Bad => return Err(pair(sym("bad-address"), to.clone())),
209        }
210        Ok(Val::Nil)
211    }
212
213    /// Reaching here is what "responsive" means, whether or not there was
214    /// anything to read: a task in a tight loop over a full mailbox is working
215    /// rather than diverging, so blocking would be the wrong test.
216    fn receive(&mut self) -> Option<Val> {
217        self.reached = true;
218        self.inbox.pop_front()
219    }
220
221    /// A watch goes wherever a message would, but what comes back is weaker off
222    /// this heap. A dead link does not mean the far task died, only that nothing
223    /// more can be learned, so it reports `noconnection` rather than an exit
224    /// value: the caller must stop waiting either way, and must not be told a
225    /// result nobody observed. A host peer that ends reports `noproc` and no
226    /// value, an adapter having nothing it could be said to have evaluated.
227    fn monitor(&mut self, target: &Val) -> Result<Val, Val> {
228        match route(target) {
229            Route::Local(target) => {
230                self.out.push(Request::Watch {
231                    watcher: self.me,
232                    target,
233                });
234                Ok(Val::Nil)
235            }
236            Route::Away => {
237                self.out.push(Request::WatchAway {
238                    watcher: self.me,
239                    target: target.clone(),
240                });
241                Ok(Val::Nil)
242            }
243            Route::Bad => Err(pair(sym("bad-address"), target.clone())),
244        }
245    }
246
247    /// Counted in turns rather than steps because the step budget is a fairness
248    /// knob and this is not: a task on an expensive semantics gets less done per
249    /// turn, but turns-to-answer is what its caller can reason about.
250    fn limit(&mut self, target: &Val, turns: i64) -> Result<Val, Val> {
251        match route(target) {
252            Route::Local(target) => {
253                self.out.push(Request::Limit {
254                    target,
255                    turns: turns as usize,
256                });
257                Ok(Val::Nil)
258            }
259            Route::Away => Err(pair(sym("not-local"), target.clone())),
260            Route::Bad => Err(pair(sym("bad-address"), target.clone())),
261        }
262    }
263}
264
265struct Task {
266    machine: Machine,
267    store: Store<Post>,
268    /// Blocked on an empty mailbox, and so not in the run queue.
269    parked: bool,
270    watchers: Vec<Id>,
271    /// Consecutive turns in which `receive` was not reached.
272    turns: usize,
273    /// `None` means a task may compute for as long as it likes.
274    limit: Option<usize>,
275}
276
277impl Task {
278    /// The thunk is applied to the task's own address, which is how a task comes
279    /// to know it - so naming oneself costs no verb and no scheduler call.
280    fn new(id: Id, thunk: Val) -> Task {
281        Task {
282            machine: Machine::applying(thunk, addr(id)),
283            store: Store::new(Post {
284                me: id,
285                inbox: VecDeque::new(),
286                out: Vec::new(),
287                reached: false,
288                next: 0,
289            }),
290            parked: false,
291            watchers: Vec::new(),
292            turns: 0,
293            limit: None,
294        }
295    }
296}
297
298pub struct Heap {
299    /// A slot goes to `None` when its task ends and is never reused, so an
300    /// address outlives its task and a send to a dead one is a drop, not a
301    /// misdelivery.
302    tasks: Vec<Option<Task>>,
303    runq: VecDeque<Id>,
304    /// Swapped into whichever task is running, so a closure that captured a cell
305    /// keeps meaning that cell after it is spawned.
306    cells: Vec<Val>,
307    /// Floor steps a task may take before it is rotated out.
308    budget: usize,
309    exits: Vec<(Id, Val)>,
310    /// Messages for addresses that are not local, in send order.
311    away: Vec<(Val, Val)>,
312    /// Watches on what is not here, as `(watcher, address)`.
313    watches: Vec<(Id, Val)>,
314    live: usize,
315}
316
317impl Heap {
318    pub fn new(budget: usize) -> Heap {
319        Heap {
320            tasks: Vec::new(),
321            runq: VecDeque::new(),
322            cells: Vec::new(),
323            budget,
324            exits: Vec::new(),
325            away: Vec::new(),
326            watches: Vec::new(),
327            live: 0,
328        }
329    }
330
331    /// Seed a task from outside. The thunk is applied to nil.
332    pub fn spawn(&mut self, thunk: Val) -> Id {
333        let id = self.tasks.len();
334        self.tasks.push(Some(Task::new(id, thunk)));
335        self.runq.push_back(id);
336        self.live += 1;
337        id
338    }
339
340    /// A heap with live tasks and an empty queue is waiting on a message from
341    /// outside; a heap with none is finished.
342    pub fn idle(&self) -> bool {
343        self.live == 0
344    }
345
346    /// Waking it if it was parked. An address for anything else is dropped,
347    /// exactly as a send to a task that has ended is.
348    pub fn post(&mut self, to: &Val, msg: Val) {
349        if let Route::Local(id) = route(to) {
350            self.deliver(id, msg);
351        }
352    }
353
354    pub fn alive(&self, id: Id) -> bool {
355        matches!(self.tasks.get(id), Some(Some(_)))
356    }
357
358    /// Tasks that have ended since this was last called, in the order they ended.
359    pub fn take_exits(&mut self) -> Vec<(Id, Val)> {
360        std::mem::take(&mut self.exits)
361    }
362
363    /// Routing these is the executor's job: another heap, another machine and a
364    /// host adapter are all just the far end of a channel, which is what lets a
365    /// syscall be an ordinary `send` rather than a floor verb.
366    pub fn take_away(&mut self) -> Vec<(Val, Val)> {
367        std::mem::take(&mut self.away)
368    }
369
370    /// As `(watcher, address)`. Whether such a thing is alive is not a question
371    /// this heap can put, so the answer is owed by whoever holds link or peer.
372    pub fn take_watches(&mut self) -> Vec<(Id, Val)> {
373        std::mem::take(&mut self.watches)
374    }
375
376    /// Until the queue empties - every task finished or parked on an empty mailbox.
377    pub fn run(&mut self) -> Result<(), Fault> {
378        while let Some(id) = self.runq.pop_front() {
379            self.turn(id)?;
380        }
381        Ok(())
382    }
383
384    fn turn(&mut self, id: Id) -> Result<(), Fault> {
385        let Some(mut task) = self.tasks[id].take() else {
386            return Ok(());
387        };
388
389        std::mem::swap(&mut self.cells, &mut task.store.cells);
390        task.store.mail.next = self.tasks.len();
391        let outcome = task.machine.turn(&mut task.store, self.budget);
392        std::mem::swap(&mut self.cells, &mut task.store.cells);
393        let out = std::mem::take(&mut task.store.mail.out);
394
395        let blocked = matches!(outcome, Ok((Step::Blocked, _)));
396        let ended = match outcome {
397            // A raise that reached the end of the task is what a failure is: it
398            // propagated past everything that might have caught it.
399            Ok((Step::Done(Val::Raise(v)), _)) => Some(pair(sym("throw"), Val::clone(&v))),
400            Ok((Step::Done(v), _)) => Some(pair(sym("ok"), v)),
401            // Not catchable, and not the task's fault: a machine invariant was
402            // violated, so the heap stops rather than reports.
403            Err(bug) => return Err(bug),
404            Ok((Step::Running, _)) => None,
405            Ok((Step::Blocked, _)) => None,
406        };
407
408        if std::mem::take(&mut task.store.mail.reached) {
409            task.turns = 0;
410        } else {
411            task.turns += 1;
412        }
413        // Killed rather than asked to stop, because the case this exists for
414        // cannot hear the asking: reporting a divergent tower needs evaluation.
415        let ended = ended.or_else(|| {
416            task.limit
417                .is_some_and(|n| task.turns > n)
418                .then(|| pair(sym("throw"), sym("unresponsive")))
419        });
420
421        match ended {
422            Some(result) => {
423                let watchers = std::mem::take(&mut task.watchers);
424                // What the turn sent, before what its ending reports. The other
425                // order lets a task's death overtake its own reply: the caller
426                // sees `callee-down` for a call that was in fact answered.
427                self.drain(out);
428                for w in watchers {
429                    self.deliver(w, task_down(id, result.clone()));
430                }
431                self.exits.push((id, result));
432                self.live -= 1;
433            }
434            None => {
435                self.tasks[id] = Some(task);
436                self.drain(out);
437                let task = self.tasks[id].as_mut().expect("just restored");
438                if blocked && task.store.mail.inbox.is_empty() {
439                    task.parked = true;
440                } else {
441                    self.runq.push_back(id);
442                }
443            }
444        }
445        Ok(())
446    }
447
448    fn drain(&mut self, out: Vec<Request>) {
449        for req in out {
450            match req {
451                Request::Send { to, msg } => self.deliver(to, msg),
452                Request::Away { to, msg } => self.away.push((to, msg)),
453                Request::WatchAway { watcher, target } => self.watches.push((watcher, target)),
454                Request::Spawn { id, thunk } => {
455                    debug_assert_eq!(id, self.tasks.len(), "ids are handed out in order");
456                    self.tasks.push(Some(Task::new(id, thunk)));
457                    self.runq.push_back(id);
458                    self.live += 1;
459                }
460                Request::Watch { watcher, target } => {
461                    match self.tasks.get_mut(target).and_then(Option::as_mut) {
462                        Some(task) => task.watchers.push(watcher),
463                        // Answering now rather than never is what makes a
464                        // monitor race-free to set.
465                        None => {
466                            let down = task_down(target, pair(sym("throw"), sym("noproc")));
467                            self.deliver(watcher, down);
468                        }
469                    }
470                }
471                // Silent for a task that has ended, as a `send` to one is.
472                Request::Limit { target, turns } => {
473                    if let Some(Some(task)) = self.tasks.get_mut(target) {
474                        task.limit = Some(turns);
475                    }
476                }
477            }
478        }
479    }
480
481    fn deliver(&mut self, to: Id, msg: Val) {
482        let Some(Some(task)) = self.tasks.get_mut(to) else {
483            return;
484        };
485        task.store.mail.inbox.push_back(msg);
486        let wake = task.parked;
487        task.parked = false;
488        if wake {
489            self.runq.push_back(to);
490        }
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::floor::machine::NoMail;
498    use crate::floor::Env;
499
500    fn val(src: &str) -> Val {
501        let Val::Code(e) = crate::surface::compile(src).expect("compiles") else {
502            panic!("not code")
503        };
504        Machine::new(Env::default(), e)
505            .run(&mut Store::<NoMail>::default())
506            .expect("no fault")
507    }
508
509    fn ok(v: Val) -> Val {
510        pair(sym("ok"), v)
511    }
512
513    fn run(h: &mut Heap) -> Vec<(Id, Val)> {
514        h.run().expect("no bug");
515        h.take_exits()
516    }
517
518    #[test]
519    fn a_task_runs_to_completion() {
520        let mut h = Heap::new(1000);
521        let id = h.spawn(val("(lambda t (_) (+ 1 2))"));
522        assert_eq!(run(&mut h), vec![(id, ok(Val::Num(3)))]);
523        assert!(!h.alive(id));
524    }
525
526    #[test]
527    fn a_throw_is_a_result_not_a_heap_failure() {
528        let mut h = Heap::new(1000);
529        let id = h.spawn(val("(lambda t (_) (throw 'nope))"));
530        assert_eq!(run(&mut h), vec![(id, pair(sym("throw"), sym("nope")))]);
531    }
532
533    #[test]
534    fn an_empty_mailbox_is_the_only_reason_to_park() {
535        let mut h = Heap::new(1000);
536        let id = h.spawn(val("(lambda t (_) (receive 0))"));
537        // Quiescent with the task still alive: it is parked, not spinning.
538        assert_eq!(run(&mut h), vec![]);
539        assert!(h.alive(id));
540
541        h.post(&addr(id), Val::Num(7));
542        assert_eq!(run(&mut h), vec![(id, ok(Val::Num(7)))]);
543    }
544
545    #[test]
546    fn a_task_is_told_its_own_address() {
547        let mut h = Heap::new(1000);
548        let id = h.spawn(val("(lambda t (me) me)"));
549        assert_eq!(run(&mut h), vec![(id, ok(addr(id)))]);
550    }
551
552    #[test]
553    fn a_send_to_oneself_is_visible_to_the_next_receive() {
554        let mut h = Heap::new(1000);
555        let id = h.spawn(val("(lambda t (me) (begin (send me 5) (receive)))"));
556        assert_eq!(run(&mut h), vec![(id, ok(Val::Num(5)))]);
557    }
558
559    #[test]
560    fn a_spawned_task_gets_the_next_address_and_runs() {
561        let mut h = Heap::new(1000);
562        let parent = h.spawn(val("(lambda t (me)
563               (let ((child (spawn (lambda c (_) (send me 'hi)))))
564                 (cons child (receive 0))))"));
565        let exits = run(&mut h);
566        assert_eq!(
567            exits,
568            vec![(1, ok(Val::Nil)), (parent, ok(pair(addr(1), sym("hi")))),]
569        );
570    }
571
572    #[test]
573    fn a_monitor_reports_a_death_it_did_not_see() {
574        let mut h = Heap::new(1000);
575        let watcher = h.spawn(val("(lambda t (_)
576               (let ((child (spawn (lambda c (_) 9))))
577                 (begin (monitor child) (receive 0))))"));
578        let exits = run(&mut h);
579        let down = task_down(1, ok(Val::Num(9)));
580        assert_eq!(exits, vec![(1, ok(Val::Num(9))), (watcher, ok(down))]);
581    }
582
583    #[test]
584    fn a_dying_task_is_heard_before_it_is_mourned() {
585        let mut h = Heap::new(1000);
586        let watcher = h.spawn(val("(lambda t (me)
587               (let ((child (spawn (lambda c (_) (begin (send me 'answer) 9)))))
588                 (begin (monitor child)
589                        (let ((first (receive 0))) (cons first (receive 0))))))"));
590        let heard = pair(sym("answer"), task_down(1, ok(Val::Num(9))));
591        assert_eq!(
592            run(&mut h),
593            vec![(1, ok(Val::Num(9))), (watcher, ok(heard))]
594        );
595    }
596
597    #[test]
598    fn monitoring_a_dead_task_answers_at_once() {
599        let mut h = Heap::new(1000);
600        let id = h.spawn(val(
601            "(lambda t (_) (begin (monitor (receive)) (receive 0)))",
602        ));
603        h.post(&addr(0), addr(99));
604        let down = task_down(99, pair(sym("throw"), sym("noproc")));
605        assert_eq!(run(&mut h), vec![(id, ok(down))]);
606    }
607
608    #[test]
609    fn a_budget_rotates_tasks_rather_than_running_one_to_the_end() {
610        // Two tasks that each take more than a turn's worth of steps. The
611        // first to finish is not simply the first started unless one ran
612        // to completion, so interleaving shows in the step count alone.
613        let src = "(lambda t (_)
614                     (let ((down (lambda d (n) (if (eq? n 0) 'done (d (- n 1))))))
615                       (down 200)))";
616        let mut h = Heap::new(8);
617        let a = h.spawn(val(src));
618        let b = h.spawn(val(src));
619        let exits = run(&mut h);
620        assert_eq!(exits.len(), 2);
621        assert_eq!(exits[0].0, a);
622        assert_eq!(exits[1].0, b);
623        assert_eq!(exits[0].1, ok(sym("done")));
624    }
625
626    #[test]
627    fn a_channel_carries_data_and_refuses_anything_else() {
628        let mut h = Heap::new(1000);
629        let cell = h.spawn(val("(lambda t (_) (send '(task 0) (cell-new 1)))"));
630        assert_eq!(
631            run(&mut h),
632            vec![(
633                cell,
634                pair(sym("throw"), pair(sym("not-data"), Val::Cell(0)))
635            )]
636        );
637
638        // Nested, because the check is structural rather than a tag test
639        // on the payload's head.
640        let mut h = Heap::new(1000);
641        let clo = h.spawn(val(
642            "(lambda t (_) (car (catch (send '(task 0) (cons 1 (lambda f (x) x))))))",
643        ));
644        assert_eq!(run(&mut h), vec![(clo, ok(sym("not-data")))]);
645    }
646
647    #[test]
648    fn an_address_that_is_not_one_is_a_throw() {
649        let mut h = Heap::new(1000);
650        let id = h.spawn(val("(lambda t (_) (send 0 'hi))"));
651        assert_eq!(
652            run(&mut h),
653            vec![(
654                id,
655                pair(sym("throw"), pair(sym("bad-address"), Val::Num(0)))
656            )]
657        );
658    }
659
660    #[test]
661    fn a_task_across_a_link_leaves_the_heap() {
662        let mut h = Heap::new(1000);
663        let to = Addr::Remote(2, 5).val();
664        h.spawn(val("(lambda t (_) (receive))"));
665        h.post(&addr(0), to.clone());
666        h.spawn(val("(lambda t (_) (send (receive) 'hi))"));
667        h.post(&addr(1), to.clone());
668        h.run().expect("no bug");
669        assert_eq!(h.take_away(), vec![(to, sym("hi"))]);
670    }
671
672    #[test]
673    fn an_address_the_heap_does_not_know_goes_out_rather_than_failing() {
674        let mut h = Heap::new(1000);
675        h.spawn(val("(lambda t (_) (send '(host nowhere) 'hi))"));
676        h.run().expect("no bug");
677        assert_eq!(h.take_away(), vec![(val("'(host nowhere)"), sym("hi"))]);
678    }
679
680    #[test]
681    fn tasks_in_one_heap_share_cells() {
682        let mut h = Heap::new(1000);
683        let id = h.spawn(val("(lambda t (_)
684               (let ((c (cell-new 0)))
685                 (let ((child (spawn (lambda k (_) (cell-set! c 42)))))
686                   (begin (monitor child) (begin (receive 0) (cell-read c))))))"));
687        assert_eq!(
688            run(&mut h),
689            vec![(1, ok(Val::Num(42))), (id, ok(Val::Num(42)))]
690        );
691    }
692}