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::{catch_unwind, AssertUnwindSafe};
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
100struct ExecInner {
101    tasks: RefCell<HashMap<TaskId, TaskEntry>>,
102    next_id: Cell<TaskId>,
103    run_queue: RefCell<VecDeque<TaskId>>,
104    woken: Arc<WokenQueue>,
105    running: Cell<bool>,
106    currently_polling: Cell<Option<TaskId>>,
107    /// Self-cancellations deferred until the poll returns.
108    cancel_pending: RefCell<Vec<TaskId>>,
109    /// Called whenever any task panics — the runner points this at
110    /// "fail the current test" (cocotb: TestManager._task_done_callback).
111    failure_sink: RefCell<Option<Box<dyn Fn(&str)>>>,
112}
113
114/// The executor (design-doc §4.3). `!Send` — it never leaves the sim thread.
115#[derive(Clone)]
116pub struct Executor {
117    inner: Rc<ExecInner>,
118}
119
120thread_local! {
121    static CURRENT: RefCell<Option<Executor>> = const { RefCell::new(None) };
122}
123
124/// Create and install a fresh executor on this thread.
125pub fn init() -> Executor {
126    let ex = Executor {
127        inner: Rc::new(ExecInner {
128            tasks: RefCell::new(HashMap::new()),
129            next_id: Cell::new(1),
130            run_queue: RefCell::new(VecDeque::new()),
131            woken: Arc::new(WokenQueue(Mutex::new(VecDeque::new()))),
132            running: Cell::new(false),
133            currently_polling: Cell::new(None),
134            cancel_pending: RefCell::new(Vec::new()),
135            failure_sink: RefCell::new(None),
136        }),
137    };
138    CURRENT.with(|c| *c.borrow_mut() = Some(ex.clone()));
139    ex
140}
141
142/// The thread's executor. Panics if `init()` has not run.
143pub fn current() -> Executor {
144    CURRENT.with(|c| c.borrow().clone().expect("rustdv executor not initialized"))
145}
146
147/// Port of `cocotb.start_soon` (mapping row 4).
148pub fn spawn<F>(fut: F) -> TaskHandle<F::Output>
149where
150    F: Future + 'static,
151{
152    current().spawn_named(fut, None)
153}
154
155pub fn spawn_named<F>(fut: F, name: &str) -> TaskHandle<F::Output>
156where
157    F: Future + 'static,
158{
159    current().spawn_named(fut, Some(name))
160}
161
162impl Executor {
163    pub fn spawn_named<F>(&self, fut: F, name: Option<&str>) -> TaskHandle<F::Output>
164    where
165        F: Future + 'static,
166    {
167        let id = self.inner.next_id.get();
168        self.inner.next_id.set(id + 1);
169        let name = name.map(|s| s.to_string()).unwrap_or_else(|| format!("task_{id}"));
170
171        let shared = Rc::new(TaskShared::<F::Output> {
172            state: Cell::new(TaskState::Unstarted),
173            result: RefCell::new(None),
174            joiners: RefCell::new(Vec::new()),
175        });
176
177        // Wrapper future: writes the value into the shared cell on success.
178        let sh = shared.clone();
179        let wrapped = async move {
180            let out = fut.await;
181            sh.state.set(TaskState::Finished);
182            sh.complete(Ok(out));
183        };
184
185        let sh_abort = shared.clone();
186        let on_abort: Rc<dyn Fn(TaskError)> = Rc::new(move |e: TaskError| {
187            sh_abort.state.set(match e {
188                TaskError::Cancelled => TaskState::Cancelled,
189                _ => TaskState::Failed,
190            });
191            sh_abort.complete(Err(e));
192        });
193        let sh_state = shared.clone();
194        let state_cell: Rc<dyn Fn(TaskState)> = Rc::new(move |s| sh_state.state.set(s));
195
196        shared.state.set(TaskState::Scheduled);
197        self.inner.tasks.borrow_mut().insert(
198            id,
199            TaskEntry {
200                fut: Box::pin(wrapped),
201                name,
202                state: TaskState::Scheduled,
203                on_abort,
204                state_cell,
205            },
206        );
207        self.inner.run_queue.borrow_mut().push_back(id);
208
209        TaskHandle { id, shared, exec: self.clone() }
210    }
211
212    /// Drain the run queue to exhaustion, then return to the simulator.
213    /// Port of `EventLoop.run` (cocotb: `_event_loop.py`).
214    pub fn run_until_idle(&self) {
215        if self.inner.running.get() {
216            return; // re-entrant call from within a poll: outer loop continues
217        }
218        self.inner.running.set(true);
219        loop {
220            self.drain_woken();
221            let next = self.inner.run_queue.borrow_mut().pop_front();
222            let Some(id) = next else { break };
223            self.poll_task(id);
224        }
225        self.inner.running.set(false);
226    }
227
228    fn drain_woken(&self) {
229        let ids: Vec<TaskId> = self.inner.woken.0.lock().unwrap().drain(..).collect();
230        for id in ids {
231            let mut tasks = self.inner.tasks.borrow_mut();
232            if let Some(t) = tasks.get_mut(&id) {
233                if t.state == TaskState::Pending {
234                    t.state = TaskState::Scheduled;
235                    (t.state_cell)(TaskState::Scheduled);
236                    self.inner.run_queue.borrow_mut().push_back(id);
237                }
238            }
239        }
240    }
241
242    fn poll_task(&self, id: TaskId) {
243        // Take the entry state to Running; leave the entry in the map so
244        // handles can query it, but take the future out to poll without
245        // holding the borrow.
246        let (mut fut, on_abort, state_cell) = {
247            let mut tasks = self.inner.tasks.borrow_mut();
248            let Some(t) = tasks.get_mut(&id) else { return };
249            if t.state != TaskState::Scheduled {
250                return; // stale duplicate wake
251            }
252            t.state = TaskState::Running;
253            (t.state_cell)(TaskState::Running);
254            // Temporarily replace the future with a no-op placeholder.
255            let fut = std::mem::replace(&mut t.fut, Box::pin(async {}));
256            (fut, t.on_abort.clone(), t.state_cell.clone())
257        };
258
259        let waker = Waker::from(Arc::new(TaskWaker { id, woken: self.inner.woken.clone() }));
260        let mut cx = Context::from_waker(&waker);
261
262        self.inner.currently_polling.set(Some(id));
263        let polled = catch_unwind(AssertUnwindSafe(|| fut.as_mut().poll(&mut cx)));
264        self.inner.currently_polling.set(None);
265
266        match polled {
267            Ok(Poll::Ready(())) => {
268                // Wrapper already stored the result and set Finished.
269                self.inner.tasks.borrow_mut().remove(&id);
270            }
271            Ok(Poll::Pending) => {
272                let mut tasks = self.inner.tasks.borrow_mut();
273                if let Some(t) = tasks.get_mut(&id) {
274                    t.fut = fut; // put the future back
275                    t.state = TaskState::Pending;
276                    (t.state_cell)(TaskState::Pending);
277                }
278                drop(tasks);
279                // Deferred self-cancellation?
280                let pending: Vec<TaskId> = self.inner.cancel_pending.borrow_mut().drain(..).collect();
281                for cid in pending {
282                    self.cancel(cid);
283                }
284            }
285            Err(p) => {
286                let msg = panic_message(p);
287                let name = self
288                    .inner
289                    .tasks
290                    .borrow()
291                    .get(&id)
292                    .map(|t| t.name.clone())
293                    .unwrap_or_default();
294                self.inner.tasks.borrow_mut().remove(&id);
295                drop(fut); // drop the future outside the map borrow
296                state_cell(TaskState::Failed);
297                on_abort(TaskError::Panicked(msg.clone()));
298                if let Some(sink) = self.inner.failure_sink.borrow().as_ref() {
299                    sink(&format!("task '{name}' panicked: {msg}"));
300                } else {
301                    eprintln!("rustdv: unhandled task panic in '{name}': {msg}");
302                }
303            }
304        }
305    }
306
307    /// Cancel = drop the future (design-doc §4.6). Cleanup happens in Drop
308    /// impls; there is no exception to catch.
309    pub fn cancel(&self, id: TaskId) {
310        if self.inner.currently_polling.get() == Some(id) {
311            self.inner.cancel_pending.borrow_mut().push(id);
312            return;
313        }
314        let entry = self.inner.tasks.borrow_mut().remove(&id);
315        if let Some(t) = entry {
316            (t.on_abort)(TaskError::Cancelled);
317            drop(t.fut);
318        }
319    }
320
321    /// Cancel every live task with id >= `watermark` (the snapshot taken
322    /// *before* the test spawned anything) — the runner uses this to kill
323    /// a test's surviving tasks at test end (design-doc §4.5).
324    pub fn cancel_after(&self, watermark: TaskId) {
325        let ids: Vec<TaskId> = self
326            .inner
327            .tasks
328            .borrow()
329            .keys()
330            .copied()
331            .filter(|&id| id >= watermark)
332            .collect();
333        for id in ids {
334            self.cancel(id);
335        }
336    }
337
338    /// Current high-water task id (snapshot before starting a test).
339    pub fn watermark(&self) -> TaskId {
340        self.inner.next_id.get()
341    }
342
343    pub fn set_failure_sink(&self, f: Box<dyn Fn(&str)>) {
344        *self.inner.failure_sink.borrow_mut() = Some(f);
345    }
346
347    pub fn live_tasks(&self) -> usize {
348        self.inner.tasks.borrow().len()
349    }
350}
351
352fn panic_message(p: Box<dyn std::any::Any + Send>) -> String {
353    if let Some(s) = p.downcast_ref::<&str>() {
354        s.to_string()
355    } else if let Some(s) = p.downcast_ref::<String>() {
356        s.clone()
357    } else {
358        "panic (non-string payload)".to_string()
359    }
360}
361
362// ---------------------------------------------------------------------------
363// TaskHandle
364// ---------------------------------------------------------------------------
365
366/// Public task control surface; also a Future (`handle.await` == awaiting
367/// completion, as in cocotb 2.x). Port of cocotb `Task` (design-doc §4.3).
368pub struct TaskHandle<T> {
369    id: TaskId,
370    shared: Rc<TaskShared<T>>,
371    exec: Executor,
372}
373
374impl<T> Clone for TaskHandle<T> {
375    fn clone(&self) -> Self {
376        TaskHandle { id: self.id, shared: self.shared.clone(), exec: self.exec.clone() }
377    }
378}
379
380impl<T> TaskHandle<T> {
381    pub fn id(&self) -> TaskId {
382        self.id
383    }
384
385    pub fn state(&self) -> TaskState {
386        self.shared.state.get()
387    }
388
389    pub fn done(&self) -> bool {
390        matches!(
391            self.state(),
392            TaskState::Finished | TaskState::Cancelled | TaskState::Failed
393        )
394    }
395
396    /// Drop-based cancellation (§4.6).
397    pub fn cancel(&self) {
398        self.exec.cancel(self.id);
399    }
400
401    /// The task's result, if complete. Consumes the stored value.
402    pub fn result(&self) -> Result<T, TaskError> {
403        match self.shared.result.borrow_mut().take() {
404            Some(r) => r,
405            None => Err(TaskError::InvalidState),
406        }
407    }
408}
409
410impl<T> Future for TaskHandle<T> {
411    type Output = Result<T, TaskError>;
412
413    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
414        if self.done() {
415            return Poll::Ready(self.result());
416        }
417        self.shared.joiners.borrow_mut().push(cx.waker().clone());
418        Poll::Pending
419    }
420}