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            param_count: cdef.param_count,
51            scope_table_idx,
52        });
53    }
54
55    // Build globals.
56    let mut globals = Vec::with_capacity(data.variables.len());
57    let mut global_map = HashMap::with_capacity(data.variables.len());
58    for (i, gvar) in data.variables.iter().enumerate() {
59        let idx = i as u32;
60        global_map.insert(gvar.id, idx);
61        globals.push(GlobalSlot {
62            id: gvar.id,
63            name: gvar.name,
64            default: gvar.default_value.clone(),
65            local: gvar.local,
66        });
67    }
68
69    // Build unified address map from containers and address defs.
70    // Containers get offset 0 (primary addresses).
71    let mut address_map = HashMap::with_capacity(data.containers.len() + data.addresses.len());
72    for (i, cdef) in data.containers.iter().enumerate() {
73        address_map.insert(cdef.id, (i as u32, 0usize));
74    }
75    // Address defs add intra-container targets (and primary addresses from converter).
76    for addr in &data.addresses {
77        let container_idx = container_map
78            .get(&addr.container_id)
79            .copied()
80            .ok_or(RuntimeError::UnresolvedDefinition(addr.container_id))?;
81        address_map.insert(addr.id, (container_idx, addr.byte_offset as usize));
82    }
83
84    // Root container is always the first entry by convention.
85    if data.containers.is_empty() {
86        return Err(RuntimeError::NoRootContainer);
87    }
88    let root_idx = 0;
89
90    let name_table = data.name_table.clone();
91
92    // Build list item map.
93    let mut list_item_map = HashMap::with_capacity(data.list_items.len());
94    for li in &data.list_items {
95        list_item_map.insert(
96            li.id,
97            ListItemEntry {
98                name: li.name,
99                ordinal: li.ordinal,
100                origin: li.origin,
101            },
102        );
103    }
104
105    // Build list defs and list def map.
106    let mut list_defs = Vec::with_capacity(data.list_defs.len());
107    let mut list_def_map = HashMap::with_capacity(data.list_defs.len());
108    for ldef in &data.list_defs {
109        let idx = list_defs.len();
110        // Collect all items belonging to this list, sorted by ordinal.
111        let mut items: Vec<_> = data
112            .list_items
113            .iter()
114            .filter(|li| li.origin == ldef.id)
115            .collect();
116        items.sort_by_key(|li| li.ordinal);
117        let item_ids: Vec<_> = items.iter().map(|li| li.id).collect();
118
119        list_def_map.insert(ldef.id, idx);
120        list_defs.push(ListDefEntry {
121            name: ldef.name,
122            items: item_ids,
123        });
124    }
125
126    // Clone list literals.
127    let list_literals = data.list_literals.clone();
128
129    // Build external function map.
130    let mut external_fns = HashMap::with_capacity(data.externals.len());
131    for ext in &data.externals {
132        external_fns.insert(
133            ext.id,
134            ExternalFnEntry {
135                name: ext.name,
136                fallback: ext.fallback,
137            },
138        );
139    }
140
141    // Build the path → address lookup used by `Program::find_address`.
142    //
143    // When the program carries an explicit `address_paths` table (compiler
144    // output), it is the source of truth: each entry's qualified path maps to
145    // its target, resolved through `address_map`. This is what enables
146    // qualified addressing of scopes (`knot`, `knot.stitch`) and author labels
147    // (`knot.label`, `knot.stitch.label`).
148    //
149    // When the table is empty (legacy `.inkb` or converter output, which does
150    // not emit it), fall back to deriving scope paths from container names —
151    // the previous behavior, which already qualifies knot/stitch scope names.
152    let mut address_by_path: HashMap<String, PathTarget> = HashMap::new();
153    if data.address_paths.is_empty() {
154        address_by_path.reserve(data.containers.len());
155        for (i, cdef) in data.containers.iter().enumerate() {
156            if let Some(name_id) = cdef.name {
157                let name = data.name_table[name_id.0 as usize].clone();
158                address_by_path.insert(
159                    name,
160                    PathTarget {
161                        id: cdef.id,
162                        container_idx: i as u32,
163                        byte_offset: 0,
164                    },
165                );
166            }
167        }
168    } else {
169        address_by_path.reserve(data.address_paths.len());
170        for ap in &data.address_paths {
171            // Resolve the target through the address map; skip anything
172            // unresolvable (defensive — should not happen for valid output).
173            if let Some(&(idx, offset)) = address_map.get(&ap.target) {
174                let name = data.name_table[ap.path.0 as usize].clone();
175                address_by_path.insert(
176                    name,
177                    PathTarget {
178                        id: ap.target,
179                        container_idx: idx,
180                        byte_offset: offset,
181                    },
182                );
183            }
184        }
185    }
186
187    // Compiled `#@local` knot/stitch defaults — the base layer of policy
188    // resolution. Sorted by path so a knot expands before its stitches.
189    let mut local_scope_defaults: Vec<(String, DefinitionId)> = data
190        .containers
191        .iter()
192        .filter(|c| c.local)
193        .filter_map(|c| {
194            c.name
195                .map(|n| (data.name_table[n.0 as usize].clone(), c.id))
196        })
197        .collect();
198    local_scope_defaults.sort();
199
200    let program = Program {
201        containers,
202        address_map,
203        scope_ids,
204        source_checksum: data.source_checksum,
205        globals,
206        global_map,
207        name_table,
208        address_by_path,
209        root_idx,
210        list_literals,
211        list_item_map,
212        list_defs,
213        list_def_map,
214        external_fns,
215        local_scope_defaults,
216    };
217    Ok((program, line_tables))
218}