Skip to main content

rustdv_sim/
executor.rs

1//! The bespoke single-threaded executor (design-doc §4.2/§4.3).
2//!
3//! Port of cocotb's event loop + Task model: a run queue drained to
4//! exhaustion after every simulator callback (cocotb: `_event_loop.py`,
5//! `EventLoop.run`), tasks with the same seven observable states
6//! (cocotb: `task.py`, `_TaskState`), and drop-based cancellation (§4.6).
7
8use std::cell::{Cell, RefCell};
9use std::collections::{HashMap, VecDeque};
10use std::fmt;
11use std::future::Future;
12use std::panic::{AssertUnwindSafe, catch_unwind};
13use std::pin::Pin;
14use std::rc::Rc;
15use std::sync::{Arc, Mutex};
16use std::task::{Context, Poll, Wake, Waker};
17
18pub type TaskId = u64;
19
20/// The seven task states, per cocotb (mapping row 5).
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22pub enum TaskState {
23    Unstarted,
24    Scheduled,
25    Running,
26    Pending,
27    Finished,
28    Cancelled,
29    Failed,
30}
31
32#[derive(Debug, Clone)]
33pub enum TaskError {
34    Cancelled,
35    Panicked(String),
36    /// result() called before completion, or awaited twice.
37    InvalidState,
38}
39
40impl fmt::Display for TaskError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            TaskError::Cancelled => write!(f, "task was cancelled"),
44            TaskError::Panicked(m) => write!(f, "task panicked: {m}"),
45            TaskError::InvalidState => write!(f, "task result not available"),
46        }
47    }
48}
49impl std::error::Error for TaskError {}
50
51// ---------------------------------------------------------------------------
52// Waker plumbing: the TriggerWaker role (design-doc §4.3). Waking pushes the
53// task id onto a queue the executor drains; the Mutex is uncontended
54// (single thread) and exists only to satisfy `Waker`'s Send+Sync contract.
55// ---------------------------------------------------------------------------
56
57struct WokenQueue(Mutex<VecDeque<TaskId>>);
58
59struct TaskWaker {
60    id: TaskId,
61    woken: Arc<WokenQueue>,
62}
63
64impl Wake for TaskWaker {
65    fn wake(self: Arc<Self>) {
66        self.woken.0.lock().unwrap().push_back(self.id);
67    }
68}
69
70// ---------------------------------------------------------------------------
71// Task bookkeeping
72// ---------------------------------------------------------------------------
73
74/// Per-task shared cell between the executor entry and its TaskHandle.
75struct TaskShared<T> {
76    state: Cell<TaskState>,
77    result: RefCell<Option<Result<T, TaskError>>>,
78    joiners: RefCell<Vec<Waker>>,
79}
80
81impl<T> TaskShared<T> {
82    fn complete(&self, r: Result<T, TaskError>) {
83        *self.result.borrow_mut() = Some(r);
84        for w in self.joiners.borrow_mut().drain(..) {
85            w.wake();
86        }
87    }
88}
89
90struct TaskEntry {
91    fut: Pin<Box<dyn Future<Output = ()>>>,
92    name: String,
93    state: TaskState, // mirror for the executor's own bookkeeping
94    /// Type-erased hook: propagate abnormal termination into TaskShared<T>.
95    on_abort: Rc<dyn Fn(TaskError)>,
96    /// Mirror of shared state for handle.state() queries.
97    state_cell: Rc<dyn Fn(TaskState)>,
98}
99
100type FailureSink = Box<dyn Fn(&str)>;
101
102struct ExecInner {
103    tasks: RefCell<HashMap<TaskId, TaskEntry>>,
104    next_id: Cell<TaskId>,
105    run_queue: RefCell<VecDeque<TaskId>>,
106    woken: Arc<WokenQueue>,
107    running: Cell<bool>,
108    currently_polling: Cell<Option<TaskId>>,
109    /// Self-cancellations deferred until the poll returns.
110    cancel_pending: RefCell<Vec<TaskId>>,
111    /// Called whenever any task panics — the runner points this at
112    /// "fail the current test" (cocotb: TestManager._task_done_callback).
113    failure_sink: RefCell<Option<FailureSink>>,
114}
115
116/// The executor (design-doc §4.3). `!Send` — it never leaves the sim thread.
117#[derive(Clone)]
118pub struct Executor {
119    inner: Rc<ExecInner>,
120}
121
122thread_local! {
123    static CURRENT: RefCell<Option<Executor>> = const { RefCell::new(None) };
124}
125
126/// Create and install a fresh executor on this thread.
127pub fn init() -> Executor {
128    let ex = Executor {
129        inner: Rc::new(ExecInner {
130            tasks: RefCell::new(HashMap::new()),
131            next_id: Cell::new(1),
132            run_queue: RefCell::new(VecDeque::new()),
133            woken: Arc::new(WokenQueue(Mutex::new(VecDeque::new()))),
134            running: Cell::new(false),
135            currently_polling: Cell::new(None),
136            cancel_pending: RefCell::new(Vec::new()),
137            failure_sink: RefCell::new(None),
138        }),
139    };
140    CURRENT.with(|c| *c.borrow_mut() = Some(ex.clone()));
141    ex
142}
143
144/// The thread's executor. Panics if `init()` has not run.
145pub fn current() -> Executor {
146    CURRENT.with(|c| c.borrow().clone().expect("rustdv executor not initialized"))
147}
148
149/// Port of `cocotb.start_soon` (mapping row 4).
150pub fn spawn<F>(fut: F) -> TaskHandle<F::Output>
151where
152    F: Future + 'static,
153{
154    current().spawn_named(fut, None)
155}
156
157pub fn spawn_named<F>(fut: F, name: &str) -> TaskHandle<F::Output>
158where
159    F: Future + 'static,
160{
161    current().spawn_named(fut, Some(name))
162}
163
164impl Executor {
165    pub fn spawn_named<F>(&self, fut: F, name: Option<&str>) -> TaskHandle<F::Output>
166    where
167        F: Future + 'static,
168    {
169        let id = self.inner.next_id.get();
170        self.inner.next_id.set(id + 1);
171        let name = name
172            .map(|s| s.to_string())
173            .unwrap_or_else(|| format!("task_{id}"));
174
175        let shared = Rc::new(TaskShared::<F::Output> {
176            state: Cell::new(TaskState::Unstarted),
177            result: RefCell::new(None),
178            joiners: RefCell::new(Vec::new()),
179        });
180
181        // Wrapper future: writes the value into the shared cell on success.
182        let sh = shared.clone();
183        let wrapped = async move {
184            let out = fut.await;
185            sh.state.set(TaskState::Finished);
186            sh.complete(Ok(out));
187        };
188
189        let sh_abort = shared.clone();
190        let on_abort: Rc<dyn Fn(TaskError)> = Rc::new(move |e: TaskError| {
191            sh_abort.state.set(match e {
192                TaskError::Cancelled => TaskState::Cancelled,
193                _ => TaskState::Failed,
194            });
195            sh_abort.complete(Err(e));
196        });
197        let sh_state = shared.clone();
198        let state_cell: Rc<dyn Fn(TaskState)> = Rc::new(move |s| sh_state.state.set(s));
199
200        shared.state.set(TaskState::Scheduled);
201        self.inner.tasks.borrow_mut().insert(
202            id,
203            TaskEntry {
204                fut: Box::pin(wrapped),
205                name,
206                state: TaskState::Scheduled,
207                on_abort,
208                state_cell,
209            },
210        );
211        self.inner.run_queue.borrow_mut().push_back(id);
212
213        TaskHandle {
214            id,
215            shared,
216            exec: self.clone(),
217        }
218    }
219
220    /// Drain the run queue to exhaustion, then return to the simulator.
221    /// Port of `EventLoop.run` (cocotb: `_event_loop.py`).
222    pub fn run_until_idle(&self) {
223        if self.inner.running.get() {
224            return; // re-entrant call from within a poll: outer loop continues
225        }
226        self.inner.running.set(true);
227        loop {
228            self.drain_woken();
229            let next = self.inner.run_queue.borrow_mut().pop_front();
230            let Some(id) = next else { break };
231            self.poll_task(id);
232        }
233        self.inner.running.set(false);
234    }
235
236    fn drain_woken(&self) {
237        let ids: Vec<TaskId> = self.inner.woken.0.lock().unwrap().drain(..).collect();
238        for id in ids {
239            let mut tasks = self.inner.tasks.borrow_mut();
240            if let Some(t) = tasks.get_mut(&id)
241                && t.state == TaskState::Pending
242            {
243                t.state = TaskState::Scheduled;
244                (t.state_cell)(TaskState::Scheduled);
245                self.inner.run_queue.borrow_mut().push_back(id);
246            }
247        }
248    }
249
250    fn poll_task(&self, id: TaskId) {
251        // Take the entry state to Running; leave the entry in the map so
252        // handles can query it, but take the future out to poll without
253        // holding the borrow.
254        let (mut fut, on_abort, state_cell) = {
255            let mut tasks = self.inner.tasks.borrow_mut();
256            let Some(t) = tasks.get_mut(&id) else { return };
257            if t.state != TaskState::Scheduled {
258                return; // stale duplicate wake
259            }
260            t.state = TaskState::Running;
261            (t.state_cell)(TaskState::Running);
262            // Temporarily replace the future with a no-op placeholder.
263            let fut = std::mem::replace(&mut t.fut, Box::pin(async {}));
264            (fut, t.on_abort.clone(), t.state_cell.clone())
265        };
266
267        let waker = Waker::from(Arc::new(TaskWaker {
268            id,
269            woken: self.inner.woken.clone(),
270        }));
271        let mut cx = Context::from_waker(&waker);
272
273        self.inner.currently_polling.set(Some(id));
274        let polled = catch_unwind(AssertUnwindSafe(|| fut.as_mut().poll(&mut cx)));
275        self.inner.currently_polling.set(None);
276
277        match polled {
278            Ok(Poll::Ready(())) => {
279                // Wrapper already stored the result and set Finished.
280                self.inner.tasks.borrow_mut().remove(&id);
281            }
282            Ok(Poll::Pending) => {
283                let mut tasks = self.inner.tasks.borrow_mut();
284                if let Some(t) = tasks.get_mut(&id) {
285                    t.fut = fut; // put the future back
286                    t.state = TaskState::Pending;
287                    (t.state_cell)(TaskState::Pending);
288                }
289                drop(tasks);
290                // Deferred self-cancellation?
291                let pending: Vec<TaskId> =
292                    self.inner.cancel_pending.borrow_mut().drain(..).collect();
293                for cid in pending {
294                    self.cancel(cid);
295                }
296            }
297            Err(p) => {
298                let msg = panic_message(p);
299                let name = self
300                    .inner
301                    .tasks
302                    .borrow()
303                    .get(&id)
304                    .map(|t| t.name.clone())
305                    .unwrap_or_default();
306                self.inner.tasks.borrow_mut().remove(&id);
307                drop(fut); // drop the future outside the map borrow
308                state_cell(TaskState::Failed);
309                on_abort(TaskError::Panicked(msg.clone()));
310                if let Some(sink) = self.inner.failure_sink.borrow().as_ref() {
311                    sink(&format!("task '{name}' panicked: {msg}"));
312                } else {
313                    eprintln!("rustdv: unhandled task panic in '{name}': {msg}");
314                }
315            }
316        }
317    }
318
319    /// Cancel = drop the future (design-doc §4.6). Cleanup happens in Drop
320    /// impls; there is no exception to catch.
321    pub fn cancel(&self, id: TaskId) {
322        if self.inner.currently_polling.get() == Some(id) {
323            self.inner.cancel_pending.borrow_mut().push(id);
324            return;
325        }
326        let entry = self.inner.tasks.borrow_mut().remove(&id);
327        if let Some(t) = entry {
328            (t.on_abort)(TaskError::Cancelled);
329            drop(t.fut);
330        }
331    }
332
333    /// Cancel every live task with id >= `watermark` (the snapshot taken
334    /// *before* the test spawned anything) — the runner uses this to kill
335    /// a test's surviving tasks at test end (design-doc §4.5).
336    pub fn cancel_after(&self, watermark: TaskId) {
337        let ids: Vec<TaskId> = self
338            .inner
339            .tasks
340            .borrow()
341            .keys()
342            .copied()
343            .filter(|&id| id >= watermark)
344            .collect();
345        for id in ids {
346            self.cancel(id);
347        }
348    }
349
350    /// Current high-water task id (snapshot before starting a test).
351    pub fn watermark(&self) -> TaskId {
352        self.inner.next_id.get()
353    }
354
355    pub fn set_failure_sink(&self, f: Box<dyn Fn(&str)>) {
356        *self.inner.failure_sink.borrow_mut() = Some(f);
357    }
358
359    pub fn live_tasks(&self) -> usize {
360        self.inner.tasks.borrow().len()
361    }
362}
363
364fn panic_message(p: Box<dyn std::any::Any + Send>) -> String {
365    if let Some(s) = p.downcast_ref::<&str>() {
366        s.to_string()
367    } else if let Some(s) = p.downcast_ref::<String>() {
368        s.clone()
369    } else {
370        "panic (non-string payload)".to_string()
371    }
372}
373
374// ---------------------------------------------------------------------------
375// TaskHandle
376// ---------------------------------------------------------------------------
377
378/// Public task control surface; also a Future (`handle.await` == awaiting
379/// completion, as in cocotb 2.x). Port of cocotb `Task` (design-doc §4.3).
380pub struct TaskHandle<T> {
381    id: TaskId,
382    shared: Rc<TaskShared<T>>,
383    exec: Executor,
384}
385
386impl<T> Clone for TaskHandle<T> {
387    fn clone(&self) -> Self {
388        TaskHandle {
389            id: self.id,
390            shared: self.shared.clone(),
391            exec: self.exec.clone(),
392        }
393    }
394}
395
396impl<T> TaskHandle<T> {
397    pub fn id(&self) -> TaskId {
398        self.id
399    }
400
401    pub fn state(&self) -> TaskState {
402        self.shared.state.get()
403    }
404
405    pub fn done(&self) -> bool {
406        matches!(
407            self.state(),
408            TaskState::Finished | TaskState::Cancelled | TaskState::Failed
409        )
410    }
411
412    /// Drop-based cancellation (§4.6).
413    pub fn cancel(&self) {
414        self.exec.cancel(self.id);
415    }
416
417    /// The task's result, if complete. Consumes the stored value.
418    pub fn result(&self) -> Result<T, TaskError> {
419        match self.shared.result.borrow_mut().take() {
420            Some(r) => r,
421            None => Err(TaskError::InvalidState),
422        }
423    }
424}
425
426impl<T> Future for TaskHandle<T> {
427    type Output = Result<T, TaskError>;
428
429    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
430        if self.done() {
431            return Poll::Ready(self.result());
432        }
433        self.shared.joiners.borrow_mut().push(cx.waker().clone());
434        Poll::Pending
435    }
436}