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