Skip to main content

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}