Skip to main content

harn_parser/
ast.rs

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