1use alloc::string::String;
4use alloc::vec::Vec;
5
6use brink_format::{DefinitionId, NameId, StoryData};
7
8use crate::collections::{Map as HashMap, map_with_capacity};
9use crate::error::RuntimeError;
10use crate::program::{
11 ExternalFnEntry, GlobalSlot, LinkTables, LinkedContainer, LinkedTarget, ListDefEntry,
12 ListItemEntry, PathTarget, Program, StructShapeEntry, linked_operand,
13};
14
15fn resolve_name(data: &StoryData, name_id: NameId) -> Result<String, RuntimeError> {
21 data.name_table
22 .get(name_id.0 as usize)
23 .cloned()
24 .ok_or_else(|| RuntimeError::InvalidNameId(name_id.0))
25}
26
27#[expect(clippy::cast_possible_truncation, clippy::too_many_lines)]
33pub fn link(
34 data: &StoryData,
35) -> Result<(Program, Vec<Vec<brink_format::LineEntry>>), RuntimeError> {
36 let mut container_map = map_with_capacity(data.containers.len());
37
38 for (i, cdef) in data.containers.iter().enumerate() {
39 let idx = i as u32;
40 container_map.insert(cdef.id, idx);
41 }
42
43 let mut scope_table_map: HashMap<DefinitionId, u32> = map_with_capacity(data.line_tables.len());
45 let mut line_tables: Vec<Vec<brink_format::LineEntry>> =
46 Vec::with_capacity(data.line_tables.len());
47 let mut scope_ids: Vec<DefinitionId> = Vec::with_capacity(data.line_tables.len());
48 for lt in &data.line_tables {
49 let idx = line_tables.len() as u32;
50 scope_table_map.insert(lt.scope_id, idx);
51 scope_ids.push(lt.scope_id);
52 line_tables.push(lt.lines.clone());
53 }
54
55 let mut containers = Vec::with_capacity(data.containers.len());
57 for cdef in &data.containers {
58 let scope_table_idx = scope_table_map.get(&cdef.scope_id).copied().unwrap_or(0);
59 containers.push(LinkedContainer {
60 id: cdef.id,
61 bytecode: cdef.bytecode.clone(),
62 counting_flags: cdef.counting_flags,
63 path_hash: cdef.path_hash,
64 param_count: cdef.param_count,
65 params: cdef.params.clone(),
66 scope_table_idx,
67 scope_id: cdef.scope_id,
68 });
69 }
70
71 let mut globals = Vec::with_capacity(data.variables.len());
73 let mut global_map = map_with_capacity(data.variables.len());
74 for (i, gvar) in data.variables.iter().enumerate() {
75 let idx = i as u32;
76 global_map.insert(gvar.id, idx);
77 globals.push(GlobalSlot {
78 id: gvar.id,
79 name: gvar.name,
80 default: gvar.default_value.clone(),
81 local: gvar.local,
82 });
83 }
84
85 let mut address_map = map_with_capacity(data.containers.len() + data.addresses.len());
88 for (i, cdef) in data.containers.iter().enumerate() {
89 address_map.insert(cdef.id, (i as u32, 0usize));
90 }
91 for addr in &data.addresses {
93 let container_idx = container_map
94 .get(&addr.container_id)
95 .copied()
96 .ok_or_else(|| RuntimeError::UnresolvedDefinition(addr.container_id))?;
97 address_map.insert(addr.id, (container_idx, addr.byte_offset as usize));
98 }
99
100 if data.containers.is_empty() {
102 return Err(RuntimeError::NoRootContainer);
103 }
104 let link = link_static_operands(&containers, &address_map, &global_map);
105
106 let root_idx = 0;
107
108 let name_table = data.name_table.clone();
109
110 let mut list_item_map = map_with_capacity(data.list_items.len());
112 for li in &data.list_items {
113 list_item_map.insert(
114 li.id,
115 ListItemEntry {
116 name: li.name,
117 ordinal: li.ordinal,
118 origin: li.origin,
119 },
120 );
121 }
122
123 let mut list_defs = Vec::with_capacity(data.list_defs.len());
125 let mut list_def_map = map_with_capacity(data.list_defs.len());
126 for ldef in &data.list_defs {
127 let idx = list_defs.len();
128 let mut items: Vec<_> = data
130 .list_items
131 .iter()
132 .filter(|li| li.origin == ldef.id)
133 .collect();
134 items.sort_by_key(|li| li.ordinal);
135 let item_ids: Vec<_> = items.iter().map(|li| li.id).collect();
136
137 list_def_map.insert(ldef.id, idx);
138 list_defs.push(ListDefEntry {
139 name: ldef.name,
140 items: item_ids,
141 });
142 }
143
144 let list_literals = data.list_literals.clone();
146
147 let literal_pool = data.literal_pool.clone();
149
150 let mut struct_shapes: Vec<StructShapeEntry> = Vec::with_capacity(data.struct_shapes.len());
155 for shape in &data.struct_shapes {
156 let idx = shape.id.0 as usize;
157 if struct_shapes.len() <= idx {
158 struct_shapes.resize_with(idx + 1, || StructShapeEntry {
159 name: NameId(0),
160 fields: Vec::new(),
161 });
162 }
163 struct_shapes[idx] = StructShapeEntry {
164 name: shape.name,
165 fields: shape.fields.clone(),
166 };
167 }
168
169 let mut external_fns = map_with_capacity(data.externals.len());
171 for ext in &data.externals {
172 external_fns.insert(
173 ext.id,
174 ExternalFnEntry {
175 name: ext.name,
176 fallback: ext.fallback,
177 },
178 );
179 }
180
181 let mut address_by_path: HashMap<String, PathTarget> = HashMap::new();
193 if data.address_paths.is_empty() {
194 #[cfg(feature = "std")]
196 address_by_path.reserve(data.containers.len());
197 for (i, cdef) in data.containers.iter().enumerate() {
198 if let Some(name_id) = cdef.name {
199 let name = resolve_name(data, name_id)?;
200 address_by_path.insert(
201 name,
202 PathTarget {
203 id: cdef.id,
204 container_idx: i as u32,
205 byte_offset: 0,
206 },
207 );
208 }
209 }
210 } else {
211 #[cfg(feature = "std")]
213 address_by_path.reserve(data.address_paths.len());
214 for ap in &data.address_paths {
215 if let Some(&(idx, offset)) = address_map.get(&ap.target) {
218 let name = resolve_name(data, ap.path)?;
219 address_by_path.insert(
220 name,
221 PathTarget {
222 id: ap.target,
223 container_idx: idx,
224 byte_offset: offset,
225 },
226 );
227 }
228 }
229 }
230
231 let mut local_scope_defaults: Vec<(String, DefinitionId)> = Vec::new();
234 for cdef in data.containers.iter().filter(|c| c.local) {
235 if let Some(n) = cdef.name {
236 local_scope_defaults.push((resolve_name(data, n)?, cdef.id));
237 }
238 }
239 local_scope_defaults.sort();
240
241 let mut private_defs: Vec<DefinitionId> = data.private_defs.clone();
246 private_defs.sort_by_key(|d| d.to_raw());
247
248 let mut alias_table = data.alias_table.clone();
253 alias_table.sort_unstable();
254
255 let program = Program {
256 containers,
257 link,
258 address_map,
259 scope_ids,
260 source_checksum: data.source_checksum,
261 globals,
262 global_map,
263 name_table,
264 container_paths: crate::program::container_paths_from(&address_by_path),
265 address_by_path,
266 root_idx,
267 list_literals,
268 literal_pool,
269 list_item_map,
270 list_defs,
271 list_def_map,
272 external_fns,
273 local_scope_defaults,
274 struct_shapes,
275 private_defs,
276 alias_table,
277 debug_info: data.debug_info.clone(),
278 };
279 Ok((program, line_tables))
280}
281
282#[expect(
293 clippy::cast_possible_truncation,
294 reason = "a target ordinal indexes a Vec built here; it cannot exceed u32"
295)]
296fn link_static_operands(
297 containers: &[LinkedContainer],
298 address_map: &HashMap<DefinitionId, (u32, usize)>,
299 global_map: &HashMap<DefinitionId, u32>,
300) -> LinkTables {
301 use brink_format::{Opcode, StaticKind};
302
303 let mut targets: Vec<LinkedTarget> = Vec::new();
304 let mut ordinals: HashMap<DefinitionId, u32> = HashMap::new();
305 let mut code = Vec::with_capacity(containers.len());
306 for container in containers {
307 let symbolic = &container.bytecode;
308 let mut linked = symbolic.clone();
309 let mut offset = 0;
310 while offset < symbolic.len() {
311 let site = Opcode::peek_static(symbolic, offset);
312 let Ok(op) = Opcode::decode(symbolic, &mut offset) else {
313 break;
314 };
315 let Some(site) = site else {
316 continue;
317 };
318 let resolved = match (site.kind, op) {
319 (
320 StaticKind::Target(_),
321 Opcode::Goto(id)
322 | Opcode::GotoIf(id)
323 | Opcode::EnterContainer(id)
324 | Opcode::Call(id)
325 | Opcode::TunnelCall(id)
326 | Opcode::ThreadCall(id)
327 | Opcode::BeginChoice(_, id),
328 ) => address_map.get(&id).map(|&(container_idx, target_offset)| {
329 *ordinals.entry(id).or_insert_with(|| {
330 targets.push(LinkedTarget {
331 container_idx,
332 offset: target_offset,
333 id,
334 });
335 (targets.len() - 1) as u32
336 })
337 }),
338 (
341 StaticKind::Global(_),
342 Opcode::GetGlobal(id) | Opcode::SetGlobal(id) | Opcode::TakeGlobal(id),
343 ) => global_map.get(&id).copied(),
344 _ => None,
345 };
346 if let Some(operand) = resolved {
347 linked[site.operand..site.end].copy_from_slice(&linked_operand(operand));
348 }
349 }
350 code.push(linked);
351 }
352 LinkTables { code, targets }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 use brink_format::Opcode;
360
361 use crate::program::linked_ordinal;
362
363 const STORY: &str = r"
367VAR x = 0
368-> top
369=== top ===
370~ x = f(1)
371-> tunnel ->
372<- side
373* [A] -> gather_here
374* [B]
375- (gather_here) Gathered.
376{ x > 0: -> top | -> END }
377=== function f(n) ===
378~ return n + 1
379=== tunnel ===
380In the tunnel.
381->->
382=== side ===
383Side thread.
384-> DONE
385";
386
387 fn compiled() -> StoryData {
388 brink_compiler::compile("main.ink", |_p| Ok(STORY.to_owned()))
389 .unwrap()
390 .data
391 }
392
393 fn global_sites(bytecode: &[u8]) -> Vec<(brink_format::StaticSite, DefinitionId)> {
396 let mut out = Vec::new();
397 let mut off = 0;
398 while off < bytecode.len() {
399 let site = Opcode::peek_static(bytecode, off);
400 let op = Opcode::decode(bytecode, &mut off).expect("symbolic bytecode decodes");
401 let Some(site) = site else { continue };
402 if !matches!(site.kind, brink_format::StaticKind::Global(_)) {
403 continue;
404 }
405 let (Opcode::GetGlobal(id) | Opcode::SetGlobal(id) | Opcode::TakeGlobal(id)) = op
406 else {
407 continue;
408 };
409 assert_eq!(site.end, off);
410 out.push((site, id));
411 }
412 out
413 }
414
415 fn target_sites(bytecode: &[u8]) -> Vec<(brink_format::TargetSite, DefinitionId)> {
418 let mut out = Vec::new();
419 let mut off = 0;
420 while off < bytecode.len() {
421 let site = Opcode::peek_target(bytecode, off);
422 let op = Opcode::decode(bytecode, &mut off).expect("symbolic bytecode decodes");
423 let Some(site) = site else { continue };
424 let (Opcode::Goto(id)
427 | Opcode::GotoIf(id)
428 | Opcode::EnterContainer(id)
429 | Opcode::Call(id)
430 | Opcode::TunnelCall(id)
431 | Opcode::ThreadCall(id)
432 | Opcode::BeginChoice(_, id)) = op
433 else {
434 continue;
435 };
436 assert_eq!(
437 site.end, off,
438 "peek and decode agree on the instruction's extent"
439 );
440 out.push((site, id));
441 }
442 out
443 }
444
445 #[test]
450 fn linked_code_holds_ordinals_for_every_resolvable_static_target() {
451 let data = compiled();
452 let (program, _) = link(&data).expect("links");
453 assert_eq!(program.link.code.len(), program.containers.len());
454
455 let mut sites_seen = 0;
456 let mut globals_seen = 0;
457 let mut kinds = alloc::collections::BTreeSet::new();
458 for (i, container) in program.containers.iter().enumerate() {
459 let symbolic = &container.bytecode;
460 let linked = &program.link.code[i];
461 assert_eq!(
462 symbolic, &data.containers[i].bytecode,
463 "symbolic copy untouched"
464 );
465 assert_eq!(symbolic.len(), linked.len(), "same length, same offsets");
466
467 let mut rewritten = alloc::vec![false; symbolic.len()];
468 for (site, id) in global_sites(symbolic) {
469 globals_seen += 1;
470 let slot = program.global_map.get(&id).copied();
471 let linked_slot = linked_ordinal(&linked[site.operand..site.end]);
472 assert_eq!(slot, linked_slot, "global site {site:?} for {id}");
473 if linked_slot.is_some() {
474 rewritten[site.operand..site.end].fill(true);
475 }
476 }
477 for (site, id) in target_sites(symbolic) {
478 sites_seen += 1;
479 kinds.insert(
480 format!("{:?}", site.kind)
481 .split('(')
482 .next()
483 .unwrap()
484 .to_owned(),
485 );
486 let expected = program.address_map.get(&id).copied();
487 let ordinal = linked_ordinal(&linked[site.operand..site.end]);
488 assert_eq!(
489 expected.is_some(),
490 ordinal.is_some(),
491 "site {site:?} for {id}: address_map {expected:?}, linked {ordinal:?}"
492 );
493 if let (Some((cidx, coff)), Some(ord)) = (expected, ordinal) {
494 let t = program.target(ord).expect("ordinal in table");
495 assert_eq!((t.container_idx, t.offset, t.id), (cidx, coff, id));
496 rewritten[site.operand..site.end].fill(true);
497 }
498 }
499 for (k, (a, b)) in symbolic.iter().zip(linked).enumerate() {
500 if !rewritten[k] {
501 assert_eq!(
502 a, b,
503 "byte {k} of container {i} outside any operand changed"
504 );
505 }
506 }
507 }
508 assert!(
509 sites_seen >= 6,
510 "the story exercises several targets: {sites_seen}"
511 );
512 assert!(
513 globals_seen >= 2,
514 "the story reads and writes a global: {globals_seen}"
515 );
516 for kind in ["Goto", "Call", "TunnelCall", "ThreadCall", "BeginChoice"] {
517 assert!(kinds.contains(kind), "story exercises {kind}: {kinds:?}");
518 }
519 let (again, _) = link(&data).expect("links");
521 assert_eq!(program.link.targets, again.link.targets);
522 assert_eq!(program.link.code, again.link.code);
523 }
524
525 #[test]
530 fn unresolvable_target_stays_symbolic() {
531 let mut data = compiled();
532 let bogus = DefinitionId::new(brink_format::DefinitionTag::Address, 0x00DE_AD00_BEEF);
534 let mut patched: Option<(usize, brink_format::TargetSite)> = None;
535 'outer: for (i, c) in data.containers.iter().enumerate() {
536 for (site, _) in target_sites(&c.bytecode) {
537 if site.kind == brink_format::TargetKind::Goto {
538 patched = Some((i, site));
539 break 'outer;
540 }
541 }
542 }
543 let (ci, site) = patched.expect("the story has a Goto");
544 data.containers[ci].bytecode[site.operand..site.end]
545 .copy_from_slice(&bogus.to_raw().to_le_bytes());
546
547 let (program, _) =
548 link(&data).expect("an unresolvable divert is a run-time error, not a link error");
549 let linked = &program.link.code[ci];
550 assert_eq!(linked_ordinal(&linked[site.operand..site.end]), None);
551 assert_eq!(
552 &linked[site.operand..site.end],
553 &bogus.to_raw().to_le_bytes()
554 );
555 assert!(
556 !program.link.targets.iter().any(|t| t.id == bogus),
557 "nothing interned for the bogus id"
558 );
559 assert!(program.resolve(bogus).is_err());
560 }
561
562 fn story_with_out_of_range_address_path_name() -> StoryData {
569 let mut data = brink_compiler::compile("main.ink", |_p| {
570 Ok("=== knot ===\nHello.\n-> END\n".to_owned())
571 })
572 .unwrap()
573 .data;
574 assert!(
575 !data.address_paths.is_empty(),
576 "compiler output should carry an address_paths table"
577 );
578 data.address_paths[0].path = NameId(u16::MAX);
579 data
580 }
581
582 #[test]
583 fn link_rejects_out_of_range_address_path_name_id() {
584 let data = story_with_out_of_range_address_path_name();
585 let result = link(&data);
586 assert!(
587 matches!(result, Err(RuntimeError::InvalidNameId(id)) if id == u16::MAX),
588 "out-of-range NameId must not link"
589 );
590 }
591}