Skip to main content

logicaffeine_compile/concurrency/
vm_driver.rs

1//! The VM task driver (T11).
2//!
3//! [`VmTask`] wraps one resumable [`Vm`] as a scheduler [`Task`]. On each `poll`
4//! it delivers the scheduler's resume value into the VM's reserved register, runs
5//! the VM until its next concurrency op (`run_until_block`), drains any output the
6//! slice produced into a shared sink, and maps the VM's [`VmBlock`] request to a
7//! scheduler [`TaskStep`]. It mirrors the tree-walker's
8//! [`super::driver::InterpreterTask`].
9
10use std::cell::RefCell;
11use std::rc::Rc;
12
13use logicaffeine_runtime::{RtPayload, Task, TaskCtx, TaskStep};
14
15use crate::interpreter::RuntimeValue;
16use crate::vm::{Value, Vm, VmBlock, VmStep};
17
18use super::driver::ErrSink;
19use super::marshal;
20
21/// Where a VM task's `Show` output goes — the one place the cooperative and
22/// work-stealing drivers diverge.
23enum OutputMode {
24    /// Cooperative (M:1): every task drains into one shared sink on the single
25    /// thread, so per-task slices interleave in pick order directly.
26    Shared(Rc<RefCell<Vec<String>>>),
27    /// Work-stealing (M:N): a worker polls task bodies off-thread, so output is
28    /// buffered locally and reported per-slice via [`Task::take_output`]; the
29    /// coordinator re-orders the slices into pick order on its thread.
30    Buffered(Vec<String>),
31}
32
33/// Drives one VM task on the scheduler. `err_sink` (first writer wins) receives
34/// the task's error if it fails.
35pub struct VmTask<'p> {
36    vm: Vm<'p>,
37    output: OutputMode,
38    err_sink: Option<ErrSink>,
39}
40
41impl<'p> VmTask<'p> {
42    /// A cooperative task draining into the shared `output` sink.
43    pub fn new(vm: Vm<'p>, output: Rc<RefCell<Vec<String>>>, err_sink: Option<ErrSink>) -> Self {
44        VmTask { vm, output: OutputMode::Shared(output), err_sink }
45    }
46
47    /// A work-stealing task: output buffers locally (reported via `take_output`),
48    /// and a spawn forwards a `SpawnDesc` the worker rebuilds — never a boxed body
49    /// (which is `!Send` and could not cross a worker boundary anyway).
50    pub fn work_stealing(vm: Vm<'p>, err_sink: Option<ErrSink>) -> Self {
51        VmTask { vm, output: OutputMode::Buffered(Vec::new()), err_sink }
52    }
53
54    fn block_to_step(&self, req: VmBlock) -> TaskStep<'p> {
55        match req {
56            VmBlock::NewChan(cap) => TaskStep::NewChan(cap),
57            VmBlock::Send(ch, p) => TaskStep::Send(ch, p),
58            VmBlock::Recv(ch) => TaskStep::Recv(ch),
59            VmBlock::TrySend(ch, p) => TaskStep::TrySend(ch, p),
60            VmBlock::TryRecv(ch) => TaskStep::TryRecv(ch),
61            VmBlock::Close(ch) => TaskStep::Close(ch),
62            VmBlock::SpawnDesc { func, args, want_handle } => match &self.output {
63                // Cooperative: build the child VM inline over the shared program +
64                // sink, and hand the scheduler the boxed body.
65                OutputMode::Shared(sink) => {
66                    let child_vm = self.vm.spawn_task_vm(func, &args);
67                    let child = VmTask::new(child_vm, sink.clone(), self.err_sink.clone());
68                    TaskStep::Spawn(Box::new(child))
69                }
70                // Work-stealing: forward the `Send` descriptor; the receiving
71                // worker builds the child locally over its own program borrow.
72                OutputMode::Buffered(_) => TaskStep::SpawnDesc { func, args, want_handle },
73            },
74            VmBlock::Await(t) => TaskStep::Await(t),
75            VmBlock::Abort(t) => TaskStep::Abort(t),
76            VmBlock::Select(arms) => TaskStep::Select(arms),
77            VmBlock::Sleep(d) => TaskStep::Sleep(d),
78            // Peer networking never reaches this cooperative-scheduler driver: a program that
79            // networks routes to the dedicated async VM runner (`run_vm_net_async`), which owns a
80            // `NetInbox` and services these directly. Defensive `IoPending` if it ever does.
81            VmBlock::NetConnect(_)
82            | VmBlock::NetListen(_)
83            | VmBlock::NetSend(_, _)
84            | VmBlock::NetStream(_, _)
85            | VmBlock::NetAwait(_, _)
86            | VmBlock::NetMakePeer(_)
87            | VmBlock::NetSync(_, _) => TaskStep::IoPending,
88        }
89    }
90}
91
92impl<'p> Task<'p> for VmTask<'p> {
93    fn poll(&mut self, ctx: &mut TaskCtx) -> TaskStep<'p> {
94        // Deliver whatever the scheduler resumed us with. A resolved `Select`
95        // routes the received value into the winning arm's var and the arm index
96        // into the `SelectWait` destination; every other block delivers a single
97        // value into the reserved register.
98        if let Some(arm) = ctx.selected_arm.take() {
99            let payload = std::mem::replace(&mut ctx.resumed_with, RtPayload::Nothing);
100            self.vm.deliver_select(arm, Value::from_runtime(marshal::rebuild(payload)));
101        } else {
102            let resume = ctx_to_value(ctx);
103            self.vm.deliver_resume(resume);
104        }
105
106        let step = self.vm.run_until_block();
107
108        // Stream this slice's output. Cooperative drains straight into the shared
109        // sink; work-stealing buffers locally for the coordinator to flush in pick
110        // order (see [`Task::take_output`]).
111        let lines = self.vm.drain_lines();
112        if !lines.is_empty() {
113            match &mut self.output {
114                OutputMode::Shared(sink) => sink.borrow_mut().extend(lines),
115                OutputMode::Buffered(buf) => buf.extend(lines),
116            }
117        }
118
119        match step {
120            Ok(VmStep::Done(result)) => {
121                let payload = marshal::materialize(&result).unwrap_or(RtPayload::Nothing);
122                TaskStep::Exit(payload)
123            }
124            Ok(VmStep::Blocked) => {
125                let req = self
126                    .vm
127                    .take_pending()
128                    .expect("a blocked VM slice must leave a pending request");
129                self.block_to_step(req)
130            }
131            Ok(VmStep::Paused) => {
132                unreachable!("the driver uses run_until_block; the debug stepper is never driven here")
133            }
134            Err(e) => {
135                if let Some(sink) = &self.err_sink {
136                    let mut slot = sink.borrow_mut();
137                    if slot.is_none() {
138                        *slot = Some(e);
139                    }
140                }
141                TaskStep::Exit(RtPayload::Nothing)
142            }
143        }
144    }
145
146    /// Hand the work-stealing coordinator this slice's buffered output so it can
147    /// be flushed in pick order. A no-op for cooperative tasks (they drain into
148    /// the shared sink directly).
149    fn take_output(&mut self) -> Vec<String> {
150        match &mut self.output {
151            OutputMode::Buffered(buf) => std::mem::take(buf),
152            OutputMode::Shared(_) => Vec::new(),
153        }
154    }
155}
156
157/// Translate the scheduler's resume context into the VM resume value: a freshly
158/// created channel id, a spawned task handle, or a received/awaited payload.
159fn ctx_to_value(ctx: &mut TaskCtx) -> Value {
160    if let Some(id) = ctx.new_chan {
161        Value::from_runtime(RuntimeValue::Chan(id))
162    } else if let Some(id) = ctx.spawned {
163        Value::from_runtime(RuntimeValue::TaskHandle(id))
164    } else {
165        let payload = std::mem::replace(&mut ctx.resumed_with, RtPayload::Nothing);
166        Value::from_runtime(marshal::rebuild(payload))
167    }
168}