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}