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, 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        body: Vec<SNode>,
104        extends: Option<String>,
105        is_pub: bool,
106    },
107    LetBinding {
108        pattern: BindingPattern,
109        type_ann: Option<TypeExpr>,
110        value: Box<SNode>,
111    },
112    VarBinding {
113        pattern: BindingPattern,
114        type_ann: Option<TypeExpr>,
115        value: Box<SNode>,
116    },
117    /// `const NAME [: Type] = EXPR` — compile-time-evaluated binding.
118    ///
119    /// The initializer is run through `harn_parser::const_eval` during
120    /// typecheck under a strict, bounded sandbox: pure arithmetic, string
121    /// concatenation, literal lists/dicts, ternaries, and reads of earlier
122    /// `const` identifiers are permitted; any call to host capabilities,
123    /// fs/net/env/process bridges, mutation, or unbounded loops/recursion
124    /// is rejected with a `HARN-CST-*` or `HARN-MET-001` diagnostic. At
125    /// runtime the binding behaves like a `let` binding — the same
126    /// expression is re-evaluated by the VM so the runtime value matches
127    /// the compile-time fold byte-for-byte (the sandbox subset is a
128    /// strict subset of runtime semantics for pure expressions).
129    ConstBinding {
130        name: String,
131        type_ann: Option<TypeExpr>,
132        value: Box<SNode>,
133    },
134    OverrideDecl {
135        name: String,
136        params: Vec<String>,
137        body: Vec<SNode>,
138    },
139    ImportDecl {
140        path: String,
141        /// When true, the wildcard import is a re-export: every public symbol
142        /// from the target module becomes part of this module's public surface.
143        is_pub: bool,
144    },
145    /// Selective import: import { foo, bar } from "module"
146    SelectiveImport {
147        names: Vec<String>,
148        path: String,
149        /// When true, the listed names are re-exported as part of this
150        /// module's public surface.
151        is_pub: bool,
152    },
153    EnumDecl {
154        name: String,
155        type_params: Vec<TypeParam>,
156        variants: Vec<EnumVariant>,
157        is_pub: bool,
158    },
159    StructDecl {
160        name: String,
161        type_params: Vec<TypeParam>,
162        fields: Vec<StructField>,
163        is_pub: bool,
164    },
165    InterfaceDecl {
166        name: String,
167        type_params: Vec<TypeParam>,
168        associated_types: Vec<(String, Option<TypeExpr>)>,
169        methods: Vec<InterfaceMethod>,
170    },
171    /// Impl block: impl TypeName { fn method(self, ...) { ... } ... }
172    ImplBlock {
173        type_name: String,
174        methods: Vec<SNode>,
175    },
176
177    IfElse {
178        condition: Box<SNode>,
179        then_body: Vec<SNode>,
180        else_body: Option<Vec<SNode>>,
181    },
182    ForIn {
183        pattern: BindingPattern,
184        iterable: Box<SNode>,
185        body: Vec<SNode>,
186    },
187    MatchExpr {
188        value: Box<SNode>,
189        arms: Vec<MatchArm>,
190    },
191    WhileLoop {
192        condition: Box<SNode>,
193        body: Vec<SNode>,
194    },
195    Retry {
196        count: Box<SNode>,
197        body: Vec<SNode>,
198    },
199    /// Scoped cost-aware LLM routing block:
200    /// `cost_route { key: value ... body }`.
201    ///
202    /// Options are inherited by nested `llm_call` invocations unless a
203    /// call explicitly overrides the same option.
204    CostRoute {
205        options: Vec<(String, SNode)>,
206        body: Vec<SNode>,
207    },
208    ReturnStmt {
209        value: Option<Box<SNode>>,
210    },
211    TryCatch {
212        body: Vec<SNode>,
213        has_catch: bool,
214        error_var: Option<String>,
215        error_type: Option<TypeExpr>,
216        catch_body: Vec<SNode>,
217        finally_body: Option<Vec<SNode>>,
218    },
219    /// Try expression: try { body } — returns Result.Ok(value), an existing Result,
220    /// or Result.Err(error).
221    TryExpr {
222        body: Vec<SNode>,
223    },
224    FnDecl {
225        name: String,
226        type_params: Vec<TypeParam>,
227        params: Vec<TypedParam>,
228        return_type: Option<TypeExpr>,
229        where_clauses: Vec<WhereClause>,
230        body: Vec<SNode>,
231        is_pub: bool,
232        is_stream: bool,
233    },
234    ToolDecl {
235        name: String,
236        description: Option<String>,
237        params: Vec<TypedParam>,
238        return_type: Option<TypeExpr>,
239        body: Vec<SNode>,
240        is_pub: bool,
241    },
242    /// Top-level `skill NAME { ... }` declaration.
243    ///
244    /// Skills bundle metadata, tool references, MCP server lists, and
245    /// optional lifecycle hooks into a typed unit. Each body entry is a
246    /// `<field_name> <expression>` pair; the compiler lowers the decl to
247    /// `skill_define(skill_registry(), NAME, { field: expr, ... })` and
248    /// binds the resulting registry dict to `NAME`.
249    SkillDecl {
250        name: String,
251        fields: Vec<(String, SNode)>,
252        is_pub: bool,
253    },
254    /// Top-level `eval_pack NAME_OR_STRING { ... }` declaration.
255    ///
256    /// The compiler lowers fields into `eval_pack_manifest({ ... })` and
257    /// binds the normalized manifest to `binding_name`. Optional executable
258    /// body statements are only run when the declaration itself is executed
259    /// in script/block position; top-level pipeline preloading registers the
260    /// manifest data without running the body.
261    EvalPackDecl {
262        binding_name: String,
263        pack_id: String,
264        fields: Vec<(String, SNode)>,
265        body: Vec<SNode>,
266        summarize: Option<Vec<SNode>>,
267        is_pub: bool,
268    },
269    TypeDecl {
270        name: String,
271        type_params: Vec<TypeParam>,
272        type_expr: TypeExpr,
273    },
274    SpawnExpr {
275        body: Vec<SNode>,
276    },
277    /// Duration literal: 500ms, 5s, 30m, 2h, 1d, 1w
278    DurationLiteral(u64),
279    /// Range expression: `start to end` (inclusive) or `start to end exclusive` (half-open)
280    RangeExpr {
281        start: Box<SNode>,
282        end: Box<SNode>,
283        inclusive: bool,
284    },
285    /// Guard clause: guard condition else { body }
286    GuardStmt {
287        condition: Box<SNode>,
288        else_body: Vec<SNode>,
289    },
290    RequireStmt {
291        condition: Box<SNode>,
292        message: Option<Box<SNode>>,
293    },
294    /// Defer statement: defer { body } — runs body at scope exit.
295    DeferStmt {
296        body: Vec<SNode>,
297    },
298    /// Deadline block: deadline DURATION { body }
299    DeadlineBlock {
300        duration: Box<SNode>,
301        body: Vec<SNode>,
302    },
303    /// Yield expression: yields control to host, optionally with a value.
304    YieldExpr {
305        value: Option<Box<SNode>>,
306    },
307    /// Emit expression: emits one value from a `gen fn` stream.
308    EmitExpr {
309        value: Box<SNode>,
310    },
311    /// Mutex block: mutual exclusion for concurrent access.
312    MutexBlock {
313        body: Vec<SNode>,
314    },
315    /// Break out of a loop.
316    BreakStmt,
317    /// Continue to next loop iteration.
318    ContinueStmt,
319
320    /// First-class HITL primitive expression.
321    ///
322    /// Lexed as a reserved keyword (`request_approval`, `dual_control`,
323    /// `ask_user`, `escalate_to`), parsed at primary-expression position
324    /// as `keyword "(" args ")"`. Each arg is either positional
325    /// (`expr`) or named (`name: expr`).
326    ///
327    /// The compiler lowers this to a call to the matching async stdlib
328    /// builtin in `crates/harn-vm/src/stdlib/hitl.rs`, packaging the
329    /// named arguments into the existing options-dict shape. The
330    /// typechecker assigns each kind its canonical envelope return type.
331    HitlExpr {
332        kind: HitlKind,
333        args: Vec<HitlArg>,
334    },
335
336    Parallel {
337        mode: ParallelMode,
338        /// For Count mode: the count expression. For Each/Settle: the list expression.
339        expr: Box<SNode>,
340        variable: Option<String>,
341        body: Vec<SNode>,
342        /// Optional trailing `with { max_concurrent: N, ... }` option block.
343        /// A vec (rather than a dict) preserves source order for error
344        /// reporting and keeps parsing cheap. Only `max_concurrent` is
345        /// currently honored; unknown keys are rejected by the parser.
346        options: Vec<(String, SNode)>,
347    },
348
349    SelectExpr {
350        cases: Vec<SelectCase>,
351        timeout: Option<(Box<SNode>, Vec<SNode>)>,
352        default_body: Option<Vec<SNode>>,
353    },
354
355    FunctionCall {
356        name: String,
357        type_args: Vec<TypeExpr>,
358        args: Vec<SNode>,
359    },
360    MethodCall {
361        object: Box<SNode>,
362        method: String,
363        args: Vec<SNode>,
364    },
365    /// Optional method call: `obj?.method(args)` — returns nil if obj is nil.
366    OptionalMethodCall {
367        object: Box<SNode>,
368        method: String,
369        args: Vec<SNode>,
370    },
371    PropertyAccess {
372        object: Box<SNode>,
373        property: String,
374    },
375    /// Optional chaining: `obj?.property` — returns nil if obj is nil.
376    OptionalPropertyAccess {
377        object: Box<SNode>,
378        property: String,
379    },
380    SubscriptAccess {
381        object: Box<SNode>,
382        index: Box<SNode>,
383    },
384    /// Optional subscript: `obj?[index]` — returns nil if obj is nil.
385    OptionalSubscriptAccess {
386        object: Box<SNode>,
387        index: Box<SNode>,
388    },
389    SliceAccess {
390        object: Box<SNode>,
391        start: Option<Box<SNode>>,
392        end: Option<Box<SNode>>,
393    },
394    BinaryOp {
395        op: String,
396        left: Box<SNode>,
397        right: Box<SNode>,
398    },
399    UnaryOp {
400        op: String,
401        operand: Box<SNode>,
402    },
403    Ternary {
404        condition: Box<SNode>,
405        true_expr: Box<SNode>,
406        false_expr: Box<SNode>,
407    },
408    Assignment {
409        target: Box<SNode>,
410        value: Box<SNode>,
411        /// None = plain `=`, Some("+") = `+=`, etc.
412        op: Option<String>,
413    },
414    ThrowStmt {
415        value: Box<SNode>,
416    },
417
418    /// Enum variant construction: EnumName.Variant(args)
419    EnumConstruct {
420        enum_name: String,
421        variant: String,
422        args: Vec<SNode>,
423    },
424    /// Struct construction: StructName { field: value, ... }
425    StructConstruct {
426        struct_name: String,
427        fields: Vec<DictEntry>,
428    },
429
430    InterpolatedString(Vec<StringSegment>),
431    StringLiteral(String),
432    /// Raw string literal `r"..."` — no escape processing.
433    RawStringLiteral(String),
434    IntLiteral(i64),
435    FloatLiteral(f64),
436    BoolLiteral(bool),
437    NilLiteral,
438    Identifier(String),
439    ListLiteral(Vec<SNode>),
440    DictLiteral(Vec<DictEntry>),
441    /// Spread expression `...expr` inside list/dict literals.
442    Spread(Box<SNode>),
443    /// Try operator: expr? — unwraps Result.Ok or propagates Result.Err.
444    TryOperator {
445        operand: Box<SNode>,
446    },
447    /// Try-star operator: `try* EXPR` — evaluates EXPR; on throw, runs
448    /// pending finally blocks up to the enclosing catch and rethrows
449    /// the original value. On success, evaluates to EXPR's value.
450    /// Lowered per spec/HARN_SPEC.md as:
451    ///   { let _r = try { EXPR }
452    ///     guard is_ok(_r) else { throw unwrap_err(_r) }
453    ///     unwrap(_r) }
454    TryStar {
455        operand: Box<SNode>,
456    },
457
458    /// Or-pattern in a `match` arm: `"ping" | "pong" -> body`. One or
459    /// more alternative patterns that share a single arm body. Only
460    /// legal inside a `MatchArm.pattern` slot.
461    OrPattern(Vec<SNode>),
462
463    Block(Vec<SNode>),
464    Closure {
465        params: Vec<TypedParam>,
466        body: Vec<SNode>,
467        /// When true, this closure was written as `fn(params) { body }`.
468        /// The formatter preserves this distinction.
469        fn_syntax: bool,
470    },
471}
472
473/// First-class human-in-the-loop primitive.
474///
475/// Each `HitlKind` is a reserved keyword expression with VM-enforced
476/// semantics: the names cannot be shadowed or rebound by user code,
477/// signatures are produced by the VM, and the audit log is recorded
478/// deterministically by the runtime.
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
480pub enum HitlKind {
481    /// `request_approval(action: ..., args: ..., quorum: ..., reviewers: ..., ...)`.
482    RequestApproval,
483    /// `dual_control(n: ..., m: ..., action: <closure>, approvers: ...)`.
484    DualControl,
485    /// `ask_user(prompt: ..., schema: ..., timeout: ..., default: ...)`.
486    AskUser,
487    /// `escalate_to(role: ..., reason: ...)`.
488    EscalateTo,
489}
490
491impl HitlKind {
492    /// Keyword surface form (matches the reserved keyword in the lexer
493    /// and the corresponding async builtin name in the VM).
494    pub fn as_keyword(self) -> &'static str {
495        match self {
496            HitlKind::RequestApproval => "request_approval",
497            HitlKind::DualControl => "dual_control",
498            HitlKind::AskUser => "ask_user",
499            HitlKind::EscalateTo => "escalate_to",
500        }
501    }
502}
503
504/// A single argument in a [`Node::HitlExpr`] call. `name` is `Some` when
505/// the caller used named-arg syntax (e.g. `quorum: 2`); positional
506/// arguments leave it as `None` and rely on the kind's parameter order.
507#[derive(Debug, Clone, PartialEq, serde::Serialize)]
508pub struct HitlArg {
509    pub name: Option<String>,
510    pub value: SNode,
511    pub span: Span,
512}
513
514/// Parallel execution mode.
515#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
516pub enum ParallelMode {
517    /// `parallel N { i -> ... }` — run N concurrent tasks.
518    Count,
519    /// `parallel each list { item -> ... }` — map over list concurrently.
520    Each,
521    /// `parallel each list { item -> ... } as stream` — emit as each task completes.
522    EachStream,
523    /// `parallel settle list { item -> ... }` — map with error collection.
524    Settle,
525}
526
527#[derive(Debug, Clone, PartialEq, serde::Serialize)]
528pub struct MatchArm {
529    pub pattern: SNode,
530    /// Optional guard: `pattern if condition -> { body }`.
531    pub guard: Option<Box<SNode>>,
532    pub body: Vec<SNode>,
533}
534
535#[derive(Debug, Clone, PartialEq, serde::Serialize)]
536pub struct SelectCase {
537    pub variable: String,
538    pub channel: Box<SNode>,
539    pub body: Vec<SNode>,
540}
541
542#[derive(Debug, Clone, PartialEq, serde::Serialize)]
543pub struct DictEntry {
544    pub key: SNode,
545    pub value: SNode,
546}
547
548/// An enum variant declaration.
549#[derive(Debug, Clone, PartialEq, serde::Serialize)]
550pub struct EnumVariant {
551    pub name: String,
552    pub fields: Vec<TypedParam>,
553}
554
555/// A struct field declaration.
556#[derive(Debug, Clone, PartialEq, serde::Serialize)]
557pub struct StructField {
558    pub name: String,
559    pub type_expr: Option<TypeExpr>,
560    pub optional: bool,
561}
562
563/// An interface method signature.
564#[derive(Debug, Clone, PartialEq, serde::Serialize)]
565pub struct InterfaceMethod {
566    pub name: String,
567    pub type_params: Vec<TypeParam>,
568    pub params: Vec<TypedParam>,
569    pub return_type: Option<TypeExpr>,
570}
571
572/// A type annotation (optional, for runtime checking).
573#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
574pub enum TypeExpr {
575    /// A named type: int, string, float, bool, nil, list, dict, closure,
576    /// or a user-defined type name.
577    Named(String),
578    /// A union type: `string | nil`, `int | float`.
579    Union(Vec<TypeExpr>),
580    /// An intersection type: `{x: int} & {y: int}`. The value must satisfy
581    /// every component simultaneously. Useful for layered context types
582    /// such as `fn use(ctx: BaseCtx & AuthCtx)`.
583    Intersection(Vec<TypeExpr>),
584    /// A dict shape type: `{name: string, age: int, active?: bool}`.
585    Shape(Vec<ShapeField>),
586    /// A list type: `list<int>`.
587    List(Box<TypeExpr>),
588    /// A dict type with key and value types: `dict<string, int>`.
589    DictType(Box<TypeExpr>, Box<TypeExpr>),
590    /// A lazy iterator type: `iter<int>`. Yields values of the inner type
591    /// via the combinator/sink protocol (`VmValue::Iter` at runtime).
592    Iter(Box<TypeExpr>),
593    /// A synchronous generator type: `Generator<int>`. Produced by a regular
594    /// `fn` body containing `yield`.
595    Generator(Box<TypeExpr>),
596    /// An asynchronous stream type: `Stream<int>`. Produced by `gen fn`.
597    Stream(Box<TypeExpr>),
598    /// An owned handle type: `owned<File>`. Marks the binding as carrying
599    /// sole ownership of a drop-able resource. The compiler emits an
600    /// auto-`drop()` at the binding's enclosing block exit; the lint
601    /// `HARN-OWN-005` flags ownership leaks (e.g. returning the value or
602    /// storing it in a non-owned field).
603    Owned(Box<TypeExpr>),
604    /// A generic type application: `Option<int>`, `Result<string, int>`.
605    Applied { name: String, args: Vec<TypeExpr> },
606    /// A function type: `fn(int, string) -> bool`.
607    FnType {
608        params: Vec<TypeExpr>,
609        return_type: Box<TypeExpr>,
610    },
611    /// The bottom type: the type of expressions that never produce a value
612    /// (return, throw, break, continue).
613    Never,
614    /// A string-literal type: `"pass"`, `"fail"`. Assignable to `string`.
615    /// Used in unions to represent enum-like discriminated values.
616    LitString(String),
617    /// An int-literal type: `0`, `1`, `-1`. Assignable to `int`.
618    LitInt(i64),
619}
620
621/// A field in a dict shape type.
622#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
623pub struct ShapeField {
624    pub name: String,
625    pub type_expr: TypeExpr,
626    pub optional: bool,
627}
628
629/// A binding pattern for destructuring in let/var/for-in.
630#[derive(Debug, Clone, PartialEq, serde::Serialize)]
631pub enum BindingPattern {
632    /// Simple identifier: `let x = ...`
633    Identifier(String),
634    /// Dict destructuring: `let {name, age} = ...`
635    Dict(Vec<DictPatternField>),
636    /// List destructuring: `let [a, b] = ...`
637    List(Vec<ListPatternElement>),
638    /// Pair destructuring for `for (a, b) in iter { ... }`. The iter must
639    /// yield `VmValue::Pair` values. Not valid in let/var bindings.
640    Pair(String, String),
641}
642
643/// `_` is the discard binding name in `let`/`var`/destructuring positions.
644pub fn is_discard_name(name: &str) -> bool {
645    name == "_"
646}
647
648/// A field in a dict destructuring pattern.
649#[derive(Debug, Clone, PartialEq, serde::Serialize)]
650pub struct DictPatternField {
651    /// The dict key to extract.
652    pub key: String,
653    /// Renamed binding (if different from key), e.g. `{name: alias}`.
654    pub alias: Option<String>,
655    /// True for `...rest` (rest pattern).
656    pub is_rest: bool,
657    /// Default value if the key is missing (nil), e.g. `{name = "default"}`.
658    pub default_value: Option<Box<SNode>>,
659}
660
661/// An element in a list destructuring pattern.
662#[derive(Debug, Clone, PartialEq, serde::Serialize)]
663pub struct ListPatternElement {
664    /// The variable name to bind.
665    pub name: String,
666    /// True for `...rest` (rest pattern).
667    pub is_rest: bool,
668    /// Default value if the index is out of bounds (nil), e.g. `[a = 0]`.
669    pub default_value: Option<Box<SNode>>,
670}
671
672/// Declared variance of a generic type parameter.
673///
674/// - `Invariant` (default, no marker): the parameter appears in both
675///   input and output positions, or mutable state. `T<A>` and `T<B>`
676///   are unrelated unless `A == B`.
677/// - `Covariant` (`out T`): the parameter appears only in output
678///   positions (produced, not consumed). `T<Sub>` flows into
679///   `T<Super>`.
680/// - `Contravariant` (`in T`): the parameter appears only in input
681///   positions (consumed, not produced). `T<Super>` flows into
682///   `T<Sub>`.
683#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
684pub enum Variance {
685    Invariant,
686    Covariant,
687    Contravariant,
688}
689
690/// A generic type parameter on a function or pipeline declaration.
691#[derive(Debug, Clone, PartialEq, serde::Serialize)]
692pub struct TypeParam {
693    pub name: String,
694    pub variance: Variance,
695}
696
697impl TypeParam {
698    /// Construct an invariant type parameter (the default for
699    /// unannotated `<T>`).
700    pub fn invariant(name: impl Into<String>) -> Self {
701        Self {
702            name: name.into(),
703            variance: Variance::Invariant,
704        }
705    }
706}
707
708/// A where-clause constraint on a generic type parameter.
709#[derive(Debug, Clone, PartialEq, serde::Serialize)]
710pub struct WhereClause {
711    pub type_name: String,
712    pub bound: String,
713}
714
715/// A parameter with an optional type annotation and optional default value.
716#[derive(Debug, Clone, PartialEq, serde::Serialize)]
717pub struct TypedParam {
718    pub name: String,
719    pub type_expr: Option<TypeExpr>,
720    pub default_value: Option<Box<SNode>>,
721    /// If true, this is a rest parameter (`...name`) that collects remaining arguments.
722    pub rest: bool,
723}
724
725impl TypedParam {
726    /// Create an untyped parameter.
727    pub fn untyped(name: impl Into<String>) -> Self {
728        Self {
729            name: name.into(),
730            type_expr: None,
731            default_value: None,
732            rest: false,
733        }
734    }
735
736    /// Create a typed parameter.
737    pub fn typed(name: impl Into<String>, type_expr: TypeExpr) -> Self {
738        Self {
739            name: name.into(),
740            type_expr: Some(type_expr),
741            default_value: None,
742            rest: false,
743        }
744    }
745
746    /// Extract just the names from a list of typed params.
747    pub fn names(params: &[TypedParam]) -> Vec<String> {
748        params.iter().map(|p| p.name.clone()).collect()
749    }
750
751    /// Return the index of the first parameter with a default value, or None.
752    pub fn default_start(params: &[TypedParam]) -> Option<usize> {
753        params.iter().position(|p| p.default_value.is_some())
754    }
755}