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}