Skip to main content

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