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        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    /// Structured-concurrency nursery: `scope { ... }`. Tasks spawned while this
278    /// block is on the task-scope stack are joined when the block exits — the
279    /// first task error cancels its siblings and propagates out of the block, so
280    /// no spawned task is orphaned or has its error silently swallowed.
281    ScopeBlock {
282        body: Vec<SNode>,
283    },
284    /// Duration literal: 500ms, 5s, 30m, 2h, 1d, 1w
285    DurationLiteral(u64),
286    /// Range expression: `start to end` (inclusive) or `start to end exclusive` (half-open)
287    RangeExpr {
288        start: Box<SNode>,
289        end: Box<SNode>,
290        inclusive: bool,
291    },
292    /// Guard clause: guard condition else { body }
293    GuardStmt {
294        condition: Box<SNode>,
295        else_body: Vec<SNode>,
296    },
297    RequireStmt {
298        condition: Box<SNode>,
299        message: Option<Box<SNode>>,
300    },
301    /// Defer statement: defer { body } — runs body at scope exit.
302    DeferStmt {
303        body: Vec<SNode>,
304    },
305    /// Deadline block: deadline DURATION { body }
306    DeadlineBlock {
307        duration: Box<SNode>,
308        body: Vec<SNode>,
309    },
310    /// Yield expression: yields control to host, optionally with a value.
311    YieldExpr {
312        value: Option<Box<SNode>>,
313    },
314    /// Emit expression: emits one value from a `gen fn` stream.
315    EmitExpr {
316        value: Box<SNode>,
317    },
318    /// Mutex block: mutual exclusion for concurrent access.
319    ///
320    /// `key` is the optional resource expression in `mutex(resource) { ... }`.
321    /// When present, all blocks acquiring the same structural key value
322    /// mutually exclude; when absent (`mutex { ... }`), the block keys on its
323    /// own lexical call-site, so two distinct `mutex {}` blocks no longer
324    /// serialize against each other.
325    MutexBlock {
326        key: Option<Box<SNode>>,
327        body: Vec<SNode>,
328    },
329    /// Break out of a loop.
330    BreakStmt,
331    /// Continue to next loop iteration.
332    ContinueStmt,
333
334    /// First-class HITL primitive expression.
335    ///
336    /// Lexed as a reserved keyword (`request_approval`, `dual_control`,
337    /// `ask_user`, `escalate_to`), parsed at primary-expression position
338    /// as `keyword "(" args ")"`. Each arg is either positional
339    /// (`expr`) or named (`name: expr`).
340    ///
341    /// The compiler lowers this to a call to the matching async stdlib
342    /// builtin in `crates/harn-vm/src/stdlib/hitl.rs`, packaging the
343    /// named arguments into the existing options-dict shape. The
344    /// typechecker assigns each kind its canonical envelope return type.
345    HitlExpr {
346        kind: HitlKind,
347        args: Vec<HitlArg>,
348    },
349
350    Parallel {
351        mode: ParallelMode,
352        /// For Count mode: the count expression. For Each/Settle: the list expression.
353        expr: Box<SNode>,
354        variable: Option<String>,
355        body: Vec<SNode>,
356        /// Optional trailing `with { max_concurrent: N, ... }` option block.
357        /// A vec (rather than a dict) preserves source order for error
358        /// reporting and keeps parsing cheap. Only `max_concurrent` is
359        /// currently honored; unknown keys are rejected by the parser.
360        options: Vec<(String, SNode)>,
361    },
362
363    SelectExpr {
364        cases: Vec<SelectCase>,
365        timeout: Option<(Box<SNode>, Vec<SNode>)>,
366        default_body: Option<Vec<SNode>>,
367    },
368
369    FunctionCall {
370        name: String,
371        type_args: Vec<TypeExpr>,
372        args: Vec<SNode>,
373    },
374    MethodCall {
375        object: Box<SNode>,
376        method: String,
377        args: Vec<SNode>,
378    },
379    /// Optional method call: `obj?.method(args)` — returns nil if obj is nil.
380    OptionalMethodCall {
381        object: Box<SNode>,
382        method: String,
383        args: Vec<SNode>,
384    },
385    PropertyAccess {
386        object: Box<SNode>,
387        property: String,
388    },
389    /// Optional chaining: `obj?.property` — returns nil if obj is nil.
390    OptionalPropertyAccess {
391        object: Box<SNode>,
392        property: String,
393    },
394    SubscriptAccess {
395        object: Box<SNode>,
396        index: Box<SNode>,
397    },
398    /// Optional subscript: `obj?[index]` — returns nil if obj is nil.
399    OptionalSubscriptAccess {
400        object: Box<SNode>,
401        index: Box<SNode>,
402    },
403    SliceAccess {
404        object: Box<SNode>,
405        start: Option<Box<SNode>>,
406        end: Option<Box<SNode>>,
407    },
408    BinaryOp {
409        op: String,
410        left: Box<SNode>,
411        right: Box<SNode>,
412    },
413    UnaryOp {
414        op: String,
415        operand: Box<SNode>,
416    },
417    Ternary {
418        condition: Box<SNode>,
419        true_expr: Box<SNode>,
420        false_expr: Box<SNode>,
421    },
422    Assignment {
423        target: Box<SNode>,
424        value: Box<SNode>,
425        /// None = plain `=`, Some("+") = `+=`, etc.
426        op: Option<String>,
427    },
428    ThrowStmt {
429        value: Box<SNode>,
430    },
431
432    /// Enum variant construction: EnumName.Variant(args)
433    EnumConstruct {
434        enum_name: String,
435        variant: String,
436        args: Vec<SNode>,
437    },
438    /// Struct construction: StructName { field: value, ... }
439    StructConstruct {
440        struct_name: String,
441        fields: Vec<DictEntry>,
442    },
443
444    InterpolatedString(Vec<StringSegment>),
445    StringLiteral(String),
446    /// Raw string literal `r"..."` — no escape processing.
447    RawStringLiteral(String),
448    IntLiteral(i64),
449    FloatLiteral(f64),
450    BoolLiteral(bool),
451    NilLiteral,
452    Identifier(String),
453    ListLiteral(Vec<SNode>),
454    DictLiteral(Vec<DictEntry>),
455    /// Spread expression `...expr` inside list/dict literals.
456    Spread(Box<SNode>),
457    /// Try operator: expr? — unwraps Result.Ok or propagates Result.Err.
458    TryOperator {
459        operand: Box<SNode>,
460    },
461    /// Try-star operator: `try* EXPR` — evaluates EXPR; on throw, runs
462    /// pending finally blocks up to the enclosing catch and rethrows
463    /// the original value. On success, evaluates to EXPR's value.
464    /// Lowered per spec/HARN_SPEC.md as:
465    ///   { let _r = try { EXPR }
466    ///     guard is_ok(_r) else { throw unwrap_err(_r) }
467    ///     unwrap(_r) }
468    TryStar {
469        operand: Box<SNode>,
470    },
471
472    /// Or-pattern in a `match` arm: `"ping" | "pong" -> body`. One or
473    /// more alternative patterns that share a single arm body. Only
474    /// legal inside a `MatchArm.pattern` slot.
475    OrPattern(Vec<SNode>),
476
477    Block(Vec<SNode>),
478    Closure {
479        params: Vec<TypedParam>,
480        body: Vec<SNode>,
481        /// When true, this closure was written as `fn(params) { body }`.
482        /// The formatter preserves this distinction.
483        fn_syntax: bool,
484    },
485}
486
487/// First-class human-in-the-loop primitive.
488///
489/// Each `HitlKind` is a reserved keyword expression with VM-enforced
490/// semantics: the names cannot be shadowed or rebound by user code,
491/// signatures are produced by the VM, and the audit log is recorded
492/// deterministically by the runtime.
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
494pub enum HitlKind {
495    /// `request_approval(action: ..., args: ..., quorum: ..., reviewers: ..., ...)`.
496    RequestApproval,
497    /// `dual_control(n: ..., m: ..., action: <closure>, approvers: ...)`.
498    DualControl,
499    /// `ask_user(prompt: ..., schema: ..., timeout: ..., default: ...)`.
500    AskUser,
501    /// `escalate_to(role: ..., reason: ...)`.
502    EscalateTo,
503}
504
505impl HitlKind {
506    /// Keyword surface form (matches the reserved keyword in the lexer
507    /// and the corresponding async builtin name in the VM).
508    pub fn as_keyword(self) -> &'static str {
509        match self {
510            HitlKind::RequestApproval => "request_approval",
511            HitlKind::DualControl => "dual_control",
512            HitlKind::AskUser => "ask_user",
513            HitlKind::EscalateTo => "escalate_to",
514        }
515    }
516}
517
518/// A single argument in a [`Node::HitlExpr`] call. `name` is `Some` when
519/// the caller used named-arg syntax (e.g. `quorum: 2`); positional
520/// arguments leave it as `None` and rely on the kind's parameter order.
521#[derive(Debug, Clone, PartialEq, serde::Serialize)]
522pub struct HitlArg {
523    pub name: Option<String>,
524    pub value: SNode,
525    pub span: Span,
526}
527
528/// Parallel execution mode.
529#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
530pub enum ParallelMode {
531    /// `parallel N { i -> ... }` — run N concurrent tasks.
532    Count,
533    /// `parallel each list { item -> ... }` — map over list concurrently.
534    Each,
535    /// `parallel each list { item -> ... } as stream` — emit as each task completes.
536    EachStream,
537    /// `parallel settle list { item -> ... }` — map with error collection.
538    Settle,
539}
540
541#[derive(Debug, Clone, PartialEq, serde::Serialize)]
542pub struct MatchArm {
543    pub pattern: SNode,
544    /// Optional guard: `pattern if condition -> { body }`.
545    pub guard: Option<Box<SNode>>,
546    pub body: Vec<SNode>,
547}
548
549#[derive(Debug, Clone, PartialEq, serde::Serialize)]
550pub struct SelectCase {
551    pub variable: String,
552    pub channel: Box<SNode>,
553    pub body: Vec<SNode>,
554}
555
556#[derive(Debug, Clone, PartialEq, serde::Serialize)]
557pub struct DictEntry {
558    pub key: SNode,
559    pub value: SNode,
560}
561
562/// An enum variant declaration.
563#[derive(Debug, Clone, PartialEq, serde::Serialize)]
564pub struct EnumVariant {
565    pub name: String,
566    pub fields: Vec<TypedParam>,
567}
568
569/// A struct field declaration.
570#[derive(Debug, Clone, PartialEq, serde::Serialize)]
571pub struct StructField {
572    pub name: String,
573    pub type_expr: Option<TypeExpr>,
574    pub optional: bool,
575}
576
577/// An interface method signature.
578#[derive(Debug, Clone, PartialEq, serde::Serialize)]
579pub struct InterfaceMethod {
580    pub name: String,
581    pub type_params: Vec<TypeParam>,
582    pub params: Vec<TypedParam>,
583    pub return_type: Option<TypeExpr>,
584}
585
586/// A type annotation (optional, for runtime checking).
587#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
588pub enum TypeExpr {
589    /// A named type: int, string, float, bool, nil, list, dict, closure,
590    /// or a user-defined type name.
591    Named(String),
592    /// A union type: `string | nil`, `int | float`.
593    Union(Vec<TypeExpr>),
594    /// An intersection type: `{x: int} & {y: int}`. The value must satisfy
595    /// every component simultaneously. Useful for layered context types
596    /// such as `fn use(ctx: BaseCtx & AuthCtx)`.
597    Intersection(Vec<TypeExpr>),
598    /// A dict shape type: `{name: string, age: int, active?: bool}`.
599    Shape(Vec<ShapeField>),
600    /// A list type: `list<int>`.
601    List(Box<TypeExpr>),
602    /// A dict type with key and value types: `dict<string, int>`.
603    DictType(Box<TypeExpr>, Box<TypeExpr>),
604    /// A lazy iterator type: `iter<int>`. Yields values of the inner type
605    /// via the combinator/sink protocol (`VmValue::Iter` at runtime).
606    Iter(Box<TypeExpr>),
607    /// A synchronous generator type: `Generator<int>`. Produced by a regular
608    /// `fn` body containing `yield`.
609    Generator(Box<TypeExpr>),
610    /// An asynchronous stream type: `Stream<int>`. Produced by `gen fn`.
611    Stream(Box<TypeExpr>),
612    /// An owned handle type: `owned<File>`. Marks the binding as carrying
613    /// sole ownership of a drop-able resource. The compiler emits an
614    /// auto-`drop()` at the binding's enclosing block exit; the lint
615    /// `HARN-OWN-005` flags ownership leaks (e.g. returning the value or
616    /// storing it in a non-owned field).
617    Owned(Box<TypeExpr>),
618    /// A generic type application: `Option<int>`, `Result<string, int>`.
619    Applied { name: String, args: Vec<TypeExpr> },
620    /// A function type: `fn(int, string) -> bool`.
621    FnType {
622        params: Vec<TypeExpr>,
623        return_type: Box<TypeExpr>,
624    },
625    /// The bottom type: the type of expressions that never produce a value
626    /// (return, throw, break, continue).
627    Never,
628    /// A string-literal type: `"pass"`, `"fail"`. Assignable to `string`.
629    /// Used in unions to represent enum-like discriminated values.
630    LitString(String),
631    /// An int-literal type: `0`, `1`, `-1`. Assignable to `int`.
632    LitInt(i64),
633}
634
635/// A field in a dict shape type.
636#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
637pub struct ShapeField {
638    pub name: String,
639    pub type_expr: TypeExpr,
640    pub optional: bool,
641}
642
643/// A binding pattern for destructuring in let/var/for-in.
644#[derive(Debug, Clone, PartialEq, serde::Serialize)]
645pub enum BindingPattern {
646    /// Simple identifier: `let x = ...`
647    Identifier(String),
648    /// Dict destructuring: `let {name, age} = ...`
649    Dict(Vec<DictPatternField>),
650    /// List destructuring: `let [a, b] = ...`
651    List(Vec<ListPatternElement>),
652    /// Pair destructuring for `for (a, b) in iter { ... }`. The iter must
653    /// yield `VmValue::Pair` values. Not valid in let/var bindings.
654    Pair(String, String),
655}
656
657/// `_` is the discard binding name in `let`/`var`/destructuring positions.
658pub fn is_discard_name(name: &str) -> bool {
659    name == "_"
660}
661
662/// A field in a dict destructuring pattern.
663#[derive(Debug, Clone, PartialEq, serde::Serialize)]
664pub struct DictPatternField {
665    /// The dict key to extract.
666    pub key: String,
667    /// Renamed binding (if different from key), e.g. `{name: alias}`.
668    pub alias: Option<String>,
669    /// True for `...rest` (rest pattern).
670    pub is_rest: bool,
671    /// Default value if the key is missing (nil), e.g. `{name = "default"}`.
672    pub default_value: Option<Box<SNode>>,
673}
674
675/// An element in a list destructuring pattern.
676#[derive(Debug, Clone, PartialEq, serde::Serialize)]
677pub struct ListPatternElement {
678    /// The variable name to bind.
679    pub name: String,
680    /// True for `...rest` (rest pattern).
681    pub is_rest: bool,
682    /// Default value if the index is out of bounds (nil), e.g. `[a = 0]`.
683    pub default_value: Option<Box<SNode>>,
684}
685
686/// Declared variance of a generic type parameter.
687///
688/// - `Invariant` (default, no marker): the parameter appears in both
689///   input and output positions, or mutable state. `T<A>` and `T<B>`
690///   are unrelated unless `A == B`.
691/// - `Covariant` (`out T`): the parameter appears only in output
692///   positions (produced, not consumed). `T<Sub>` flows into
693///   `T<Super>`.
694/// - `Contravariant` (`in T`): the parameter appears only in input
695///   positions (consumed, not produced). `T<Super>` flows into
696///   `T<Sub>`.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
698pub enum Variance {
699    Invariant,
700    Covariant,
701    Contravariant,
702}
703
704/// A generic type parameter on a function or pipeline declaration.
705#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
706pub struct TypeParam {
707    pub name: String,
708    pub variance: Variance,
709}
710
711impl TypeParam {
712    /// Construct an invariant type parameter (the default for
713    /// unannotated `<T>`).
714    pub fn invariant(name: impl Into<String>) -> Self {
715        Self {
716            name: name.into(),
717            variance: Variance::Invariant,
718        }
719    }
720}
721
722/// A where-clause constraint on a generic type parameter.
723#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
724pub struct WhereClause {
725    pub type_name: String,
726    pub bound: String,
727}
728
729/// A parameter with an optional type annotation and optional default value.
730#[derive(Debug, Clone, PartialEq, serde::Serialize)]
731pub struct TypedParam {
732    pub name: String,
733    pub type_expr: Option<TypeExpr>,
734    pub default_value: Option<Box<SNode>>,
735    /// If true, this is a rest parameter (`...name`) that collects remaining arguments.
736    pub rest: bool,
737}
738
739impl TypedParam {
740    /// Create an untyped parameter.
741    pub fn untyped(name: impl Into<String>) -> Self {
742        Self {
743            name: name.into(),
744            type_expr: None,
745            default_value: None,
746            rest: false,
747        }
748    }
749
750    /// Create a typed parameter.
751    pub fn typed(name: impl Into<String>, type_expr: TypeExpr) -> Self {
752        Self {
753            name: name.into(),
754            type_expr: Some(type_expr),
755            default_value: None,
756            rest: false,
757        }
758    }
759
760    /// Extract just the names from a list of typed params.
761    pub fn names(params: &[TypedParam]) -> Vec<String> {
762        params.iter().map(|p| p.name.clone()).collect()
763    }
764
765    /// Return the index of the first parameter with a default value, or None.
766    pub fn default_start(params: &[TypedParam]) -> Option<usize> {
767        params.iter().position(|p| p.default_value.is_some())
768    }
769}