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