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, Mutex};
9
10use crate::compiler::Program;
11use crate::compiler::ast::Stmt;
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::{Value, VariableStorage};
18
19/// Execution state of the runner.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub(super) enum State {
22    Idle,
23    Running,
24    AwaitingOption,
25    Done,
26}
27
28/// A frame on the call stack.
29#[derive(Debug, Clone)]
30pub(super) struct Frame {
31    pub(super) node: String,
32    pub(super) stmts: VecDeque<Stmt>,
33}
34
35impl Frame {
36    pub(super) fn new(node: String, body: Vec<Stmt>) -> Self {
37        Self {
38            node,
39            stmts: VecDeque::from(body),
40        }
41    }
42}
43
44/// Option bodies held during `AwaitingOption` state.
45type OptionBodies = Vec<Vec<Stmt>>;
46
47/// Drives execution of a compiled [`Program`], yielding [`DialogueEvent`]s one at a time.
48///
49/// # Pull model
50/// The host calls [`Runner::next_event`] in a loop until it returns `Ok(None)` (dialogue
51/// ended) or until it receives a [`DialogueEvent::Options`], at which point it must call
52/// [`Runner::select_option`] before continuing.
53pub struct Runner<S: VariableStorage> {
54    pub(super) program: Program,
55    pub(super) storage: S,
56    pub(super) state: State,
57    pub(super) stack: Vec<Frame>,
58    pub(super) pending: VecDeque<DialogueEvent>,
59    pub(super) option_bodies: OptionBodies,
60    pub(super) library: FunctionLibrary,
61    pub(super) visits: Arc<Mutex<HashMap<String, usize>>>,
62    pub(super) once_seen: HashSet<String>,
63    pub(super) saliency: Box<dyn SaliencyStrategy>,
64    pub(super) provider: Box<dyn LineProvider>,
65}
66
67impl<S: VariableStorage> Runner<S> {
68    /// Creates a new runner for the given program and variable storage.
69    ///
70    /// # Panics
71    /// Panics only if the internal `Mutex` is poisoned, which cannot happen in normal use.
72    #[must_use]
73    pub fn new(program: Program, storage: S) -> Self {
74        let visits: Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));
75        let mut library = FunctionLibrary::new();
76
77        let v1 = Arc::clone(&visits);
78        library.register("visited", move |args| {
79            let title = match args.as_slice() {
80                [Value::Text(t)] => t.clone(),
81                _ => {
82                    return Err(DialogueError::Function {
83                        name: "visited".into(),
84                        message: "expected one string argument".into(),
85                    });
86                }
87            };
88            Ok(Value::Bool(
89                *v1.lock().unwrap().get(&title).unwrap_or(&0) > 0,
90            ))
91        });
92        let v2 = Arc::clone(&visits);
93        library.register("visited_count", move |args| {
94            let title = match args.as_slice() {
95                [Value::Text(t)] => t.clone(),
96                _ => {
97                    return Err(DialogueError::Function {
98                        name: "visited_count".into(),
99                        message: "expected one string argument".into(),
100                    });
101                }
102            };
103            let count = *v2.lock().unwrap().get(&title).unwrap_or(&0);
104            #[allow(clippy::cast_precision_loss)]
105            Ok(Value::Number(count as f64))
106        });
107
108        Self {
109            program,
110            storage,
111            state: State::Idle,
112            stack: Vec::new(),
113            pending: VecDeque::new(),
114            option_bodies: Vec::new(),
115            library,
116            visits,
117            once_seen: HashSet::new(),
118            saliency: Box::new(FirstAvailable),
119            provider: Box::new(PassthroughProvider),
120        }
121    }
122
123    /// Starts execution at the given node.
124    ///
125    /// # Errors
126    /// Returns [`DialogueError::UnknownNode`] if the title does not exist in the program.
127    ///
128    /// # Panics
129    /// Panics only if the internal `Mutex` is poisoned, which cannot happen in normal use.
130    pub fn start(&mut self, node: &str) -> Result<()> {
131        if !self.program.node_exists(node) {
132            return Err(DialogueError::UnknownNode(node.to_owned()));
133        }
134        let body = self.pick_node_body(node)?;
135        self.stack.clear();
136        self.stack.push(Frame::new(node.to_owned(), body));
137        self.state = State::Running;
138        *self
139            .visits
140            .lock()
141            .unwrap()
142            .entry(node.to_owned())
143            .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::Runtime(
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    /// Returns [`DialogueError::Runtime`] if called when not awaiting an option or index is out of
180    /// range.
181    pub fn select_option(&mut self, index: usize) -> Result<()> {
182        if self.state != State::AwaitingOption {
183            return Err(DialogueError::Runtime(
184                "select_option() called when not awaiting an option".into(),
185            ));
186        }
187        let body = self.option_bodies.get(index).cloned().ok_or_else(|| {
188            DialogueError::Runtime(format!(
189                "option index {index} out of range ({})",
190                self.option_bodies.len()
191            ))
192        })?;
193        self.option_bodies.clear();
194        self.state = State::Running;
195        self.push_inline_frame(body);
196        Ok(())
197    }
198
199    /// Pushes `body` as a new frame on top of the stack, inheriting the
200    /// current frame's node title. No-op when `body` is empty.
201    ///
202    /// Used by `<<if>>`, `<<once>>`, and option-body execution.
203    pub(super) fn push_inline_frame(&mut self, body: Vec<Stmt>) {
204        if body.is_empty() {
205            return;
206        }
207        let title = self
208            .stack
209            .last()
210            .map(|f| f.node.clone())
211            .unwrap_or_default();
212        self.stack.push(Frame::new(title, body));
213    }
214
215    /// Returns a shared reference to the variable storage.
216    #[must_use]
217    pub const fn storage(&self) -> &S {
218        &self.storage
219    }
220
221    /// Returns a mutable reference to the variable storage.
222    pub const fn storage_mut(&mut self) -> &mut S {
223        &mut self.storage
224    }
225
226    /// Returns a mutable reference to the function library (for registering host functions).
227    pub const fn library_mut(&mut self) -> &mut FunctionLibrary {
228        &mut self.library
229    }
230
231    /// Replaces the saliency strategy used for line and node group selection.
232    pub fn set_saliency(&mut self, strategy: impl SaliencyStrategy) {
233        self.saliency = Box::new(strategy);
234    }
235
236    /// Sets the line provider used for localisation lookup.
237    pub fn set_provider(&mut self, provider: impl LineProvider) {
238        self.provider = Box::new(provider);
239    }
240
241    // ── save / load ───────────────────────────────────────────────────────────
242
243    /// Captures the current session state into a serialisable `RunnerSnapshot`.
244    ///
245    /// The snapshot records the active node title, visit counts, and the set of
246    /// exhausted `<<once>>` blocks.  Variable storage is **not** included; serialise
247    /// it via [`Runner::storage`] alongside the snapshot.
248    ///
249    /// Restoring with [`Runner::restore`] will restart execution from the beginning
250    /// of the snapshotted node.
251    ///
252    /// Only available with the `serde` feature.
253    ///
254    /// # Panics
255    ///
256    /// Panics if the internal visits `Mutex` is poisoned (only possible if a
257    /// previous thread panicked while holding it, which is not expected in normal use).
258    #[cfg(feature = "serde")]
259    #[must_use]
260    pub fn snapshot(&self) -> crate::runtime::RunnerSnapshot {
261        crate::runtime::RunnerSnapshot {
262            current_node: self.stack.last().map(|f| f.node.clone()),
263            visits: self.visits.lock().unwrap().clone(),
264            once_seen: self.once_seen.clone(),
265        }
266    }
267
268    /// Applies a previously captured `RunnerSnapshot`, restoring visit counts
269    /// and the `<<once>>` exhaustion set, then re-enters the snapshotted node
270    /// from its beginning.
271    ///
272    /// # Errors
273    ///
274    /// Returns [`DialogueError::UnknownNode`] if the snapshotted node no longer
275    /// exists in the program (e.g. after a script update).
276    ///
277    /// # Panics
278    ///
279    /// Panics if the internal visits lock is poisoned (not expected in normal use).
280    ///
281    /// Only available with the `serde` feature.
282    #[cfg(feature = "serde")]
283    pub fn restore(&mut self, snapshot: crate::runtime::RunnerSnapshot) -> Result<()> {
284        *self.visits.lock().unwrap() = snapshot.visits;
285        self.once_seen = snapshot.once_seen;
286        self.stack.clear();
287        self.pending.clear();
288        self.option_bodies.clear();
289        self.state = State::Idle;
290        if let Some(node) = snapshot.current_node {
291            self.start(&node)?;
292        }
293        Ok(())
294    }
295}