Skip to main content

bombay_engine/
driver.rs

1//! Production Driver using Machine Executor's `ExclusiveExecutor`.
2//!
3//! The [`Driver`] stores an [`ExclusiveExecutor`]`<BehaviorMachine<B>>` and
4//! delegates every production event through [`ExclusiveExecutor::turn`], which
5//! calls [`Machine::step`] (Transition) → [`Behavior::transition`] (Behavior)
6//! and returns the [`BehaviorActed`] output directly. Effect interpretation is
7//! async and happens after the turn, outside the poison boundary.
8//!
9//! # Execution flow
10//!
11//! ```text
12//! init: Behavior::init → BehaviorMachine → ExclusiveExecutor::new
13//! loop: env.next() → executor.turn(event) → BehaviorActed
14//!       → await Environment::interpret → loop
15//! ```
16//!
17//! # Poison boundary
18//!
19//! [`ExclusiveExecutor::turn`] installs a poisoned seat before calling
20//! [`Machine::step`]. A transition panic unwinds through the turn, leaves the
21//! seat poisoned, and the driver propagates the unwind (bombay classifies it
22//! as `TaskOutcome::Panicked`). If an outer caller catches that unwind and
23//! reuses the public Driver, `run_loop` detects poison before polling the
24//! environment, retires it, and returns [`RunError::Poisoned`].
25//!
26//! # Inversion guarantee (type-level, not runtime)
27//!
28//! `Machine::step` consumes `self` (affine). The machine is owned by
29//! [`ExclusiveExecutor`], which exposes no way to step it other than
30//! [`ExclusiveExecutor::turn`]. The type system therefore makes `turn` the
31//! only transition path; a runtime test cannot detect a bypass the compiler
32//! already rejects.
33
34use behavior::{Actions, Address, Behavior, BirthMode, Exit, Never, SendAlgebra, Step};
35use bombay_machine_executor::{ExclusiveExecutor, ExclusiveState};
36
37use crate::{BehaviorMachine, Environment, RunError, RunExit};
38
39/// One behavior transition with `become` removed.
40pub struct RuntimeEffects<A: Address, Sends, Birth: BirthMode> {
41    pub sends: Sends,
42    pub creates: Vec<behavior::Create<A, Birth::Child>>,
43}
44
45async fn interpret<A, Sends, Birth, E>(
46    actions: Actions<A, Never, Sends, Birth>,
47    environment: &mut E,
48) -> Result<Option<Exit<A>>, E::Error>
49where
50    A: Address,
51    Sends: SendAlgebra,
52    Birth: BirthMode,
53    E: Environment<Effect = RuntimeEffects<A, Sends, Birth>>,
54{
55    let Actions {
56        sends,
57        creates,
58        become_,
59    } = actions;
60    environment
61        .interpret(RuntimeEffects { sends, creates })
62        .await?;
63    Ok(match become_ {
64        Step::Continue => None,
65        Step::Goto(never) => match never {},
66        Step::Stop(exit) => Some(exit),
67    })
68}
69
70/// Typestate for the driver lifecycle — no `Option` plus `expect`.
71enum State<B: Behavior> {
72    Uninitialized(B),
73    Running(ExclusiveExecutor<BehaviorMachine<B>>),
74    Terminated,
75    Retired,
76}
77
78/// Application core driving one behavior through one runtime port.
79///
80/// Uses [`ExclusiveExecutor`] for allocation-free exclusive turns. Every event
81/// goes through `turn`; effect interpretation is async, outside the executor.
82///
83/// # Compile-time bound
84///
85/// The `B: Behavior` bound rejects a payload type that does not implement
86/// [`Behavior`]. This is a narrow static bound, not a proof that every
87/// invalid composition is unrepresentable:
88///
89/// ```compile_fail
90/// use bombay_engine::Driver;
91/// // `u32` does not implement `Behavior`; this must not compile.
92/// let _: Driver<u32, ()> = Driver::new(42u32, ());
93/// ```
94pub struct Driver<B: Behavior, E> {
95    state: State<B>,
96    environment: E,
97}
98
99impl<B, E> Driver<B, E>
100where
101    B: Behavior,
102{
103    pub fn new(behavior: B, environment: E) -> Self {
104        Self {
105            state: State::Uninitialized(behavior),
106            environment,
107        }
108    }
109}
110
111impl<B, E> Driver<B, E>
112where
113    B: Behavior<Ph = Never> + Send,
114    B::Event: Send,
115    E: Environment<Event = B::Event, Effect = RuntimeEffects<B::Addr, B::Sends, B::Birth>>,
116{
117    /// Run initialization, construct the executor, interpret init effects.
118    ///
119    /// Transitions `Uninitialized → Running` for a continuing behavior or
120    /// `Uninitialized → Terminated` for terminal initialization.
121    /// Returns the terminal exit in the latter case.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`RunError::Behavior`] when [`Behavior::init`] fails.
126    /// Returns [`RunError::Environment`] when the environment rejects
127    /// an initialization effect.
128    ///
129    /// # Panics
130    ///
131    /// Panics if called on a driver that is not in the `Uninitialized` state.
132    pub async fn run_init(
133        &mut self,
134    ) -> Result<Option<Exit<B::Addr>>, RunError<B::Error, E::Error>> {
135        let State::Uninitialized(behavior) = std::mem::replace(&mut self.state, State::Retired)
136        else {
137            panic!("run_init called on non-uninitialized driver");
138        };
139
140        let mut machine = BehaviorMachine::for_runtime(behavior);
141        let initial = machine.behavior_mut().init().map_err(RunError::Behavior)?;
142
143        let exit = interpret(initial, &mut self.environment)
144            .await
145            .map_err(RunError::Environment)?;
146
147        self.state = if exit.is_some() {
148            State::Terminated
149        } else {
150            State::Running(ExclusiveExecutor::new(machine))
151        };
152        Ok(exit)
153    }
154
155    /// Run the event loop. Every event goes through
156    /// [`ExclusiveExecutor::turn`].
157    ///
158    /// # Errors
159    ///
160    /// Returns [`RunError::Behavior`] when a [`Behavior::transition`] fails.
161    /// Returns [`RunError::Environment`] when the environment rejects an
162    /// effect.
163    ///
164    /// # Panics
165    ///
166    /// Panics if the driver is not in the `Running` state. A transition panic
167    /// unwinds through this method (leaving the executor poisoned).
168    pub async fn run_loop(
169        &mut self,
170    ) -> Result<RunExit<Exit<B::Addr>>, RunError<B::Error, E::Error>> {
171        loop {
172            // A caller can retain and reuse the Driver after catching a panic
173            // from a previously-polled run_loop future. Detect that terminal
174            // executor state before pulling another event from the environment;
175            // otherwise the intact PoisonedInput returned by turn would merely
176            // be consumed and discarded by this adapter.
177            let poisoned = match &self.state {
178                State::Running(executor) => executor.state() == ExclusiveState::Poisoned,
179                _ => panic!("run_loop on non-running driver"),
180            };
181            if poisoned {
182                self.environment.retire().await;
183                self.state = State::Retired;
184                return Err(RunError::Poisoned);
185            }
186
187            let Some(event) = self.environment.next().await else {
188                self.state = State::Terminated;
189                return Ok(RunExit::EnvironmentClosed);
190            };
191
192            let output = match &mut self.state {
193                State::Running(executor) => executor.turn(event),
194                _ => panic!("run_loop on non-running driver"),
195            };
196
197            // The pre-check above handles retained poison. This arm remains a
198            // defensive boundary in case the executor contract grows another
199            // safe way for turn to reject an input.
200            let actions = match output {
201                Ok(Ok(actions)) => actions,
202                Ok(Err(error)) => {
203                    self.state = State::Terminated;
204                    return Err(RunError::Behavior(error));
205                }
206                Err(_poisoned) => {
207                    self.environment.retire().await;
208                    self.state = State::Retired;
209                    return Err(RunError::Poisoned);
210                }
211            };
212
213            match interpret(actions, &mut self.environment).await {
214                Ok(Some(exit)) => {
215                    self.state = State::Terminated;
216                    return Ok(RunExit::Stopped(exit));
217                }
218                Ok(None) => {}
219                Err(error) => {
220                    self.state = State::Terminated;
221                    return Err(RunError::Environment(error));
222                }
223            }
224        }
225    }
226
227    /// Retire the environment.
228    pub async fn retire(&mut self) {
229        self.environment.retire().await;
230        self.state = State::Retired;
231    }
232
233    /// Run init, then event loop, then retire.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`RunError::Behavior`] when the behavior fails during
238    /// initialization or a transition. Returns [`RunError::Environment`]
239    /// when the environment rejects an effect.
240    pub async fn run(&mut self) -> Result<RunExit<Exit<B::Addr>>, RunError<B::Error, E::Error>> {
241        let result = async {
242            if let Some(exit) = self.run_init().await? {
243                return Ok(RunExit::Stopped(exit));
244            }
245            self.run_loop().await
246        }
247        .await;
248        self.environment.retire().await;
249        self.state = State::Retired;
250        result
251    }
252}