logicaffeine_runtime/executor.rs
1//! The work-stealing M:N native driver (Phase 7 of work/FINISH_INTERPRETER.md).
2//!
3//! Genuine multicore for the VM/tree-walker tiers, native-only (`std::thread` +
4//! `std::sync`; never tokio, never `crossbeam` — this crate is dependency-free by
5//! charter). The design turns on one fact: a task body (`Vm`/`Interpreter`) holds
6//! `Rc` heaps and is irreducibly `!Send`, so **tasks never cross threads**. What
7//! crosses is only a `Send` *descriptor*; each worker *builds* its `!Send` task
8//! locally from its own clone of the program.
9//!
10//! ## Architecture — a central coordinator owns every decision
11//!
12//! ```text
13//! ┌───────────────────────────────────────────┐
14//! │ Coordinator (the authoritative Scheduler + │
15//! │ the SINGLE Chooser seed choke point): │
16//! │ channels, timers, the work deque │
17//! └───────────────┬───────────────────────────┘
18//! Send descriptors ↓ ↑ Send StepReport
19//! ┌─────────────────┴──┐ ┌┴────────────────────┐
20//! │ Worker 0 (thread) │ │ Worker N-1 │
21//! │ own program clone │ │ own program clone │
22//! │ builds !Send task │ │ builds !Send task │
23//! │ polls body → block│ │ polls body → block │
24//! └────────────────────┘ └─────────────────────┘
25//! ```
26//!
27//! Workers never touch channels, timers, or the `Chooser` directly: they poll a
28//! task body (the expensive part — arithmetic, loops, JIT regions) and report the
29//! resulting `StepReport` back. The coordinator applies every report through the
30//! *same* [`crate::scheduler::Scheduler`] state machine and the *same* `Chooser`,
31//! in a deterministic order, so for a fixed seed the trace is **identical to the
32//! cooperative run** — parallelism only reorders the wall-clock arrival of reports,
33//! which a logical clock + a `TaskId`-sorted apply order erase.
34//!
35//! Determinism is therefore preserved *by construction*: there is one choice
36//! point. "Spawns are stealable; resumptions are pinned" — a freshly spawned task
37//! (just `(func, args)`) can be built by any idle worker, but an already-built
38//! `!Send` continuation stays on the worker that last ran it.
39
40use crate::channel::ChanId;
41use crate::payload::RtPayload;
42use crate::task::{SelectArm, TaskId};
43
44/// An opaque function index, from the runtime's view — the compile crate's
45/// `FuncIdx` (a `u16`), kept local so this crate stays dependency-free.
46pub type FuncIdx = u16;
47
48/// A `Send` task descriptor: which function to run, its already-materialised
49/// arguments, its scheduling priority, and whether it is the run's main task.
50/// This is what a worker turns into a `!Send` task body locally.
51pub struct SpawnDesc {
52 pub func: FuncIdx,
53 pub args: Vec<RtPayload>,
54 pub priority: u8,
55 pub is_main: bool,
56}
57
58/// How a worker should (re)enter a task body on its next poll. Every variant is
59/// `Send` (`FuncIdx` is `u16`, `RtPayload` is `Send`, ids are `Copy`).
60pub enum ResumeKind {
61 /// First entry / a block that yields no value (`Send`/`Close`/`Yield`/`Sleep`).
62 Nothing,
63 /// Fresh spawn: build the body from this function + materialised args.
64 /// `is_main` distinguishes the program's root task (whose body is the
65 /// top-level program, not a spawned function) so the builder positions it
66 /// correctly.
67 Spawn { func: FuncIdx, args: Vec<RtPayload>, priority: u8, is_main: bool },
68 /// Resume a pinned continuation with a delivered value (`Recv`/`TryRecv`/`Await`).
69 Payload(RtPayload),
70 /// Resume a `Select` winner: the winning arm index + its (recv) payload.
71 Select { arm: usize, payload: RtPayload },
72 /// Resume after `NewChan` with the freshly-created channel id.
73 NewChan(ChanId),
74 /// Resume after a handle-bearing spawn with the child's task id.
75 SpawnedHandle(TaskId),
76}
77
78/// The `Send` projection of a [`crate::task::TaskStep`] a worker reports after
79/// polling one task slice (the `tid` lives on [`WorkerReport`]). The crucial
80/// difference from `TaskStep`: the spawn variant carries a *descriptor*, not a
81/// `Box<dyn Task>` — so the `!Send` body is built and consumed on one worker.
82enum ReportStep {
83 Yield,
84 Recv(ChanId),
85 Send(ChanId, RtPayload),
86 TrySend(ChanId, RtPayload),
87 TryRecv(ChanId),
88 Select(Vec<SelectArm>),
89 Sleep(u64),
90 NewChan(Option<usize>),
91 Spawn { func: FuncIdx, args: Vec<RtPayload>, want_handle: bool },
92 Await(TaskId),
93 Abort(TaskId),
94 Close(ChanId),
95 Exit(RtPayload),
96}
97
98/// What one worker sends back after a poll slice: which task, the output that
99/// slice produced (flushed by the coordinator in pick order), and the projected
100/// step.
101struct WorkerReport {
102 tid: TaskId,
103 output: Vec<String>,
104 step: ReportStep,
105}
106
107/// A unit of work the coordinator sends to a worker.
108enum WorkMsg {
109 Run { tid: TaskId, resume: ResumeKind },
110 Shutdown,
111}
112
113/// The result of a work-stealing run.
114pub struct WsOutcome {
115 pub outcome: crate::scheduler::RunOutcome,
116 pub output: Vec<String>,
117 pub trace: crate::seed::SchedTrace,
118}
119
120/// Compile-time proof that the entire cross-thread boundary is `Send`. The task
121/// bodies (`Vm`/`Interpreter`, `Box<dyn Task>`, any `Rc`) never cross.
122#[allow(dead_code)]
123fn _assert_boundary_send() {
124 fn s<T: Send>() {}
125 s::<SpawnDesc>();
126 s::<ResumeKind>();
127 s::<WorkerReport>();
128 s::<WorkMsg>();
129 s::<RtPayload>();
130}
131
132use std::collections::HashMap;
133use std::sync::mpsc;
134
135use crate::scheduler::{RunOutcome, Scheduler};
136use crate::seed::{Chooser, SchedSeed};
137use crate::config::SchedulerConfig;
138use crate::task::{TaskCtx, TaskStep};
139
140/// Run a concurrent program on the **work-stealing M:N driver** under `seed`.
141///
142/// `build` turns a `Send` [`SpawnDesc`] into a worker-local `!Send` task body; it
143/// is cloned once per worker, so each worker constructs its tasks over its own
144/// resources (e.g. its own program clone) and no body ever crosses a thread. The
145/// coordinator owns the single [`Scheduler`] + `Chooser`, so the seeded trace is
146/// identical to the cooperative run; workers only poll task *bodies* in parallel.
147pub fn run_workstealing_seeded<'env, B>(
148 config: SchedulerConfig,
149 seed: SchedSeed,
150 main: SpawnDesc,
151 build: B,
152) -> WsOutcome
153where
154 B: Fn(SpawnDesc) -> Box<dyn crate::task::Task<'env> + 'env> + Sync,
155{
156 let n = config.workers.max(1);
157 // The coordinator's `Scheduler` only ever holds *metadata* slots (task bodies
158 // live worker-side), so its task lifetime is vacuous — `'static`.
159 let mut sched: Scheduler<'static> = Scheduler::new(config, Chooser::record(seed));
160 let (report_tx, report_rx) = mpsc::channel::<WorkerReport>();
161
162 // The main task is a metadata slot; its body is built worker-side from `main`.
163 let main_tid = sched.spawn_meta(main.priority, true);
164 let mut fresh: HashMap<TaskId, SpawnDesc> = HashMap::new();
165 fresh.insert(main_tid, main);
166
167 // Scoped threads let workers BORROW `&build` (and through it the program), so
168 // the bodies need not be `'static` — no leak, no unsafe.
169 let build_ref = &build;
170 let (outcome, output, trace) = std::thread::scope(|scope| {
171 // coordinator→worker work (one channel each, so a pinned resume routes to
172 // its owning worker).
173 let mut work_txs = Vec::with_capacity(n);
174 for _ in 0..n {
175 let (work_tx, work_rx) = mpsc::channel::<WorkMsg>();
176 work_txs.push(work_tx);
177 let rtx = report_tx.clone();
178 scope.spawn(move || worker_main(work_rx, rtx, build_ref));
179 }
180 drop(report_tx); // so `report_rx` closes once every worker's clone drops
181
182 let mut owner: HashMap<TaskId, usize> = HashMap::new();
183 let mut next_w = 0usize;
184 let mut output: Vec<String> = Vec::new();
185
186 let outcome = loop {
187 // The ready batch, in the SAME pick order the cooperative scheduler uses.
188 let mut batch: Vec<crate::scheduler::WsDispatch> = Vec::new();
189 while let Some(d) = sched.ws_next_dispatch() {
190 batch.push(d);
191 }
192 if batch.is_empty() {
193 if sched.ws_all_done() {
194 break RunOutcome::Done(sched.ws_main_result());
195 }
196 if sched.ws_has_timers() {
197 sched.ws_advance_timers();
198 continue;
199 }
200 break RunOutcome::Deadlock;
201 }
202
203 // Dispatch all ready tasks (workers poll their bodies in parallel). A
204 // fresh task is assigned round-robin (deterministic); a resume goes to
205 // its owning worker (the body is pinned there).
206 let order: Vec<TaskId> = batch.iter().map(|d| d.tid).collect();
207 for d in batch {
208 let w = if fresh.contains_key(&d.tid) {
209 let w = next_w;
210 next_w = (next_w + 1) % n;
211 owner.insert(d.tid, w);
212 w
213 } else {
214 owner[&d.tid]
215 };
216 let resume = if let Some(desc) = fresh.remove(&d.tid) {
217 ResumeKind::Spawn {
218 func: desc.func,
219 args: desc.args,
220 priority: desc.priority,
221 is_main: desc.is_main,
222 }
223 } else if let Some(arm) = d.selected_arm {
224 ResumeKind::Select { arm, payload: d.resume }
225 } else if let Some(ch) = d.new_chan {
226 ResumeKind::NewChan(ch)
227 } else if let Some(t) = d.spawned {
228 ResumeKind::SpawnedHandle(t)
229 } else {
230 match d.resume {
231 RtPayload::Nothing => ResumeKind::Nothing,
232 p => ResumeKind::Payload(p),
233 }
234 };
235 work_txs[w].send(WorkMsg::Run { tid: d.tid, resume }).expect("worker alive");
236 }
237
238 // Barrier: collect this batch's reports (arrival order is irrelevant).
239 let mut reports: HashMap<TaskId, WorkerReport> = HashMap::new();
240 for _ in 0..order.len() {
241 let r = report_rx.recv().expect("worker report");
242 reports.insert(r.tid, r);
243 }
244
245 // Apply in PICK ORDER: flush each slice's output, then apply its step
246 // through the SAME Scheduler methods the cooperative driver uses — this
247 // is what makes output + channel resolution identical to cooperative.
248 for tid in &order {
249 let WorkerReport { tid: rt, output: slice_out, step } =
250 reports.remove(tid).expect("report for dispatched task");
251 output.extend(slice_out);
252 apply_step(&mut sched, &mut fresh, rt, step);
253 }
254 };
255
256 for tx in &work_txs {
257 let _ = tx.send(WorkMsg::Shutdown);
258 }
259 drop(work_txs);
260 (outcome, output, sched.into_trace())
261 });
262
263 WsOutcome { outcome, output, trace }
264}
265
266/// Apply one worker's report through the authoritative `Scheduler` (reusing its
267/// channel/decision methods), turning a spawn report into a metadata child +
268/// recorded descriptor.
269fn apply_step(
270 sched: &mut Scheduler<'static>,
271 fresh: &mut HashMap<TaskId, SpawnDesc>,
272 tid: TaskId,
273 step: ReportStep,
274) {
275 match step {
276 ReportStep::Yield => sched.process_step(tid, TaskStep::Yield),
277 ReportStep::Recv(ch) => sched.process_step(tid, TaskStep::Recv(ch)),
278 ReportStep::Send(ch, p) => sched.process_step(tid, TaskStep::Send(ch, p)),
279 ReportStep::TrySend(ch, p) => sched.process_step(tid, TaskStep::TrySend(ch, p)),
280 ReportStep::TryRecv(ch) => sched.process_step(tid, TaskStep::TryRecv(ch)),
281 ReportStep::Select(arms) => sched.process_step(tid, TaskStep::Select(arms)),
282 ReportStep::Sleep(t) => sched.process_step(tid, TaskStep::Sleep(t)),
283 ReportStep::NewChan(cap) => sched.process_step(tid, TaskStep::NewChan(cap)),
284 ReportStep::Await(t) => sched.process_step(tid, TaskStep::Await(t)),
285 ReportStep::Abort(t) => sched.process_step(tid, TaskStep::Abort(t)),
286 ReportStep::Close(ch) => sched.process_step(tid, TaskStep::Close(ch)),
287 ReportStep::Exit(v) => sched.process_step(tid, TaskStep::Exit(v)),
288 ReportStep::Spawn { func, args, want_handle: _ } => {
289 // Mirror the cooperative `Spawn` step: create the child + hand the
290 // parent the child id; record the child's build descriptor.
291 let child = sched.ws_spawn_child(tid, 0);
292 fresh.insert(child, SpawnDesc { func, args, priority: 0, is_main: false });
293 }
294 }
295}
296
297/// One worker thread: builds (fresh) or looks up (resume) the `!Send` task body,
298/// polls one slice, and reports the projected step + the slice's output.
299fn worker_main<'env, B>(
300 work_rx: mpsc::Receiver<WorkMsg>,
301 report_tx: mpsc::Sender<WorkerReport>,
302 build: &B,
303) where
304 B: Fn(SpawnDesc) -> Box<dyn crate::task::Task<'env> + 'env>,
305{
306 let mut local: HashMap<TaskId, Box<dyn crate::task::Task<'env> + 'env>> = HashMap::new();
307 while let Ok(msg) = work_rx.recv() {
308 let (tid, resume) = match msg {
309 WorkMsg::Shutdown => break,
310 WorkMsg::Run { tid, resume } => (tid, resume),
311 };
312 let mut ctx = TaskCtx {
313 resumed_with: RtPayload::Nothing,
314 selected_arm: None,
315 new_chan: None,
316 spawned: None,
317 };
318 match resume {
319 ResumeKind::Nothing => {}
320 ResumeKind::Spawn { func, args, priority, is_main } => {
321 local.insert(tid, build(SpawnDesc { func, args, priority, is_main }));
322 }
323 ResumeKind::Payload(p) => ctx.resumed_with = p,
324 ResumeKind::Select { arm, payload } => {
325 ctx.selected_arm = Some(arm);
326 ctx.resumed_with = payload;
327 }
328 ResumeKind::NewChan(ch) => ctx.new_chan = Some(ch),
329 ResumeKind::SpawnedHandle(t) => ctx.spawned = Some(t),
330 }
331 let task = local.get_mut(&tid).expect("task body present on its owning worker");
332 let step = task.poll(&mut ctx);
333 let out = task.take_output();
334 let (rstep, done) = project_step(step);
335 if done {
336 local.remove(&tid);
337 }
338 if report_tx.send(WorkerReport { tid, output: out, step: rstep }).is_err() {
339 break;
340 }
341 }
342}
343
344/// Project a worker's `TaskStep` into the `Send` [`ReportStep`]; the `bool` is
345/// whether the task finished (so the worker drops its body).
346fn project_step<'env>(step: TaskStep<'env>) -> (ReportStep, bool) {
347 match step {
348 TaskStep::Yield => (ReportStep::Yield, false),
349 TaskStep::Recv(ch) => (ReportStep::Recv(ch), false),
350 TaskStep::Send(ch, p) => (ReportStep::Send(ch, p), false),
351 TaskStep::TrySend(ch, p) => (ReportStep::TrySend(ch, p), false),
352 TaskStep::TryRecv(ch) => (ReportStep::TryRecv(ch), false),
353 TaskStep::Select(arms) => (ReportStep::Select(arms), false),
354 TaskStep::Sleep(t) => (ReportStep::Sleep(t), false),
355 TaskStep::NewChan(cap) => (ReportStep::NewChan(cap), false),
356 TaskStep::SpawnDesc { func, args, want_handle } => {
357 (ReportStep::Spawn { func, args, want_handle }, false)
358 }
359 TaskStep::Spawn(_) => {
360 panic!("work-stealing tasks must spawn via SpawnDesc, not a boxed Spawn")
361 }
362 TaskStep::Await(t) => (ReportStep::Await(t), false),
363 TaskStep::Abort(t) => (ReportStep::Abort(t), false),
364 TaskStep::Close(ch) => (ReportStep::Close(ch), false),
365 TaskStep::IoPending => {
366 // Networking runs on the tree-walker (async) tier, never the work-stealing
367 // compute tier, so a worker never parks on external I/O.
368 panic!("work-stealing tasks do not perform external I/O (IoPending)")
369 }
370 TaskStep::Exit(v) => (ReportStep::Exit(v), true),
371 }
372}