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}