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