must/runtime/mod.rs
1//! Processes as stackless coroutines that implement [`Program`] by replay. A [`System`]
2//! registers process bodies as re-runnable factories; computing `next(traces)` re-creates
3//! each body and polls it once, letting the trace replay the events already in the graph
4//! and stopping at the first new one.
5//!
6//! This module knows nothing about execution graphs, consistency or the explorer - it
7//! depends only on `event` and `program`.
8//!
9//! ## Contract and honest limitations
10//!
11//! A process body must be a pure function of the values it receives, and cooperatively
12//! bounded:
13//! * A pure CPU loop with no API calls (e.g. `loop {}`) cannot be interrupted: the event
14//! budget only bounds bodies that reach `send`/`recv`/`assert_that`. A synchronous loop
15//! that never awaits never yields control, so bounding it is the caller's job (unroll it,
16//! or add a step limit in the body).
17//! * Signal assertion failures with [`Ctx::assert_that`] (which emits `Label::Error`),
18//! never `panic!` - a `panic!` inside a body crashes the checker.
19//! * A body may only `.await` the futures returned by [`Ctx::recv`], [`Ctx::recv_timeout`]
20//! and [`Ctx::nondet`]. Awaiting any other future is reported as a `Label::Error`.
21
22mod replay;
23
24use std::cell::RefCell;
25use std::future::Future;
26use std::pin::Pin;
27use std::rc::Rc;
28
29use crate::event::{Model, Tid, Val};
30use crate::program::{Program, ThreadNext};
31
32use replay::{run_once, ThreadCell};
33
34/// The boxed, pinned future produced by a process body - polled with a no-op waker.
35pub type LocalFut = Pin<Box<dyn Future<Output = ()>>>;
36
37/// A receive predicate closure, boxed so it can be stored and later moved into a
38/// `Label::Recv`. `Send + Sync` so the resulting `Pred`/`Label`/`ExecutionGraph` can
39/// cross threads (see [`crate::event::Pred`]).
40pub(crate) type BoxedPred = Box<dyn Fn(&str) -> bool + Send + Sync>;
41
42/// Default per-thread event budget: a body emitting more events than this yields
43/// `Label::Error` rather than diverging.
44pub const DEFAULT_MAX_EVENTS: usize = 10_000;
45
46/// A system of processes. Each process is stored as a factory `Fn(Ctx) -> LocalFut` so
47/// it can be re-executed from scratch on every replay; the checker never keeps a
48/// suspended coroutine around.
49pub struct System {
50 factories: Vec<Box<dyn Fn(Ctx) -> LocalFut>>,
51 max_events: usize,
52}
53
54impl Default for System {
55 fn default() -> Self {
56 System::new()
57 }
58}
59
60impl System {
61 pub fn new() -> Self {
62 System {
63 factories: Vec::new(),
64 max_events: DEFAULT_MAX_EVENTS,
65 }
66 }
67
68 /// Set the per-thread event budget (see [`DEFAULT_MAX_EVENTS`]).
69 pub fn with_max_events(mut self, max_events: usize) -> Self {
70 self.max_events = max_events;
71 self
72 }
73
74 /// Register a process body; returns its thread id (`0`-based, in registration
75 /// order). `body` is a `Fn` (not `FnOnce`) because it is re-run on every replay.
76 pub fn add<F, Fut>(&mut self, body: F) -> Tid
77 where
78 F: Fn(Ctx) -> Fut + 'static,
79 Fut: Future<Output = ()> + 'static,
80 {
81 let tid = self.factories.len();
82 self.factories
83 .push(Box::new(move |ctx| Box::pin(body(ctx)) as LocalFut));
84 tid
85 }
86
87 /// Replay thread `tid` against `trace` and read off its next event.
88 fn run_thread(&self, tid: Tid, trace: Vec<Option<Val>>) -> ThreadNext {
89 let cell = Rc::new(RefCell::new(ThreadCell::new(trace, self.max_events)));
90 let ctx = Ctx {
91 tid,
92 cell: cell.clone(),
93 };
94 let fut = (self.factories[tid])(ctx);
95 run_once(fut, &cell)
96 }
97}
98
99impl Program for System {
100 fn num_threads(&self) -> usize {
101 self.factories.len()
102 }
103
104 fn next(&self, traces: &[Vec<Option<Val>>]) -> Vec<ThreadNext> {
105 debug_assert_eq!(
106 traces.len(),
107 self.num_threads(),
108 "expected one trace per thread"
109 );
110 (0..self.factories.len())
111 .map(|tid| {
112 let trace = traces.get(tid).cloned().unwrap_or_default();
113 self.run_thread(tid, trace)
114 })
115 .collect()
116 }
117
118 /// Per-thread replay: re-run only thread `tid`'s body, not all of them.
119 fn next_thread(&self, tid: Tid, trace: &[Option<Val>]) -> ThreadNext {
120 self.run_thread(tid, trace.to_vec())
121 }
122}
123
124/// What a process body sees. Its methods drive the per-thread replay of the trace; the
125/// body itself must be a pure function of the values it receives.
126pub struct Ctx {
127 tid: Tid,
128 cell: Rc<RefCell<ThreadCell>>,
129}
130
131impl Ctx {
132 /// This process's thread id.
133 pub fn tid(&self) -> Tid {
134 self.tid
135 }
136
137 /// Send `msg` to thread `to` under communication model `model` (fire-and-forget).
138 /// Synchronous: emits a `Label::Send`. Sending to oneself (`to == self.tid()`) is
139 /// allowed.
140 pub fn send(&self, to: Tid, msg: impl Into<Val>, model: Model) {
141 self.cell.borrow_mut().record_send(to, msg.into(), model);
142 }
143
144 /// Blocking selective receive: awaits a message satisfying `pred` and returns it.
145 /// During replay it resolves to the value the graph already gave this receive; if
146 /// this is the thread's next event it parks, emitting `Label::Recv`.
147 pub fn recv(&self, pred: impl Fn(&str) -> bool + Send + Sync + 'static) -> RecvFuture {
148 RecvFuture {
149 cell: self.cell.clone(),
150 pred_fn: Some(Box::new(pred)),
151 }
152 }
153
154 /// Non-blocking selective receive: awaits a message satisfying `pred` and returns
155 /// `Some(v)`, or `None` when the timeout fires (reading no message). When it parks it
156 /// emits `Label::recv_nb`.
157 ///
158 /// In the DPOR, "no message" is an extra rf source on top of every consistent send,
159 /// and - unlike a send - may be read by several non-blocking receives at once. This is
160 /// deliberately not a nondet-encoded timeout: N threads each doing one `recv_timeout`
161 /// explore exactly one execution, not `2^N`.
162 pub fn recv_timeout(
163 &self,
164 pred: impl Fn(&str) -> bool + Send + Sync + 'static,
165 ) -> RecvTimeoutFuture {
166 RecvTimeoutFuture {
167 cell: self.cell.clone(),
168 pred_fn: Some(Box::new(pred)),
169 }
170 }
171
172 /// Data non-determinism: returns some value of the finite option set `set`. The search
173 /// enumerates every value of `set`; during replay it resolves to the value already
174 /// committed for this choice, or — when this is the thread's next event — parks and emits
175 /// `Label::nondet(set)`. `set` must be non-empty.
176 pub fn nondet(&self, set: impl IntoIterator<Item = impl Into<Val>>) -> NondetFuture {
177 NondetFuture {
178 cell: self.cell.clone(),
179 set: Some(set.into_iter().map(Into::into).collect()),
180 }
181 }
182
183 /// Assertion: if `cond` is false, emit `Label::Error` as this thread's next event. A
184 /// holding assertion is pure control flow.
185 pub fn assert_that(&self, cond: bool, msg: &str) {
186 self.cell.borrow_mut().record_assert(cond, msg);
187 }
188}
189
190/// Future returned by [`Ctx::recv`]; resolves to the received message.
191pub struct RecvFuture {
192 cell: Rc<RefCell<ThreadCell>>,
193 /// Predicate closure, moved into the emitted `Label::Recv` when the receive parks.
194 pred_fn: Option<BoxedPred>,
195}
196
197impl Future for RecvFuture {
198 type Output = String;
199
200 fn poll(self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<String> {
201 use std::task::Poll;
202 // RecvFuture is Unpin (Rc + Option<Box>), so projecting the fields is safe.
203 let this = self.get_mut();
204 let mut cell = this.cell.borrow_mut();
205 // The graph stores payloads as interned `Sym`s; the process body works in `String`,
206 // so resolve at the await boundary.
207 match cell.poll_recv(&mut this.pred_fn, true) {
208 Poll::Ready(Some(v)) => Poll::Ready(crate::intern::resolve(v).to_owned()),
209 // A blocking receive never resolves to `None`: on a committed no-message read
210 // `poll_recv` blocks the thread and returns `Pending` instead of `Ready(None)`.
211 Poll::Ready(None) => unreachable!("blocking receive resolved to nothing"),
212 Poll::Pending => Poll::Pending,
213 }
214 }
215}
216
217/// Future returned by [`Ctx::recv_timeout`]; resolves to `Some(v)` for a received message
218/// or `None` when the timeout fires (no message). Unlike [`RecvFuture`], a committed
219/// no-message read resolves the future rather than blocking the thread.
220pub struct RecvTimeoutFuture {
221 cell: Rc<RefCell<ThreadCell>>,
222 /// Predicate closure, moved into the emitted `Label::recv_nb` when the receive parks.
223 pred_fn: Option<BoxedPred>,
224}
225
226impl Future for RecvTimeoutFuture {
227 type Output = Option<String>;
228
229 fn poll(
230 self: Pin<&mut Self>,
231 _cx: &mut std::task::Context<'_>,
232 ) -> std::task::Poll<Option<String>> {
233 // RecvTimeoutFuture is Unpin (Rc + Option<Box>), so projecting the fields is safe.
234 let this = self.get_mut();
235 let mut cell = this.cell.borrow_mut();
236 // Resolve the interned payload to a `String` for the body (see `RecvFuture::poll`).
237 match cell.poll_recv(&mut this.pred_fn, false) {
238 std::task::Poll::Ready(opt) => {
239 std::task::Poll::Ready(opt.map(|v| crate::intern::resolve(v).to_owned()))
240 }
241 std::task::Poll::Pending => std::task::Poll::Pending,
242 }
243 }
244}
245
246/// Future returned by [`Ctx::nondet`]; resolves to the chosen nondet value. Like
247/// [`RecvFuture`] it is a real `Future` so replay can park at the choice point: a nondet
248/// value already committed in the trace resolves immediately, otherwise the choice parks
249/// and emits `Label::nondet(set)`. A nondet event never resolves to nothing.
250pub struct NondetFuture {
251 cell: Rc<RefCell<ThreadCell>>,
252 /// Option set, moved into the emitted `Label::nondet` when the choice parks.
253 set: Option<Vec<Val>>,
254}
255
256impl Future for NondetFuture {
257 type Output = String;
258
259 fn poll(self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<String> {
260 // NondetFuture is Unpin (Rc + Option<Vec>), so projecting the fields is safe.
261 let this = self.get_mut();
262 let mut cell = this.cell.borrow_mut();
263 // Resolve the interned choice to a `String` for the body (see `RecvFuture::poll`).
264 match cell.poll_nondet(&mut this.set) {
265 std::task::Poll::Ready(v) => {
266 std::task::Poll::Ready(crate::intern::resolve(v).to_owned())
267 }
268 std::task::Poll::Pending => std::task::Poll::Pending,
269 }
270 }
271}