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