1use std::collections::HashMap;
4
5use brink_format::{CountingFlags, DefinitionId, ListValue, NameId, Value};
6
7pub struct Program {
12 pub(crate) containers: Vec<LinkedContainer>,
13 pub(crate) address_map: HashMap<DefinitionId, (u32, usize)>,
16 pub(crate) scope_ids: Vec<DefinitionId>,
19 pub(crate) source_checksum: u32,
21 pub(crate) globals: Vec<GlobalSlot>,
22 pub(crate) global_map: HashMap<DefinitionId, u32>,
23 pub(crate) name_table: Vec<String>,
24 pub(crate) address_by_path: HashMap<String, (u32, usize)>,
28 pub(crate) root_idx: u32,
29 pub(crate) list_literals: Vec<ListValue>,
31 pub(crate) list_item_map: HashMap<DefinitionId, ListItemEntry>,
33 pub(crate) list_defs: Vec<ListDefEntry>,
35 pub(crate) list_def_map: HashMap<DefinitionId, usize>,
37 pub(crate) external_fns: HashMap<DefinitionId, ExternalFnEntry>,
39}
40
41pub(crate) struct LinkedContainer {
42 pub id: DefinitionId,
43 pub bytecode: Vec<u8>,
44 pub counting_flags: CountingFlags,
45 pub path_hash: i32,
46 pub scope_table_idx: u32,
48}
49
50pub(crate) struct GlobalSlot {
51 #[expect(dead_code, reason = "needed for save/load serialization and debugging")]
52 pub id: DefinitionId,
53 #[cfg_attr(
54 not(feature = "testing"),
55 expect(dead_code, reason = "needed for save/load serialization and debugging")
56 )]
57 pub name: NameId,
58 pub default: Value,
59}
60
61pub(crate) struct ListItemEntry {
63 pub name: NameId,
64 pub ordinal: i32,
65 pub origin: DefinitionId,
66}
67
68pub(crate) struct ListDefEntry {
70 pub name: NameId,
71 pub items: Vec<DefinitionId>,
73}
74
75pub(crate) struct ExternalFnEntry {
77 pub name: NameId,
78 pub fallback: Option<DefinitionId>,
79}
80
81impl Program {
82 pub(crate) fn resolve_target(&self, id: DefinitionId) -> Option<(u32, usize)> {
84 self.address_map.get(&id).copied()
85 }
86
87 #[cfg(feature = "testing")]
89 pub fn resolve_address(&self, id: DefinitionId) -> Option<(u32, usize)> {
90 self.resolve_target(id)
91 }
92
93 pub(crate) fn container(&self, idx: u32) -> &LinkedContainer {
95 &self.containers[idx as usize]
96 }
97
98 #[cfg(feature = "testing")]
100 pub fn container_bytecode(&self, idx: u32) -> &[u8] {
101 &self.containers[idx as usize].bytecode
102 }
103
104 #[cfg(feature = "testing")]
106 #[expect(
107 clippy::cast_possible_truncation,
108 reason = "container count fits in u32"
109 )]
110 pub fn container_count(&self) -> u32 {
111 self.containers.len() as u32
112 }
113
114 pub fn source_checksum(&self) -> u32 {
116 self.source_checksum
117 }
118
119 pub(crate) fn scope_table_idx(&self, container_idx: u32) -> u32 {
121 self.containers[container_idx as usize].scope_table_idx
122 }
123
124 pub(crate) fn name(&self, id: NameId) -> &str {
126 &self.name_table[id.0 as usize]
127 }
128
129 pub(crate) fn resolve_global(&self, id: DefinitionId) -> Option<u32> {
131 self.global_map.get(&id).copied()
132 }
133
134 pub(crate) fn root_idx(&self) -> u32 {
136 self.root_idx
137 }
138
139 #[must_use]
154 pub fn find_address(&self, path: &str) -> Option<(u32, usize)> {
155 self.address_by_path.get(path).copied()
156 }
157
158 pub fn global_defaults(&self) -> Vec<Value> {
160 self.globals.iter().map(|s| s.default.clone()).collect()
161 }
162
163 pub(crate) fn list_literal(&self, idx: u16) -> &ListValue {
165 &self.list_literals[idx as usize]
166 }
167
168 pub(crate) fn list_item(&self, id: DefinitionId) -> Option<&ListItemEntry> {
170 self.list_item_map.get(&id)
171 }
172
173 pub(crate) fn list_def(&self, id: DefinitionId) -> Option<&ListDefEntry> {
175 self.list_def_map.get(&id).map(|&idx| &self.list_defs[idx])
176 }
177
178 pub(crate) fn list_def_by_name(&self, name: &str) -> Option<&ListDefEntry> {
180 self.list_defs
181 .iter()
182 .find(|def| self.name(def.name) == name)
183 }
184
185 pub(crate) fn external_fn(&self, id: DefinitionId) -> Option<&ExternalFnEntry> {
187 self.external_fns.get(&id)
188 }
189
190 #[cfg(feature = "testing")]
192 pub fn global_name(&self, idx: u32) -> Option<&str> {
193 self.globals
194 .get(idx as usize)
195 .map(|slot| self.name(slot.name))
196 }
197
198 #[cfg(feature = "testing")]
200 #[expect(clippy::cast_possible_truncation, reason = "global count fits in u32")]
201 pub fn global_count(&self) -> u32 {
202 self.globals.len() as u32
203 }
204}
205
206#[cfg(test)]
207mod find_address_tests {
208 use super::*;
209
210 fn make_program_with_named_containers(names: &[&str]) -> Program {
211 let mut address_by_path = HashMap::new();
215 for (i, name) in names.iter().enumerate() {
216 #[expect(clippy::cast_possible_truncation, reason = "test fixture")]
217 address_by_path.insert((*name).to_string(), (i as u32, 0));
218 }
219 Program {
220 containers: Vec::new(),
221 address_map: HashMap::new(),
222 scope_ids: Vec::new(),
223 source_checksum: 0,
224 globals: Vec::new(),
225 global_map: HashMap::new(),
226 name_table: Vec::new(),
227 address_by_path,
228 root_idx: 0,
229 list_literals: Vec::new(),
230 list_item_map: HashMap::new(),
231 list_defs: Vec::new(),
232 list_def_map: HashMap::new(),
233 external_fns: HashMap::new(),
234 }
235 }
236
237 #[test]
238 fn finds_known_knot() {
239 let program = make_program_with_named_containers(&["intro", "outro"]);
240 assert_eq!(program.find_address("intro"), Some((0, 0)));
241 assert_eq!(program.find_address("outro"), Some((1, 0)));
242 }
243
244 #[test]
245 fn returns_none_for_unknown_knot() {
246 let program = make_program_with_named_containers(&["intro"]);
247 assert_eq!(program.find_address("nope"), None);
248 }
249
250 #[test]
251 fn empty_program_returns_none() {
252 let program = make_program_with_named_containers(&[]);
253 assert_eq!(program.find_address("anything"), None);
254 }
255}