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}