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