brink-ir 0.0.2

Intermediate representations for inkle's ink narrative scripting language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
use brink_format::{CountingFlags, DefinitionId, NameId};

use crate::{AssignOp, InfixOp, PostfixOp, PrefixOp, SequenceType};

// ─── Program ─────────────────────────────────────────────────────────

/// The complete LIR program — a single merged, resolved representation
/// of all source files, ready for backend consumption.
pub struct Program {
    /// The root container — the top of the container tree.
    /// Child containers (knots, stitches, gathers, choice targets)
    /// are nested via `Container.children`.
    pub root: Container,

    /// Global variable definitions (VAR and CONST), with evaluated defaults.
    pub globals: Vec<GlobalDef>,

    /// List (enum) definitions with their items.
    pub lists: Vec<ListDef>,

    /// Individual list item definitions (each is independently addressable
    /// because bare item names are implicitly global in ink).
    pub list_items: Vec<ListItemDef>,

    /// External function declarations.
    pub externals: Vec<ExternalDef>,

    /// Interned name strings. Indexed by `NameId`. Contains definition
    /// names, variable names, list names, etc. — anything the runtime
    /// needs as a string for debugging, host binding, or inspection.
    pub name_table: Vec<String>,
}

// ─── Definitions ─────────────────────────────────────────────────────

/// A global variable or constant definition with its compile-time default.
pub struct GlobalDef {
    pub id: DefinitionId,
    pub name: NameId,
    pub mutable: bool,
    pub default: ConstValue,
}

/// A list definition.
pub struct ListDef {
    pub id: DefinitionId,
    pub name: NameId,
    /// `(item_name, ordinal)` pairs in declaration order.
    pub items: Vec<(NameId, i32)>,
}

/// A single list item, independently addressable by its `DefinitionId`.
pub struct ListItemDef {
    pub id: DefinitionId,
    pub name: NameId,
    /// The parent list definition this item belongs to.
    pub origin: DefinitionId,
    pub ordinal: i32,
}

/// An external function declaration.
pub struct ExternalDef {
    pub id: DefinitionId,
    pub name: NameId,
    pub arg_count: u8,
    /// Ink-defined fallback body container, if any.
    pub fallback: Option<DefinitionId>,
}

/// A compile-time constant value for global variable defaults and
/// const initializers. These are always statically evaluable.
#[derive(Debug, Clone, PartialEq)]
pub enum ConstValue {
    Int(i32),
    Float(f32),
    Bool(bool),
    String(String),
    /// A list value — the set of active items, plus origin lists
    /// for typed empties.
    List {
        items: Vec<DefinitionId>,
        origins: Vec<DefinitionId>,
    },
    DivertTarget(DefinitionId),
    Null,
}

// ─── Containers ──────────────────────────────────────────────────────

/// A single container — the fundamental compilation unit.
///
/// Every knot, stitch, gather, and choice target body is a container.
/// At this level there is no distinction between them — that's what
/// `kind` is for (diagnostics, debug output, and counting flag defaults).
///
/// Containers form a tree: the root contains knots, knots contain stitches
/// and choice/gather children, etc. The `children` vec holds nested containers.
pub struct Container {
    pub id: DefinitionId,
    /// Local name of this container (e.g. `"order"` for stitch `tavern.order`).
    /// `None` for the root container and anonymous gathers.
    pub name: Option<String>,
    pub kind: ContainerKind,
    /// Parameters (only meaningful for knots/stitches/functions).
    pub params: Vec<Param>,
    /// The body — a sequence of structured statements.
    pub body: Vec<Stmt>,
    /// Nested child containers (stitches, choice targets, gathers).
    pub children: Vec<Container>,
    pub counting_flags: CountingFlags,
    /// Total temp slot count for this scope. Only meaningful on scope
    /// roots (knots/functions). Child containers share the parent's
    /// call frame and use slots from this same pool.
    pub temp_slot_count: u16,
    /// Whether this container originated from a source-level label
    /// (e.g. `- (loop)` gather or `* (firstOpt) [text]` choice).
    /// Used by counting flags: labeled containers with visit references
    /// get `COUNT_START_ONLY` so self-goto loops increment correctly.
    pub labeled: bool,
    /// When true, this container is emitted inline in the parent's body
    /// contents rather than as a named entry in `named_content`. Used by
    /// the first container in a gather-choice chain (`- * hello`).
    pub inline: bool,
    /// Whether this knot is a function (`== function foo ==`).
    /// Only meaningful when `kind == ContainerKind::Knot`.
    /// Used by codegen to decide whether inklecate's implicit stitch
    /// prefix (`.0`) should be inserted in container paths.
    pub is_function: bool,
}

/// What source construct this container originated from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContainerKind {
    /// The implicit root container (top-level content before first knot).
    Root,
    /// A `== knot ==` or `== function knot ==`.
    Knot,
    /// A `= stitch` within a knot.
    Stitch,
    /// A gather (`-`) — convergence point after choices.
    Gather,
    /// The body of a selected choice.
    ChoiceTarget,
    /// A wrapper container for a sequence (stopping, cycle, once, shuffle).
    /// Uses visit counting to select the active branch.
    Sequence,
    /// A single branch body within a sequence wrapper container.
    SequenceBranch,
    /// A single branch body within a block-level conditional.
    ConditionalBranch,
}

/// A parameter on a container (knot, stitch, or function).
pub struct Param {
    pub name: NameId,
    /// The temp slot index this parameter occupies in the call frame.
    pub slot: u16,
    /// `ref` parameter — caller passes a pointer.
    pub is_ref: bool,
    /// `->` parameter — caller passes a divert target.
    pub is_divert: bool,
}

// ─── Statements ──────────────────────────────────────────────────────

/// A statement within a container body. Structured — branches and
/// choice sets preserve their shape for both backends to consume.
#[derive(Clone)]
pub enum Stmt {
    /// Emit a line of text content (with optional inline elements and tags).
    EmitContent(Content),

    /// Emit a recognized line (pattern recognizer matched).
    EmitLine(ContentEmission),

    /// Evaluate a recognized line and push the result onto the value stack.
    /// Used for choice display text that has been promoted to a line table entry.
    EvalLine(ContentEmission),

    /// Emit choice output content (start + inner) at the top of a choice
    /// target container. Emits content parts only — no newline or divert.
    /// The divert and newline are handled by the body stmts that follow.
    ///
    /// Skipped entirely by the JSON codegen — inklecate structures this
    /// content via child container references, not inline.
    ///
    /// When `emission` is `Some`, bytecode codegen uses `EmitLine` for the
    /// recognized line table entry instead of emitting inline content parts.
    ChoiceOutput {
        content: Content,
        emission: Option<ContentEmission>,
    },

    /// `-> target` — divert to another container, DONE, or END.
    Divert(Divert),

    /// `->-> target` — tunnel call (push return, enter target).
    TunnelCall(TunnelCall),

    /// `<- target` — fork a thread.
    ThreadStart(ThreadStart),

    /// `~ temp x = expr` — declare a temp variable at a slot index.
    DeclareTemp {
        slot: u16,
        name: NameId,
        value: Option<Expr>,
    },

    /// `~ x = expr` / `~ x += expr` — assign to a variable.
    Assign {
        target: AssignTarget,
        op: AssignOp,
        value: Expr,
    },

    /// `~ return expr` (function) or `->->` (tunnel return).
    Return {
        value: Option<Expr>,
        /// When true, emit `TunnelReturn` instead of `Return`.
        is_tunnel: bool,
        /// Arguments for `->-> target(args)` tunnel onwards — pushed before
        /// the divert target value on the value stack.
        args: Vec<CallArg>,
    },

    /// A set of choices presented to the player.
    ChoiceSet(ChoiceSet),

    /// Multiline `{ - cond: ... }` — block-level conditional.
    Conditional(Conditional),

    /// Multiline `{stopping: - ... - ...}` — block-level sequence.
    Sequence(Sequence),

    /// Enter a child container (used for sequence wrappers).
    EnterContainer(brink_format::DefinitionId),

    /// `~ expr` — expression evaluated for side effects.
    ExprStmt(Expr),

    /// End-of-line marker — emitted after content (and any trailing inline
    /// divert on the same line). JSON backend emits `"\n"`, bytecode backend
    /// emits `EmitNewline` opcode.
    EndOfLine,
}

/// The resolved target of an assignment.
#[derive(Clone)]
pub enum AssignTarget {
    Global(DefinitionId),
    Temp(u16, NameId),
}

// ─── Control flow ────────────────────────────────────────────────────

/// A divert — goto another container, DONE, or END.
#[derive(Clone)]
pub struct Divert {
    pub target: DivertTarget,
    pub args: Vec<CallArg>,
}

/// A tunnel call — push return point, enter target.
/// Chained tunnels (`->-> a ->-> b`) produce multiple targets.
#[derive(Clone)]
pub struct TunnelCall {
    pub targets: Vec<TunnelTarget>,
}

/// A single target in a tunnel call chain.
#[derive(Clone)]
pub struct TunnelTarget {
    pub target: DivertTarget,
    pub args: Vec<CallArg>,
}

/// A thread fork — `<- target`.
#[derive(Clone)]
pub struct ThreadStart {
    pub target: DivertTarget,
    pub args: Vec<CallArg>,
}

/// A resolved divert destination.
#[derive(Clone)]
pub enum DivertTarget {
    /// A named address.
    Address(DefinitionId),
    /// A global variable holding a divert target value — `-> x` where `x` is a global variable.
    Variable(DefinitionId),
    /// A temp/parameter variable holding a divert target value — `-> x` where `x` is a parameter.
    VariableTemp(u16, NameId),
    /// `-> DONE` — pause execution, can resume.
    Done,
    /// `-> END` — permanently end the story.
    End,
}

/// An argument at a call site, with ref-passing resolved.
#[derive(Clone)]
pub enum CallArg {
    /// A normal value argument.
    Value(Expr),
    /// `ref` argument targeting a global variable — emits `PushVarPointer`.
    RefGlobal(DefinitionId),
    /// `ref` argument targeting a temp variable — emits `PushTempPointer`.
    RefTemp(u16, NameId),
}

// ─── Choice sets ─────────────────────────────────────────────────────

/// A set of choices presented to the player, with container boundaries
/// already decided.
#[derive(Clone)]
pub struct ChoiceSet {
    pub choices: Vec<Choice>,
    /// The gather container that loose-end choices implicitly divert to.
    /// `None` if all choices have explicit diverts.
    pub gather_target: Option<DefinitionId>,
}

/// A single choice within a choice set.
///
/// Content is stored as the original three-part split from the HIR:
/// - `start_content` = text before `[` — shared between display and output
/// - `choice_only_content` = text inside `[...]` — display only
/// - `inner_content` = text after `]` — output only
///
/// The choice body lives in a separate `Container` referenced by `target`.
#[derive(Clone)]
pub struct Choice {
    /// `+` (sticky) vs `*` (once-only).
    pub is_sticky: bool,
    /// Invisible default choice (fallback).
    pub is_fallback: bool,
    /// Condition expression — choice is only available when true.
    pub condition: Option<Expr>,
    /// Text before `[` — appears in both choice list and output.
    pub start_content: Option<Content>,
    /// Text inside `[...]` — appears only in the choice list.
    pub choice_only_content: Option<Content>,
    /// Text after `]` — appears only after selection.
    pub inner_content: Option<Content>,
    /// Recognized display text (start+bracket) for the line table.
    /// `Some` when pattern recognition succeeds on the composed display content.
    pub display_emission: Option<ContentEmission>,
    /// Recognized output text (start+inner) for the line table.
    /// `Some` when pattern recognition succeeds on the composed output content.
    pub output_emission: Option<ContentEmission>,
    /// The container holding the choice body (content after selection).
    pub target: DefinitionId,
    pub tags: Vec<Vec<ContentPart>>,
}

// ─── Conditionals and sequences ──────────────────────────────────────

/// Distinguishes the semantic forms of conditional blocks in LIR.
#[derive(Clone)]
pub enum CondKind {
    /// The first branch's condition is the initial condition of the
    /// conditional itself (emitted flat, not wrapped in the branch container).
    InitialCondition,
    /// Each branch has an independent boolean condition (wrapped inside
    /// its own container).
    IfElse,
    /// One expression evaluated once; each branch is a case value compared with `==`.
    Switch(Expr),
}

/// A block-level conditional with resolved branch conditions.
#[derive(Clone)]
pub struct Conditional {
    pub kind: CondKind,
    pub branches: Vec<CondBranch>,
}

/// A single branch in a conditional.
#[derive(Clone)]
pub struct CondBranch {
    /// `None` for the else branch.
    pub condition: Option<Expr>,
    pub body: Vec<Stmt>,
}

/// A block-level sequence (stopping, cycle, once, shuffle).
#[derive(Clone)]
pub struct Sequence {
    pub kind: SequenceType,
    pub branches: Vec<Vec<Stmt>>,
}

// ─── Recognized content (pattern recognizer output) ──────────────────

/// Metadata computed during recognition while HIR provenance is available.
#[derive(Clone)]
pub struct LineMetadata {
    pub source_hash: u64,
    pub slot_info: Vec<brink_format::SlotInfo>,
    pub source_location: Option<brink_format::SourceLocation>,
}

/// A recognized line pattern from content analysis.
#[derive(Clone)]
pub enum RecognizedLine {
    Plain(String),
    Template {
        parts: Vec<brink_format::LinePart>,
        slot_exprs: Vec<Expr>,
    },
}

/// Result of pattern recognition on a content line.
#[derive(Clone)]
pub struct ContentEmission {
    pub line: RecognizedLine,
    pub metadata: LineMetadata,
    pub tags: Vec<Vec<ContentPart>>,
}

// ─── Content and inline elements ─────────────────────────────────────

/// A line of text output with inline elements and tags.
///
/// Each `Content` maps to one line table entry in the bytecode output.
/// Backends decide the entry format: plain text for content with no
/// dynamic parts, or a template with slots for interpolated content.
#[derive(Clone)]
pub struct Content {
    pub parts: Vec<ContentPart>,
    pub tags: Vec<Vec<ContentPart>>,
}

/// A fragment within a content line.
#[derive(Clone)]
pub enum ContentPart {
    /// Literal text.
    Text(String),
    /// `<>` — glue (suppresses line break).
    Glue,
    /// Word-break spring — conditional space resolved by the runtime.
    Spring,
    /// `{expr}` — interpolated expression, resolved.
    Interpolation(Expr),
    /// `{cond: a | b}` — inline conditional with resolved conditions.
    InlineConditional(Conditional),
    /// `{&a|b|c}` — inline sequence.
    InlineSequence(Sequence),
    /// Enter a child sequence container (inline sequence wrapper).
    EnterSequence(brink_format::DefinitionId),
}

// ─── Expressions ─────────────────────────────────────────────────────

/// A resolved expression. All paths have been replaced with concrete
/// targets (global `DefinitionId`, temp slot, visit count, etc.).
#[derive(Clone)]
pub enum Expr {
    // ── Literals ─────────────────────────────────────────────────
    Int(i32),
    Float(f32),
    Bool(bool),
    String(StringExpr),
    Null,

    // ── Resolved references ─────────────────────────────────────
    /// Read a global variable (VAR, CONST, or list variable).
    GetGlobal(DefinitionId),
    /// Read a temp variable by slot index and name.
    GetTemp(u16, NameId),
    /// The visit count of a container (knot/stitch/label name used
    /// in expression context).
    VisitCount(DefinitionId),
    /// A divert target as a value (`-> knot` in expression context).
    DivertTarget(DefinitionId),
    /// A list literal — set of active item `DefinitionId`s, plus
    /// origin list `DefinitionId`s for typed empties.
    ListLiteral {
        items: Vec<DefinitionId>,
        origins: Vec<DefinitionId>,
    },

    // ── Operations ──────────────────────────────────────────────
    Prefix(PrefixOp, Box<Expr>),
    Infix(Box<Expr>, InfixOp, Box<Expr>),
    Postfix(Box<Expr>, PostfixOp),

    // ── Calls ───────────────────────────────────────────────────
    /// Call a knot/stitch as a function (ink `== function`).
    Call {
        target: DefinitionId,
        args: Vec<CallArg>,
    },
    /// Call an external function.
    CallExternal {
        target: DefinitionId,
        args: Vec<CallArg>,
        arg_count: u8,
    },
    /// Call a function through a global variable holding a divert target.
    CallVariable {
        target: DefinitionId,
        args: Vec<CallArg>,
    },
    /// Call a function through a temp/param variable holding a divert target.
    CallVariableTemp {
        slot: u16,
        name: NameId,
        args: Vec<CallArg>,
    },
    /// Call a built-in function (`TURNS_SINCE`, `LIST_COUNT`, etc.).
    CallBuiltin {
        builtin: BuiltinFn,
        args: Vec<Expr>,
    },
}

impl Expr {
    /// Returns true if this expression is a function call that may produce
    /// localized text output (`Call`, `CallVariable`, `CallVariableTemp`, `CallExternal`).
    /// Builtins (`TURNS_SINCE`, `LIST_COUNT`, etc.) are not included — they
    /// produce numeric/list values, not localized text.
    pub fn is_function_call(&self) -> bool {
        matches!(
            self,
            Self::Call { .. }
                | Self::CallVariable { .. }
                | Self::CallVariableTemp { .. }
                | Self::CallExternal { .. }
        )
    }
}

/// A string literal, possibly with interpolation.
#[derive(Clone)]
pub struct StringExpr {
    pub parts: Vec<StringPart>,
}

/// A part of a string literal.
#[derive(Clone)]
pub enum StringPart {
    /// Literal text.
    Literal(String),
    /// `{expr}` — interpolation within a string, resolved.
    Interpolation(Box<Expr>),
}

// ─── Built-in functions ──────────────────────────────────────────────

/// Ink built-in functions that compile to dedicated opcodes rather
/// than container calls.
///
/// These are recognized by name during HIR → LIR lowering. The analyzer
/// does not resolve them (they have no declaration) — LIR lowering
/// intercepts `Expr::Call` nodes whose paths match known built-in names.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuiltinFn {
    // ── Intrinsics ──────────────────────────────────────────────
    /// `TURNS_SINCE(-> container)` → turns since container was visited.
    TurnsSince,
    /// `READ_COUNT(-> container)` → visit count of container.
    ReadCount,
    /// `TURNS()` → current turn index (0-based).
    Turns,
    /// `CHOICE_COUNT()` → number of currently available choices.
    ChoiceCount,
    /// `RANDOM(min, max)` → random integer in range.
    Random,
    /// `SEED_RANDOM(seed)` → seed the RNG.
    SeedRandom,

    // ── Casts ───────────────────────────────────────────────────
    /// `INT(x)` → cast to integer.
    CastToInt,
    /// `FLOAT(x)` → cast to float.
    CastToFloat,

    // ── Math ────────────────────────────────────────────────────
    /// `FLOOR(x)` → floor.
    Floor,
    /// `CEILING(x)` → ceiling.
    Ceiling,
    /// `POW(a, b)` → exponentiation.
    Pow,
    /// `MIN(a, b)` → minimum.
    Min,
    /// `MAX(a, b)` → maximum.
    Max,

    // ── List operations ─────────────────────────────────────────
    /// `LIST_COUNT(list)` → number of set items.
    ListCount,
    /// `LIST_MIN(list)` → item with lowest ordinal.
    ListMin,
    /// `LIST_MAX(list)` → item with highest ordinal.
    ListMax,
    /// `LIST_ALL(list)` → all items from the list's origin.
    ListAll,
    /// `LIST_INVERT(list)` → complement (all items NOT in the set).
    ListInvert,
    /// `LIST_RANGE(list, min, max)` → subset by ordinal range.
    ListRange,
    /// `LIST_RANDOM(list)` → random item from the set.
    ListRandom,
    /// `LIST_VALUE(item)` → ordinal value as integer.
    ListValue,
    /// `LIST_FROM_INT(list_origin, value)` → item by ordinal.
    ListFromInt,
}