Skip to main content

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}