Skip to main content

brink_runtime/
linker.rs

1//! Links [`StoryData`] into an executable [`Program`].
2
3use std::collections::HashMap;
4
5use brink_format::{DefinitionId, StoryData};
6
7use crate::error::RuntimeError;
8use crate::program::{
9    ExternalFnEntry, GlobalSlot, LinkedContainer, ListDefEntry, ListItemEntry, PathTarget, Program,
10};
11
12/// Link a [`StoryData`] into an executable [`Program`].
13///
14/// Builds lookup tables mapping [`DefinitionId`]s to flat array indices.
15/// The root container is `containers[0]` by convention — both the converter
16/// and the brink compiler emit the root first.
17#[expect(clippy::cast_possible_truncation, clippy::too_many_lines)]
18pub fn link(
19    data: &StoryData,
20) -> Result<(Program, Vec<Vec<brink_format::LineEntry>>), RuntimeError> {
21    let mut container_map = HashMap::with_capacity(data.containers.len());
22
23    for (i, cdef) in data.containers.iter().enumerate() {
24        let idx = i as u32;
25        container_map.insert(cdef.id, idx);
26    }
27
28    // Build scope line tables and a map from scope_id → table index.
29    let mut scope_table_map: HashMap<DefinitionId, u32> =
30        HashMap::with_capacity(data.line_tables.len());
31    let mut line_tables: Vec<Vec<brink_format::LineEntry>> =
32        Vec::with_capacity(data.line_tables.len());
33    let mut scope_ids: Vec<DefinitionId> = Vec::with_capacity(data.line_tables.len());
34    for lt in &data.line_tables {
35        let idx = line_tables.len() as u32;
36        scope_table_map.insert(lt.scope_id, idx);
37        scope_ids.push(lt.scope_id);
38        line_tables.push(lt.lines.clone());
39    }
40
41    // Build containers with scope_table_idx.
42    let mut containers = Vec::with_capacity(data.containers.len());
43    for cdef in &data.containers {
44        let scope_table_idx = scope_table_map.get(&cdef.scope_id).copied().unwrap_or(0);
45        containers.push(LinkedContainer {
46            id: cdef.id,
47            bytecode: cdef.bytecode.clone(),
48            counting_flags: cdef.counting_flags,
49            path_hash: cdef.path_hash,
50            scope_table_idx,
51        });
52    }
53
54    // Build globals.
55    let mut globals = Vec::with_capacity(data.variables.len());
56    let mut global_map = HashMap::with_capacity(data.variables.len());
57    for (i, gvar) in data.variables.iter().enumerate() {
58        let idx = i as u32;
59        global_map.insert(gvar.id, idx);
60        globals.push(GlobalSlot {
61            id: gvar.id,
62            name: gvar.name,
63            default: gvar.default_value.clone(),
64        });
65    }
66
67    // Build unified address map from containers and address defs.
68    // Containers get offset 0 (primary addresses).
69    let mut address_map = HashMap::with_capacity(data.containers.len() + data.addresses.len());
70    for (i, cdef) in data.containers.iter().enumerate() {
71        address_map.insert(cdef.id, (i as u32, 0usize));
72    }
73    // Address defs add intra-container targets (and primary addresses from converter).
74    for addr in &data.addresses {
75        let container_idx = container_map
76            .get(&addr.container_id)
77            .copied()
78            .ok_or(RuntimeError::UnresolvedDefinition(addr.container_id))?;
79        address_map.insert(addr.id, (container_idx, addr.byte_offset as usize));
80    }
81
82    // Root container is always the first entry by convention.
83    if data.containers.is_empty() {
84        return Err(RuntimeError::NoRootContainer);
85    }
86    let root_idx = 0;
87
88    let name_table = data.name_table.clone();
89
90    // Build list item map.
91    let mut list_item_map = HashMap::with_capacity(data.list_items.len());
92    for li in &data.list_items {
93        list_item_map.insert(
94            li.id,
95            ListItemEntry {
96                name: li.name,
97                ordinal: li.ordinal,
98                origin: li.origin,
99            },
100        );
101    }
102
103    // Build list defs and list def map.
104    let mut list_defs = Vec::with_capacity(data.list_defs.len());
105    let mut list_def_map = HashMap::with_capacity(data.list_defs.len());
106    for ldef in &data.list_defs {
107        let idx = list_defs.len();
108        // Collect all items belonging to this list, sorted by ordinal.
109        let mut items: Vec<_> = data
110            .list_items
111            .iter()
112            .filter(|li| li.origin == ldef.id)
113            .collect();
114        items.sort_by_key(|li| li.ordinal);
115        let item_ids: Vec<_> = items.iter().map(|li| li.id).collect();
116
117        list_def_map.insert(ldef.id, idx);
118        list_defs.push(ListDefEntry {
119            name: ldef.name,
120            items: item_ids,
121        });
122    }
123
124    // Clone list literals.
125    let list_literals = data.list_literals.clone();
126
127    // Build external function map.
128    let mut external_fns = HashMap::with_capacity(data.externals.len());
129    for ext in &data.externals {
130        external_fns.insert(
131            ext.id,
132            ExternalFnEntry {
133                name: ext.name,
134                fallback: ext.fallback,
135            },
136        );
137    }
138
139    // Build the path → address lookup used by `Program::find_address`.
140    //
141    // When the program carries an explicit `address_paths` table (compiler
142    // output), it is the source of truth: each entry's qualified path maps to
143    // its target, resolved through `address_map`. This is what enables
144    // qualified addressing of scopes (`knot`, `knot.stitch`) and author labels
145    // (`knot.label`, `knot.stitch.label`).
146    //
147    // When the table is empty (legacy `.inkb` or converter output, which does
148    // not emit it), fall back to deriving scope paths from container names —
149    // the previous behavior, which already qualifies knot/stitch scope names.
150    let mut address_by_path: HashMap<String, PathTarget> = HashMap::new();
151    if data.address_paths.is_empty() {
152        address_by_path.reserve(data.containers.len());
153        for (i, cdef) in data.containers.iter().enumerate() {
154            if let Some(name_id) = cdef.name {
155                let name = data.name_table[name_id.0 as usize].clone();
156                address_by_path.insert(
157                    name,
158                    PathTarget {
159                        id: cdef.id,
160                        container_idx: i as u32,
161                        byte_offset: 0,
162                    },
163                );
164            }
165        }
166    } else {
167        address_by_path.reserve(data.address_paths.len());
168        for ap in &data.address_paths {
169            // Resolve the target through the address map; skip anything
170            // unresolvable (defensive — should not happen for valid output).
171            if let Some(&(idx, offset)) = address_map.get(&ap.target) {
172                let name = data.name_table[ap.path.0 as usize].clone();
173                address_by_path.insert(
174                    name,
175                    PathTarget {
176                        id: ap.target,
177                        container_idx: idx,
178                        byte_offset: offset,
179                    },
180                );
181            }
182        }
183    }
184
185    let program = Program {
186        containers,
187        address_map,
188        scope_ids,
189        source_checksum: data.source_checksum,
190        globals,
191        global_map,
192        name_table,
193        address_by_path,
194        root_idx,
195        list_literals,
196        list_item_map,
197        list_defs,
198        list_def_map,
199        external_fns,
200    };
201    Ok((program, line_tables))
202}