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