Skip to main content

bubbles/compiler/
program.rs

1//! [`Program`] — the compiled output of one or more `.bub` sources.
2
3use indexmap::IndexMap;
4
5use crate::compiler::ast::{Node, Stmt};
6use crate::error::{DialogueError, Result};
7
8/// A `<<declare $var = expr>>` entry collected from a compiled program.
9///
10/// These declarations are useful for save systems that need to know the
11/// full set of variables a script uses and their default expressions.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct VariableDecl {
14    /// Variable name including the `$` sigil.
15    pub name: String,
16    /// The default-value expression source, as written in the script.
17    pub default_src: String,
18}
19
20/// A compiled dialogue program ready to be executed by the runner.
21#[derive(Debug, Clone)]
22pub struct Program {
23    /// Nodes keyed by title. Multiple nodes with the same title form a node group.
24    pub(crate) nodes: IndexMap<String, Vec<Node>>,
25    /// All `<<declare>>` statements collected across every node, in encounter order.
26    declarations: Vec<VariableDecl>,
27}
28
29impl Program {
30    /// Returns `true` if the program contains a node with the given title.
31    ///
32    /// # Example
33    ///
34    /// ```rust
35    /// let prog = bubbles::compile("title: Start\n---\n===\n").unwrap();
36    /// assert!(prog.node_exists("Start"));
37    /// assert!(!prog.node_exists("Missing"));
38    /// ```
39    #[must_use]
40    pub fn node_exists(&self, title: &str) -> bool {
41        self.nodes.contains_key(title)
42    }
43
44    /// Iterates over all node titles in insertion order.
45    pub fn node_titles(&self) -> impl Iterator<Item = &str> {
46        self.nodes.keys().map(String::as_str)
47    }
48
49    /// Returns the tags of the first node with the given title, if any.
50    #[must_use]
51    pub fn node_tags(&self, title: &str) -> Option<&[String]> {
52        self.nodes.get(title)?.first().map(|n| n.tags.as_slice())
53    }
54
55    /// Returns all `<<declare>>` variable declarations found in the program.
56    ///
57    /// This is useful for save systems that need to enumerate every variable
58    /// a script declares, and for editor tooling.
59    ///
60    /// # Example
61    ///
62    /// ```rust
63    /// let prog = bubbles::compile(
64    ///     "title: Start\n---\n<<declare $health = 100>>\n===\n"
65    /// ).unwrap();
66    /// let decls = prog.variable_declarations();
67    /// assert_eq!(decls.len(), 1);
68    /// assert_eq!(decls[0].name, "$health");
69    /// assert_eq!(decls[0].default_src, "100");
70    /// ```
71    #[must_use]
72    pub fn variable_declarations(&self) -> &[VariableDecl] {
73        &self.declarations
74    }
75
76    pub(crate) fn node_group(&self, title: &str) -> Option<&[Node]> {
77        self.nodes.get(title).map(Vec::as_slice)
78    }
79
80    pub(crate) fn from_nodes(nodes: Vec<Node>) -> Result<Self> {
81        let mut map: IndexMap<String, Vec<Node>> = IndexMap::new();
82        let mut declarations: Vec<VariableDecl> = Vec::new();
83        let mut seen_decls: indexmap::IndexSet<String> = indexmap::IndexSet::new();
84
85        for node in nodes {
86            collect_declarations(&node.body, &mut declarations, &mut seen_decls);
87            let entry = map.entry(node.title.clone()).or_default();
88            // Duplicate non-grouped nodes are an error.
89            if !entry.is_empty() {
90                let existing_ungrouped = entry.iter().all(|n| n.when.is_none());
91                if existing_ungrouped && node.when.is_none() {
92                    return Err(DialogueError::DuplicateNode(node.title));
93                }
94            }
95            entry.push(node);
96        }
97        Ok(Self {
98            nodes: map,
99            declarations,
100        })
101    }
102}
103
104/// Recursively collect `Stmt::Declare` entries from a body, deduplicating by name.
105fn collect_declarations(
106    stmts: &[Stmt],
107    out: &mut Vec<VariableDecl>,
108    seen: &mut indexmap::IndexSet<String>,
109) {
110    for stmt in stmts {
111        match stmt {
112            Stmt::Declare {
113                name, default_src, ..
114            } if seen.insert(name.clone()) => {
115                out.push(VariableDecl {
116                    name: name.clone(),
117                    default_src: default_src.clone(),
118                });
119            }
120            Stmt::If {
121                branches,
122                else_body,
123            } => {
124                for b in branches {
125                    collect_declarations(&b.body, out, seen);
126                }
127                collect_declarations(else_body, out, seen);
128            }
129            Stmt::Once {
130                body, else_body, ..
131            } => {
132                collect_declarations(body, out, seen);
133                collect_declarations(else_body, out, seen);
134            }
135            Stmt::Options(items) => {
136                for item in items {
137                    collect_declarations(&item.body, out, seen);
138                }
139            }
140            _ => {}
141        }
142    }
143}