brink_format/definition.rs
1use crate::counting::CountingFlags;
2use crate::id::{DefinitionId, NameId};
3use crate::line::LineContent;
4use crate::value::{Value, ValueType};
5
6/// A compiled container (knot, stitch, gather, or anonymous flow block).
7#[derive(Debug, Clone, PartialEq)]
8pub struct ContainerDef {
9 pub id: DefinitionId,
10 /// The lexical scope this container belongs to.
11 /// For scope containers (root, knot, stitch): `scope_id == id`.
12 /// For child containers (gather, choice target, sequence, etc.): `scope_id` is
13 /// the enclosing scope's `DefinitionId`.
14 pub scope_id: DefinitionId,
15 /// Human-readable name for scope-owning containers (root, knot, stitch).
16 /// `None` for child containers.
17 pub name: Option<NameId>,
18 pub bytecode: Vec<u8>,
19 pub counting_flags: CountingFlags,
20 /// Sum of char values from the container's ink path string.
21 /// Used to seed the RNG for shuffle sequences.
22 pub path_hash: i32,
23 /// Number of parameters this container declares (a parameterized knot,
24 /// stitch, or function — e.g. `=== call(action, present) ===` has 2). The
25 /// container's prologue binds them with that many leading `DeclareTemp`s.
26 /// `0` for the vast majority of containers. Lets the runtime arity-check a
27 /// host-directed entry (`choose_path_string_with_args`) or `call_function`.
28 /// The converter reference pipeline leaves this `0` (inklecate's JSON does
29 /// not expose it); only the brink compiler populates the true count.
30 pub param_count: u8,
31}
32
33/// Metadata for a single interpolation slot in a template line.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct SlotInfo {
36 pub index: u8,
37 pub name: String,
38}
39
40/// Source location of a line in the original `.ink` file.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct SourceLocation {
43 pub file: String,
44 pub range_start: u32,
45 pub range_end: u32,
46}
47
48/// One entry in a container's line table.
49#[derive(Debug, Clone, PartialEq)]
50pub struct LineEntry {
51 pub content: LineContent,
52 pub flags: crate::LineFlags,
53 pub source_hash: u64,
54 pub audio_ref: Option<String>,
55 pub slot_info: Vec<SlotInfo>,
56 pub source_location: Option<SourceLocation>,
57}
58
59/// A locale line entry — content + optional audio, no source metadata.
60#[derive(Debug, Clone, PartialEq)]
61pub struct LocaleLineEntry {
62 pub content: LineContent,
63 pub audio_ref: Option<String>,
64}
65
66/// A per-scope locale line table.
67#[derive(Debug, Clone, PartialEq)]
68pub struct LocaleScopeTable {
69 pub scope_id: DefinitionId,
70 pub lines: Vec<LocaleLineEntry>,
71}
72
73/// Complete locale overlay data from a `.inkl` file.
74#[derive(Debug, Clone, PartialEq)]
75pub struct LocaleData {
76 pub locale_tag: String,
77 pub base_checksum: u32,
78 pub line_tables: Vec<LocaleScopeTable>,
79}
80
81/// Per-scope line table, stored separately from [`ContainerDef`] for
82/// locale overlay swapping (`.inkl`).
83///
84/// All containers within a lexical scope (knot, stitch, or root) share one
85/// `ScopeLineTable`. `EmitLine(idx)` indices are scope-relative.
86#[derive(Debug, Clone, PartialEq)]
87pub struct ScopeLineTable {
88 pub scope_id: DefinitionId,
89 pub lines: Vec<LineEntry>,
90}
91
92/// A global variable definition.
93#[derive(Debug, Clone, PartialEq)]
94pub struct GlobalVarDef {
95 pub id: DefinitionId,
96 pub name: NameId,
97 pub value_type: ValueType,
98 pub default_value: Value,
99 pub mutable: bool,
100}
101
102/// A list (enum-like set) definition.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ListDef {
105 pub id: DefinitionId,
106 pub name: NameId,
107 /// `(item_name, ordinal)` pairs in declaration order.
108 pub items: Vec<(NameId, i32)>,
109}
110
111/// A single list item definition.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct ListItemDef {
114 pub id: DefinitionId,
115 pub origin: DefinitionId,
116 pub ordinal: i32,
117 pub name: NameId,
118}
119
120/// An address pointing to a specific byte offset within a container.
121///
122/// Addresses are used for divert targets, visit tracking, and any definition
123/// that maps to a position within a container. A "primary" address has
124/// `byte_offset == 0` and the same `id` as its `container_id`, functioning
125/// like the old `Container` tag. Intra-container addresses have non-zero
126/// offsets and distinct IDs.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct AddressDef {
129 pub id: DefinitionId,
130 pub container_id: DefinitionId,
131 pub byte_offset: u32,
132}
133
134/// Maps a qualified author path (e.g. `knot`, `knot.stitch`, `knot.label`,
135/// `knot.stitch.label`) to the [`DefinitionId`] it addresses.
136///
137/// This is the source of truth for path → address lookup
138/// ([`Program::find_address`](../../brink_runtime/struct.Program.html#method.find_address)):
139/// the linker resolves each `target` through its address map. The compiler
140/// emits one entry per scope container (knot/stitch) and per author-labeled
141/// gather/choice. `path` indexes the name table; `target` is the addressed
142/// container/label id.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct AddressPath {
145 pub path: NameId,
146 pub target: DefinitionId,
147}
148
149/// Compute a deterministic hash of line content text.
150///
151/// Used by both the compiler codegen and the converter to populate
152/// [`LineEntry::source_hash`]. The hash detects when source text has
153/// changed across builds, enabling the regeneration workflow in the
154/// internationalization pipeline.
155pub fn content_hash(text: &str) -> u64 {
156 use std::hash::{Hash, Hasher};
157 let mut hasher = std::collections::hash_map::DefaultHasher::new();
158 text.hash(&mut hasher);
159 hasher.finish()
160}
161
162/// An externally-bound function definition.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct ExternalFnDef {
165 pub id: DefinitionId,
166 pub name: NameId,
167 pub arg_count: u8,
168 pub fallback: Option<DefinitionId>,
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn content_hash_deterministic() {
177 let a = content_hash("Hello, world!");
178 let b = content_hash("Hello, world!");
179 assert_eq!(a, b);
180 }
181
182 #[test]
183 fn content_hash_non_zero_for_non_empty() {
184 assert_ne!(content_hash("some text"), 0);
185 assert_ne!(content_hash("x"), 0);
186 }
187
188 #[test]
189 fn content_hash_differs_for_different_input() {
190 assert_ne!(content_hash("hello"), content_hash("world"));
191 }
192}