Skip to main content

bock_air/
node.rs

1//! AIR node definitions — the unified intermediate representation.
2//!
3//! Every construct in a Bock program is represented as an [`AIRNode`] with a
4//! [`NodeKind`] discriminant that carries typed children. All four AIR layers
5//! (S-AIR, T-AIR, C-AIR, TR-AIR) use the same node type; later passes fill
6//! in the layer slots that start as `None`.
7
8use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU32, Ordering};
10
11use bock_ast::{
12    Annotation, AssignOp, BinOp, GenericParam, Ident, ImportItems, Literal, ModulePath,
13    PropertyBinding, RecordDeclField, TypeConstraint, TypePath, UnaryOp, Visibility,
14};
15use bock_errors::Span;
16
17use crate::stubs::{
18    Capability, ContextBlock, EffectRef, OwnershipInfo, TargetInfo, TypeInfo, Value,
19};
20
21// ─── NodeId ───────────────────────────────────────────────────────────────────
22
23/// Unique identifier for an AIR node within a compilation session.
24pub type NodeId = u32;
25
26/// A monotonic counter that generates unique [`NodeId`]s.
27///
28/// Typically one `NodeIdGen` is created per compilation session and shared
29/// (via `&NodeIdGen`) across all lowering passes.
30#[derive(Debug, Default)]
31pub struct NodeIdGen {
32    counter: AtomicU32,
33}
34
35impl NodeIdGen {
36    /// Creates a new generator starting at zero.
37    #[must_use]
38    pub fn new() -> Self {
39        Self {
40            counter: AtomicU32::new(0),
41        }
42    }
43
44    /// Returns the next unique [`NodeId`].
45    #[must_use]
46    pub fn next(&self) -> NodeId {
47        self.counter.fetch_add(1, Ordering::SeqCst)
48    }
49}
50
51// ─── AIR node ─────────────────────────────────────────────────────────────────
52
53/// A single node in the Bock Intermediate Representation.
54///
55/// Each `AIRNode` carries:
56/// - A unique [`NodeId`] and source [`Span`]
57/// - A [`NodeKind`] with typed, structured children
58/// - Optional slots for each AIR layer (initially `None`, filled by passes)
59/// - An extensible metadata map for pass-specific annotations
60#[derive(Debug, Clone, PartialEq)]
61pub struct AIRNode {
62    /// Unique identifier for this node in the session.
63    pub id: NodeId,
64    /// Source location of this node.
65    pub span: Span,
66    /// Discriminant and typed children of this node.
67    pub kind: NodeKind,
68    // ── Layer 1 slots (populated by the type checker) ──────────────────────
69    /// Resolved type of this node (set by T-AIR pass).
70    pub type_info: Option<TypeInfo>,
71    /// Ownership/borrow annotation (set by T-AIR pass).
72    pub ownership: Option<OwnershipInfo>,
73    /// Algebraic effects this node may perform (set by T-AIR pass).
74    pub effects: HashSet<EffectRef>,
75    /// Capabilities this node requires (set by T-AIR pass).
76    pub capabilities: HashSet<Capability>,
77    // ── Layer 2 slot (populated by the context resolver) ──────────────────
78    /// Context annotations (set by C-AIR pass).
79    pub context: Option<ContextBlock>,
80    // ── Layer 3 slot (populated by the target analyzer) ───────────────────
81    /// Target-specific information (set by TR-AIR pass).
82    pub target: Option<TargetInfo>,
83    // ── Extensible metadata ────────────────────────────────────────────────
84    /// Arbitrary pass-specific metadata keyed by string.
85    pub metadata: HashMap<String, Value>,
86}
87
88impl AIRNode {
89    /// Creates a new S-AIR node with all layer slots empty.
90    #[must_use]
91    pub fn new(id: NodeId, span: Span, kind: NodeKind) -> Self {
92        Self {
93            id,
94            span,
95            kind,
96            type_info: None,
97            ownership: None,
98            effects: HashSet::new(),
99            capabilities: HashSet::new(),
100            context: None,
101            target: None,
102            metadata: HashMap::new(),
103        }
104    }
105}
106
107// ─── Auxiliary types ──────────────────────────────────────────────────────────
108
109/// A named argument in a call expression: `label: value`.
110#[derive(Debug, Clone, PartialEq)]
111pub struct AirArg {
112    /// Optional call-site label (e.g. `with:`, `from:`).
113    pub label: Option<Ident>,
114    /// The argument expression.
115    pub value: AIRNode,
116}
117
118/// A field in a record construction expression.
119#[derive(Debug, Clone, PartialEq)]
120pub struct AirRecordField {
121    /// Field name.
122    pub name: Ident,
123    /// `None` means shorthand: `{ name }` ≡ `{ name: name }`.
124    pub value: Option<Box<AIRNode>>,
125}
126
127/// A field binding inside a record pattern.
128#[derive(Debug, Clone, PartialEq)]
129pub struct AirRecordPatternField {
130    /// Field name.
131    pub name: Ident,
132    /// `None` means shorthand: `{ name }` ≡ `{ name: name }`.
133    pub pattern: Option<Box<AIRNode>>,
134}
135
136/// A key-value entry in a map literal.
137#[derive(Debug, Clone, PartialEq)]
138pub struct AirMapEntry {
139    pub key: AIRNode,
140    pub value: AIRNode,
141}
142
143/// A handler pair in a `handling` block: `Effect with handler`.
144#[derive(Debug, Clone, PartialEq)]
145pub struct AirHandlerPair {
146    /// The effect being handled.
147    pub effect: TypePath,
148    /// The handler expression node.
149    pub handler: Box<AIRNode>,
150}
151
152/// A segment of a string interpolation expression.
153#[derive(Debug, Clone, PartialEq)]
154pub enum AirInterpolationPart {
155    /// A literal string segment.
156    Literal(String),
157    /// An embedded expression `${expr}`.
158    Expr(Box<AIRNode>),
159}
160
161/// `Ok` or `Err` variant for result construction.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum ResultVariant {
164    Ok,
165    Err,
166}
167
168// ─── NodeKind ─────────────────────────────────────────────────────────────────
169
170/// Discriminant and typed children of an [`AIRNode`].
171///
172/// Children are structured per variant (not a flat `Vec<AIRNode>`), mirroring
173/// the AST but lowered into the unified AIR representation.
174#[derive(Debug, Clone, PartialEq)]
175#[non_exhaustive]
176pub enum NodeKind {
177    // ── Module ────────────────────────────────────────────────────────────
178    /// The root node of a compiled Bock source file.
179    Module {
180        path: Option<ModulePath>,
181        /// Module-level annotations (`@context`, `@requires`, etc.).
182        annotations: Vec<Annotation>,
183        /// Import declarations (`NodeKind::ImportDecl`).
184        imports: Vec<AIRNode>,
185        /// Top-level items.
186        items: Vec<AIRNode>,
187    },
188
189    /// An import declaration: `import Foo.Bar.{ A, B }`.
190    ImportDecl {
191        path: ModulePath,
192        items: ImportItems,
193    },
194
195    // ── Declarations ──────────────────────────────────────────────────────
196    /// A function declaration.
197    FnDecl {
198        annotations: Vec<Annotation>,
199        visibility: Visibility,
200        is_async: bool,
201        name: Ident,
202        generic_params: Vec<GenericParam>,
203        /// Parameter nodes (`NodeKind::Param`).
204        params: Vec<AIRNode>,
205        /// Optional return-type node (a type-expression variant).
206        return_type: Option<Box<AIRNode>>,
207        /// Effect names listed in the `with` clause.
208        effect_clause: Vec<TypePath>,
209        where_clause: Vec<TypeConstraint>,
210        /// Body block node (`NodeKind::Block`).
211        body: Box<AIRNode>,
212    },
213
214    /// A record (value-type) declaration.
215    RecordDecl {
216        annotations: Vec<Annotation>,
217        visibility: Visibility,
218        name: Ident,
219        generic_params: Vec<GenericParam>,
220        fields: Vec<RecordDeclField>,
221    },
222
223    /// An enum (algebraic data type) declaration.
224    EnumDecl {
225        annotations: Vec<Annotation>,
226        visibility: Visibility,
227        name: Ident,
228        generic_params: Vec<GenericParam>,
229        /// Variant nodes (unit, struct, or tuple — see AST `EnumVariant`).
230        variants: Vec<AIRNode>,
231    },
232
233    /// An enum variant — unit, struct-like, or tuple-like.
234    EnumVariant {
235        name: Ident,
236        /// `None` = unit; `Some` = struct fields or positional types.
237        payload: EnumVariantPayload,
238    },
239
240    /// A class declaration.
241    ClassDecl {
242        annotations: Vec<Annotation>,
243        visibility: Visibility,
244        name: Ident,
245        generic_params: Vec<GenericParam>,
246        base: Option<TypePath>,
247        traits: Vec<TypePath>,
248        fields: Vec<RecordDeclField>,
249        /// Method nodes (`NodeKind::FnDecl`).
250        methods: Vec<AIRNode>,
251    },
252
253    /// A trait (or platform-trait) declaration.
254    TraitDecl {
255        annotations: Vec<Annotation>,
256        visibility: Visibility,
257        is_platform: bool,
258        name: Ident,
259        generic_params: Vec<GenericParam>,
260        associated_types: Vec<bock_ast::AssociatedType>,
261        /// Method nodes (`NodeKind::FnDecl`).
262        methods: Vec<AIRNode>,
263    },
264
265    /// An `impl Trait for Type` or `impl Type` block.
266    ImplBlock {
267        annotations: Vec<Annotation>,
268        generic_params: Vec<GenericParam>,
269        trait_path: Option<TypePath>,
270        /// Type arguments applied to the trait, e.g. `[Int]` in
271        /// `impl From[Int] for Float`. Each is a type-expression node. Empty
272        /// for non-parameterized traits.
273        trait_args: Vec<AIRNode>,
274        /// The type being implemented (a type-expression node).
275        target: Box<AIRNode>,
276        where_clause: Vec<TypeConstraint>,
277        /// Method nodes (`NodeKind::FnDecl`).
278        methods: Vec<AIRNode>,
279    },
280
281    /// An algebraic effect declaration.
282    EffectDecl {
283        annotations: Vec<Annotation>,
284        visibility: Visibility,
285        name: Ident,
286        generic_params: Vec<GenericParam>,
287        /// Component effects for composite effects: `effect IO = Log + Clock`.
288        components: Vec<TypePath>,
289        /// Operation nodes (`NodeKind::FnDecl`).
290        operations: Vec<AIRNode>,
291    },
292
293    /// A type alias: `type Name[T] = ...`.
294    TypeAlias {
295        annotations: Vec<Annotation>,
296        visibility: Visibility,
297        name: Ident,
298        generic_params: Vec<GenericParam>,
299        /// The aliased type-expression node.
300        ty: Box<AIRNode>,
301        where_clause: Vec<TypeConstraint>,
302    },
303
304    /// A constant declaration: `const NAME: Type = value`.
305    ConstDecl {
306        annotations: Vec<Annotation>,
307        visibility: Visibility,
308        name: Ident,
309        /// Type annotation node (type-expression variant).
310        ty: Box<AIRNode>,
311        /// Initialiser expression node.
312        value: Box<AIRNode>,
313    },
314
315    /// A module-level `handle Effect with handler` declaration.
316    ModuleHandle {
317        effect: TypePath,
318        /// Handler expression node.
319        handler: Box<AIRNode>,
320    },
321
322    /// A `property("name") { forall(...) { ... } }` property-based test.
323    PropertyTest {
324        name: String,
325        bindings: Vec<PropertyBinding>,
326        /// Body block node (`NodeKind::Block`).
327        body: Box<AIRNode>,
328    },
329
330    // ── Function parameter ────────────────────────────────────────────────
331    /// A single function/lambda parameter.
332    Param {
333        /// Pattern node (a pattern variant).
334        pattern: Box<AIRNode>,
335        /// Optional type annotation node (type-expression variant).
336        ty: Option<Box<AIRNode>>,
337        /// Optional default-value expression node.
338        default: Option<Box<AIRNode>>,
339    },
340
341    // ── Type expressions ──────────────────────────────────────────────────
342    /// A named type, possibly with generic arguments: `List[Int]`.
343    TypeNamed {
344        path: TypePath,
345        /// Generic argument nodes (type-expression variants).
346        args: Vec<AIRNode>,
347    },
348
349    /// A tuple type: `(Int, String)`.
350    TypeTuple {
351        /// Element type nodes (type-expression variants).
352        elems: Vec<AIRNode>,
353    },
354
355    /// A function type: `Fn(Int) -> String with Log`.
356    TypeFunction {
357        /// Parameter type nodes (type-expression variants).
358        params: Vec<AIRNode>,
359        /// Return type node (type-expression variant).
360        ret: Box<AIRNode>,
361        /// Effects listed in the `with` clause.
362        effects: Vec<TypePath>,
363    },
364
365    /// An optional type: `Int?`.
366    TypeOptional {
367        /// Inner type node (type-expression variant).
368        inner: Box<AIRNode>,
369    },
370
371    /// The `Self` type in a trait/impl context.
372    TypeSelf,
373
374    // ── Expressions ───────────────────────────────────────────────────────
375    /// A literal value.
376    Literal { lit: Literal },
377
378    /// An identifier reference.
379    Identifier { name: Ident },
380
381    /// A binary operation: `a + b`.
382    BinaryOp {
383        op: BinOp,
384        left: Box<AIRNode>,
385        right: Box<AIRNode>,
386    },
387
388    /// A unary operation: `-x`, `!flag`.
389    UnaryOp { op: UnaryOp, operand: Box<AIRNode> },
390
391    /// An assignment expression: `x = 5`, `x += 1`.
392    Assign {
393        op: AssignOp,
394        target: Box<AIRNode>,
395        value: Box<AIRNode>,
396    },
397
398    /// A function call: `f(a, b)`.
399    Call {
400        callee: Box<AIRNode>,
401        args: Vec<AirArg>,
402        type_args: Vec<AIRNode>,
403    },
404
405    /// A method call: `obj.method(a, b)`.
406    MethodCall {
407        receiver: Box<AIRNode>,
408        method: Ident,
409        type_args: Vec<AIRNode>,
410        args: Vec<AirArg>,
411    },
412
413    /// Field access: `obj.field`.
414    FieldAccess { object: Box<AIRNode>, field: Ident },
415
416    /// Index access: `arr[i]`.
417    Index {
418        object: Box<AIRNode>,
419        index: Box<AIRNode>,
420    },
421
422    /// Error propagation: `expr?` — maps to spec's `Propagate`.
423    Propagate { expr: Box<AIRNode> },
424
425    /// A lambda: `(x) => x * 2`.
426    Lambda {
427        /// Parameter nodes (`NodeKind::Param`).
428        params: Vec<AIRNode>,
429        /// Body expression node.
430        body: Box<AIRNode>,
431    },
432
433    /// Pipe operator: `data |> parse`.
434    Pipe {
435        left: Box<AIRNode>,
436        right: Box<AIRNode>,
437    },
438
439    /// Function composition: `parse >> validate`.
440    Compose {
441        left: Box<AIRNode>,
442        right: Box<AIRNode>,
443    },
444
445    /// An `await` expression.
446    Await { expr: Box<AIRNode> },
447
448    /// A range: `1..10` (exclusive) or `1..=10` (inclusive).
449    Range {
450        lo: Box<AIRNode>,
451        hi: Box<AIRNode>,
452        inclusive: bool,
453    },
454
455    /// Record construction: `User { id: 1, name, ..defaults }`.
456    RecordConstruct {
457        path: TypePath,
458        fields: Vec<AirRecordField>,
459        spread: Option<Box<AIRNode>>,
460    },
461
462    /// List literal: `[1, 2, 3]`.
463    ListLiteral { elems: Vec<AIRNode> },
464
465    /// Map literal: `{"key": value}`.
466    MapLiteral { entries: Vec<AirMapEntry> },
467
468    /// Set literal: `#{"a", "b"}`.
469    SetLiteral { elems: Vec<AIRNode> },
470
471    /// Tuple literal: `("hello", 42)`.
472    TupleLiteral { elems: Vec<AIRNode> },
473
474    /// String interpolation: `"Hello, ${name}!"`.
475    Interpolation { parts: Vec<AirInterpolationPart> },
476
477    /// A placeholder `_` used in pipe expressions.
478    Placeholder,
479
480    /// `unreachable` — a diverging expression.
481    Unreachable,
482
483    /// Explicit `Ok(v)` or `Err(e)` result construction.
484    ResultConstruct {
485        variant: ResultVariant,
486        value: Option<Box<AIRNode>>,
487    },
488
489    // ── Control flow ──────────────────────────────────────────────────────
490    /// An `if` / `if-let` expression.
491    If {
492        /// For `if let pat = expr`, holds the pattern node.
493        let_pattern: Option<Box<AIRNode>>,
494        condition: Box<AIRNode>,
495        /// Then-branch block node (`NodeKind::Block`).
496        then_block: Box<AIRNode>,
497        /// Optional else branch (block or nested if node).
498        else_block: Option<Box<AIRNode>>,
499    },
500
501    /// A `guard condition else { ... }` statement.
502    ///
503    /// When `let_pattern` is `Some`, this is `guard (let pat = expr) else { ... }`.
504    Guard {
505        /// For `guard (let pat = expr)`, the pattern node.
506        let_pattern: Option<Box<AIRNode>>,
507        condition: Box<AIRNode>,
508        else_block: Box<AIRNode>,
509    },
510
511    /// A `match` expression.
512    Match {
513        scrutinee: Box<AIRNode>,
514        /// Match arm nodes (`NodeKind::MatchArm`).
515        arms: Vec<AIRNode>,
516    },
517
518    /// One arm of a `match` expression.
519    MatchArm {
520        /// Pattern node (a pattern variant).
521        pattern: Box<AIRNode>,
522        /// Optional guard expression.
523        guard: Option<Box<AIRNode>>,
524        /// Body expression node.
525        body: Box<AIRNode>,
526    },
527
528    /// A `for` loop.
529    For {
530        /// Loop variable pattern node (a pattern variant).
531        pattern: Box<AIRNode>,
532        iterable: Box<AIRNode>,
533        body: Box<AIRNode>,
534    },
535
536    /// A `while` loop.
537    While {
538        condition: Box<AIRNode>,
539        body: Box<AIRNode>,
540    },
541
542    /// An infinite `loop`.
543    Loop { body: Box<AIRNode> },
544
545    /// A block of statements with an optional tail expression.
546    Block {
547        stmts: Vec<AIRNode>,
548        tail: Option<Box<AIRNode>>,
549    },
550
551    /// A `return` expression.
552    Return { value: Option<Box<AIRNode>> },
553
554    /// A `break` expression, optionally with a value.
555    Break { value: Option<Box<AIRNode>> },
556
557    /// A `continue` expression.
558    Continue,
559
560    // ── Ownership ─────────────────────────────────────────────────────────
561    /// A `let [mut] pattern [: Type] = value` binding.
562    LetBinding {
563        is_mut: bool,
564        pattern: Box<AIRNode>,
565        ty: Option<Box<AIRNode>>,
566        value: Box<AIRNode>,
567    },
568
569    /// An explicit move of ownership: `move expr`.
570    Move { expr: Box<AIRNode> },
571
572    /// An immutable borrow: `&expr`.
573    Borrow { expr: Box<AIRNode> },
574
575    /// A mutable borrow: `&mut expr`.
576    MutableBorrow { expr: Box<AIRNode> },
577
578    // ── Effects ───────────────────────────────────────────────────────────
579    /// An algebraic-effect operation invocation.
580    EffectOp {
581        effect: TypePath,
582        operation: Ident,
583        args: Vec<AirArg>,
584    },
585
586    /// A `handling (Effect with handler, ...) { body }` block.
587    HandlingBlock {
588        handlers: Vec<AirHandlerPair>,
589        body: Box<AIRNode>,
590    },
591
592    /// A reference to an effect type (used in type positions and signatures).
593    EffectRef { path: TypePath },
594
595    // ── Patterns ──────────────────────────────────────────────────────────
596    /// `_` — wildcard pattern, matches anything.
597    WildcardPat,
598
599    /// `name` or `mut name` — bind pattern.
600    BindPat { name: Ident, is_mut: bool },
601
602    /// A literal pattern: `42`, `"hello"`, `true`.
603    LiteralPat { lit: Literal },
604
605    /// An enum constructor pattern: `Some(x)`, `Ok(v)`.
606    ConstructorPat {
607        path: TypePath,
608        /// Positional field pattern nodes.
609        fields: Vec<AIRNode>,
610    },
611
612    /// A record pattern: `User { name, age }`.
613    RecordPat {
614        path: TypePath,
615        fields: Vec<AirRecordPatternField>,
616        /// `true` when the pattern contains a `..` rest marker.
617        rest: bool,
618    },
619
620    /// A tuple pattern: `(a, b, c)`.
621    TuplePat { elems: Vec<AIRNode> },
622
623    /// A list pattern: `[head, ..tail]`.
624    ListPat {
625        elems: Vec<AIRNode>,
626        rest: Option<Box<AIRNode>>,
627    },
628
629    /// An or-pattern: `A | B`.
630    OrPat { alternatives: Vec<AIRNode> },
631
632    /// A guard pattern (in pattern-matching guard position).
633    GuardPat {
634        pattern: Box<AIRNode>,
635        guard: Box<AIRNode>,
636    },
637
638    /// A range pattern: `1..10` or `1..=10`.
639    RangePat {
640        lo: Box<AIRNode>,
641        hi: Box<AIRNode>,
642        inclusive: bool,
643    },
644
645    /// A rest pattern `..` (inside list/tuple patterns).
646    RestPat,
647
648    // ── Error recovery ────────────────────────────────────────────────────
649    /// An error-recovery node wrapping tokens that could not be lowered.
650    Error,
651}
652
653/// The payload of an enum variant in the AIR.
654#[derive(Debug, Clone, PartialEq)]
655pub enum EnumVariantPayload {
656    /// Unit variant: `Variant`.
657    Unit,
658    /// Struct-like variant: `Variant { field: Type, ... }`.
659    Struct(Vec<RecordDeclField>),
660    /// Tuple-like variant: `Variant(Type, Type)`.
661    Tuple(Vec<AIRNode>),
662}
663
664// ─── Tests ───────────────────────────────────────────────────────────────────
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use bock_errors::FileId;
670
671    fn dummy_span() -> Span {
672        Span {
673            file: FileId(0),
674            start: 0,
675            end: 0,
676        }
677    }
678
679    fn dummy_ident(name: &str) -> Ident {
680        Ident {
681            name: name.to_string(),
682            span: dummy_span(),
683        }
684    }
685
686    fn make_node(id: NodeId, kind: NodeKind) -> AIRNode {
687        AIRNode::new(id, dummy_span(), kind)
688    }
689
690    // ── NodeIdGen ──────────────────────────────────────────────────────────
691
692    #[test]
693    fn node_id_gen_monotonic() {
694        let gen = NodeIdGen::new();
695        let a = gen.next();
696        let b = gen.next();
697        let c = gen.next();
698        assert_eq!(a, 0);
699        assert_eq!(b, 1);
700        assert_eq!(c, 2);
701    }
702
703    #[test]
704    fn node_id_gen_thread_safe() {
705        use std::sync::Arc;
706        use std::thread;
707
708        let gen = Arc::new(NodeIdGen::new());
709        let handles: Vec<_> = (0..4)
710            .map(|_| {
711                let g = Arc::clone(&gen);
712                thread::spawn(move || g.next())
713            })
714            .collect();
715        let mut ids: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
716        ids.sort();
717        // All IDs should be distinct (0..4 in some order)
718        assert_eq!(ids, vec![0, 1, 2, 3]);
719    }
720
721    // ── AIRNode basics ─────────────────────────────────────────────────────
722
723    #[test]
724    fn air_node_new_has_empty_slots() {
725        let node = make_node(42, NodeKind::Continue);
726        assert_eq!(node.id, 42);
727        assert!(node.type_info.is_none());
728        assert!(node.ownership.is_none());
729        assert!(node.effects.is_empty());
730        assert!(node.capabilities.is_empty());
731        assert!(node.context.is_none());
732        assert!(node.target.is_none());
733        assert!(node.metadata.is_empty());
734    }
735
736    #[test]
737    fn air_node_debug_contains_kind() {
738        let node = make_node(0, NodeKind::Unreachable);
739        let s = format!("{node:?}");
740        assert!(s.contains("Unreachable"));
741    }
742
743    // ── NodeKind coverage ─────────────────────────────────────────────────
744
745    #[test]
746    fn module_node() {
747        let n = make_node(
748            0,
749            NodeKind::Module {
750                path: None,
751                annotations: vec![],
752                imports: vec![],
753                items: vec![],
754            },
755        );
756        assert!(matches!(n.kind, NodeKind::Module { .. }));
757    }
758
759    #[test]
760    fn fn_decl_node() {
761        let body = make_node(
762            1,
763            NodeKind::Block {
764                stmts: vec![],
765                tail: None,
766            },
767        );
768        let n = make_node(
769            0,
770            NodeKind::FnDecl {
771                annotations: vec![],
772                visibility: Visibility::Public,
773                is_async: false,
774                name: dummy_ident("foo"),
775                generic_params: vec![],
776                params: vec![],
777                return_type: None,
778                effect_clause: vec![],
779                where_clause: vec![],
780                body: Box::new(body),
781            },
782        );
783        assert!(matches!(n.kind, NodeKind::FnDecl { .. }));
784    }
785
786    #[test]
787    fn binary_op_node() {
788        let left = make_node(
789            1,
790            NodeKind::Literal {
791                lit: Literal::Int("1".into()),
792            },
793        );
794        let right = make_node(
795            2,
796            NodeKind::Literal {
797                lit: Literal::Int("2".into()),
798            },
799        );
800        let n = make_node(
801            0,
802            NodeKind::BinaryOp {
803                op: BinOp::Add,
804                left: Box::new(left),
805                right: Box::new(right),
806            },
807        );
808        assert!(matches!(n.kind, NodeKind::BinaryOp { op: BinOp::Add, .. }));
809    }
810
811    #[test]
812    fn pattern_nodes() {
813        let wildcard = make_node(0, NodeKind::WildcardPat);
814        let bind = make_node(
815            1,
816            NodeKind::BindPat {
817                name: dummy_ident("x"),
818                is_mut: false,
819            },
820        );
821        let lit = make_node(
822            2,
823            NodeKind::LiteralPat {
824                lit: Literal::Bool(true),
825            },
826        );
827        assert!(matches!(wildcard.kind, NodeKind::WildcardPat));
828        assert!(matches!(bind.kind, NodeKind::BindPat { .. }));
829        assert!(matches!(lit.kind, NodeKind::LiteralPat { .. }));
830    }
831
832    #[test]
833    fn control_flow_nodes() {
834        let body = Box::new(make_node(
835            1,
836            NodeKind::Block {
837                stmts: vec![],
838                tail: None,
839            },
840        ));
841        let cond = Box::new(make_node(
842            2,
843            NodeKind::Literal {
844                lit: Literal::Bool(true),
845            },
846        ));
847
848        let while_node = make_node(
849            0,
850            NodeKind::While {
851                condition: cond.clone(),
852                body: body.clone(),
853            },
854        );
855        let loop_node = make_node(3, NodeKind::Loop { body: body.clone() });
856        let return_node = make_node(4, NodeKind::Return { value: None });
857        let break_node = make_node(5, NodeKind::Break { value: None });
858        let continue_node = make_node(6, NodeKind::Continue);
859
860        assert!(matches!(while_node.kind, NodeKind::While { .. }));
861        assert!(matches!(loop_node.kind, NodeKind::Loop { .. }));
862        assert!(matches!(return_node.kind, NodeKind::Return { value: None }));
863        assert!(matches!(break_node.kind, NodeKind::Break { value: None }));
864        assert!(matches!(continue_node.kind, NodeKind::Continue));
865    }
866
867    #[test]
868    fn ownership_nodes() {
869        let expr = Box::new(make_node(
870            1,
871            NodeKind::Identifier {
872                name: dummy_ident("x"),
873            },
874        ));
875        let mv = make_node(0, NodeKind::Move { expr: expr.clone() });
876        let borrow = make_node(2, NodeKind::Borrow { expr: expr.clone() });
877        let mut_borrow = make_node(3, NodeKind::MutableBorrow { expr: expr.clone() });
878        assert!(matches!(mv.kind, NodeKind::Move { .. }));
879        assert!(matches!(borrow.kind, NodeKind::Borrow { .. }));
880        assert!(matches!(mut_borrow.kind, NodeKind::MutableBorrow { .. }));
881    }
882
883    #[test]
884    fn effect_nodes() {
885        let handler = Box::new(make_node(
886            1,
887            NodeKind::Identifier {
888                name: dummy_ident("h"),
889            },
890        ));
891        let body = Box::new(make_node(
892            2,
893            NodeKind::Block {
894                stmts: vec![],
895                tail: None,
896            },
897        ));
898        let tp = TypePath {
899            segments: vec![dummy_ident("Log")],
900            span: dummy_span(),
901        };
902        let handling = make_node(
903            0,
904            NodeKind::HandlingBlock {
905                handlers: vec![AirHandlerPair {
906                    effect: tp.clone(),
907                    handler,
908                }],
909                body,
910            },
911        );
912        let effect_ref = make_node(3, NodeKind::EffectRef { path: tp });
913        assert!(matches!(handling.kind, NodeKind::HandlingBlock { .. }));
914        assert!(matches!(effect_ref.kind, NodeKind::EffectRef { .. }));
915    }
916
917    #[test]
918    fn type_expr_nodes() {
919        let named = make_node(
920            0,
921            NodeKind::TypeNamed {
922                path: TypePath {
923                    segments: vec![dummy_ident("Int")],
924                    span: dummy_span(),
925                },
926                args: vec![],
927            },
928        );
929        let self_ty = make_node(1, NodeKind::TypeSelf);
930        let opt = make_node(
931            2,
932            NodeKind::TypeOptional {
933                inner: Box::new(named.clone()),
934            },
935        );
936        assert!(matches!(named.kind, NodeKind::TypeNamed { .. }));
937        assert!(matches!(self_ty.kind, NodeKind::TypeSelf));
938        assert!(matches!(opt.kind, NodeKind::TypeOptional { .. }));
939    }
940
941    #[test]
942    fn metadata_and_effects_mutable() {
943        let mut node = make_node(0, NodeKind::Continue);
944        node.metadata
945            .insert("pass".into(), crate::stubs::Value::String("T-AIR".into()));
946        node.effects.insert(EffectRef::new("Std.Io.Log"));
947        node.capabilities
948            .insert(Capability::new("Std.Io.FileSystem"));
949        assert_eq!(node.metadata.len(), 1);
950        assert_eq!(node.effects.len(), 1);
951        assert_eq!(node.capabilities.len(), 1);
952    }
953}