brink_format/definition.rs
1use alloc::string::String;
2use alloc::vec::Vec;
3
4use crate::counting::CountingFlags;
5use crate::id::{DefinitionId, NameId};
6use crate::line::LineContent;
7use crate::value::{ShapeId, Value, ValueType};
8
9/// A compiled container (knot, stitch, gather, or anonymous flow block).
10#[derive(Debug, Clone, PartialEq)]
11pub struct ContainerDef {
12 pub id: DefinitionId,
13 /// The lexical scope this container belongs to.
14 /// For scope containers (root, knot, stitch): `scope_id == id`.
15 /// For child containers (gather, choice target, sequence, etc.): `scope_id` is
16 /// the enclosing scope's `DefinitionId`.
17 pub scope_id: DefinitionId,
18 /// Human-readable name for scope-owning containers (root, knot, stitch).
19 /// `None` for child containers.
20 pub name: Option<NameId>,
21 pub bytecode: Vec<u8>,
22 pub counting_flags: CountingFlags,
23 /// Sum of char values from the container's ink path string.
24 /// Used to seed the RNG for shuffle sequences.
25 pub path_hash: i32,
26 /// Number of parameters this container declares (a parameterized knot,
27 /// stitch, or function — e.g. `=== call(action, present) ===` has 2). The
28 /// container's prologue binds them with that many leading `DeclareTemp`s.
29 /// `0` for the vast majority of containers. Lets the runtime arity-check a
30 /// host-directed entry (`choose_path_string_with_args`) or `call_function`.
31 /// The converter reference pipeline leaves this `0` (inklecate's JSON does
32 /// not expose it); only the brink compiler populates the true count.
33 pub param_count: u8,
34 /// Per-parameter name and mode metadata, in declared order (T1c,
35 /// `docs/t1c-spec.md` §6). Empty for the vast majority of containers.
36 ///
37 /// Carried so the runtime can validate a **rehydrated function value**
38 /// against the *current* signature: a `#fn`/closure saved before a
39 /// recompile stores its bound params' names and modes, and on load/invoke
40 /// they are checked against this table — a renamed or re-moded param is a
41 /// defined fault, never a silent misbinding (spec §6). `len()` always
42 /// equals [`param_count`](Self::param_count); both are kept (the count is
43 /// the pre-T1c arity-check field, the metadata is additive). The converter
44 /// reference pipeline leaves this empty.
45 pub params: Vec<ParamMeta>,
46 /// Compiled scope default: `true` for a flow-private (`#@local`) knot or
47 /// stitch. Only ever set on scope-owning containers; subtree coverage of
48 /// interior containers is resolved by the runtime at policy resolution
49 /// (`docs/directive-annotations-spec.md`). The converter always emits
50 /// `false` (inklecate has no flow-private concept).
51 pub local: bool,
52}
53
54/// Name + mode of one declared parameter of a container (T1c,
55/// `docs/t1c-spec.md` §6). See [`ContainerDef::params`].
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct ParamMeta {
58 /// The parameter's interned name.
59 pub name: NameId,
60 /// `true` if declared `ref`, `false` for a by-value param.
61 pub is_ref: bool,
62}
63
64/// Metadata for a single interpolation slot in a template line.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct SlotInfo {
67 pub index: u8,
68 pub name: String,
69}
70
71/// Source location of a line in the original `.ink` file.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct SourceLocation {
74 pub file: String,
75 pub range_start: u32,
76 pub range_end: u32,
77}
78
79/// One entry in a container's line table.
80#[derive(Debug, Clone, PartialEq)]
81pub struct LineEntry {
82 pub content: LineContent,
83 pub flags: crate::LineFlags,
84 pub source_hash: u64,
85 pub audio_ref: Option<String>,
86 pub slot_info: Vec<SlotInfo>,
87 pub source_location: Option<SourceLocation>,
88}
89
90/// A locale line entry — content + optional audio, no source metadata.
91#[derive(Debug, Clone, PartialEq)]
92pub struct LocaleLineEntry {
93 pub content: LineContent,
94 pub audio_ref: Option<String>,
95}
96
97/// A per-scope locale line table.
98#[derive(Debug, Clone, PartialEq)]
99pub struct LocaleScopeTable {
100 pub scope_id: DefinitionId,
101 pub lines: Vec<LocaleLineEntry>,
102}
103
104/// Complete locale overlay data from a `.inkl` file.
105#[derive(Debug, Clone, PartialEq)]
106pub struct LocaleData {
107 pub locale_tag: String,
108 pub base_checksum: u32,
109 pub line_tables: Vec<LocaleScopeTable>,
110}
111
112/// Per-scope line table, stored separately from [`ContainerDef`] for
113/// locale overlay swapping (`.inkl`).
114///
115/// All containers within a lexical scope (knot, stitch, or root) share one
116/// `ScopeLineTable`. `EmitLine(idx)` indices are scope-relative.
117#[derive(Debug, Clone, PartialEq)]
118pub struct ScopeLineTable {
119 pub scope_id: DefinitionId,
120 pub lines: Vec<LineEntry>,
121}
122
123/// A global variable definition.
124#[derive(Debug, Clone, PartialEq)]
125pub struct GlobalVarDef {
126 pub id: DefinitionId,
127 pub name: NameId,
128 pub value_type: ValueType,
129 pub default_value: Value,
130 pub mutable: bool,
131 /// Compiled scope default: `true` for a flow-private (`#@local`)
132 /// variable, `false` for ordinary shared state. Consumed by the runtime
133 /// as the base layer of `WorldPolicy` resolution
134 /// (`docs/directive-annotations-spec.md`).
135 pub local: bool,
136}
137
138/// A list (enum-like set) definition.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct ListDef {
141 pub id: DefinitionId,
142 pub name: NameId,
143 /// `(item_name, ordinal)` pairs in declaration order.
144 pub items: Vec<(NameId, i32)>,
145}
146
147/// A `STRUCT` shape definition (TM-4, `docs/typed-mode-spec.md` §6;
148/// `StructShapes` section, `docs/format-spec.md` tag `0x0C`).
149///
150/// Closed shape: `fields` is the ordered set of declared field names — the
151/// same order [`crate::value::Value::Record`]'s flat field vector follows,
152/// and the order `RecordNew`/static `RecordGet`/`RecordSet` offsets index
153/// into.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct StructShapeDef {
156 pub id: ShapeId,
157 pub name: NameId,
158 pub fields: Vec<NameId>,
159}
160
161/// One old→new `DefinitionId` rename record (M-3, `docs/modules-spec.md`
162/// §5): the compiler emits one of these per `#@was(old_name)` directive —
163/// on a module, one entry per definition the renamed module currently
164/// owns; on a single definition, one entry for it. Rehydration
165/// (`brink-runtime`'s `load_state`) consults the table **only on the miss
166/// path**: a saved fn token, divert value, or visit-count key that the
167/// current program doesn't recognize is looked up here before being
168/// treated as genuinely gone.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
170pub struct AliasEntry {
171 /// The identity a save from before the rename may still carry.
172 pub old: DefinitionId,
173 /// The definition's current identity.
174 pub new: DefinitionId,
175}
176
177/// The capability-parameter slot carried by every call atom in a factored
178/// effect row (T2-3, `docs/effects-spec.md` §11; ruled 2026-07-14,
179/// `docs/t1d-spec.md` §7).
180///
181/// v1 populates every atom as [`CapabilityParam::Any`] — component-granular,
182/// the whole capability unrefined. Path-granular refinement (#826) and the
183/// instance-resolving handle parameter are later narrowing rungs; their
184/// discriminants are reserved (the strict reader rejects them until a section
185/// version graduates them), the same reservation discipline the projection
186/// range segment follows.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188pub enum CapabilityParam {
189 /// The whole capability, unrefined — the only value v1 ever emits.
190 #[default]
191 Any,
192}
193
194/// A single call atom in an effect row's direct part (`docs/effects-spec.md`
195/// §2/§11): the name of an `EXTERNAL` binding (a call-kind), plus its
196/// [capability-parameter slot](CapabilityParam) and a **reserved
197/// handle-parameter slot**.
198///
199/// The handle-parameter slot (`docs/t1d-spec.md` §7) is where a
200/// handle-parameterized atom (`Transform(@argN)`) will record which minted
201/// handle bounds the capability; v1 leaves it `None` (the reserved wire byte is
202/// `0`). Possession-bounded capabilities are the tier-2 security model — out of
203/// scope for this slice, but the slot ships now so the row encoding need not
204/// change to carry it later.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct CallAtom {
207 /// The `EXTERNAL` binding name (interned into the story's `name_table`).
208 pub name: NameId,
209 /// Capability-parameter slot — v1 always [`CapabilityParam::Any`].
210 pub capability: CapabilityParam,
211 /// Reserved handle-parameter slot — v1 always `None`. A non-`None` value
212 /// is never emitted in this section version.
213 pub handle_param: Option<u8>,
214}
215
216/// The direct part of a factored effect row (`docs/effects-spec.md` §7/§11):
217/// the atoms a definition (and everything it statically calls) may perform,
218/// independent of any dispatch-cell narrowing. Mirrors the analyzer's flat
219/// `EffectRow`, lowered to wire vocabulary (cells as [`DefinitionId`]s, call
220/// kinds as [`CallAtom`]s).
221///
222/// Sets are stored as vectors already sorted/deduplicated by the producer so
223/// the encoding is deterministic (the analyzer sources them from `BTreeSet`s).
224#[derive(Debug, Clone, Default, PartialEq, Eq)]
225#[expect(
226 clippy::struct_excessive_bools,
227 reason = "the four bools are independent wire dimensions of one factored \
228 effect row (opaque + the NS-A2 emits/tags/faults flags), not a \
229 state machine in disguise — mirrors the analyzer's EffectRow"
230)]
231pub struct DirectEffects {
232 /// Global cells this row may read.
233 pub reads: Vec<DefinitionId>,
234 /// Global cells this row may write.
235 pub writes: Vec<DefinitionId>,
236 /// Call-kind atoms this row may transitively perform.
237 pub calls: Vec<CallAtom>,
238 /// The pessimal top element (`docs/effects-spec.md` §3): this row performs
239 /// a call whose effects inference cannot summarize.
240 pub opaque: bool,
241 /// NS-A2 (issue #1108, from #1087): the definition may produce content —
242 /// narration/dialogue fragments a host renders (glue-only output counts;
243 /// tag-only lines do NOT — those set [`Self::tags`]). Bool v1.
244 pub emits: bool,
245 /// NS-A2 (issue #1108, from #1087's second ruling): the definition may
246 /// touch the tag channel. Independent of [`Self::emits`]. Bool v1.
247 pub tags: bool,
248 /// NS-A2 (issue #1108, from #1097): the definition may raise a
249 /// turn-terminating fault. Bool v1 — per-fault-kind granularity is the
250 /// reserved refinement and graduates via a section-version bump, the
251 /// same reservation discipline the capability/handle slots follow.
252 pub faults: bool,
253}
254
255/// A per-dispatch entry in a factored effect row (`docs/effects-spec.md` §7):
256/// the row a call through a dispatch `cell` contributes, whether that dispatch
257/// is runtime-**narrowable** (its cell is not in the entry's own write set),
258/// and the **static fallback** row used when narrowing does not apply.
259///
260/// v1 emits none of these (call-through-value is inferred as opaque, folded
261/// into the direct part) — but the encoding ships the structure now, because a
262/// flat row structurally forecloses the §7 narrowing the host will do at
263/// schedule-commit. The reader round-trips a populated dispatch list so writer
264/// and reader stay paired (the #742 lesson).
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct DispatchEntry {
267 /// The dispatch cell whose live fn tokens the host may narrow against.
268 pub cell: DefinitionId,
269 /// Whether this dispatch is statically narrowable (`docs/effects-spec.md`
270 /// §7 soundness gate: the cell is not in the entry's own write set).
271 pub narrowable: bool,
272 /// The static fallback row — the conservative join used when the host does
273 /// not (or cannot) narrow.
274 pub fallback: DirectEffects,
275}
276
277/// One entry in the `EffectRows` `DefinitionId → row` table (T2-3,
278/// `docs/effects-spec.md` §11, `docs/format-v4-rfc.md` §2 `EffectRows`
279/// reservation): a factored effect row for one definition.
280///
281/// Every knot/stitch ships one — the per-container row is the host's
282/// resume-scheduling estimate (`docs/effects-spec.md` §12.1: a flow resumes
283/// from wherever it parked). The row is additive metadata; the runtime does
284/// not consume it yet (`sleep`/narrowing are the future clients), so a story
285/// carrying rows is byte-identical in behavior to one without.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct EffectRowEntry {
288 /// The definition (knot/stitch) this row summarizes.
289 pub def: DefinitionId,
290 /// The freeze bit (#882, `docs/effects-spec.md` §10 sitting-2 ruling;
291 /// corroborated on `main` by `docs/modules-spec.md` §4 boundary rule 1/2
292 /// and the 2026-07-14 "Modules & visibility rulings" decision-log entry —
293 /// `docs/effects-spec.md` itself never merged past `docs/effects-skeleton`).
294 ///
295 /// `true` for every def by default: the row is a legitimate host
296 /// **entry point** (host-callable by path/name — `begin_function_eval`,
297 /// entry lookup, play-from-here). `false` for a `#@private` definition:
298 /// **not an entry** — host-facing name lookup on it is refused (the
299 /// load-error class effects-spec §10 rules) — but the row itself is not
300 /// dropped from the table. `#@private` hides the *name*, not the *cell*:
301 /// a private knot/stitch can still be captured as a first-class fn-value
302 /// token that a *public* path holds and later calls through, and the
303 /// dispatch-narrowing machinery (§7) resolves such a token by
304 /// `DefinitionId`, not by name — so the row must stay resolvable in this
305 /// table regardless of `is_entry`. This is unconditional (never a
306 /// reachability computation over whether some public path actually holds
307 /// such a token today): proving that would need whole-program fn-value
308 /// capture analysis, which conservative-total rows do not attempt.
309 /// Dev-tooling (play-from-here, `brink ide` effects-diff) is the
310 /// documented visibility override (modules-spec §4 rule 3) and may read
311 /// a non-entry row directly from this table; only *host* semantic lookup
312 /// respects `is_entry`.
313 pub is_entry: bool,
314 /// The direct part — atoms independent of dispatch narrowing.
315 pub direct: DirectEffects,
316 /// Per-dispatch entries (empty in v1).
317 pub dispatches: Vec<DispatchEntry>,
318}
319
320/// The name-keyed **frame shape** for one `await` site
321/// (`docs/flow-suspension-spec.md` §4/§11): the static description of which
322/// locals cross the park at that site, so the runtime knows what to
323/// spill on park and restore on wake.
324///
325/// Emitted into the `FrameShapes` [`StoryData`](crate::StoryData) section
326/// (`.inkb` tag `0x10`,
327/// `.inkt` `(frame_shapes …)`). The shape is **name-keyed** — the runtime
328/// spills/restores crossing locals by name, riding the same rehydration
329/// machinery as `#@was`/saves (spec §7), so a frame survives recompiles
330/// without instruction offsets (spec §2/§3).
331///
332/// **Reserved-through-fence**: the FS-3c compiler slice lands this section's
333/// encoding (writer + reader + round-trips) but does not yet *emit* a
334/// non-empty table — the E052 `await` lowering fence keeps `await` from
335/// producing any `StoryData`. First emission rides the continuation-splitting
336/// codegen when the fence drops (FS-3r), the same reserved-then-materialized
337/// discipline `StructShapes` followed. `frame_shapes` is therefore empty for
338/// every story compiled today (and for all converter output).
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct FrameShapeDef {
341 /// The `await` site's stable identity — the [`DefinitionId`] of the
342 /// synthesized resume/continuation container (spec §11.1: stable identity
343 /// = module + enclosing def + site index). This is both the wake-policy
344 /// site id and the container the runtime enters from its top on resume.
345 pub site: DefinitionId,
346 /// The name-keyed crossing locals, in stable declared order (spec §4).
347 /// Each entry is the local's interned [`NameId`] (into the story's
348 /// `name_table`); the runtime's frame record is keyed by these names.
349 /// Values are not stored here — the shape is static; the live values live
350 /// in the save-time `SuspendedFlow.frame` (`docs/flow-suspension-spec.md`
351 /// §2, FS-1).
352 pub slots: Vec<NameId>,
353}
354
355/// A single list item definition.
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub struct ListItemDef {
358 pub id: DefinitionId,
359 pub origin: DefinitionId,
360 pub ordinal: i32,
361 pub name: NameId,
362}
363
364/// An address pointing to a specific byte offset within a container.
365///
366/// Addresses are used for divert targets, visit tracking, and any definition
367/// that maps to a position within a container. A "primary" address has
368/// `byte_offset == 0` and the same `id` as its `container_id`, functioning
369/// like the old `Container` tag. Intra-container addresses have non-zero
370/// offsets and distinct IDs.
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub struct AddressDef {
373 pub id: DefinitionId,
374 pub container_id: DefinitionId,
375 pub byte_offset: u32,
376}
377
378/// Maps a qualified author path (e.g. `knot`, `knot.stitch`, `knot.label`,
379/// `knot.stitch.label`) to the [`DefinitionId`] it addresses.
380///
381/// This is the source of truth for path → address lookup
382/// ([`Program::find_address`](../../brink_runtime/struct.Program.html#method.find_address)):
383/// the linker resolves each `target` through its address map. The compiler
384/// emits one entry per scope container (knot/stitch) and per author-labeled
385/// gather/choice. `path` indexes the name table; `target` is the addressed
386/// container/label id.
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub struct AddressPath {
389 pub path: NameId,
390 pub target: DefinitionId,
391}
392
393/// Compute a deterministic hash of line content text.
394///
395/// Used by both the compiler codegen and the converter to populate
396/// [`LineEntry::source_hash`]. The hash detects when source text has
397/// changed across builds, enabling the regeneration workflow in the
398/// internationalization pipeline.
399pub fn content_hash(text: &str) -> u64 {
400 #[cfg(feature = "std")]
401 {
402 use std::hash::{Hash, Hasher};
403 let mut hasher = std::collections::hash_map::DefaultHasher::new();
404 text.hash(&mut hasher);
405 hasher.finish()
406 }
407 #[cfg(not(feature = "std"))]
408 {
409 // `std::collections::hash_map::DefaultHasher` isn't available
410 // without `std`. This is a plain FNV-1a fallback: still
411 // deterministic, but NOT bit-identical to the `std` path above —
412 // nothing compares hashes produced by the two builds against each
413 // other, so that's fine.
414 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
415 for byte in text.as_bytes() {
416 hash ^= u64::from(*byte);
417 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
418 }
419 hash
420 }
421}
422
423/// An externally-bound function definition.
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub struct ExternalFnDef {
426 pub id: DefinitionId,
427 pub name: NameId,
428 pub arg_count: u8,
429 pub fallback: Option<DefinitionId>,
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn content_hash_deterministic() {
438 let a = content_hash("Hello, world!");
439 let b = content_hash("Hello, world!");
440 assert_eq!(a, b);
441 }
442
443 #[test]
444 fn content_hash_non_zero_for_non_empty() {
445 assert_ne!(content_hash("some text"), 0);
446 assert_ne!(content_hash("x"), 0);
447 }
448
449 #[test]
450 fn content_hash_differs_for_different_input() {
451 assert_ne!(content_hash("hello"), content_hash("world"));
452 }
453}