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, PathTarget>,
29 pub(crate) root_idx: u32,
30 pub(crate) list_literals: Vec<ListValue>,
32 pub(crate) list_item_map: HashMap<DefinitionId, ListItemEntry>,
34 pub(crate) list_defs: Vec<ListDefEntry>,
36 pub(crate) list_def_map: HashMap<DefinitionId, usize>,
38 pub(crate) external_fns: HashMap<DefinitionId, ExternalFnEntry>,
40}
41
42pub(crate) struct LinkedContainer {
43 pub id: DefinitionId,
44 pub bytecode: Vec<u8>,
45 pub counting_flags: CountingFlags,
46 pub path_hash: i32,
47 pub scope_table_idx: u32,
49}
50
51pub(crate) struct GlobalSlot {
52 #[expect(dead_code, reason = "needed for save/load serialization and debugging")]
53 pub id: DefinitionId,
54 pub name: NameId,
55 pub default: Value,
56}
57
58pub(crate) struct ListItemEntry {
60 pub name: NameId,
61 pub ordinal: i32,
62 pub origin: DefinitionId,
63}
64
65pub(crate) struct ListDefEntry {
67 pub name: NameId,
68 pub items: Vec<DefinitionId>,
70}
71
72pub(crate) struct ExternalFnEntry {
74 pub name: NameId,
75 pub fallback: Option<DefinitionId>,
76}
77
78#[derive(Debug, Clone, Copy)]
82pub(crate) struct PathTarget {
83 pub id: DefinitionId,
84 pub container_idx: u32,
85 pub byte_offset: usize,
86}
87
88impl Program {
89 pub(crate) fn resolve_target(&self, id: DefinitionId) -> Option<(u32, usize)> {
91 self.address_map.get(&id).copied()
92 }
93
94 #[cfg(feature = "testing")]
96 pub fn resolve_address(&self, id: DefinitionId) -> Option<(u32, usize)> {
97 self.resolve_target(id)
98 }
99
100 pub(crate) fn container(&self, idx: u32) -> &LinkedContainer {
102 &self.containers[idx as usize]
103 }
104
105 #[cfg(feature = "testing")]
107 pub fn container_bytecode(&self, idx: u32) -> &[u8] {
108 &self.containers[idx as usize].bytecode
109 }
110
111 #[cfg(feature = "testing")]
113 #[expect(
114 clippy::cast_possible_truncation,
115 reason = "container count fits in u32"
116 )]
117 pub fn container_count(&self) -> u32 {
118 self.containers.len() as u32
119 }
120
121 pub fn source_checksum(&self) -> u32 {
123 self.source_checksum
124 }
125
126 pub(crate) fn scope_table_idx(&self, container_idx: u32) -> u32 {
128 self.containers[container_idx as usize].scope_table_idx
129 }
130
131 pub(crate) fn name(&self, id: NameId) -> &str {
133 &self.name_table[id.0 as usize]
134 }
135
136 pub(crate) fn resolve_global(&self, id: DefinitionId) -> Option<u32> {
138 self.global_map.get(&id).copied()
139 }
140
141 pub(crate) fn root_idx(&self) -> u32 {
143 self.root_idx
144 }
145
146 #[must_use]
161 pub fn find_address(&self, path: &str) -> Option<(u32, usize)> {
162 self.address_by_path
163 .get(path)
164 .map(|t| (t.container_idx, t.byte_offset))
165 }
166
167 pub(crate) fn find_path_target(&self, path: &str) -> Option<DefinitionId> {
172 self.address_by_path.get(path).map(|t| t.id)
173 }
174
175 pub fn global_defaults(&self) -> Vec<Value> {
177 self.globals.iter().map(|s| s.default.clone()).collect()
178 }
179
180 #[expect(clippy::cast_possible_truncation, reason = "global count fits in u32")]
183 pub fn global_index(&self, name: &str) -> Option<u32> {
184 self.globals
185 .iter()
186 .position(|slot| self.name(slot.name) == name)
187 .map(|i| i as u32)
188 }
189
190 pub(crate) fn list_literal(&self, idx: u16) -> &ListValue {
192 &self.list_literals[idx as usize]
193 }
194
195 pub(crate) fn list_item(&self, id: DefinitionId) -> Option<&ListItemEntry> {
197 self.list_item_map.get(&id)
198 }
199
200 pub(crate) fn list_def(&self, id: DefinitionId) -> Option<&ListDefEntry> {
202 self.list_def_map.get(&id).map(|&idx| &self.list_defs[idx])
203 }
204
205 pub(crate) fn list_def_by_name(&self, name: &str) -> Option<&ListDefEntry> {
207 self.list_defs
208 .iter()
209 .find(|def| self.name(def.name) == name)
210 }
211
212 pub(crate) fn external_fn(&self, id: DefinitionId) -> Option<&ExternalFnEntry> {
214 self.external_fns.get(&id)
215 }
216
217 pub fn global_name(&self, idx: u32) -> Option<&str> {
225 self.globals
226 .get(idx as usize)
227 .map(|slot| self.name(slot.name))
228 }
229
230 #[expect(clippy::cast_possible_truncation, reason = "global count fits in u32")]
232 pub fn global_count(&self) -> u32 {
233 self.globals.len() as u32
234 }
235
236 pub(crate) fn global_slot_name(&self, idx: usize) -> Option<&str> {
240 self.globals.get(idx).map(|slot| self.name(slot.name))
241 }
242
243 pub(crate) fn global_var_name(&self, id: DefinitionId) -> Option<&str> {
246 let slot = self.resolve_global(id)?;
247 self.global_slot_name(slot as usize)
248 }
249
250 pub(crate) fn list_item_name(&self, id: DefinitionId) -> Option<&str> {
252 self.list_item(id).map(|item| self.name(item.name))
253 }
254}
255
256#[cfg(test)]
257mod find_address_tests {
258 use super::*;
259
260 fn make_program_with_named_containers(names: &[&str]) -> Program {
261 let mut address_by_path = HashMap::new();
265 for (i, name) in names.iter().enumerate() {
266 #[expect(clippy::cast_possible_truncation, reason = "test fixture")]
267 address_by_path.insert(
268 (*name).to_string(),
269 PathTarget {
270 id: DefinitionId::new(brink_format::DefinitionTag::Address, i as u64),
271 container_idx: i as u32,
272 byte_offset: 0,
273 },
274 );
275 }
276 Program {
277 containers: Vec::new(),
278 address_map: HashMap::new(),
279 scope_ids: Vec::new(),
280 source_checksum: 0,
281 globals: Vec::new(),
282 global_map: HashMap::new(),
283 name_table: Vec::new(),
284 address_by_path,
285 root_idx: 0,
286 list_literals: Vec::new(),
287 list_item_map: HashMap::new(),
288 list_defs: Vec::new(),
289 list_def_map: HashMap::new(),
290 external_fns: HashMap::new(),
291 }
292 }
293
294 #[test]
295 fn finds_known_knot() {
296 let program = make_program_with_named_containers(&["intro", "outro"]);
297 assert_eq!(program.find_address("intro"), Some((0, 0)));
298 assert_eq!(program.find_address("outro"), Some((1, 0)));
299 }
300
301 #[test]
302 fn returns_none_for_unknown_knot() {
303 let program = make_program_with_named_containers(&["intro"]);
304 assert_eq!(program.find_address("nope"), None);
305 }
306
307 #[test]
308 fn empty_program_returns_none() {
309 let program = make_program_with_named_containers(&[]);
310 assert_eq!(program.find_address("anything"), None);
311 }
312}