mod executor;
mod object;
mod process;
use std::{
error::Error,
fmt::{self, Debug},
};
pub use object::{Object, ObjectID, Transition};
pub use process::{ProcessID, ProcessResult};
pub(crate) use executor::{Executor, pid};
use crate::sync::{Atomic, ChannelHandle, Receiver, Sender};
pub struct World<'a> {
objects: Vec<Box<dyn Object>>,
exec: Executor<'a>,
process_names: Vec<String>,
object_names: Vec<String>,
}
impl<'a> World<'a> {
pub(crate) fn new() -> Self {
Self {
objects: Vec::new(),
exec: Executor::default(),
process_names: Vec::new(),
object_names: Vec::new(),
}
}
pub fn spawn(
&mut self,
name: impl Into<String>,
code: impl Future<Output = ProcessResult> + 'a,
) {
let id = self.exec.schedule(code);
debug_assert_eq!(id, self.process_names.len(), "pid must match name index");
self.process_names.push(name.into());
}
#[must_use = "the returned handle must be given to a process; an object no process holds is never operated on"]
pub fn atomic<T: Copy + PartialEq + Debug + 'static>(
&mut self,
name: impl Into<String>,
value: T,
) -> Atomic<T> {
self.register(name, |id| Atomic::new(id, value))
}
#[must_use = "the returned sender/receiver must be given to processes; an unused channel is never operated on"]
pub fn channel<T: Debug + 'static>(
&mut self,
name: impl Into<String>,
) -> (Sender<T>, Receiver<T>) {
let driver = self.register(name, ChannelHandle::new);
driver.split()
}
#[must_use = "the returned handle must be given to a process; an object no process holds is never operated on"]
pub fn register<O>(&mut self, name: impl Into<String>, build: impl FnOnce(ObjectID) -> O) -> O
where
O: Object + Clone + 'static,
{
let id = self.objects.len();
let handle = build(id);
self.objects.push(Box::new(handle.clone()));
self.object_names.push(name.into());
handle
}
pub fn process(&self, t: Transition) -> &str {
&self.process_names[t.pid]
}
pub fn object(&self, t: Transition) -> &str {
&self.object_names[t.oid]
}
pub fn label(&self, t: Transition) -> String {
self.objects[t.oid].label(t)
}
pub fn processes(&self) -> &[String] {
&self.process_names
}
pub fn objects(&self) -> &[String] {
&self.object_names
}
fn run(&mut self) -> Option<FailureReason> {
self.exec.execute().err().map(|raw| {
FailureReason::Process(ProcessError {
process: self.process_names[raw.pid].clone(),
source: raw.source,
})
})
}
}
impl Debug for World<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("World")
.field("processes", &self.process_names)
.field("objects", &self.object_names)
.finish_non_exhaustive()
}
}
#[derive(Debug, thiserror::Error)]
#[error("process {process} failed: {source}")]
pub struct ProcessError {
process: String,
source: Box<dyn Error + Send + Sync>,
}
impl ProcessError {
pub fn process(&self) -> &str {
&self.process
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FailureReason {
#[error("{0}")]
Process(#[source] ProcessError),
#[error("deadlock")]
Deadlock,
}
pub struct State<'a> {
world: World<'a>,
setup: &'a dyn Fn(&mut World<'a>),
trace: Vec<Transition>,
failure: Option<FailureReason>,
enabled: Vec<Transition>,
non_blocking: bool,
}
impl Debug for State<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("State")
.field("trace", &self.trace)
.field("failure", &self.failure)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub(crate) struct StateView<'a> {
setup: &'a dyn Fn(&mut World<'a>),
trace: Vec<Transition>,
}
impl<'a> StateView<'a> {
pub(crate) fn new(setup: &'a dyn Fn(&mut World<'a>)) -> Self {
Self {
setup,
trace: Vec::new(),
}
}
pub(crate) fn trace(&self) -> &[Transition] {
&self.trace
}
pub(crate) fn replay(&self, trace: &[Transition]) -> State<'a> {
let mut state = State::new(self.setup);
for &t in trace {
debug_assert!(state.failure.is_none(), "replay diverged: early failure");
debug_assert!(
state.enabled().contains(&t),
"replay diverged at {t:?}: model is non-deterministic"
);
state.apply(t);
}
state
}
pub(crate) fn state(&self) -> State<'a> {
self.replay(&self.trace)
}
}
impl<'a> State<'a> {
fn new(setup: &'a dyn Fn(&mut World<'a>)) -> Self {
let mut world = World::new();
setup(&mut world);
let failure = world.run();
let non_blocking = world.objects.iter().all(|o| !o.may_block());
let mut state = Self {
world,
setup,
trace: Vec::new(),
failure,
enabled: Vec::new(),
non_blocking,
};
state.recompute_enabled();
state.settle();
state
}
pub(crate) fn non_blocking(&self) -> bool {
self.non_blocking
}
pub fn world(&self) -> &World<'a> {
&self.world
}
pub fn trace(&self) -> &[Transition] {
&self.trace
}
pub fn failure_reason(&self) -> Option<&FailureReason> {
self.failure.as_ref()
}
pub fn is_terminal(&self) -> bool {
self.failure.is_some() || self.enabled.is_empty()
}
pub(crate) fn is_failed(&self) -> bool {
self.failure.is_some()
}
pub(crate) fn pending(&self) -> usize {
self.world.exec.pending()
}
pub(crate) fn enabled(&self) -> &[Transition] {
&self.enabled
}
fn recompute_enabled(&mut self) {
let mut buf = std::mem::take(&mut self.enabled);
buf.clear();
for o in &self.world.objects {
o.enabled_into(&mut buf);
}
self.enabled = buf;
}
pub fn depends(&self, t1: Transition, t2: Transition) -> bool {
t1.oid == t2.oid && self.world.objects[t1.oid].depends(t1, t2)
}
pub(crate) fn apply(&mut self, t: Transition) {
debug_assert!(self.failure.is_none(), "apply on an already-failed state");
self.trace.push(t);
self.world.objects[t.oid].apply(t);
self.failure = self.world.run();
self.recompute_enabled();
self.settle();
}
fn settle(&mut self) {
if self.failure.is_none() && self.enabled.is_empty() && self.pending() > 0 {
self.failure = Some(FailureReason::Deadlock);
}
}
pub(crate) fn view(&self) -> StateView<'a> {
StateView {
setup: self.setup,
trace: self.trace.clone(),
}
}
#[cfg(test)]
pub(crate) fn fork(&self) -> Self {
self.view().state()
}
pub(crate) fn into_failure(self) -> (FailureReason, StateView<'a>) {
let reason = self.failure.expect("into_failure on a healthy state");
let view = StateView {
setup: self.setup,
trace: self.trace,
};
(reason, view)
}
}
#[cfg(test)]
mod tests {
use super::{FailureReason, State, Transition, World};
const WRITER: usize = 0;
const READER: usize = 1;
fn program(world: &mut World) {
let atomic = world.atomic("x", 123);
let writer = atomic.clone();
let reader = atomic;
world.spawn("writer", async move {
writer.store(222).await;
Ok(())
});
world.spawn("reader", async move {
if reader.load().await == 222 {
Ok(())
} else {
Err("unexpected value".into())
}
});
}
fn enabled_op(state: &State, pid: usize) -> Transition {
*state.enabled().iter().find(|t| t.pid == pid).unwrap()
}
#[test]
fn store_then_load() {
let mut state = State::new(&program);
state.apply(enabled_op(&state, WRITER));
state.apply(enabled_op(&state, READER));
assert!(state.enabled().is_empty());
assert!(state.failure_reason().is_none());
}
#[test]
fn load_then_store_records_failure() {
let mut state = State::new(&program);
let load = enabled_op(&state, READER);
state.apply(load);
assert!(matches!(
state.failure_reason(),
Some(FailureReason::Process(_))
));
assert_eq!(state.world().label(load), "load -> 123");
}
}