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<TypedParam>,
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 /// allowlist 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<AssociatedType>,
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 /// Source extent of the `then` block, including its braces.
198 then_span: Span,
199 else_body: Option<Vec<SNode>>,
200 /// Source extent of a braced `else` block, including its braces.
201 /// `None` identifies the synthetic wrapper used for `else if`.
202 else_span: Option<Span>,
203 },
204 ForIn {
205 pattern: BindingPattern,
206 iterable: Box<SNode>,
207 body: Vec<SNode>,
208 },
209 MatchExpr {
210 value: Box<SNode>,
211 arms: Vec<MatchArm>,
212 },
213 WhileLoop {
214 condition: Box<SNode>,
215 body: Vec<SNode>,
216 },
217 Retry {
218 count: Box<SNode>,
219 body: Vec<SNode>,
220 },
221 /// Scoped cost-aware LLM routing block:
222 /// `cost_route { key: value ... body }`.
223 ///
224 /// Options are inherited by nested `llm_call` invocations unless a
225 /// call explicitly overrides the same option.
226 CostRoute {
227 options: Vec<(String, SNode)>,
228 body: Vec<SNode>,
229 },
230 ReturnStmt {
231 value: Option<Box<SNode>>,
232 },
233 TryCatch {
234 body: Vec<SNode>,
235 /// Source extent of the `try` block, including its braces.
236 try_span: Span,
237 has_catch: bool,
238 error_var: Option<String>,
239 error_type: Option<TypeExpr>,
240 catch_body: Vec<SNode>,
241 /// Source extent of the `catch` block, including its braces.
242 catch_span: Option<Span>,
243 finally_body: Option<Vec<SNode>>,
244 /// Source extent of the `finally` block, including its braces.
245 finally_span: Option<Span>,
246 },
247 /// Try expression: try { body } — returns Result.Ok(value), an existing Result,
248 /// or Result.Err(error).
249 TryExpr {
250 body: Vec<SNode>,
251 },
252 FnDecl {
253 name: String,
254 type_params: Vec<TypeParam>,
255 params: Vec<TypedParam>,
256 return_type: Option<TypeExpr>,
257 /// Declared exception channel `throws E` / `throws (E1 | E2)`; see the
258 /// [`Node::Pipeline`] `throws` field. `None` = unconstrained.
259 throws: Option<TypeExpr>,
260 where_clauses: Vec<WhereClause>,
261 body: Vec<SNode>,
262 is_pub: bool,
263 is_stream: bool,
264 },
265 ToolDecl {
266 name: String,
267 description: Option<String>,
268 params: Vec<TypedParam>,
269 return_type: Option<TypeExpr>,
270 /// Declared exception channel; see the [`Node::Pipeline`] `throws`
271 /// field. `None` = unconstrained.
272 throws: Option<TypeExpr>,
273 body: Vec<SNode>,
274 is_pub: bool,
275 },
276 /// Top-level `skill NAME { ... }` declaration.
277 ///
278 /// Skills bundle metadata, tool references, MCP server lists, and
279 /// optional lifecycle hooks into a typed unit. Each body entry is a
280 /// `<field_name> <expression>` pair; the compiler lowers the decl to
281 /// `skill_define(skill_registry(), NAME, { field: expr, ... })` and
282 /// binds the resulting registry dict to `NAME`.
283 SkillDecl {
284 name: String,
285 fields: Vec<(String, SNode)>,
286 is_pub: bool,
287 },
288 /// Top-level `eval_pack NAME_OR_STRING { ... }` declaration.
289 ///
290 /// The compiler lowers fields into `eval_pack_manifest({ ... })` and
291 /// binds the normalized manifest to `binding_name`. Optional executable
292 /// body statements are only run when the declaration itself is executed
293 /// in script/block position; top-level pipeline preloading registers the
294 /// manifest data without running the body.
295 EvalPackDecl {
296 binding_name: String,
297 pack_id: String,
298 fields: Vec<(String, SNode)>,
299 body: Vec<SNode>,
300 summarize: Option<Vec<SNode>>,
301 is_pub: bool,
302 },
303 TypeDecl {
304 name: String,
305 type_params: Vec<TypeParam>,
306 type_expr: TypeExpr,
307 is_pub: bool,
308 },
309 SpawnExpr {
310 body: Vec<SNode>,
311 },
312 /// Structured-concurrency nursery: `scope { ... }`. Tasks spawned while this
313 /// block is on the task-scope stack are joined when the block exits — the
314 /// first task error cancels its siblings and propagates out of the block, so
315 /// no spawned task is orphaned or has its error silently swallowed.
316 ScopeBlock {
317 body: Vec<SNode>,
318 },
319 /// Duration literal: 500ms, 5s, 30m, 2h, 1d, 1w
320 DurationLiteral(u64),
321 /// Range expression: `start to end` (inclusive) or `start to end exclusive` (half-open)
322 RangeExpr {
323 start: Box<SNode>,
324 end: Box<SNode>,
325 inclusive: bool,
326 },
327 /// Guard clause: guard condition else { body }
328 GuardStmt {
329 condition: Box<SNode>,
330 else_body: Vec<SNode>,
331 },
332 RequireStmt {
333 condition: Box<SNode>,
334 message: Option<Box<SNode>>,
335 },
336 /// Defer statement: defer { body } — runs body at scope exit.
337 DeferStmt {
338 body: Vec<SNode>,
339 },
340 /// Deadline block: deadline DURATION { body }
341 DeadlineBlock {
342 duration: Box<SNode>,
343 body: Vec<SNode>,
344 },
345 /// Yield expression: yields control to host, optionally with a value.
346 YieldExpr {
347 value: Option<Box<SNode>>,
348 },
349 /// Emit expression: emits one value from a `gen fn` stream.
350 EmitExpr {
351 value: Box<SNode>,
352 },
353 /// Mutex block: mutual exclusion for concurrent access.
354 ///
355 /// `key` is the optional resource expression in `mutex(resource) { ... }`.
356 /// When present, all blocks acquiring the same structural key value
357 /// mutually exclude; when absent (`mutex { ... }`), the block keys on its
358 /// own lexical call-site, so two distinct `mutex {}` blocks no longer
359 /// serialize against each other.
360 MutexBlock {
361 key: Option<Box<SNode>>,
362 body: Vec<SNode>,
363 },
364 /// Break out of a loop.
365 BreakStmt,
366 /// Continue to next loop iteration.
367 ContinueStmt,
368
369 /// First-class HITL primitive expression.
370 ///
371 /// Lexed as a reserved keyword (`request_approval`, `dual_control`,
372 /// `ask_user`, `escalate_to`), parsed at primary-expression position
373 /// as `keyword "(" args ")"`. Each arg is either positional
374 /// (`expr`) or named (`name: expr`).
375 ///
376 /// The compiler lowers this to a call to the matching async stdlib
377 /// builtin in `crates/harn-vm/src/stdlib/hitl.rs`, packaging the
378 /// named arguments into the existing options-dict shape. The
379 /// typechecker assigns each kind its canonical envelope return type.
380 HitlExpr {
381 kind: HitlKind,
382 args: Vec<HitlArg>,
383 },
384
385 Parallel {
386 mode: ParallelMode,
387 /// For Count mode: the count expression. For Each/Settle: the list expression.
388 expr: Box<SNode>,
389 variable: Option<String>,
390 body: Vec<SNode>,
391 /// Optional trailing `with { max_concurrent: N, ... }` option block.
392 /// A vec (rather than a dict) preserves source order for error
393 /// reporting and keeps parsing cheap. Only `max_concurrent` is
394 /// currently honored; unknown keys are rejected by the parser.
395 options: Vec<(String, SNode)>,
396 },
397
398 SelectExpr {
399 cases: Vec<SelectCase>,
400 timeout: Option<(Box<SNode>, Vec<SNode>)>,
401 default_body: Option<Vec<SNode>>,
402 },
403
404 FunctionCall {
405 name: String,
406 type_args: Vec<TypeExpr>,
407 args: Vec<SNode>,
408 },
409 MethodCall {
410 object: Box<SNode>,
411 method: String,
412 args: Vec<SNode>,
413 },
414 /// Optional method call: `obj?.method(args)` — returns nil if obj is nil.
415 OptionalMethodCall {
416 object: Box<SNode>,
417 method: String,
418 args: Vec<SNode>,
419 },
420 PropertyAccess {
421 object: Box<SNode>,
422 property: String,
423 },
424 /// Optional chaining: `obj?.property` — returns nil if obj is nil.
425 OptionalPropertyAccess {
426 object: Box<SNode>,
427 property: String,
428 },
429 SubscriptAccess {
430 object: Box<SNode>,
431 index: Box<SNode>,
432 },
433 /// Optional subscript: `obj?.[index]` — returns nil if obj is nil.
434 OptionalSubscriptAccess {
435 object: Box<SNode>,
436 index: Box<SNode>,
437 },
438 SliceAccess {
439 object: Box<SNode>,
440 start: Option<Box<SNode>>,
441 end: Option<Box<SNode>>,
442 },
443 BinaryOp {
444 op: String,
445 left: Box<SNode>,
446 right: Box<SNode>,
447 },
448 UnaryOp {
449 op: String,
450 operand: Box<SNode>,
451 },
452 Ternary {
453 condition: Box<SNode>,
454 true_expr: Box<SNode>,
455 false_expr: Box<SNode>,
456 },
457 Assignment {
458 target: Box<SNode>,
459 value: Box<SNode>,
460 /// None = plain `=`, Some("+") = `+=`, etc.
461 op: Option<String>,
462 },
463 ThrowStmt {
464 value: Box<SNode>,
465 },
466
467 /// Enum variant construction: EnumName.Variant(args)
468 EnumConstruct {
469 enum_name: String,
470 variant: String,
471 args: Vec<SNode>,
472 },
473 /// Struct construction: StructName { field: value, ... }
474 StructConstruct {
475 struct_name: String,
476 fields: Vec<DictEntry>,
477 },
478
479 InterpolatedString(Vec<StringSegment>),
480 StringLiteral(String),
481 /// Raw string literal `r"..."` — no escape processing.
482 RawStringLiteral(String),
483 IntLiteral(i64),
484 FloatLiteral(f64),
485 BoolLiteral(bool),
486 NilLiteral,
487 Identifier(String),
488 ListLiteral(Vec<SNode>),
489 DictLiteral(Vec<DictEntry>),
490 /// Spread expression `...expr` inside list/dict literals.
491 Spread(Box<SNode>),
492 /// Try operator: expr? — unwraps Result.Ok or propagates Result.Err.
493 TryOperator {
494 operand: Box<SNode>,
495 },
496 /// Non-null assertion: `expr!` — asserts the operand is not `nil`.
497 /// Statically strips `nil` from the operand's type (`T | nil` -> `T`);
498 /// at runtime it is identity when the value is present and throws a
499 /// structured `unwrap_nil` error when it is `nil`.
500 NonNullAssert {
501 operand: Box<SNode>,
502 },
503 /// Try-star operator: `try* EXPR` — evaluates EXPR; on throw, runs
504 /// pending finally blocks up to the enclosing catch and rethrows
505 /// the original value. On success, evaluates to EXPR's value.
506 /// Lowered per spec/HARN_SPEC.md as:
507 /// { let _r = try { EXPR }
508 /// guard is_ok(_r) else { throw unwrap_err(_r) }
509 /// unwrap(_r) }
510 TryStar {
511 operand: Box<SNode>,
512 },
513
514 /// Or-pattern in a `match` arm: `"ping" | "pong" -> body`. One or
515 /// more alternative patterns that share a single arm body. Only
516 /// legal inside a `MatchArm.pattern` slot.
517 OrPattern(Vec<SNode>),
518
519 Block(Vec<SNode>),
520 Closure {
521 params: Vec<TypedParam>,
522 return_type: Option<TypeExpr>,
523 /// Declared exception channel; see the [`Node::Pipeline`] `throws`
524 /// field. `None` = unconstrained. Only the `fn(params) -> R throws E`
525 /// closure spelling can carry it; the bare `x -> expr` arrow form has
526 /// no place to put a clause and always parses `None`.
527 throws: Option<TypeExpr>,
528 body: Vec<SNode>,
529 /// When true, this closure was written as `fn(params) { body }`.
530 /// The formatter preserves this distinction.
531 fn_syntax: bool,
532 },
533}
534
535/// First-class human-in-the-loop primitive.
536///
537/// Each `HitlKind` is a reserved keyword expression with VM-enforced
538/// semantics: the names cannot be shadowed or rebound by user code,
539/// signatures are produced by the VM, and the audit log is recorded
540/// deterministically by the runtime.
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
542pub enum HitlKind {
543 /// `request_approval(action: ..., args: ..., quorum: ..., reviewers: ..., ...)`.
544 RequestApproval,
545 /// `dual_control(n: ..., m: ..., action: <closure>, approvers: ...)`.
546 DualControl,
547 /// `ask_user(prompt: ..., schema: ..., timeout: ..., default: ...)`.
548 AskUser,
549 /// `escalate_to(role: ..., reason: ...)`.
550 EscalateTo,
551}
552
553impl HitlKind {
554 /// Keyword surface form (matches the reserved keyword in the lexer
555 /// and the corresponding async builtin name in the VM).
556 pub fn as_keyword(self) -> &'static str {
557 match self {
558 HitlKind::RequestApproval => "request_approval",
559 HitlKind::DualControl => "dual_control",
560 HitlKind::AskUser => "ask_user",
561 HitlKind::EscalateTo => "escalate_to",
562 }
563 }
564}
565
566/// A single argument in a [`Node::HitlExpr`] call. `name` is `Some` when
567/// the caller used named-arg syntax (e.g. `quorum: 2`); positional
568/// arguments leave it as `None` and rely on the kind's parameter order.
569#[derive(Debug, Clone, PartialEq, serde::Serialize)]
570pub struct HitlArg {
571 pub name: Option<String>,
572 pub value: SNode,
573 pub span: Span,
574}
575
576/// Parallel execution mode.
577#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
578pub enum ParallelMode {
579 /// `parallel N { i -> ... }` — run N concurrent tasks.
580 Count,
581 /// `parallel each list { item -> ... }` — map over list concurrently.
582 Each,
583 /// `parallel each list { item -> ... } as stream` — emit as each task completes.
584 EachStream,
585 /// `parallel settle list { item -> ... }` — map with error collection.
586 Settle,
587}
588
589#[derive(Debug, Clone, PartialEq, serde::Serialize)]
590pub struct MatchArm {
591 pub pattern: SNode,
592 /// Optional guard: `pattern if condition -> { body }`.
593 pub guard: Option<Box<SNode>>,
594 pub body: Vec<SNode>,
595 /// Source extent of the whole arm, pattern through closing brace. The
596 /// pattern's own span cannot bound the arm's body, so without this a
597 /// comment after the arm's last statement has no range to be flushed in.
598 /// See `StructField::span`.
599 pub span: Span,
600}
601
602#[derive(Debug, Clone, PartialEq, serde::Serialize)]
603pub struct SelectCase {
604 pub variable: String,
605 pub channel: Box<SNode>,
606 pub body: Vec<SNode>,
607}
608
609#[derive(Debug, Clone, PartialEq, serde::Serialize)]
610pub struct DictEntry {
611 pub key: SNode,
612 pub value: SNode,
613}
614
615/// An enum variant declaration.
616#[derive(Debug, Clone, PartialEq, serde::Serialize)]
617pub struct EnumVariant {
618 pub name: String,
619 pub fields: Vec<TypedParam>,
620 /// Source extent of the variant. A member without a span cannot anchor a
621 /// comment written against it; see `StructField::span`.
622 pub span: Span,
623}
624
625/// A struct field declaration.
626#[derive(Debug, Clone, PartialEq, serde::Serialize)]
627pub struct StructField {
628 pub name: String,
629 pub type_expr: Option<TypeExpr>,
630 pub optional: bool,
631 /// Source extent of the field.
632 ///
633 /// A comment's meaning lives entirely in where it sits, and a comment can
634 /// only be placed next to something that knows its own source lines. Member
635 /// items carrying no span is what let `harn fmt` evict a field's doc
636 /// comment out of the struct and re-attach it to the next declaration,
637 /// where it then described unrelated code.
638 pub span: Span,
639}
640
641/// An associated-type entry in an interface body: `type Item` or
642/// `type Item = string`.
643#[derive(Debug, Clone, PartialEq, serde::Serialize)]
644pub struct AssociatedType {
645 pub name: String,
646 /// The default, when written as `type Item = <default>`.
647 pub default: Option<TypeExpr>,
648 /// Source extent of the entry. See `StructField::span`.
649 pub span: Span,
650}
651
652impl AssociatedType {
653 /// The name/default pair, dropping source position — the shape the
654 /// typechecker's semantic tables carry, which have no use for a span.
655 pub fn to_binding(&self) -> (String, Option<TypeExpr>) {
656 (self.name.clone(), self.default.clone())
657 }
658
659 /// Project a parsed interface body's associated types onto the binding
660 /// pairs the semantic tables hold. The one place this conversion lives.
661 pub fn bindings(items: &[AssociatedType]) -> Vec<(String, Option<TypeExpr>)> {
662 items.iter().map(AssociatedType::to_binding).collect()
663 }
664}
665
666/// An interface method signature.
667#[derive(Debug, Clone, PartialEq, serde::Serialize)]
668pub struct InterfaceMethod {
669 pub name: String,
670 pub type_params: Vec<TypeParam>,
671 pub params: Vec<TypedParam>,
672 pub return_type: Option<TypeExpr>,
673 /// Source extent of the method signature. See `StructField::span`.
674 pub span: Span,
675}
676
677/// A type annotation (optional, for runtime checking).
678#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
679pub enum TypeExpr {
680 /// A named type: int, string, float, bool, nil, list, dict, closure,
681 /// or a user-defined type name.
682 Named(String),
683 /// A union type: `string | nil`, `int | float`.
684 Union(Vec<TypeExpr>),
685 /// An intersection type: `{x: int} & {y: int}`. The value must satisfy
686 /// every component simultaneously. Useful for layered context types
687 /// such as `fn use(ctx: BaseCtx & AuthCtx)`.
688 Intersection(Vec<TypeExpr>),
689 /// A dict shape type: `{name: string, age: int, active?: bool}`.
690 Shape(Vec<ShapeField>),
691 /// An **open** record / row-polymorphic shape: a set of explicit fields
692 /// plus one or more trailing **row tails** (`{id: string, ...R}`,
693 /// `{...R1, ...R2}`). Each tail in `rests` is a row variable
694 /// (`Named(rowvar)`), a gradual map tail (`dict` / `dict<string, V>`), or a
695 /// nested shape — folded left-to-right with right-biased merge once the row
696 /// variables are bound. A closed shape stays `Shape` (empty `rests`).
697 OpenShape {
698 fields: Vec<ShapeField>,
699 rests: Vec<TypeExpr>,
700 },
701 /// A list type: `list<int>`.
702 List(Box<TypeExpr>),
703 /// A dict type with key and value types: `dict<string, int>`.
704 DictType(Box<TypeExpr>, Box<TypeExpr>),
705 /// A lazy iterator type: `iter<int>`. Yields values of the inner type
706 /// via the combinator/sink protocol (`VmValue::Iter` at runtime).
707 Iter(Box<TypeExpr>),
708 /// A synchronous generator type: `Generator<int>`. Produced by a regular
709 /// `fn` body containing `yield`.
710 Generator(Box<TypeExpr>),
711 /// An asynchronous stream type: `Stream<int>`. Produced by `gen fn`.
712 Stream(Box<TypeExpr>),
713 /// An owned handle type: `owned<File>`. Marks the binding as carrying
714 /// sole ownership of a drop-able resource. The compiler emits an
715 /// auto-`drop()` at the binding's enclosing block exit; the lint
716 /// `HARN-OWN-005` flags ownership leaks (e.g. returning the value or
717 /// storing it in a non-owned field).
718 Owned(Box<TypeExpr>),
719 /// A generic type application: `Option<int>`, `Result<string, int>`.
720 Applied { name: String, args: Vec<TypeExpr> },
721 /// A function type: `fn(int, string) -> bool`.
722 FnType {
723 params: Vec<TypeExpr>,
724 return_type: Box<TypeExpr>,
725 },
726 /// The bottom type: the type of expressions that never produce a value
727 /// (return, throw, break, continue).
728 Never,
729 /// A string-literal type: `"pass"`, `"fail"`. Assignable to `string`.
730 /// Used in unions to represent enum-like discriminated values.
731 LitString(String),
732 /// An int-literal type: `0`, `1`, `-1`. Assignable to `int`.
733 LitInt(i64),
734}
735
736/// A field in a dict shape type.
737#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
738pub struct ShapeField {
739 pub name: String,
740 pub type_expr: TypeExpr,
741 pub optional: bool,
742 /// Source extent of this field. This is formatter/LSP provenance, not part
743 /// of a shape's semantic identity or serialized type contract.
744 #[serde(skip, default = "Span::dummy")]
745 pub span: Span,
746}
747
748impl ShapeField {
749 /// Construct a field synthesized by inference or generated metadata.
750 pub fn synthetic(name: impl Into<String>, type_expr: TypeExpr, optional: bool) -> Self {
751 Self {
752 name: name.into(),
753 type_expr,
754 optional,
755 span: Span::dummy(),
756 }
757 }
758}
759
760impl PartialEq for ShapeField {
761 fn eq(&self, other: &Self) -> bool {
762 self.name == other.name
763 && self.type_expr == other.type_expr
764 && self.optional == other.optional
765 }
766}
767
768#[cfg(test)]
769mod shape_field_tests {
770 use super::*;
771
772 #[test]
773 fn source_provenance_does_not_change_shape_field_equality() {
774 let synthetic = ShapeField::synthetic("retries", TypeExpr::Named("int".into()), false);
775 let parsed = ShapeField {
776 span: Span::with_offsets(12, 24, 3, 3),
777 ..synthetic.clone()
778 };
779
780 assert_eq!(synthetic, parsed);
781 }
782
783 #[test]
784 fn source_provenance_is_not_part_of_the_serialized_type_contract() {
785 let field = ShapeField {
786 span: Span::with_offsets(12, 24, 3, 3),
787 ..ShapeField::synthetic("retries", TypeExpr::Named("int".into()), false)
788 };
789
790 let encoded = serde_json::to_string(&field).expect("shape fields serialize");
791 let decoded: ShapeField = serde_json::from_str(&encoded).expect("shape fields deserialize");
792
793 assert!(
794 !encoded.contains("span"),
795 "source provenance leaked: {encoded}"
796 );
797 assert_eq!(decoded, field);
798 assert_eq!(decoded.span, Span::dummy());
799 }
800}
801
802/// A binding pattern for destructuring in let/var/for-in.
803#[derive(Debug, Clone, PartialEq, serde::Serialize)]
804pub enum BindingPattern {
805 /// Simple identifier: `let x = ...`
806 Identifier(String),
807 /// Dict destructuring: `let {name, age} = ...`
808 Dict(Vec<DictPatternField>),
809 /// List destructuring: `let [a, b] = ...`
810 List(Vec<ListPatternElement>),
811 /// Pair destructuring for `for (a, b) in iter { ... }`. The iter must
812 /// yield `VmValue::Pair` values. Not valid in let/var bindings.
813 Pair(String, String),
814}
815
816/// `_` is the discard binding name in `let`/`const`/destructuring positions.
817pub fn is_discard_name(name: &str) -> bool {
818 name == "_"
819}
820
821/// A field in a dict destructuring pattern.
822#[derive(Debug, Clone, PartialEq, serde::Serialize)]
823pub struct DictPatternField {
824 /// The dict key to extract.
825 pub key: String,
826 /// Renamed binding (if different from key), e.g. `{name: alias}`.
827 pub alias: Option<String>,
828 /// True for `...rest` (rest pattern).
829 pub is_rest: bool,
830 /// Default value if the key is missing (nil), e.g. `{name = "default"}`.
831 pub default_value: Option<Box<SNode>>,
832}
833
834/// An element in a list destructuring pattern.
835#[derive(Debug, Clone, PartialEq, serde::Serialize)]
836pub struct ListPatternElement {
837 /// The variable name to bind.
838 pub name: String,
839 /// True for `...rest` (rest pattern).
840 pub is_rest: bool,
841 /// Default value if the index is out of bounds (nil), e.g. `[a = 0]`.
842 pub default_value: Option<Box<SNode>>,
843}
844
845/// Declared variance of a generic type parameter.
846///
847/// - `Invariant` (default, no marker): the parameter appears in both
848/// input and output positions, or mutable state. `T<A>` and `T<B>`
849/// are unrelated unless `A == B`.
850/// - `Covariant` (`out T`): the parameter appears only in output
851/// positions (produced, not consumed). `T<Sub>` flows into
852/// `T<Super>`.
853/// - `Contravariant` (`in T`): the parameter appears only in input
854/// positions (consumed, not produced). `T<Super>` flows into
855/// `T<Sub>`.
856#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
857pub enum Variance {
858 Invariant,
859 Covariant,
860 Contravariant,
861}
862
863/// A generic type parameter on a function or pipeline declaration.
864#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
865pub struct TypeParam {
866 pub name: String,
867 pub variance: Variance,
868}
869
870impl TypeParam {
871 /// Construct an invariant type parameter (the default for
872 /// unannotated `<T>`).
873 pub fn invariant(name: impl Into<String>) -> Self {
874 Self {
875 name: name.into(),
876 variance: Variance::Invariant,
877 }
878 }
879}
880
881/// A where-clause constraint on a generic type parameter.
882#[derive(Debug, Clone, PartialEq, serde::Serialize)]
883pub struct WhereClause {
884 pub type_name: String,
885 pub bound: TypeExpr,
886}
887
888/// A parameter with an optional type annotation and optional default value.
889#[derive(Debug, Clone, PartialEq, serde::Serialize)]
890pub struct TypedParam {
891 pub name: String,
892 pub type_expr: Option<TypeExpr>,
893 pub default_value: Option<Box<SNode>>,
894 /// If true, this is a rest parameter (`...name`) that collects remaining arguments.
895 pub rest: bool,
896}
897
898impl TypedParam {
899 /// Create an untyped parameter.
900 pub fn untyped(name: impl Into<String>) -> Self {
901 Self {
902 name: name.into(),
903 type_expr: None,
904 default_value: None,
905 rest: false,
906 }
907 }
908
909 /// Create a typed parameter.
910 pub fn typed(name: impl Into<String>, type_expr: TypeExpr) -> Self {
911 Self {
912 name: name.into(),
913 type_expr: Some(type_expr),
914 default_value: None,
915 rest: false,
916 }
917 }
918
919 /// Extract just the names from a list of typed params.
920 pub fn names(params: &[TypedParam]) -> Vec<String> {
921 params.iter().map(|p| p.name.clone()).collect()
922 }
923
924 /// Return the index of the first parameter with a default value, or None.
925 pub fn default_start(params: &[TypedParam]) -> Option<usize> {
926 params.iter().position(|p| p.default_value.is_some())
927 }
928}