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