bubbles/runtime/runner/mod.rs
1//! [`Runner`] - the public entry point for executing a compiled [`Program`].
2
3pub(super) mod evaluation;
4pub(super) mod execute;
5pub(super) mod node_body;
6
7use std::borrow::Cow;
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::sync::Arc;
10
11use crate::compiler::Program;
12use crate::compiler::ast::StmtList;
13use crate::error::{DialogueError, Result};
14use crate::library::FunctionLibrary;
15use crate::runtime::event::DialogueEvent;
16use crate::runtime::provider::{LineProvider, PassthroughProvider};
17use crate::saliency::{FirstAvailable, SaliencyStrategy};
18use crate::value::{Value, VariableStorage};
19
20/// Where the [`Runner`] is in its `start` / `next_event` / [`Runner::select_option`] protocol.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum RunnerPhase {
23 /// No dialogue running; call [`Runner::start`].
24 Idle,
25 /// Advancing lines and statements; call [`Runner::next_event`].
26 Running,
27 /// The last event was [`DialogueEvent::Options`]; call [`Runner::select_option`] before
28 /// [`Runner::next_event`].
29 AwaitingOption,
30 /// The current node finished; the stream is finished until the next [`Runner::start`].
31 Done,
32}
33
34/// Execution state of the runner.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(super) enum State {
37 Idle,
38 Running,
39 AwaitingOption,
40 Done,
41}
42
43/// A frame on the call stack.
44///
45/// Frames reference a shared [`StmtList`] and advance a program counter.
46/// No statements are cloned when a frame is pushed - only the `Arc` is
47/// bumped - so control-flow constructs (`<<if>>`, `<<once>>`, options,
48/// detours, jumps) are O(1) regardless of body size.
49#[derive(Debug, Clone)]
50pub(super) struct Frame {
51 pub(super) node: Arc<str>,
52 pub(super) body: StmtList,
53 pub(super) ip: usize,
54}
55
56impl Frame {
57 pub(super) const fn new(node: Arc<str>, body: StmtList) -> Self {
58 Self { node, body, ip: 0 }
59 }
60}
61
62/// Option bodies held during `AwaitingOption` state.
63type OptionBodies = Vec<StmtList>;
64
65/// Drives execution of a compiled [`Program`], yielding [`DialogueEvent`]s one at a time.
66///
67/// # Pull model
68/// The host calls [`Runner::next_event`] in a loop until it returns `Ok(None)` (dialogue
69/// ended) or until it receives a [`DialogueEvent::Options`], at which point it must call
70/// [`Runner::select_option`] before continuing.
71pub struct Runner<S: VariableStorage> {
72 pub(super) program: Program,
73 pub(super) storage: S,
74 pub(super) state: State,
75 pub(super) stack: Vec<Frame>,
76 pub(super) pending: VecDeque<DialogueEvent>,
77 pub(super) option_bodies: OptionBodies,
78 pub(super) library: FunctionLibrary,
79 pub(super) visits: HashMap<String, u32>,
80 pub(super) once_seen: HashSet<String>,
81 pub(super) saliency: Box<dyn SaliencyStrategy>,
82 pub(super) provider: Box<dyn LineProvider>,
83}
84
85impl<S: VariableStorage> Runner<S> {
86 fn clear_event_queues(&mut self) {
87 self.pending.clear();
88 self.option_bodies.clear();
89 }
90
91 /// Returns read-only access to the compiled program (node titles, declarations, etc.).
92 #[must_use]
93 pub const fn program(&self) -> &Program {
94 &self.program
95 }
96
97 /// Returns the runner’s current phase for UI or protocol guards.
98 #[must_use]
99 pub const fn phase(&self) -> RunnerPhase {
100 match self.state {
101 State::Idle => RunnerPhase::Idle,
102 State::Running => RunnerPhase::Running,
103 State::AwaitingOption => RunnerPhase::AwaitingOption,
104 State::Done => RunnerPhase::Done,
105 }
106 }
107
108 /// Creates a new runner for the given program and variable storage.
109 #[must_use]
110 pub fn new(program: Program, storage: S) -> Self {
111 Self {
112 program,
113 storage,
114 state: State::Idle,
115 stack: Vec::new(),
116 pending: VecDeque::new(),
117 option_bodies: Vec::new(),
118 library: FunctionLibrary::new(),
119 visits: HashMap::new(),
120 once_seen: HashSet::new(),
121 saliency: Box::new(FirstAvailable),
122 provider: Box::new(PassthroughProvider),
123 }
124 }
125
126 /// Starts execution at the given node.
127 ///
128 /// Clears any queued events and abandons an in-flight choice so a new conversation
129 /// cannot inherit stale [`DialogueEvent::DialogueComplete`] or option state from a
130 /// prior [`Runner::start`].
131 ///
132 /// # Errors
133 /// Returns [`DialogueError::UnknownNode`] if the title does not exist in the program.
134 pub fn start(&mut self, node: &str) -> Result<()> {
135 if !self.program.node_exists(node) {
136 return Err(DialogueError::UnknownNode(node.to_owned()));
137 }
138 let body = self.pick_node_body(node)?;
139 self.clear_event_queues();
140 let title: Arc<str> = Arc::from(node);
141 self.stack.clear();
142 self.stack.push(Frame::new(title, body));
143 self.state = State::Running;
144 *self.visits.entry(node.to_owned()).or_insert(0) += 1;
145 self.pending
146 .push_back(DialogueEvent::NodeStarted(node.to_owned()));
147 Ok(())
148 }
149
150 /// Returns the next event, or `Ok(None)` when dialogue is finished.
151 ///
152 /// # Errors
153 /// Returns a [`DialogueError`] on runtime failures.
154 pub fn next_event(&mut self) -> Result<Option<DialogueEvent>> {
155 if let Some(ev) = self.pending.pop_front() {
156 return Ok(Some(ev));
157 }
158 match self.state {
159 State::Idle | State::Done => Ok(None),
160 State::AwaitingOption => Err(DialogueError::ProtocolViolation(
161 "call select_option() before next_event()".into(),
162 )),
163 State::Running => loop {
164 if let Some(ev) = self.pending.pop_front() {
165 return Ok(Some(ev));
166 }
167 if self.state != State::Running {
168 return Ok(None);
169 }
170 if let Some(ev) = self.step()? {
171 return Ok(Some(ev));
172 }
173 },
174 }
175 }
176
177 /// Selects an option by index after receiving [`DialogueEvent::Options`].
178 ///
179 /// # Errors
180 ///
181 /// Returns [`DialogueError::ProtocolViolation`] if called when not awaiting an option
182 /// selection, or if `index` is out of range.
183 pub fn select_option(&mut self, index: usize) -> Result<()> {
184 if self.state != State::AwaitingOption {
185 return Err(DialogueError::ProtocolViolation(
186 "select_option() called when not awaiting an option".into(),
187 ));
188 }
189 let body = self.option_bodies.get(index).cloned().ok_or_else(|| {
190 DialogueError::ProtocolViolation(format!(
191 "option index {index} out of range ({})",
192 self.option_bodies.len()
193 ))
194 })?;
195 self.option_bodies.clear();
196 self.state = State::Running;
197 self.push_inline_frame(body);
198 Ok(())
199 }
200
201 /// Pushes `body` as a new frame on top of the stack, inheriting the
202 /// current frame's node title. No-op when `body` is empty.
203 ///
204 /// Used by `<<if>>`, `<<once>>`, and option-body execution.
205 pub(super) fn push_inline_frame(&mut self, body: StmtList) {
206 if body.is_empty() {
207 return;
208 }
209 let title = self
210 .stack
211 .last()
212 .map_or_else(|| Arc::from(""), |f| Arc::clone(&f.node));
213 self.stack.push(Frame::new(title, body));
214 }
215
216 /// Pushes a frame whose node title matches the supplied owned string.
217 ///
218 /// Used by `<<jump>>` / `<<detour>>` / `start()` - sites that already
219 /// know the target node title and want to install it as the top-of-stack
220 /// frame in one call.
221 pub(super) fn push_node_frame(&mut self, node: &str, body: StmtList) {
222 self.stack.push(Frame::new(Arc::from(node), body));
223 }
224
225 /// Returns a shared reference to the variable storage.
226 #[must_use]
227 pub const fn storage(&self) -> &S {
228 &self.storage
229 }
230
231 /// Returns a mutable reference to the variable storage.
232 pub const fn storage_mut(&mut self) -> &mut S {
233 &mut self.storage
234 }
235
236 /// Returns all variables known to storage (see [`VariableStorage::all_variables`]).
237 ///
238 /// For [`HashMapStorage`](crate::HashMapStorage) this is every key/value pair.
239 /// Custom stores return whatever their [`VariableStorage::all_variables`]
240 /// implementation provides (often empty unless overridden).
241 #[must_use]
242 pub fn all_variables(&self) -> Vec<(String, Value)> {
243 self.storage.all_variables()
244 }
245
246 /// Returns a clone of the value for `name`, or `None` if unset.
247 #[must_use]
248 pub fn variable(&self, name: &str) -> Option<Value> {
249 self.storage.get(name)
250 }
251
252 /// Borrows the value for `name` when the storage can lend it without cloning.
253 #[must_use]
254 pub fn variable_ref(&self, name: &str) -> Option<Cow<'_, Value>> {
255 self.storage.get_ref(name)
256 }
257
258 /// Returns a mutable reference to the function library (for registering host functions).
259 pub const fn library_mut(&mut self) -> &mut FunctionLibrary {
260 &mut self.library
261 }
262
263 /// Replaces the saliency strategy used for line and node group selection.
264 pub fn set_saliency(&mut self, strategy: impl SaliencyStrategy) {
265 self.saliency = Box::new(strategy);
266 }
267
268 /// Sets the line provider used for localisation lookup.
269 pub fn set_provider(&mut self, provider: impl LineProvider) {
270 self.provider = Box::new(provider);
271 }
272
273 /// Replaces the saliency strategy from an already-boxed value.
274 ///
275 /// Used by [`crate::runtime::RunnerBuilder`] to avoid double-boxing.
276 pub(crate) fn set_saliency_box(&mut self, strategy: Box<dyn SaliencyStrategy>) {
277 self.saliency = strategy;
278 }
279
280 /// Sets the line provider from an already-boxed value.
281 ///
282 /// Used by [`crate::runtime::RunnerBuilder`] to avoid double-boxing.
283 pub(crate) fn set_provider_box(&mut self, provider: Box<dyn LineProvider>) {
284 self.provider = provider;
285 }
286
287 // ── save / load ───────────────────────────────────────────────────────────
288
289 /// Captures the current session state into a [`RunnerSnapshot`](crate::RunnerSnapshot).
290 ///
291 /// The snapshot records the active node title, visit counts, and the set of
292 /// exhausted `<<once>>` blocks. Variable storage is **not** included; persist
293 /// it via [`Runner::storage`] alongside the snapshot when saving.
294 ///
295 /// Restoring with [`Runner::restore`] will restart execution from the beginning
296 /// of the snapshotted node.
297 ///
298 /// Enable the `serde` feature on `bubbles-dialogue` if you need `Serialize` /
299 /// `Deserialize` on [`RunnerSnapshot`](crate::RunnerSnapshot).
300 #[must_use]
301 pub fn snapshot(&self) -> crate::runtime::RunnerSnapshot {
302 crate::runtime::RunnerSnapshot {
303 current_node: self.stack.last().map(|f| f.node.as_ref().to_owned()),
304 visits: self.visits.clone(),
305 once_seen: self.once_seen.clone(),
306 }
307 }
308
309 /// Applies a previously captured `RunnerSnapshot`, restoring visit counts
310 /// and the `<<once>>` exhaustion set, then re-enters the snapshotted node
311 /// from its beginning.
312 ///
313 /// # Errors
314 ///
315 /// Returns [`DialogueError::UnknownNode`] if the snapshotted node no longer
316 /// exists in the program (e.g. after a script update).
317 pub fn restore(&mut self, snapshot: crate::runtime::RunnerSnapshot) -> Result<()> {
318 self.visits = snapshot.visits;
319 self.once_seen = snapshot.once_seen;
320 self.stack.clear();
321 self.clear_event_queues();
322 self.state = State::Idle;
323 if let Some(node) = snapshot.current_node {
324 self.start(&node)?;
325 }
326 Ok(())
327 }
328}