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, Option<String>, 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, once_id, body) =
191            self.option_bodies.get(index).cloned().ok_or_else(|| {
192                DialogueError::ProtocolViolation(format!(
193                    "option index {index} out of range ({})",
194                    self.option_bodies.len()
195                ))
196            })?;
197        if !available {
198            return Err(DialogueError::ProtocolViolation(format!(
199                "option index {index} is unavailable (guard not satisfied)"
200            )));
201        }
202        if let Some(id) = once_id {
203            self.once_seen.insert(id);
204        }
205        self.option_bodies.clear();
206        self.state = State::Running;
207        self.push_inline_frame(body);
208        Ok(())
209    }
210
211    /// Pushes `body` as a new frame on top of the stack, inheriting the
212    /// current frame's node title. No-op when `body` is empty.
213    ///
214    /// Used by `<<if>>`, `<<once>>`, and option-body execution.
215    pub(super) fn push_inline_frame(&mut self, body: StmtList) {
216        if body.is_empty() {
217            return;
218        }
219        let title = self
220            .stack
221            .last()
222            .map_or_else(|| Arc::from(""), |f| Arc::clone(&f.node));
223        self.stack.push(Frame::new(title, body));
224    }
225
226    /// Pushes a frame whose node title matches the supplied owned string.
227    ///
228    /// Used by `<<jump>>` / `<<detour>>` / `start()` - sites that already
229    /// know the target node title and want to install it as the top-of-stack
230    /// frame in one call.
231    pub(super) fn push_node_frame(&mut self, node: &str, body: StmtList) {
232        self.stack.push(Frame::new(Arc::from(node), body));
233    }
234
235    /// Returns a shared reference to the variable storage.
236    #[must_use]
237    pub const fn storage(&self) -> &S {
238        &self.storage
239    }
240
241    /// Returns a mutable reference to the variable storage.
242    pub const fn storage_mut(&mut self) -> &mut S {
243        &mut self.storage
244    }
245
246    /// Returns all variables known to storage (see [`VariableStorage::all_variables`]).
247    ///
248    /// For [`HashMapStorage`](crate::HashMapStorage) this is every key/value pair.
249    /// Custom stores return whatever their [`VariableStorage::all_variables`]
250    /// implementation provides (often empty unless overridden).
251    #[must_use]
252    pub fn all_variables(&self) -> Vec<(String, Value)> {
253        self.storage.all_variables()
254    }
255
256    /// Returns a clone of the value for `name`, or `None` if unset.
257    #[must_use]
258    pub fn variable(&self, name: &str) -> Option<Value> {
259        self.storage.get(name)
260    }
261
262    /// Borrows the value for `name` when the storage can lend it without cloning.
263    #[must_use]
264    pub fn variable_ref(&self, name: &str) -> Option<Cow<'_, Value>> {
265        self.storage.get_ref(name)
266    }
267
268    /// Returns a mutable reference to the function library (for registering host functions).
269    pub const fn library_mut(&mut self) -> &mut FunctionLibrary {
270        &mut self.library
271    }
272
273    /// Replaces the saliency strategy used for line and node group selection.
274    pub fn set_saliency(&mut self, strategy: impl SaliencyStrategy) {
275        self.saliency = Box::new(strategy);
276    }
277
278    /// Sets the line provider used for localisation lookup.
279    pub fn set_provider(&mut self, provider: impl LineProvider) {
280        self.provider = Box::new(provider);
281    }
282
283    /// Replaces the saliency strategy from an already-boxed value.
284    ///
285    /// Used by [`crate::runtime::RunnerBuilder`] to avoid double-boxing.
286    pub(crate) fn set_saliency_box(&mut self, strategy: Box<dyn SaliencyStrategy>) {
287        self.saliency = strategy;
288    }
289
290    /// Sets the line provider from an already-boxed value.
291    ///
292    /// Used by [`crate::runtime::RunnerBuilder`] to avoid double-boxing.
293    pub(crate) fn set_provider_box(&mut self, provider: Box<dyn LineProvider>) {
294        self.provider = provider;
295    }
296}