brink-ir 0.0.17

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
//! Opaque source provenance for IR nodes (contract Q1(b), issue #1148).
//!
//! Replaces the ink-CST-welded `AstPtr<ast::X>` / `SyntaxNodePtr` /
//! `ContainerPtr` fields (`docs/hir-admission-contract.md` D1): every HIR
//! node carries a [`Provenance`] — file + range + a node-kind token — and
//! the pipeline treats it as plain data. Resolving provenance back to a
//! live syntax node is delegated to the *producing frontend* via
//! [`ProvenanceResolver`]; a headless compile never resolves provenance
//! (contract §4.3), which is precisely why native codegen can ship before
//! native IDE support.
//!
//! [`Provenance`] is deliberately **plain, publicly constructible data** —
//! no frontend handle, no tree reference, no private field. A value
//! reconstructed from serialized parts (e.g. a future debug-info section:
//! bytecode offset → stored `(file, range, token)` → resolver → live node)
//! is indistinguishable from the value the frontend originally stamped,
//! and resolvers must treat it identically. The module lives at the crate
//! root (not under `hir`) because LIR reuses the same type verbatim in a
//! later epic.
//!
//! The token ([`KindToken`]) has two halves with different visibility
//! contracts:
//!
//! - [`KindToken::class`] — a frontend-agnostic [`NodeClass`] with a
//!   **stable public `u16` repr**, the **only** part of the token the
//!   pipeline may interpret. It carries the former `ContainerPtr`
//!   variant-discrimination role (F-I#5, the #626 floating-stitch trap): a
//!   top-level `= stitch` promoted to knot status keeps
//!   [`NodeClass::Stitch`] while a real `== knot` carries
//!   [`NodeClass::Knot`]. B0.3's admission validator checks this class
//!   against the indexed `SymbolKind`.
//! - [`KindToken::raw`] — the producing frontend's own syntax-kind value,
//!   opaque to everyone but that frontend's resolver. The ink frontend
//!   stamps `SyntaxKind as u16`; the native frontend (B0.5+) will stamp
//!   its own kind space. The pipeline must never branch on `raw`, and its
//!   values are **not** stable across frontends or versions.
//!
//! All provenance types are `Copy + Eq + Hash` — ranges keep their dual
//! cache-poison/identity-key role (contract F-J: salsa early-cutoff
//! compares `HirFile`s structurally, provenance included).

use rowan::TextRange;

use crate::hir::FileId;

// ─── Node classes ───────────────────────────────────────────────────

/// Frontend-agnostic class of the IR node a [`Provenance`] is stamped on.
///
/// This is an IR-level vocabulary, not a syntax-kind space: every frontend
/// maps its own grammar onto these classes when it stamps provenance. The
/// pipeline interprets **only** this class (today: the knot/stitch
/// container discrimination; B0.3 adds class ⇄ `SymbolKind` admission
/// checks). Per-frontend syntax kinds stay behind [`KindToken::raw`] and
/// the frontend's [`ProvenanceResolver`].
///
/// # Stable numeric namespace
///
/// Discriminants are a public, stable, append-only `u16` namespace
/// (convert with [`Self::as_u16`] / [`Self::from_u16`]): values are never
/// reused or renumbered once assigned, so downstream artifacts (e.g. a
/// debug-info projection into `brink-format`, which can never depend on
/// this crate) can store and reconstruct them without a hand-synced shadow
/// table. `0..=15` are reserved *generic* classes for producers that have
/// no more specific class; specific classes start at `16`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum NodeClass {
    // ── Reserved generic classes (0..=15) ───────────────────────────
    /// Generic statement — reserved coarse fallback for a producer with no
    /// more specific class to stamp. As of issue #3183, the ink LIR
    /// lowering itself uses it in a handful of best-effort-anchor cases
    /// that have no dedicated HIR span to point at:
    /// `lower::stmts::stmt_provenance`'s `ChoiceSet` and `LabeledBlock` arms
    /// (anchored via the same range the `E059` diagnostic already
    /// computes), and the synthetic root-terminus statement in
    /// `lower::mod`. Otherwise reserved for projections/producers without a
    /// specific class.
    Stmt = 0,
    /// Generic expression — reserved coarse fallback (see [`Self::Stmt`]).
    Expr = 1,

    // ── Specific classes (16..) — append-only, never reused ─────────
    // (Appending one also means adding its `from_u16` arm and bumping the
    // `node_class_u16_round_trips` sentinel to the new last variant.)
    /// A `Tag` attached to content.
    Tag = 16,
    /// A knot definition (`== knot`). A `hir::Knot` carries this class only
    /// when it originated from a real knot definition; see [`Self::Stitch`].
    Knot = 17,
    /// A stitch definition (`= stitch`) — including a top-level stitch
    /// *promoted* to `hir::Knot` status during lowering (the promoted node
    /// keeps `Stitch` class; this is the former `ContainerPtr::Stitch`
    /// discrimination, F-I#5).
    Stitch = 18,
    /// A `~ { … }` logic block.
    LogicBlock = 19,
    /// A `break` statement inside a logic block.
    Break = 20,
    /// A `continue` statement inside a logic block.
    Continue = 21,
    /// An `if` statement inside a logic block.
    If = 22,
    /// A `while` statement inside a logic block.
    While = 23,
    /// An `await` suspension point.
    Await = 24,
    /// A `for` statement inside a logic block.
    For = 25,
    /// A choice line (`*` / `+`).
    Choice = 26,
    /// A content (text output) line.
    Content = 27,
    /// A conditional block (multiline or promoted inline).
    Conditional = 28,
    /// A sequence block (stopping/cycle/once/shuffle).
    Sequence = 29,
    /// A divert (`-> target`).
    Divert = 30,
    /// A tunnel call (`-> target ->`).
    TunnelCall = 31,
    /// A thread start (`<- target`).
    ThreadStart = 32,
    /// A `~ return` statement.
    Return = 33,
    /// A `#fn(…)` function-value literal.
    FnLiteral = 34,
    /// A `ref lvalue` path-projection expression.
    RefArg = 35,
    /// A `Name#{…}` struct-construction literal.
    StructLiteral = 36,
    /// A `base.field` field access.
    FieldAccess = 37,
    /// A `#[…]` array literal.
    ArrayLiteral = 38,
    /// A `#{…}` map literal.
    MapLiteral = 39,
    /// A `base[index]` index expression.
    Index = 40,
    /// A `start..end` range literal.
    Range = 41,
    /// A `VAR` declaration.
    VarDecl = 42,
    /// A `CONST` declaration.
    ConstDecl = 43,
    /// A `~ temp` declaration.
    TempDecl = 44,
    /// An assignment statement.
    Assignment = 45,
    /// A `LIST` declaration.
    ListDecl = 46,
    /// A `STRUCT` declaration.
    StructDecl = 47,
    /// An `EXTERNAL` declaration.
    ExternalDecl = 48,
    /// An `INCLUDE` site.
    Include = 49,
    /// An infix (binary) operation — `lhs op rhs` (issue #1517).
    Infix = 50,
    /// One branch of a multiline/inline conditional (issue #404) — the
    /// branch's condition-plus-body span, distinct from the enclosing
    /// [`Self::Conditional`]'s whole-construct span. Lets a diagnostic or
    /// editor decoration (e.g. a fold run) anchor to a single `- else:`
    /// arm instead of the entire `{ ... }` block.
    ConditionalBranch = 51,
    /// One branch (alternative) of a sequence/alternation block (issue
    /// #404), mirroring [`Self::ConditionalBranch`] for `- ...` sequence
    /// arms and `|`-separated inline alternatives.
    SequenceBranch = 52,
    /// A `|x| …` lambda expression — the native surface's anonymous fn
    /// value (RULED 2026-07-19, issue #1685). Native-only: ink's grammar
    /// cannot spell a lambda.
    Lambda = 53,
    /// An inline markup span (`<name attr="v">…</name>`, issue #1716).
    /// Native-only: ink's grammar cannot spell markup. Stamped per-span so
    /// diagnostics (`E164`/`E165`, issue #1782) can point at the exact span
    /// rather than its enclosing content line.
    Span = 54,
    /// One `name="value"` attribute on an inline markup span (issue #1829).
    /// Stamped per-attribute so `E165` (undeclared attribute) can point at
    /// the exact `name="value"` pair rather than the whole enclosing span —
    /// the attribute-axis counterpart of [`Self::Span`]'s span-axis fix
    /// (#1782): two undeclared attributes on one span now get distinct
    /// ranges instead of colliding on the span's whole range.
    SpanAttr = 55,
}

impl NodeClass {
    /// The stable `u16` value of this class (see the type docs for the
    /// namespace rules).
    #[must_use]
    pub const fn as_u16(self) -> u16 {
        self as u16
    }

    /// Reconstruct a class from its stable `u16` value. `None` for values
    /// this crate version doesn't know (reserved-but-unassigned or newer).
    #[must_use]
    pub const fn from_u16(value: u16) -> Option<Self> {
        Some(match value {
            0 => Self::Stmt,
            1 => Self::Expr,
            16 => Self::Tag,
            17 => Self::Knot,
            18 => Self::Stitch,
            19 => Self::LogicBlock,
            20 => Self::Break,
            21 => Self::Continue,
            22 => Self::If,
            23 => Self::While,
            24 => Self::Await,
            25 => Self::For,
            26 => Self::Choice,
            27 => Self::Content,
            28 => Self::Conditional,
            29 => Self::Sequence,
            30 => Self::Divert,
            31 => Self::TunnelCall,
            32 => Self::ThreadStart,
            33 => Self::Return,
            34 => Self::FnLiteral,
            35 => Self::RefArg,
            36 => Self::StructLiteral,
            37 => Self::FieldAccess,
            38 => Self::ArrayLiteral,
            39 => Self::MapLiteral,
            40 => Self::Index,
            41 => Self::Range,
            42 => Self::VarDecl,
            43 => Self::ConstDecl,
            44 => Self::TempDecl,
            45 => Self::Assignment,
            46 => Self::ListDecl,
            47 => Self::StructDecl,
            48 => Self::ExternalDecl,
            49 => Self::Include,
            50 => Self::Infix,
            51 => Self::ConditionalBranch,
            52 => Self::SequenceBranch,
            53 => Self::Lambda,
            54 => Self::Span,
            55 => Self::SpanAttr,
            _ => return None,
        })
    }
}

// ─── Kind token ─────────────────────────────────────────────────────

/// The node-kind token component of [`Provenance`].
///
/// See the module docs for the class/raw visibility split. The whole token
/// round-trips through a `u32` ([`Self::as_u32`] / [`Self::from_u32`]) for
/// storage in artifacts that cannot depend on this crate; only the class
/// half of that value is stable — `raw` is frontend-private.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KindToken {
    /// Frontend-agnostic node class — the only pipeline-interpretable half.
    pub class: NodeClass,
    /// Frontend-private raw syntax kind (ink: `SyntaxKind as u16`).
    /// [`Self::SYNTHETIC_RAW`] marks fabricated provenance that no
    /// frontend's resolver will ever resolve.
    pub raw: u16,
}

impl KindToken {
    /// Raw-kind value stamped on fabricated (synthesized/test) provenance.
    /// No frontend occupies this value, so synthetic provenance never
    /// resolves — the same posture as the retired `AstPtr::from_range`
    /// dummies (which stamped `SyntaxKind::ERROR`).
    pub const SYNTHETIC_RAW: u16 = u16::MAX;

    /// A token with the given class and the synthetic raw kind.
    #[must_use]
    pub const fn synthetic(class: NodeClass) -> Self {
        Self {
            class,
            raw: Self::SYNTHETIC_RAW,
        }
    }

    /// Pack the token into a `u32`: class in the high half, raw in the low.
    #[must_use]
    pub const fn as_u32(self) -> u32 {
        ((self.class.as_u16() as u32) << 16) | self.raw as u32
    }

    /// Unpack a token packed by [`Self::as_u32`]. `None` when the class
    /// half is unknown to this crate version.
    #[must_use]
    pub fn from_u32(value: u32) -> Option<Self> {
        // Both halves are lossless: `>> 16` and `& 0xFFFF` fit u16.
        let Ok(class) = u16::try_from(value >> 16) else {
            return None;
        };
        let Ok(raw) = u16::try_from(value & 0xFFFF) else {
            return None;
        };
        NodeClass::from_u16(class).map(|class| Self { class, raw })
    }
}

// ─── Provenance ─────────────────────────────────────────────────────

/// Opaque source provenance carried by every HIR node.
///
/// `file` + `range` locate the originating source text; `kind` carries the
/// node-kind token (see [`KindToken`]). The range is real source geometry —
/// diagnostic anchor, IDE geometry, and (for referencing expressions via
/// `Name`/`Path` ranges) resolution join key — and must be non-empty and
/// in-bounds for admission (contract §1.3, checked loudly from B0.3 on).
///
/// Plain data by design: construct it with [`Self::new`] from any source —
/// a frontend lowering, a test, or deserialized debug info. There is no
/// hidden state; two values with equal fields are the same provenance.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Provenance {
    /// The source file this node was lowered from.
    pub file: FileId,
    /// The originating node's source range.
    pub range: TextRange,
    /// The node-kind token (class + frontend-private raw kind).
    pub kind: KindToken,
}

impl Provenance {
    /// Construct provenance from its parts.
    #[must_use]
    pub const fn new(file: FileId, range: TextRange, kind: KindToken) -> Self {
        Self { file, range, kind }
    }

    /// The source range this provenance points at.
    ///
    /// Named for continuity with the retired `AstPtr::text_range` /
    /// `SyntaxNodePtr::text_range` accessors so range-only consumers
    /// migrate mechanically.
    #[must_use]
    pub const fn text_range(&self) -> TextRange {
        self.range
    }

    /// The frontend-agnostic node class.
    #[must_use]
    pub const fn class(&self) -> NodeClass {
        self.kind.class
    }

    /// Fabricated provenance for synthesized or test-built nodes.
    ///
    /// Carries a real `range` (ranges stay identity keys even on synthetic
    /// nodes) but a file/raw pair no frontend claims, so it never resolves
    /// against any syntax tree.
    #[must_use]
    pub fn synthetic(class: NodeClass, range: TextRange) -> Self {
        Self {
            file: FileId(u32::MAX),
            range,
            kind: KindToken::synthetic(class),
        }
    }
}

// ─── Resolver seam ──────────────────────────────────────────────────

/// Frontend-supplied node resolution — the contract's provenance seam
/// (Q1(b), `docs/hir-admission-contract.md` D1/§4.3).
///
/// The pipeline stores only opaque [`Provenance`]; mapping it back to a
/// live syntax node is the producing frontend's job. IDE features that
/// need a live node (rename, extract) go through the frontend's resolver;
/// everything headless (analysis, LIR, codegen) consumes provenance as
/// data and never resolves it.
///
/// Resolution is keyed by the provenance **value** alone: a resolver must
/// accept any well-formed [`Provenance`] — including one reconstructed
/// from serialized parts it never minted — and answer `None` (a normal
/// answer, not an error) for anything foreign, synthetic, or stale.
///
/// The ink frontend's implementation is
/// [`crate::hir::InkProvenanceResolver`]; the native frontend (B0.5+)
/// supplies its own.
pub trait ProvenanceResolver {
    /// The frontend's live syntax-node type.
    type Node;

    /// Resolve `provenance` back to a live node.
    ///
    /// Returns `None` when the provenance belongs to another file or
    /// frontend, is synthetic, or is stale (the tree changed since it was
    /// stamped).
    fn resolve(&self, provenance: Provenance) -> Option<Self::Node>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use rowan::{TextRange, TextSize};

    #[test]
    fn node_class_u16_round_trips() {
        for v in 0..=u16::MAX {
            if let Some(class) = NodeClass::from_u16(v) {
                assert_eq!(class.as_u16(), v);
            }
        }
        // One past the *last* assigned class is unknown. Appending a class
        // to the enum means bumping this name to the new last variant —
        // otherwise the sentinel starts naming an assigned value and this
        // assertion fails.
        assert_eq!(NodeClass::from_u16(NodeClass::SpanAttr.as_u16() + 1), None);
        assert_eq!(NodeClass::from_u16(2), None, "generic range is reserved");
    }

    #[test]
    fn kind_token_u32_round_trips() {
        let token = KindToken {
            class: NodeClass::Stitch,
            raw: 137,
        };
        assert_eq!(KindToken::from_u32(token.as_u32()), Some(token));
        // Unknown class half → None.
        assert_eq!(KindToken::from_u32(0xFFFF_0000), None);
    }

    #[test]
    fn provenance_is_plain_reconstructible_data() {
        let range = TextRange::new(TextSize::new(3), TextSize::new(9));
        let original = Provenance::new(
            FileId(7),
            range,
            KindToken {
                class: NodeClass::Knot,
                raw: 42,
            },
        );
        // Reconstruct from serialized-style parts.
        let rebuilt = Provenance::new(
            FileId(original.file.0),
            original.range,
            KindToken::from_u32(original.kind.as_u32()).unwrap(),
        );
        assert_eq!(original, rebuilt);
    }
}