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