Skip to main content

harn_parser/
ast.rs

1use harn_lexer::{Span, StringSegment};
2
3/// A node wrapped with source location information.
4#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
5pub struct Spanned<T> {
6    pub node: T,
7    pub span: Span,
8}
9
10impl<T> Spanned<T> {
11    pub fn new(node: T, span: Span) -> Self {
12        Self { node, span }
13    }
14
15    pub fn dummy(node: T) -> Self {
16        Self {
17            node,
18            span: Span::dummy(),
19        }
20    }
21}
22
23/// A spanned AST node — the primary unit throughout the compiler.
24pub type SNode = Spanned<Node>;
25
26/// Helper to wrap a node with a span.
27pub fn spanned(node: Node, span: Span) -> SNode {
28    SNode::new(node, span)
29}
30
31/// If `node` is an `AttributedDecl`, returns `(attrs, inner)`; otherwise
32/// returns an empty attribute slice and the node itself. Use at the top
33/// of any consumer that processes top-level statements so attributes
34/// flow through transparently.
35pub fn peel_attributes(node: &SNode) -> (&[Attribute], &SNode) {
36    match &node.node {
37        Node::AttributedDecl { attributes, inner } => (attributes.as_slice(), inner.as_ref()),
38        _ => (&[], node),
39    }
40}
41
42/// A single argument to an attribute. Positional args have `name = None`;
43/// named args use `name: Some("key")`. Values are restricted to
44/// compile-time metadata expressions by the parser (literal scalars,
45/// identifiers, lists, dicts, and call-shaped sentinels).
46#[derive(Debug, Clone, PartialEq, serde::Serialize)]
47pub struct AttributeArg {
48    pub name: Option<String>,
49    pub value: SNode,
50    pub span: Span,
51}
52
53/// An attribute attached to a declaration: `@deprecated(since: "0.8")`.
54#[derive(Debug, Clone, PartialEq, serde::Serialize)]
55pub struct Attribute {
56    pub name: String,
57    pub args: Vec<AttributeArg>,
58    pub span: Span,
59}
60
61impl Attribute {
62    /// Find a named argument by key.
63    pub fn named_arg(&self, key: &str) -> Option<&SNode> {
64        self.args
65            .iter()
66            .find(|a| a.name.as_deref() == Some(key))
67            .map(|a| &a.value)
68    }
69
70    /// First positional argument, if any.
71    pub fn positional(&self, idx: usize) -> Option<&SNode> {
72        self.args
73            .iter()
74            .filter(|a| a.name.is_none())
75            .nth(idx)
76            .map(|a| &a.value)
77    }
78
79    /// Convenience: extract a string-literal arg by name.
80    pub fn string_arg(&self, key: &str) -> Option<String> {
81        match self.named_arg(key).map(|n| &n.node) {
82            Some(Node::StringLiteral(s)) => Some(s.clone()),
83            Some(Node::RawStringLiteral(s)) => Some(s.clone()),
84            _ => None,
85        }
86    }
87}
88
89/// AST nodes for the Harn language.
90#[derive(Debug, Clone, PartialEq, serde::Serialize)]
91pub enum Node {
92    /// A declaration carrying one or more attributes (`@attr`). The inner
93    /// node is always one of: FnDecl, ToolDecl, Pipeline, StructDecl,
94    /// EnumDecl, TypeDecl, InterfaceDecl, ImplBlock.
95    AttributedDecl {
96        attributes: Vec<Attribute>,
97        inner: Box<SNode>,
98    },
99    Pipeline {
100        name: String,
101        params: Vec<String>,
102        return_type: Option<TypeExpr>,
103        /// Declared exception channel: `throws E` / `throws (E1 | E2)`, parsed
104        /// as a single [`TypeExpr`] (a `throws (E1 | E2)` clause is a
105        /// [`TypeExpr::Union`]). `None` leaves the callable's thrown-type set
106        /// unconstrained — the historical default, so the annotation is purely
107        /// additive and no existing code is forced to declare it.
108        throws: Option<TypeExpr>,
109        body: Vec<SNode>,
110        extends: Option<String>,
111        is_pub: bool,
112    },
113    /// `let PATTERN [: Type] = EXPR` — a **mutable** binding (reassignable).
114    ///
115    /// This is the TypeScript-aligned `let`: a normal block-scoped mutable
116    /// variable. (Before the const/let keyword re-platform, `let` was
117    /// immutable and `var` was the mutable form; `var` has been removed and
118    /// its mutable role is now `let`.)
119    LetBinding {
120        pattern: BindingPattern,
121        type_ann: Option<TypeExpr>,
122        value: Box<SNode>,
123        /// `true` for a top-level `pub let` — the binding's value is exported
124        /// as part of the module's public surface (bound by value in
125        /// importers, like every other cross-module value). Always `false` for
126        /// block-scoped bindings.
127        is_pub: bool,
128    },
129    /// `const PATTERN [: Type] = EXPR` — an **immutable** binding.
130    ///
131    /// The TypeScript-aligned `const`: the default, immutable binding form
132    /// (reassignment is rejected). When the initializer falls in the pure,
133    /// bounded const-eval subset (literal arithmetic, string concat, literal
134    /// lists/dicts, ternaries, reads of earlier `const` identifiers, and a
135    /// whitelist of pure builtins) it is **folded at compile time** via
136    /// `harn_parser::const_eval`; otherwise it is an ordinary immutable
137    /// runtime binding. Unlike the pre-re-platform `const`, an impure
138    /// initializer is *not* an error (it simply is not folded), and a
139    /// destructuring `pattern` is permitted (only a plain identifier pattern
140    /// is eligible for folding). At runtime a folded binding re-evaluates the
141    /// same expression so the value matches the compile-time fold byte-for-byte.
142    ConstBinding {
143        pattern: BindingPattern,
144        type_ann: Option<TypeExpr>,
145        value: Box<SNode>,
146        /// `true` for a top-level `pub const` — the (compile-time-folded or
147        /// runtime) value is exported as part of the module's public surface.
148        /// Always `false` for block-scoped bindings.
149        is_pub: bool,
150    },
151    OverrideDecl {
152        name: String,
153        params: Vec<String>,
154        body: Vec<SNode>,
155    },
156    ImportDecl {
157        path: String,
158        /// When true, the wildcard import is a re-export: every public symbol
159        /// from the target module becomes part of this module's public surface.
160        is_pub: bool,
161    },
162    /// Selective import: import { foo, bar } from "module"
163    SelectiveImport {
164        names: Vec<String>,
165        path: String,
166        /// When true, the listed names are re-exported as part of this
167        /// module's public surface.
168        is_pub: bool,
169    },
170    EnumDecl {
171        name: String,
172        type_params: Vec<TypeParam>,
173        variants: Vec<EnumVariant>,
174        is_pub: bool,
175    },
176    StructDecl {
177        name: String,
178        type_params: Vec<TypeParam>,
179        fields: Vec<StructField>,
180        is_pub: bool,
181    },
182    InterfaceDecl {
183        name: String,
184        type_params: Vec<TypeParam>,
185        associated_types: Vec<AssociatedType>,
186        methods: Vec<InterfaceMethod>,
187    },
188    /// Impl block: impl TypeName { fn method(self, ...) { ... } ... }
189    ImplBlock {
190        type_name: String,
191        methods: Vec<SNode>,
192    },
193
194    IfElse {
195        condition: Box<SNode>,
196        then_body: Vec<SNode>,
197        else_body: Option<Vec<SNode>>,
198    },
199    ForIn {
200        pattern: BindingPattern,
201        iterable: Box<SNode>,
202        body: Vec<SNode>,
203    },
204    MatchExpr {
205        value: Box<SNode>,
206        arms: Vec<MatchArm>,
207    },
208    WhileLoop {
209        condition: Box<SNode>,
210        body: Vec<SNode>,
211    },
212    Retry {
213        count: Box<SNode>,
214        body: Vec<SNode>,
215    },
216    /// Scoped cost-aware LLM routing block:
217    /// `cost_route { key: value ... body }`.
218    ///
219    /// Options are inherited by nested `llm_call` invocations unless a
220    /// call explicitly overrides the same option.
221    CostRoute {
222        options: Vec<(String, SNode)>,
223        body: Vec<SNode>,
224    },
225    ReturnStmt {
226        value: Option<Box<SNode>>,
227    },
228    TryCatch {
229        body: Vec<SNode>,
230        has_catch: bool,
231        error_var: Option<String>,
232        error_type: Option<TypeExpr>,
233        catch_body: Vec<SNode>,
234        finally_body: Option<Vec<SNode>>,
235    },
236    /// Try expression: try { body } — returns Result.Ok(value), an existing Result,
237    /// or Result.Err(error).
238    TryExpr {
239        body: Vec<SNode>,
240    },
241    FnDecl {
242        name: String,
243        type_params: Vec<TypeParam>,
244        params: Vec<TypedParam>,
245        return_type: Option<TypeExpr>,
246        /// Declared exception channel `throws E` / `throws (E1 | E2)`; see the
247        /// [`Node::Pipeline`] `throws` field. `None` = unconstrained.
248        throws: Option<TypeExpr>,
249        where_clauses: Vec<WhereClause>,
250        body: Vec<SNode>,
251        is_pub: bool,
252        is_stream: bool,
253    },
254    ToolDecl {
255        name: String,
256        description: Option<String>,
257        params: Vec<TypedParam>,
258        return_type: Option<TypeExpr>,
259        /// Declared exception channel; see the [`Node::Pipeline`] `throws`
260        /// field. `None` = unconstrained.
261        throws: Option<TypeExpr>,
262        body: Vec<SNode>,
263        is_pub: bool,
264    },
265    /// Top-level `skill NAME { ... }` declaration.
266    ///
267    /// Skills bundle metadata, tool references, MCP server lists, and
268    /// optional lifecycle hooks into a typed unit. Each body entry is a
269    /// `<field_name> <expression>` pair; the compiler lowers the decl to
270    /// `skill_define(skill_registry(), NAME, { field: expr, ... })` and
271    /// binds the resulting registry dict to `NAME`.
272    SkillDecl {
273        name: String,
274        fields: Vec<(String, SNode)>,
275        is_pub: bool,
276    },
277    /// Top-level `eval_pack NAME_OR_STRING { ... }` declaration.
278    ///
279    /// The compiler lowers fields into `eval_pack_manifest({ ... })` and
280    /// binds the normalized manifest to `binding_name`. Optional executable
281    /// body statements are only run when the declaration itself is executed
282    /// in script/block position; top-level pipeline preloading registers the
283    /// manifest data without running the body.
284    EvalPackDecl {
285        binding_name: String,
286        pack_id: String,
287        fields: Vec<(String, SNode)>,
288        body: Vec<SNode>,
289        summarize: Option<Vec<SNode>>,
290        is_pub: bool,
291    },
292    TypeDecl {
293        name: String,
294        type_params: Vec<TypeParam>,
295        type_expr: TypeExpr,
296        is_pub: bool,
297    },
298    SpawnExpr {
299        body: Vec<SNode>,
300    },
301    /// Structured-concurrency nursery: `scope { ... }`. Tasks spawned while this
302    /// block is on the task-scope stack are joined when the block exits — the
303    /// first task error cancels its siblings and propagates out of the block, so
304    /// no spawned task is orphaned or has its error silently swallowed.
305    ScopeBlock {
306        body: Vec<SNode>,
307    },
308    /// Duration literal: 500ms, 5s, 30m, 2h, 1d, 1w
309    DurationLiteral(u64),
310    /// Range expression: `start to end` (inclusive) or `start to end exclusive` (half-open)
311    RangeExpr {
312        start: Box<SNode>,
313        end: Box<SNode>,
314        inclusive: bool,
315    },
316    /// Guard clause: guard condition else { body }
317    GuardStmt {
318        condition: Box<SNode>,
319        else_body: Vec<SNode>,
320    },
321    RequireStmt {
322        condition: Box<SNode>,
323        message: Option<Box<SNode>>,
324    },
325    /// Defer statement: defer { body } — runs body at scope exit.
326    DeferStmt {
327        body: Vec<SNode>,
328    },
329    /// Deadline block: deadline DURATION { body }
330    DeadlineBlock {
331        duration: Box<SNode>,
332        body: Vec<SNode>,
333    },
334    /// Yield expression: yields control to host, optionally with a value.
335    YieldExpr {
336        value: Option<Box<SNode>>,
337    },
338    /// Emit expression: emits one value from a `gen fn` stream.
339    EmitExpr {
340        value: Box<SNode>,
341    },
342    /// Mutex block: mutual exclusion for concurrent access.
343    ///
344    /// `key` is the optional resource expression in `mutex(resource) { ... }`.
345    /// When present, all blocks acquiring the same structural key value
346    /// mutually exclude; when absent (`mutex { ... }`), the block keys on its
347    /// own lexical call-site, so two distinct `mutex {}` blocks no longer
348    /// serialize against each other.
349    MutexBlock {
350        key: Option<Box<SNode>>,
351        body: Vec<SNode>,
352    },
353    /// Break out of a loop.
354    BreakStmt,
355    /// Continue to next loop iteration.
356    ContinueStmt,
357
358    /// First-class HITL primitive expression.
359    ///
360    /// Lexed as a reserved keyword (`request_approval`, `dual_control`,
361    /// `ask_user`, `escalate_to`), parsed at primary-expression position
362    /// as `keyword "(" args ")"`. Each arg is either positional
363    /// (`expr`) or named (`name: expr`).
364    ///
365    /// The compiler lowers this to a call to the matching async stdlib
366    /// builtin in `crates/harn-vm/src/stdlib/hitl.rs`, packaging the
367    /// named arguments into the existing options-dict shape. The
368    /// typechecker assigns each kind its canonical envelope return type.
369    HitlExpr {
370        kind: HitlKind,
371        args: Vec<HitlArg>,
372    },
373
374    Parallel {
375        mode: ParallelMode,
376        /// For Count mode: the count expression. For Each/Settle: the list expression.
377        expr: Box<SNode>,
378        variable: Option<String>,
379        body: Vec<SNode>,
380        /// Optional trailing `with { max_concurrent: N, ... }` option block.
381        /// A vec (rather than a dict) preserves source order for error
382        /// reporting and keeps parsing cheap. Only `max_concurrent` is
383        /// currently honored; unknown keys are rejected by the parser.
384        options: Vec<(String, SNode)>,
385    },
386
387    SelectExpr {
388        cases: Vec<SelectCase>,
389        timeout: Option<(Box<SNode>, Vec<SNode>)>,
390        default_body: Option<Vec<SNode>>,
391    },
392
393    FunctionCall {
394        name: String,
395        type_args: Vec<TypeExpr>,
396        args: Vec<SNode>,
397    },
398    MethodCall {
399        object: Box<SNode>,
400        method: String,
401        args: Vec<SNode>,
402    },
403    /// Optional method call: `obj?.method(args)` — returns nil if obj is nil.
404    OptionalMethodCall {
405        object: Box<SNode>,
406        method: String,
407        args: Vec<SNode>,
408    },
409    PropertyAccess {
410        object: Box<SNode>,
411        property: String,
412    },
413    /// Optional chaining: `obj?.property` — returns nil if obj is nil.
414    OptionalPropertyAccess {
415        object: Box<SNode>,
416        property: String,
417    },
418    SubscriptAccess {
419        object: Box<SNode>,
420        index: Box<SNode>,
421    },
422    /// Optional subscript: `obj?.[index]` — returns nil if obj is nil.
423    OptionalSubscriptAccess {
424        object: Box<SNode>,
425        index: Box<SNode>,
426    },
427    SliceAccess {
428        object: Box<SNode>,
429        start: Option<Box<SNode>>,
430        end: Option<Box<SNode>>,
431    },
432    BinaryOp {
433        op: String,
434        left: Box<SNode>,
435        right: Box<SNode>,
436    },
437    UnaryOp {
438        op: String,
439        operand: Box<SNode>,
440    },
441    Ternary {
442        condition: Box<SNode>,
443        true_expr: Box<SNode>,
444        false_expr: Box<SNode>,
445    },
446    Assignment {
447        target: Box<SNode>,
448        value: Box<SNode>,
449        /// None = plain `=`, Some("+") = `+=`, etc.
450        op: Option<String>,
451    },
452    ThrowStmt {
453        value: Box<SNode>,
454    },
455
456    /// Enum variant construction: EnumName.Variant(args)
457    EnumConstruct {
458        enum_name: String,
459        variant: String,
460        args: Vec<SNode>,
461    },
462    /// Struct construction: StructName { field: value, ... }
463    StructConstruct {
464        struct_name: String,
465        fields: Vec<DictEntry>,
466    },
467
468    InterpolatedString(Vec<StringSegment>),
469    StringLiteral(String),
470    /// Raw string literal `r"..."` — no escape processing.
471    RawStringLiteral(String),
472    IntLiteral(i64),
473    FloatLiteral(f64),
474    BoolLiteral(bool),
475    NilLiteral,
476    Identifier(String),
477    ListLiteral(Vec<SNode>),
478    DictLiteral(Vec<DictEntry>),
479    /// Spread expression `...expr` inside list/dict literals.
480    Spread(Box<SNode>),
481    /// Try operator: expr? — unwraps Result.Ok or propagates Result.Err.
482    TryOperator {
483        operand: Box<SNode>,
484    },
485    /// Non-null assertion: `expr!` — asserts the operand is not `nil`.
486    /// Statically strips `nil` from the operand's type (`T | nil` -> `T`);
487    /// at runtime it is identity when the value is present and throws a
488    /// structured `unwrap_nil` error when it is `nil`.
489    NonNullAssert {
490        operand: Box<SNode>,
491    },
492    /// Try-star operator: `try* EXPR` — evaluates EXPR; on throw, runs
493    /// pending finally blocks up to the enclosing catch and rethrows
494    /// the original value. On success, evaluates to EXPR's value.
495    /// Lowered per spec/HARN_SPEC.md as:
496    ///   { let _r = try { EXPR }
497    ///     guard is_ok(_r) else { throw unwrap_err(_r) }
498    ///     unwrap(_r) }
499    TryStar {
500        operand: Box<SNode>,
501    },
502
503    /// Or-pattern in a `match` arm: `"ping" | "pong" -> body`. One or
504    /// more alternative patterns that share a single arm body. Only
505    /// legal inside a `MatchArm.pattern` slot.
506    OrPattern(Vec<SNode>),
507
508    Block(Vec<SNode>),
509    Closure {
510        params: Vec<TypedParam>,
511        return_type: Option<TypeExpr>,
512        /// Declared exception channel; see the [`Node::Pipeline`] `throws`
513        /// field. `None` = unconstrained. Only the `fn(params) -> R throws E`
514        /// closure spelling can carry it; the bare `x -> expr` arrow form has
515        /// no place to put a clause and always parses `None`.
516        throws: Option<TypeExpr>,
517        body: Vec<SNode>,
518        /// When true, this closure was written as `fn(params) { body }`.
519        /// The formatter preserves this distinction.
520        fn_syntax: bool,
521    },
522}
523
524/// First-class human-in-the-loop primitive.
525///
526/// Each `HitlKind` is a reserved keyword expression with VM-enforced
527/// semantics: the names cannot be shadowed or rebound by user code,
528/// signatures are produced by the VM, and the audit log is recorded
529/// deterministically by the runtime.
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
531pub enum HitlKind {
532    /// `request_approval(action: ..., args: ..., quorum: ..., reviewers: ..., ...)`.
533    RequestApproval,
534    /// `dual_control(n: ..., m: ..., action: <closure>, approvers: ...)`.
535    DualControl,
536    /// `ask_user(prompt: ..., schema: ..., timeout: ..., default: ...)`.
537    AskUser,
538    /// `escalate_to(role: ..., reason: ...)`.
539    EscalateTo,
540}
541
542impl HitlKind {
543    /// Keyword surface form (matches the reserved keyword in the lexer
544    /// and the corresponding async builtin name in the VM).
545    pub fn as_keyword(self) -> &'static str {
546        match self {
547            HitlKind::RequestApproval => "request_approval",
548            HitlKind::DualControl => "dual_control",
549            HitlKind::AskUser => "ask_user",
550            HitlKind::EscalateTo => "escalate_to",
551        }
552    }
553}
554
555/// A single argument in a [`Node::HitlExpr`] call. `name` is `Some` when
556/// the caller used named-arg syntax (e.g. `quorum: 2`); positional
557/// arguments leave it as `None` and rely on the kind's parameter order.
558#[derive(Debug, Clone, PartialEq, serde::Serialize)]
559pub struct HitlArg {
560    pub name: Option<String>,
561    pub value: SNode,
562    pub span: Span,
563}
564
565/// Parallel execution mode.
566#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
567pub enum ParallelMode {
568    /// `parallel N { i -> ... }` — run N concurrent tasks.
569    Count,
570    /// `parallel each list { item -> ... }` — map over list concurrently.
571    Each,
572    /// `parallel each list { item -> ... } as stream` — emit as each task completes.
573    EachStream,
574    /// `parallel settle list { item -> ... }` — map with error collection.
575    Settle,
576}
577
578#[derive(Debug, Clone, PartialEq, serde::Serialize)]
579pub struct MatchArm {
580    pub pattern: SNode,
581    /// Optional guard: `pattern if condition -> { body }`.
582    pub guard: Option<Box<SNode>>,
583    pub body: Vec<SNode>,
584    /// Source extent of the whole arm, pattern through closing brace. The
585    /// pattern's own span cannot bound the arm's body, so without this a
586    /// comment after the arm's last statement has no range to be flushed in.
587    /// See `StructField::span`.
588    pub span: Span,
589}
590
591#[derive(Debug, Clone, PartialEq, serde::Serialize)]
592pub struct SelectCase {
593    pub variable: String,
594    pub channel: Box<SNode>,
595    pub body: Vec<SNode>,
596}
597
598#[derive(Debug, Clone, PartialEq, serde::Serialize)]
599pub struct DictEntry {
600    pub key: SNode,
601    pub value: SNode,
602}
603
604/// An enum variant declaration.
605#[derive(Debug, Clone, PartialEq, serde::Serialize)]
606pub struct EnumVariant {
607    pub name: String,
608    pub fields: Vec<TypedParam>,
609    /// Source extent of the variant. A member without a span cannot anchor a
610    /// comment written against it; see `StructField::span`.
611    pub span: Span,
612}
613
614/// A struct field declaration.
615#[derive(Debug, Clone, PartialEq, serde::Serialize)]
616pub struct StructField {
617    pub name: String,
618    pub type_expr: Option<TypeExpr>,
619    pub optional: bool,
620    /// Source extent of the field.
621    ///
622    /// A comment's meaning lives entirely in where it sits, and a comment can
623    /// only be placed next to something that knows its own source lines. Member
624    /// items carrying no span is what let `harn fmt` evict a field's doc
625    /// comment out of the struct and re-attach it to the next declaration,
626    /// where it then described unrelated code.
627    pub span: Span,
628}
629
630/// An associated-type entry in an interface body: `type Item` or
631/// `type Item = string`.
632#[derive(Debug, Clone, PartialEq, serde::Serialize)]
633pub struct AssociatedType {
634    pub name: String,
635    /// The default, when written as `type Item = <default>`.
636    pub default: Option<TypeExpr>,
637    /// Source extent of the entry. See `StructField::span`.
638    pub span: Span,
639}
640
641impl AssociatedType {
642    /// The name/default pair, dropping source position — the shape the
643    /// typechecker's semantic tables carry, which have no use for a span.
644    pub fn to_binding(&self) -> (String, Option<TypeExpr>) {
645        (self.name.clone(), self.default.clone())
646    }
647
648    /// Project a parsed interface body's associated types onto the binding
649    /// pairs the semantic tables hold. The one place this conversion lives.
650    pub fn bindings(items: &[AssociatedType]) -> Vec<(String, Option<TypeExpr>)> {
651        items.iter().map(AssociatedType::to_binding).collect()
652    }
653}
654
655/// An interface method signature.
656#[derive(Debug, Clone, PartialEq, serde::Serialize)]
657pub struct InterfaceMethod {
658    pub name: String,
659    pub type_params: Vec<TypeParam>,
660    pub params: Vec<TypedParam>,
661    pub return_type: Option<TypeExpr>,
662    /// Source extent of the method signature. See `StructField::span`.
663    pub span: Span,
664}
665
666/// A type annotation (optional, for runtime checking).
667#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
668pub enum TypeExpr {
669    /// A named type: int, string, float, bool, nil, list, dict, closure,
670    /// or a user-defined type name.
671    Named(String),
672    /// A union type: `string | nil`, `int | float`.
673    Union(Vec<TypeExpr>),
674    /// An intersection type: `{x: int} & {y: int}`. The value must satisfy
675    /// every component simultaneously. Useful for layered context types
676    /// such as `fn use(ctx: BaseCtx & AuthCtx)`.
677    Intersection(Vec<TypeExpr>),
678    /// A dict shape type: `{name: string, age: int, active?: bool}`.
679    Shape(Vec<ShapeField>),
680    /// An **open** record / row-polymorphic shape: a set of explicit fields
681    /// plus one or more trailing **row tails** (`{id: string, ...R}`,
682    /// `{...R1, ...R2}`). Each tail in `rests` is a row variable
683    /// (`Named(rowvar)`), a gradual map tail (`dict` / `dict<string, V>`), or a
684    /// nested shape — folded left-to-right with right-biased merge once the row
685    /// variables are bound. A closed shape stays `Shape` (empty `rests`).
686    OpenShape {
687        fields: Vec<ShapeField>,
688        rests: Vec<TypeExpr>,
689    },
690    /// A list type: `list<int>`.
691    List(Box<TypeExpr>),
692    /// A dict type with key and value types: `dict<string, int>`.
693    DictType(Box<TypeExpr>, Box<TypeExpr>),
694    /// A lazy iterator type: `iter<int>`. Yields values of the inner type
695    /// via the combinator/sink protocol (`VmValue::Iter` at runtime).
696    Iter(Box<TypeExpr>),
697    /// A synchronous generator type: `Generator<int>`. Produced by a regular
698    /// `fn` body containing `yield`.
699    Generator(Box<TypeExpr>),
700    /// An asynchronous stream type: `Stream<int>`. Produced by `gen fn`.
701    Stream(Box<TypeExpr>),
702    /// An owned handle type: `owned<File>`. Marks the binding as carrying
703    /// sole ownership of a drop-able resource. The compiler emits an
704    /// auto-`drop()` at the binding's enclosing block exit; the lint
705    /// `HARN-OWN-005` flags ownership leaks (e.g. returning the value or
706    /// storing it in a non-owned field).
707    Owned(Box<TypeExpr>),
708    /// A generic type application: `Option<int>`, `Result<string, int>`.
709    Applied { name: String, args: Vec<TypeExpr> },
710    /// A function type: `fn(int, string) -> bool`.
711    FnType {
712        params: Vec<TypeExpr>,
713        return_type: Box<TypeExpr>,
714    },
715    /// The bottom type: the type of expressions that never produce a value
716    /// (return, throw, break, continue).
717    Never,
718    /// A string-literal type: `"pass"`, `"fail"`. Assignable to `string`.
719    /// Used in unions to represent enum-like discriminated values.
720    LitString(String),
721    /// An int-literal type: `0`, `1`, `-1`. Assignable to `int`.
722    LitInt(i64),
723}
724
725/// A field in a dict shape type.
726#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
727pub struct ShapeField {
728    pub name: String,
729    pub type_expr: TypeExpr,
730    pub optional: bool,
731}
732
733/// A binding pattern for destructuring in let/var/for-in.
734#[derive(Debug, Clone, PartialEq, serde::Serialize)]
735pub enum BindingPattern {
736    /// Simple identifier: `let x = ...`
737    Identifier(String),
738    /// Dict destructuring: `let {name, age} = ...`
739    Dict(Vec<DictPatternField>),
740    /// List destructuring: `let [a, b] = ...`
741    List(Vec<ListPatternElement>),
742    /// Pair destructuring for `for (a, b) in iter { ... }`. The iter must
743    /// yield `VmValue::Pair` values. Not valid in let/var bindings.
744    Pair(String, String),
745}
746
747/// `_` is the discard binding name in `let`/`var`/destructuring positions.
748pub fn is_discard_name(name: &str) -> bool {
749    name == "_"
750}
751
752/// A field in a dict destructuring pattern.
753#[derive(Debug, Clone, PartialEq, serde::Serialize)]
754pub struct DictPatternField {
755    /// The dict key to extract.
756    pub key: String,
757    /// Renamed binding (if different from key), e.g. `{name: alias}`.
758    pub alias: Option<String>,
759    /// True for `...rest` (rest pattern).
760    pub is_rest: bool,
761    /// Default value if the key is missing (nil), e.g. `{name = "default"}`.
762    pub default_value: Option<Box<SNode>>,
763}
764
765/// An element in a list destructuring pattern.
766#[derive(Debug, Clone, PartialEq, serde::Serialize)]
767pub struct ListPatternElement {
768    /// The variable name to bind.
769    pub name: String,
770    /// True for `...rest` (rest pattern).
771    pub is_rest: bool,
772    /// Default value if the index is out of bounds (nil), e.g. `[a = 0]`.
773    pub default_value: Option<Box<SNode>>,
774}
775
776/// Declared variance of a generic type parameter.
777///
778/// - `Invariant` (default, no marker): the parameter appears in both
779///   input and output positions, or mutable state. `T<A>` and `T<B>`
780///   are unrelated unless `A == B`.
781/// - `Covariant` (`out T`): the parameter appears only in output
782///   positions (produced, not consumed). `T<Sub>` flows into
783///   `T<Super>`.
784/// - `Contravariant` (`in T`): the parameter appears only in input
785///   positions (consumed, not produced). `T<Super>` flows into
786///   `T<Sub>`.
787#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
788pub enum Variance {
789    Invariant,
790    Covariant,
791    Contravariant,
792}
793
794/// A generic type parameter on a function or pipeline declaration.
795#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
796pub struct TypeParam {
797    pub name: String,
798    pub variance: Variance,
799}
800
801impl TypeParam {
802    /// Construct an invariant type parameter (the default for
803    /// unannotated `<T>`).
804    pub fn invariant(name: impl Into<String>) -> Self {
805        Self {
806            name: name.into(),
807            variance: Variance::Invariant,
808        }
809    }
810}
811
812/// A where-clause constraint on a generic type parameter.
813#[derive(Debug, Clone, PartialEq, serde::Serialize)]
814pub struct WhereClause {
815    pub type_name: String,
816    pub bound: TypeExpr,
817}
818
819/// A parameter with an optional type annotation and optional default value.
820#[derive(Debug, Clone, PartialEq, serde::Serialize)]
821pub struct TypedParam {
822    pub name: String,
823    pub type_expr: Option<TypeExpr>,
824    pub default_value: Option<Box<SNode>>,
825    /// If true, this is a rest parameter (`...name`) that collects remaining arguments.
826    pub rest: bool,
827}
828
829impl TypedParam {
830    /// Create an untyped parameter.
831    pub fn untyped(name: impl Into<String>) -> Self {
832        Self {
833            name: name.into(),
834            type_expr: None,
835            default_value: None,
836            rest: false,
837        }
838    }
839
840    /// Create a typed parameter.
841    pub fn typed(name: impl Into<String>, type_expr: TypeExpr) -> Self {
842        Self {
843            name: name.into(),
844            type_expr: Some(type_expr),
845            default_value: None,
846            rest: false,
847        }
848    }
849
850    /// Extract just the names from a list of typed params.
851    pub fn names(params: &[TypedParam]) -> Vec<String> {
852        params.iter().map(|p| p.name.clone()).collect()
853    }
854
855    /// Return the index of the first parameter with a default value, or None.
856    pub fn default_start(params: &[TypedParam]) -> Option<usize> {
857        params.iter().position(|p| p.default_value.is_some())
858    }
859}