Skip to main content

rivet/
executor.rs

1//! Priority-aware async executor with single-stack task polling.
2//!
3//! All cooperative tasks share one system stack. The executor polls tasks
4//! in priority order. When a task returns `Poll::Pending`, its state lives
5//! in a static `TaskCell` referenced by the task's `TaskReg.user_data` —
6//! no per-task stack allocation is needed.
7//!
8//! When all tasks are pending, the executor calls `port::arch::idle()` to enter
9//! a low-power state.
10
11use core::task::Poll;
12
13use crate::task::{iter_task_regs, TaskReg, TaskRegistry};
14use crate::waker;
15
16/// Identity of the task currently being polled, encoded as
17/// `(priority << 8) | index`. `u32::MAX` means "not currently polling a task".
18/// Set by the executor immediately before calling a task's `poll_fn` and
19/// read by sync primitives (`Semaphore`, `Channel`, `Sleep`) so they know
20/// which task to register as a waiter, without needing the user to pass
21/// priority/index manually.
22#[cfg(not(loom))]
23static CURRENT_TASK: crate::sync::atomic::AtomicU32 = crate::sync::atomic::AtomicU32::new(u32::MAX);
24#[cfg(loom)]
25loom::lazy_static! {
26    static ref CURRENT_TASK: crate::sync::atomic::AtomicU32 = crate::sync::atomic::AtomicU32::new(u32::MAX);
27}
28
29fn set_current(id: crate::task::TaskId) {
30    CURRENT_TASK.store(id.as_u16() as u32, crate::sync::atomic::Ordering::Release);
31}
32
33fn clear_current() {
34    CURRENT_TASK.store(u32::MAX, crate::sync::atomic::Ordering::Release);
35}
36
37/// Test-only: simulate being inside a task's poll, so `Semaphore::acquire()`,
38/// `Channel::send()/recv()`, and `Sleep` can be exercised directly from host
39/// tests without spinning up the full executor loop.
40#[doc(hidden)]
41pub fn set_current_for_test(priority: u8, index: u8) {
42    set_current(crate::task::TaskId::new(priority, index));
43}
44
45/// Test-only: clear the simulated task context set by [`set_current_for_test`].
46#[doc(hidden)]
47pub fn clear_current_for_test() {
48    clear_current();
49}
50
51/// The identity of the task currently being polled. `None` if called
52/// outside of a task's poll (e.g. from an ISR or before the executor starts).
53///
54/// Used by `Semaphore::acquire()`, `Channel::send()/recv()`, and `Sleep`
55/// to register themselves as waiters without requiring the caller to pass
56/// `(priority, index)` explicitly.
57pub fn current_task() -> Option<crate::task::TaskId> {
58    let v = CURRENT_TASK.load(crate::sync::atomic::Ordering::Acquire);
59    if v == u32::MAX {
60        None
61    } else {
62        Some(crate::task::TaskId::from_u16(v as u16))
63    }
64}
65
66/// Number of tasks not yet completed (plan.md [B10]): decremented the
67/// first time a task's poll returns `Ready`. Used to skip re-polling
68/// completed tasks and to know when the cooperative tier is idle.
69///
70/// A standalone static, not an `Executor` field: `EXECUTOR` below is a
71/// `static mut` with a `const fn` initializer (required — it has no other
72/// construction point), and loom's atomics are not const-constructible
73/// (same reason `Semaphore`/`Channel`/`Signal` split their `new()` by
74/// `#[cfg(loom)]`). Keeping this counter out of `Executor` lets
75/// `Executor::new()` stay `const` unconditionally instead of needing that
76/// same split plus a non-`static mut`-compatible loom variant of
77/// `EXECUTOR` itself.
78#[cfg(not(loom))]
79static LIVE_TASKS: crate::sync::atomic::AtomicUsize = crate::sync::atomic::AtomicUsize::new(0);
80#[cfg(loom)]
81loom::lazy_static! {
82    static ref LIVE_TASKS: crate::sync::atomic::AtomicUsize = crate::sync::atomic::AtomicUsize::new(0);
83}
84
85/// The global executor singleton.
86pub struct Executor {
87    registry: TaskRegistry,
88}
89
90impl Executor {
91    pub const fn new() -> Self {
92        Self {
93            registry: TaskRegistry::new(),
94        }
95    }
96
97    /// Discover tasks from the `.rivet_tasks` linker section.
98    /// Assigns per-priority indices. Must be called once before `run()`.
99    pub fn init(&mut self) {
100        let mut counts: [u8; 32] = [0; 32];
101
102        for reg in iter_task_regs() {
103            let prio = reg.priority as usize;
104            if prio > (crate::task::MAX_PRIORITY as usize) {
105                panic!(
106                    "rivet: task priority {} exceeds MAX_PRIORITY {} \
107                     (check #[rivet::task(priority = ...)])",
108                    reg.priority,
109                    crate::task::MAX_PRIORITY
110                );
111            }
112            let idx = counts[prio] as usize;
113            if idx >= crate::task::MAX_TASKS {
114                // plan.md [B12]: overflow must be loud, not a silent drop.
115                panic!(
116                    "rivet: too many #[rivet::task]s at priority {} \
117                     (limit MAX_TASKS = {} per priority)",
118                    reg.priority,
119                    crate::task::MAX_TASKS
120                );
121            }
122
123            self.registry.tasks[prio][idx] = Some(reg as *const TaskReg);
124            counts[prio] += 1;
125            self.registry.total += 1;
126            LIVE_TASKS.fetch_add(1, crate::sync::atomic::Ordering::Relaxed);
127        }
128
129        self.registry.count_per_priority[..=(crate::task::MAX_PRIORITY as usize)]
130            .copy_from_slice(&counts[..=(crate::task::MAX_PRIORITY as usize)]);
131
132        // Mark all tasks as initially ready so the executor polls them once
133        // to start their async state machines running.
134        for (p, &count) in counts[..=(crate::task::MAX_PRIORITY as usize)]
135            .iter()
136            .enumerate()
137        {
138            for i in 0..(count as usize) {
139                crate::waker::mark_ready(crate::task::TaskId::new(p as u8, i as u8));
140            }
141        }
142    }
143
144    /// Main executor loop. Never returns.
145    pub fn run(&self) -> ! {
146        loop {
147            waker::clear_pend();
148
149            // Poll all ready tasks, highest priority first.
150            while let Some(id) = waker::next_ready() {
151                let reg = match self.lookup_task(id.priority(), id.index()) {
152                    Some(r) => r,
153                    None => continue,
154                };
155
156                // Skip completed tasks woken by a stale registration
157                // (plan.md [B10]).
158                // SAFETY: `reg.completed_fn` was paired with this task's
159                // `TaskCell` by `#[rivet::task]`/`register_task!`.
160                unsafe {
161                    if (reg.completed_fn)(reg.user_data) {
162                        continue;
163                    }
164                }
165
166                let task_waker = waker::task_waker(id);
167
168                set_current(id);
169                // SAFETY: `reg.poll_fn` is a type-erased poll function
170                // paired with `reg.user_data` (a `TaskCell` pointer) at
171                // registration time by `#[rivet::task]`/`register_task!`.
172                // The executor only ever polls a task that was registered
173                // with a matching (poll_fn, user_data) pair, and never
174                // re-enters a task while it's being polled.
175                let result = unsafe { (reg.poll_fn)(reg.user_data, &task_waker) };
176                clear_current();
177
178                if result == Poll::Ready(()) {
179                    LIVE_TASKS.fetch_sub(1, crate::sync::atomic::Ordering::Relaxed);
180                }
181            }
182
183            // All tasks pending — sleep until an interrupt wakes us.
184            if !waker::has_pending() {
185                crate::port::arch::idle();
186            }
187        }
188    }
189
190    fn lookup_task(&self, priority: u8, index: u8) -> Option<&'static TaskReg> {
191        let p = priority as usize;
192        let i = index as usize;
193        if p > (crate::task::MAX_PRIORITY as usize) || i >= crate::task::MAX_TASKS {
194            return None;
195        }
196        // SAFETY: the pointer came from the linker-section walk in `init()`
197        // and points at a `static TaskReg` that lives for the program's
198        // lifetime; the registry is written once at boot and only read
199        // afterwards.
200        self.registry.tasks[p][i].map(|ptr| unsafe { &*ptr })
201    }
202}
203
204/// Test-only: reset the simulated-task-context global. Part of the global
205/// reset done by [`crate::kernel_test!`].
206#[cfg(feature = "test-support")]
207pub(crate) fn reset_for_test() {
208    clear_current();
209}
210
211impl Default for Executor {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217/// Global executor singleton.
218pub static mut EXECUTOR: Executor = Executor::new();