Skip to main content

daml_parser/
ast.rs

1//! Typed, lossless parse tree produced by the recursive-descent parser
2//! (src/parse.rs).
3//!
4//! Every node carries a source position and byte span. Downstream crates
5//! consume this tree directly: daml-fmt re-prints layout from the spans, and
6//! daml-lint lowers it onto its own rule-facing IR.
7
8pub use crate::lexer::{Identifier, ModuleName, Operator, Pos};
9
10/// Byte span of an AST node.
11///
12/// `[start, end)` into the original source, same basis as `Token::start`/
13/// `Token::end`. Covers every (non-virtual) token that belongs to the node —
14/// first token's `start` to last token's `end`.
15///
16/// Invariants the parser maintains (checked over the corpus by
17/// `render_from_ast`): a child's span is contained in its parent's span, and
18/// sibling spans are ordered and non-overlapping. Trivia (comments, blank
19/// lines) live *between* sibling spans.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub struct Span {
22    pub start: usize,
23    pub end: usize,
24}
25
26impl Span {
27    #[must_use]
28    pub const fn new(start: usize, end: usize) -> Self {
29        Self { start, end }
30    }
31
32    /// True when the span is well-formed (`start <= end`).
33    #[must_use]
34    pub const fn is_valid(&self) -> bool {
35        self.start <= self.end
36    }
37
38    /// True for a zero-width but still valid span.
39    #[must_use]
40    pub const fn is_empty(&self) -> bool {
41        self.start == self.end
42    }
43
44    #[must_use]
45    pub const fn range(&self) -> std::ops::Range<usize> {
46        self.start..self.end
47    }
48
49    #[must_use]
50    pub fn get<'a>(&self, source: &'a str) -> Option<&'a str> {
51        if self.start <= self.end
52            && self.end <= source.len()
53            && source.is_char_boundary(self.start)
54            && source.is_char_boundary(self.end)
55        {
56            Some(&source[self.start..self.end])
57        } else {
58            None
59        }
60    }
61
62    /// `self` fully contains `other`.
63    #[must_use]
64    pub const fn contains(&self, other: &Self) -> bool {
65        self.is_valid() && other.is_valid() && self.start <= other.start && other.end <= self.end
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum LitKind {
72    Int,
73    Decimal,
74    Text,
75    Char,
76}
77
78/// Side of an operator section.
79///
80/// `(+ 1)` stores `SectionSide::Right`, while `(1 +)` stores
81/// `SectionSide::Left`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83#[non_exhaustive]
84pub enum SectionSide {
85    /// Right section: operator followed by right operand.
86    Right,
87    /// Left section: left operand followed by operator.
88    Left,
89}
90
91/// Import syntax style.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum ImportStyle {
95    /// Qualified import (`import qualified Foo.Bar`, `import Foo.Bar qualified`).
96    Qualified,
97    /// Unqualified import (`import Foo.Bar`, `import Foo.Bar as Baz`).
98    Unqualified,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FieldAssign {
103    pub name: Identifier,
104    /// None for record puns (`Foo with owner` meaning `owner = owner`)
105    /// and `..` wildcards.
106    pub value: Option<Expr>,
107    pub pos: Pos,
108    pub span: Span,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Alt {
113    pub pat: Pat,
114    pub body: Expr,
115    pub pos: Pos,
116    pub span: Span,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Binding {
121    /// Left-hand side: a variable with parameters, or a destructuring pattern.
122    pub pat: Pat,
123    /// Parameter patterns when the LHS is a function binding (`f x y = ...`).
124    pub params: Vec<Pat>,
125    pub expr: Expr,
126    pub pos: Pos,
127    pub span: Span,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum Pat {
133    Var {
134        name: Identifier,
135        pos: Pos,
136        span: Span,
137    },
138    Wild {
139        pos: Pos,
140        span: Span,
141    },
142    Con {
143        qualifier: Option<ModuleName>,
144        name: Identifier,
145        args: Vec<Self>,
146        pos: Pos,
147        span: Span,
148    },
149    Tuple {
150        items: Vec<Self>,
151        pos: Pos,
152        span: Span,
153    },
154    List {
155        items: Vec<Self>,
156        pos: Pos,
157        span: Span,
158    },
159    Lit {
160        kind: LitKind,
161        text: String,
162        pos: Pos,
163        span: Span,
164    },
165    /// `name@pat`
166    As {
167        name: Identifier,
168        pat: Box<Self>,
169        pos: Pos,
170        span: Span,
171    },
172    /// Anything the parser couldn't classify; raw text preserved.
173    Other {
174        raw: String,
175        pos: Pos,
176        span: Span,
177    },
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181#[non_exhaustive]
182pub enum Expr {
183    /// Lowercase variable reference, possibly qualified.
184    Var {
185        qualifier: Option<ModuleName>,
186        name: Identifier,
187        pos: Pos,
188        span: Span,
189    },
190    /// Constructor / data-constructor reference, possibly qualified.
191    Con {
192        qualifier: Option<ModuleName>,
193        name: Identifier,
194        pos: Pos,
195        span: Span,
196    },
197    Lit {
198        kind: LitKind,
199        text: String,
200        pos: Pos,
201        span: Span,
202    },
203    /// Application, flattened: `f a b c` is one App with three args.
204    App {
205        func: Box<Self>,
206        args: Vec<Self>,
207        pos: Pos,
208        span: Span,
209    },
210    /// Binary operator application with source-level operator text.
211    BinOp {
212        op: Operator,
213        lhs: Box<Self>,
214        rhs: Box<Self>,
215        pos: Pos,
216        span: Span,
217    },
218    /// Unary negation.
219    Neg {
220        expr: Box<Self>,
221        pos: Pos,
222        span: Span,
223    },
224    Lambda {
225        params: Vec<Pat>,
226        body: Box<Self>,
227        pos: Pos,
228        span: Span,
229    },
230    If {
231        cond: Box<Self>,
232        then_branch: Box<Self>,
233        else_branch: Box<Self>,
234        pos: Pos,
235        span: Span,
236    },
237    Case {
238        scrutinee: Box<Self>,
239        alts: Vec<Alt>,
240        pos: Pos,
241        span: Span,
242    },
243    Do {
244        stmts: Vec<DoStmt>,
245        pos: Pos,
246        span: Span,
247    },
248    LetIn {
249        bindings: Vec<Binding>,
250        body: Box<Self>,
251        pos: Pos,
252        span: Span,
253    },
254    /// `base with f = e, ...` — record construction when base is a Con,
255    /// record update otherwise.
256    Record {
257        base: Box<Self>,
258        fields: Vec<FieldAssign>,
259        pos: Pos,
260        span: Span,
261    },
262    Tuple {
263        items: Vec<Self>,
264        pos: Pos,
265        span: Span,
266    },
267    List {
268        items: Vec<Self>,
269        pos: Pos,
270        span: Span,
271    },
272    /// `try <body> catch <alts>`
273    Try {
274        body: Box<Self>,
275        handlers: Vec<Alt>,
276        pos: Pos,
277        span: Span,
278    },
279    /// Right operator section like `(+ 1)` / left section `(1 +)`.
280    Section {
281        op: Operator,
282        operand: Option<Box<Self>>,
283        side: SectionSide,
284        pos: Pos,
285        span: Span,
286    },
287    /// Expression the parser could not understand; raw text preserved so
288    /// a parse failure degrades to the shim's behavior instead of dying.
289    Error { raw: String, pos: Pos, span: Span },
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293#[non_exhaustive]
294pub enum DoStmt {
295    /// `pat <- expr`
296    Bind {
297        pat: Pat,
298        expr: Expr,
299        pos: Pos,
300        span: Span,
301    },
302    /// `let x = e` (no `in`) inside a do block.
303    Let {
304        bindings: Vec<Binding>,
305        pos: Pos,
306        span: Span,
307    },
308    /// Bare expression statement.
309    Expr { expr: Expr, pos: Pos, span: Span },
310}
311
312/// Structured Daml type, parsed from the real token stream.
313///
314/// Scoped to the forms the corpus actually contains; it exists so consumers can
315/// tell a type *application* from a *function arrow* from an
316/// atomic constructor — a distinction a string matcher structurally cannot make.
317/// Every node carries a byte span so consumers can render exact source text from
318/// `(source, span)`.
319#[derive(Debug, Clone)]
320#[non_exhaustive]
321pub enum Type {
322    /// Type constructor, possibly qualified: `Party`, `DA.Map.Map`.
323    Con {
324        qualifier: Option<ModuleName>,
325        name: Identifier,
326        span: Span,
327    },
328    /// Type application, head applied to one or more args: `ContractId Foo`,
329    /// `Map Text Int`, `Script ()`. Type-level nat literals (the `10` in
330    /// `Numeric 10`) are NOT types, so they are dropped from the arg list — a
331    /// `Numeric 10` collapses to the bare head `Con "Numeric"`.
332    App(Box<Self>, Vec<Self>, Span),
333    /// List type `[T]`.
334    List(Box<Self>, Span),
335    /// Tuple type `(a, b, ...)`.
336    Tuple(Vec<Self>, Span),
337    /// Function type `a -> b` (right-associative).
338    Fun(Box<Self>, Box<Self>, Span),
339    /// Lowercase type variable: `a`, `n`.
340    Var(Identifier, Span),
341    /// The unit type `()`.
342    Unit(Span),
343    /// A constrained type `C a => T`: the context is not modeled, the body `T`
344    /// is kept.
345    Constrained(Box<Self>, Span),
346    /// Type-level string or char literal, e.g. the `"observers"` in
347    /// `HasField "observers" t PartiesMap`.
348    Lit {
349        kind: LitKind,
350        text: String,
351        span: Span,
352    },
353}
354
355/// Type equality intentionally ignores source spans.
356///
357/// Spans describe where equivalent type syntax appeared in a source file; they
358/// are not part of structural type identity used by parser consumers.
359impl PartialEq for Type {
360    fn eq(&self, other: &Self) -> bool {
361        match (self, other) {
362            (
363                Self::Con {
364                    qualifier: aq,
365                    name: an,
366                    ..
367                },
368                Self::Con {
369                    qualifier: bq,
370                    name: bn,
371                    ..
372                },
373            ) => aq == bq && an == bn,
374            (Self::App(ah, aa, _), Self::App(bh, ba, _)) => ah == bh && aa == ba,
375            (Self::List(a, _), Self::List(b, _))
376            | (Self::Constrained(a, _), Self::Constrained(b, _)) => a == b,
377            (Self::Tuple(a, _), Self::Tuple(b, _)) => a == b,
378            (Self::Fun(al, ar, _), Self::Fun(bl, br, _)) => al == bl && ar == br,
379            (Self::Var(a, _), Self::Var(b, _)) => a == b,
380            (Self::Unit(_), Self::Unit(_)) => true,
381            (
382                Self::Lit {
383                    kind: ak, text: at, ..
384                },
385                Self::Lit {
386                    kind: bk, text: bt, ..
387                },
388            ) => ak == bk && at == bt,
389            _ => false,
390        }
391    }
392}
393
394impl Eq for Type {}
395
396impl Type {
397    #[must_use]
398    pub const fn span(&self) -> Span {
399        match self {
400            Self::Con { span, .. }
401            | Self::App(_, _, span)
402            | Self::List(_, span)
403            | Self::Tuple(_, span)
404            | Self::Fun(_, _, span)
405            | Self::Var(_, span)
406            | Self::Unit(span)
407            | Self::Constrained(_, span)
408            | Self::Lit { span, .. } => *span,
409        }
410    }
411
412    pub(crate) const fn with_span(mut self, span: Span) -> Self {
413        match &mut self {
414            Self::Con { span: s, .. }
415            | Self::App(_, _, s)
416            | Self::List(_, s)
417            | Self::Tuple(_, s)
418            | Self::Fun(_, _, s)
419            | Self::Var(_, s)
420            | Self::Unit(s)
421            | Self::Constrained(_, s)
422            | Self::Lit { span: s, .. } => *s = span,
423        }
424        self
425    }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct FieldDecl {
430    pub name: Identifier,
431    /// Structured field type parsed from the token stream. `None` when the type
432    /// could not be parsed cleanly (analysis treats it as unknown).
433    pub ty: Option<Type>,
434    pub pos: Pos,
435    pub span: Span,
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439#[non_exhaustive]
440pub enum Consuming {
441    Consuming,
442    NonConsuming,
443    PreConsuming,
444    PostConsuming,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct ChoiceDecl {
449    pub name: Identifier,
450    pub consuming: Consuming,
451    /// Structured return type. `None` if it could not be parsed cleanly or the
452    /// choice declared no return type.
453    pub return_ty: Option<Type>,
454    pub params: Vec<FieldDecl>,
455    /// Comma-separated controller expressions.
456    pub controllers: Vec<Expr>,
457    /// Choice observers, if any.
458    pub observers: Vec<Expr>,
459    pub body: Option<Expr>,
460    pub pos: Pos,
461    pub span: Span,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq)]
465#[non_exhaustive]
466pub enum TemplateBodyDecl {
467    Signatory {
468        parties: Vec<Expr>,
469        pos: Pos,
470        span: Span,
471    },
472    Observer {
473        parties: Vec<Expr>,
474        pos: Pos,
475        span: Span,
476    },
477    Ensure {
478        expr: Expr,
479        pos: Pos,
480        span: Span,
481    },
482    Key {
483        expr: Expr,
484        /// Structured key type. `None` if absent or not cleanly parseable.
485        ty: Option<Type>,
486        pos: Pos,
487        span: Span,
488    },
489    Maintainer {
490        expr: Expr,
491        pos: Pos,
492        span: Span,
493    },
494    Choice(ChoiceDecl),
495    InterfaceInstance(InterfaceInstanceDecl),
496    /// `agreement`, `let` blocks, deprecated `controller ... can`, etc.
497    Other {
498        raw: String,
499        pos: Pos,
500        span: Span,
501    },
502}
503
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct InterfaceInstanceDecl {
506    /// Interface being implemented (`Disclosure.I`).
507    pub interface_name: ModuleName,
508    /// Explicit template from `for Foo`; `None` when omitted (the enclosing
509    /// template when declared inside one).
510    pub for_template: Option<ModuleName>,
511    /// Method implementations: name → bound expression.
512    pub methods: Vec<Binding>,
513    pub pos: Pos,
514    pub span: Span,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq)]
518pub struct TemplateDecl {
519    pub name: Identifier,
520    pub fields: Vec<FieldDecl>,
521    pub body: Vec<TemplateBodyDecl>,
522    pub pos: Pos,
523    pub span: Span,
524}
525
526#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct InterfaceDecl {
528    pub name: Identifier,
529    /// Interfaces this interface requires (`requires Lockable.I, ...`).
530    pub requires: Vec<ModuleName>,
531    pub viewtype: Option<ModuleName>,
532    /// Method signatures: name and type text.
533    pub methods: Vec<FieldDecl>,
534    pub choices: Vec<ChoiceDecl>,
535    pub pos: Pos,
536    pub span: Span,
537}
538
539#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct Equation {
541    pub params: Vec<Pat>,
542    pub body: Expr,
543    /// Guarded equations keep their guards as (guard, body) pairs; `body`
544    /// then holds the first guarded body for convenience.
545    pub guards: Vec<(Expr, Expr)>,
546    /// `where` helper bindings attached to this equation.
547    pub where_bindings: Vec<Binding>,
548    pub pos: Pos,
549    pub span: Span,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
553pub struct FunctionDecl {
554    pub name: Identifier,
555    pub ty: Option<Type>,
556    pub equations: Vec<Equation>,
557    pub pos: Pos,
558    /// Span of the function's first appearance (signature or first equation).
559    /// Convenience anchor; a multi-equation function's precise ranges are the
560    /// per-`Equation` spans, since equations need not be contiguous in source.
561    pub span: Span,
562    /// Span of the standalone type signature `name : Type`, if one was seen.
563    pub sig_span: Option<Span>,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct ImportDecl {
568    pub module_name: ModuleName,
569    pub style: ImportStyle,
570    pub alias: Option<ModuleName>,
571    pub pos: Pos,
572    pub span: Span,
573}
574
575#[derive(Debug, Clone, PartialEq, Eq)]
576#[non_exhaustive]
577pub enum Decl {
578    Template(TemplateDecl),
579    Interface(InterfaceDecl),
580    Function(FunctionDecl),
581    /// data/type/class/instance/exception — recorded with name + span.
582    TypeDef {
583        keyword: String,
584        name: Identifier,
585        pos: Pos,
586        span: Span,
587    },
588    /// Anything unparseable at the top level (diagnostic already emitted).
589    Unknown {
590        raw: String,
591        pos: Pos,
592        span: Span,
593    },
594}
595
596#[derive(Debug, Clone, PartialEq, Eq)]
597pub struct Module {
598    pub name: ModuleName,
599    pub pos: Pos,
600    /// Whole-module extent: `[0, source.len())`. Container for all decls.
601    pub span: Span,
602    /// Span of the `module M (...) where` header clause; empty when the file
603    /// has no module header. Lets the span oracle treat header tokens as
604    /// covered without a dedicated header node.
605    pub header: Span,
606    pub imports: Vec<ImportDecl>,
607    pub decls: Vec<Decl>,
608}
609
610/// Why a [`ParseDiagnostic`] fired.
611///
612/// Lets a consumer separate syntax the parser deliberately does not model (still
613/// safe, just unanalyzed) from a genuine malformation, a recursion-limit
614/// degradation, or a lexical error.
615#[derive(Debug, Clone, Copy, PartialEq, Eq)]
616#[non_exhaustive]
617pub enum DiagnosticCategory {
618    /// A whole declaration could not be parsed and was skipped to the next item.
619    SkippedDecl,
620    /// A malformed expression, pattern, or expected-token error inside an
621    /// otherwise-recognized construct.
622    Malformed,
623    /// A construct the parser intentionally does not support, e.g. legacy
624    /// `controller ... can` choice syntax.
625    UnsupportedSyntax,
626    /// Expression/pattern nesting exceeded the recursion bound and was degraded
627    /// to raw text.
628    RecursionLimit,
629    /// A lexical error (unterminated string/comment, stray character).
630    Lex,
631}
632
633impl DiagnosticCategory {
634    /// Stable kebab-case tag for machine-readable output (JSON/SARIF) and logs.
635    #[must_use]
636    pub const fn as_str(self) -> &'static str {
637        match self {
638            Self::SkippedDecl => "skipped-declaration",
639            Self::Malformed => "malformed",
640            Self::UnsupportedSyntax => "unsupported-syntax",
641            Self::RecursionLimit => "recursion-limit",
642            Self::Lex => "lexical-error",
643        }
644    }
645}
646
647/// Parse diagnostic — never fatal under tolerant parsing.
648///
649/// Under [`crate::parse::parse_module`] the scan continues. Strict callers that
650/// use [`crate::parse::parse_module_strict`] or
651/// [`crate::parse::ParseModuleResult::into_result`] treat any diagnostic as
652/// [`crate::parse::ParseModuleError`].
653#[derive(Debug, Clone, PartialEq, Eq)]
654pub struct ParseDiagnostic {
655    pub message: String,
656    pub pos: Pos,
657    /// Byte span of the offending region. The end is the actionable addition
658    /// over `pos`-alone; zero-width when only a point is known (lex errors,
659    /// EOF).
660    pub span: Span,
661    pub category: DiagnosticCategory,
662}
663
664impl Expr {
665    #[must_use]
666    pub const fn pos(&self) -> Pos {
667        match self {
668            Self::Var { pos, .. }
669            | Self::Con { pos, .. }
670            | Self::Lit { pos, .. }
671            | Self::App { pos, .. }
672            | Self::BinOp { pos, .. }
673            | Self::Neg { pos, .. }
674            | Self::Lambda { pos, .. }
675            | Self::If { pos, .. }
676            | Self::Case { pos, .. }
677            | Self::Do { pos, .. }
678            | Self::LetIn { pos, .. }
679            | Self::Record { pos, .. }
680            | Self::Tuple { pos, .. }
681            | Self::List { pos, .. }
682            | Self::Try { pos, .. }
683            | Self::Section { pos, .. }
684            | Self::Error { pos, .. } => *pos,
685        }
686    }
687
688    /// Byte span covering the whole expression.
689    #[must_use]
690    pub const fn span(&self) -> Span {
691        match self {
692            Self::Var { span, .. }
693            | Self::Con { span, .. }
694            | Self::Lit { span, .. }
695            | Self::App { span, .. }
696            | Self::BinOp { span, .. }
697            | Self::Neg { span, .. }
698            | Self::Lambda { span, .. }
699            | Self::If { span, .. }
700            | Self::Case { span, .. }
701            | Self::Do { span, .. }
702            | Self::LetIn { span, .. }
703            | Self::Record { span, .. }
704            | Self::Tuple { span, .. }
705            | Self::List { span, .. }
706            | Self::Try { span, .. }
707            | Self::Section { span, .. }
708            | Self::Error { span, .. } => *span,
709        }
710    }
711
712    /// Render back to compact, source-*like* text for diagnostics and `raw`
713    /// fields.
714    ///
715    /// This is **lossy and normalizing**, not byte-faithful: original layout is
716    /// dropped (e.g. `do`/`let` statements are joined with `; `), operators and
717    /// spacing are normalized, and comments/trivia are gone. Use it for a quick
718    /// human-readable echo of an expression; for source-exact reconstruction use
719    /// the node's [`span`](Self::span) into the original text (that is how
720    /// `daml-fmt` and [`crate::ast_span::render_from_ast`] stay lossless).
721    #[must_use]
722    pub fn render(&self) -> String {
723        match self {
724            Self::Var {
725                qualifier, name, ..
726            }
727            | Self::Con {
728                qualifier, name, ..
729            } => qualifier
730                .as_ref()
731                .map_or_else(|| name.to_string(), |q| format!("{q}.{name}")),
732            Self::Lit { kind, text, .. } => match kind {
733                LitKind::Text => format!("{text:?}"),
734                LitKind::Char => format!("'{text}'"),
735                _ => text.clone(),
736            },
737            Self::App { func, args, .. } => {
738                let mut s = func.render_atomic();
739                for a in args {
740                    s.push(' ');
741                    s.push_str(&a.render_atomic());
742                }
743                s
744            }
745            Self::BinOp { op, lhs, rhs, .. } => {
746                if *op == "." {
747                    // Record projection / composition: `account.custodian`.
748                    format!("{}.{}", lhs.render_atomic(), rhs.render_atomic())
749                } else {
750                    format!("{} {} {}", lhs.render_atomic(), op, rhs.render_atomic())
751                }
752            }
753            Self::Neg { expr, .. } => format!("-{}", expr.render_atomic()),
754            Self::Lambda { params, body, .. } => {
755                let ps: Vec<String> = params.iter().map(|p| p.render()).collect();
756                format!("\\{} -> {}", ps.join(" "), body.render())
757            }
758            Self::If {
759                cond,
760                then_branch,
761                else_branch,
762                ..
763            } => format!(
764                "if {} then {} else {}",
765                cond.render(),
766                then_branch.render(),
767                else_branch.render()
768            ),
769            Self::Case {
770                scrutinee, alts, ..
771            } => {
772                let arms: Vec<String> = alts
773                    .iter()
774                    .map(|a| format!("{} -> {}", a.pat.render(), a.body.render()))
775                    .collect();
776                format!("case {} of {}", scrutinee.render(), arms.join("; "))
777            }
778            Self::Do { stmts, .. } => {
779                let body: Vec<String> = stmts.iter().map(render_do_stmt).collect();
780                format!("do {}", body.join("; "))
781            }
782            Self::LetIn { bindings, body, .. } => {
783                let bs: Vec<String> = bindings.iter().map(render_binding).collect();
784                format!("let {} in {}", bs.join("; "), body.render())
785            }
786            Self::Record { base, fields, .. } => {
787                let fs: Vec<String> = fields
788                    .iter()
789                    .map(|f| {
790                        f.value.as_ref().map_or_else(
791                            || f.name.to_string(),
792                            |v| format!("{} = {}", f.name, v.render()),
793                        )
794                    })
795                    .collect();
796                format!("{} with {}", base.render_atomic(), fs.join("; "))
797            }
798            Self::Tuple { items, .. } => {
799                let xs: Vec<String> = items.iter().map(|e| e.render()).collect();
800                format!("({})", xs.join(", "))
801            }
802            Self::List { items, .. } => {
803                let xs: Vec<String> = items.iter().map(|e| e.render()).collect();
804                format!("[{}]", xs.join(", "))
805            }
806            Self::Try { body, handlers, .. } => {
807                let hs: Vec<String> = handlers
808                    .iter()
809                    .map(|a| format!("{} -> {}", a.pat.render(), a.body.render()))
810                    .collect();
811                format!("try {} catch {}", body.render(), hs.join("; "))
812            }
813            Self::Section {
814                op, operand, side, ..
815            } => match (operand, side) {
816                (Some(e), SectionSide::Left) => format!("({} {})", e.render(), op),
817                (Some(e), SectionSide::Right) => format!("({} {})", op, e.render()),
818                (None, _) => format!("({op})"),
819            },
820            Self::Error { raw, .. } => raw.clone(),
821        }
822    }
823
824    /// Render with parentheses if this expression wouldn't survive as an
825    /// application argument.
826    fn render_atomic(&self) -> String {
827        match self {
828            Self::Var { .. }
829            | Self::Con { .. }
830            | Self::Lit { .. }
831            | Self::Tuple { .. }
832            | Self::List { .. }
833            | Self::Section { .. }
834            | Self::Error { .. } => self.render(),
835            _ => format!("({})", self.render()),
836        }
837    }
838
839    /// The head of an application spine: for `Foo.exercise cid X`, the
840    /// `Foo.exercise` Var. For non-apps, the expression itself.
841    #[must_use]
842    pub fn application_head(&self) -> &Self {
843        match self {
844            Self::App { func, .. } => func.application_head(),
845            _ => self,
846        }
847    }
848
849    /// Application arguments, empty for non-apps. The `App` spine is flattened
850    /// (see the [`App`](Self::App) variant), so for `f a b c` this returns all
851    /// three arguments `[a, b, c]`, not a single curried layer.
852    #[must_use]
853    pub fn application_args(&self) -> &[Self] {
854        match self {
855            Self::App { args, .. } => args,
856            _ => &[],
857        }
858    }
859}
860
861fn render_do_stmt(s: &DoStmt) -> String {
862    match s {
863        DoStmt::Bind { pat, expr, .. } => format!("{} <- {}", pat.render(), expr.render()),
864        DoStmt::Let { bindings, .. } => {
865            let bs: Vec<String> = bindings.iter().map(render_binding).collect();
866            format!("let {}", bs.join("; "))
867        }
868        DoStmt::Expr { expr, .. } => expr.render(),
869    }
870}
871
872fn render_binding(b: &Binding) -> String {
873    let mut s = b.pat.render();
874    for p in &b.params {
875        s.push(' ');
876        s.push_str(&p.render());
877    }
878    format!("{} = {}", s, b.expr.render())
879}
880
881impl Pat {
882    #[must_use]
883    pub const fn pos(&self) -> Pos {
884        match self {
885            Self::Var { pos, .. }
886            | Self::Wild { pos, .. }
887            | Self::Con { pos, .. }
888            | Self::Tuple { pos, .. }
889            | Self::List { pos, .. }
890            | Self::Lit { pos, .. }
891            | Self::As { pos, .. }
892            | Self::Other { pos, .. } => *pos,
893        }
894    }
895
896    /// Byte span covering the whole pattern.
897    #[must_use]
898    pub const fn span(&self) -> Span {
899        match self {
900            Self::Var { span, .. }
901            | Self::Wild { span, .. }
902            | Self::Con { span, .. }
903            | Self::Tuple { span, .. }
904            | Self::List { span, .. }
905            | Self::Lit { span, .. }
906            | Self::As { span, .. }
907            | Self::Other { span, .. } => *span,
908        }
909    }
910
911    /// Render back to compact, source-*like* text. Lossy and normalizing in the
912    /// same way as [`Expr::render`]; use the node's [`span`](Self::span) for
913    /// byte-faithful text.
914    #[must_use]
915    pub fn render(&self) -> String {
916        match self {
917            Self::Var { name, .. } => name.to_string(),
918            Self::Wild { .. } => "_".to_string(),
919            Self::Con {
920                qualifier,
921                name,
922                args,
923                ..
924            } => {
925                let head = qualifier
926                    .as_ref()
927                    .map_or_else(|| name.to_string(), |q| format!("{q}.{name}"));
928                if args.is_empty() {
929                    head
930                } else {
931                    let parts: Vec<String> = args.iter().map(|p| p.render()).collect();
932                    format!("({} {})", head, parts.join(" "))
933                }
934            }
935            Self::Tuple { items, .. } => {
936                let xs: Vec<String> = items.iter().map(|p| p.render()).collect();
937                format!("({})", xs.join(", "))
938            }
939            Self::List { items, .. } => {
940                let xs: Vec<String> = items.iter().map(|p| p.render()).collect();
941                format!("[{}]", xs.join(", "))
942            }
943            Self::Lit { kind, text, .. } => match kind {
944                LitKind::Text => format!("{text:?}"),
945                LitKind::Char => format!("'{text}'"),
946                _ => text.clone(),
947            },
948            Self::As { name, pat, .. } => format!("{}@{}", name, pat.render()),
949            Self::Other { raw, .. } => raw.clone(),
950        }
951    }
952}
953
954impl std::fmt::Display for Expr {
955    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
956        f.write_str(&self.render())
957    }
958}
959
960impl std::fmt::Display for Pat {
961    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
962        f.write_str(&self.render())
963    }
964}
965
966#[cfg(test)]
967mod tests {
968    use super::*;
969
970    fn pos() -> Pos {
971        Pos { line: 1, column: 1 }
972    }
973
974    fn span(start: usize, end: usize) -> Span {
975        Span::new(start, end)
976    }
977
978    #[test]
979    fn span_distinguishes_empty_from_invalid() {
980        assert!(span(3, 3).is_valid());
981        assert!(span(3, 3).is_empty());
982
983        assert!(!span(4, 3).is_valid());
984        assert!(!span(4, 3).is_empty());
985    }
986
987    #[test]
988    fn contains_rejects_invalid_spans() {
989        let parent = span(1, 10);
990
991        assert!(parent.contains(&span(3, 7)));
992        assert!(!parent.contains(&span(7, 3)));
993        assert!(!span(10, 1).contains(&span(3, 7)));
994    }
995
996    #[test]
997    fn span_range_and_get_share_source_bytes_safely() {
998        let source = "foo: Int";
999
1000        assert_eq!(span(0, 3).range(), 0..3);
1001        assert_eq!(span(0, 3).get(source), Some("foo"));
1002        assert_eq!(span(3, 7).get(source), Some(": In"));
1003        assert!(span(3, 100).get(source).is_none());
1004    }
1005
1006    #[test]
1007    fn expr_render_keeps_normalized_application_and_projection_shape() {
1008        let projection = Expr::BinOp {
1009            op: ".".into(),
1010            lhs: Box::new(Expr::Var {
1011                qualifier: None,
1012                name: "this".into(),
1013                pos: pos(),
1014                span: span(0, 4),
1015            }),
1016            rhs: Box::new(Expr::Var {
1017                qualifier: None,
1018                name: "note".into(),
1019                pos: pos(),
1020                span: span(5, 9),
1021            }),
1022            pos: pos(),
1023            span: span(0, 9),
1024        };
1025
1026        let expr = Expr::App {
1027            func: Box::new(Expr::Var {
1028                qualifier: None,
1029                name: "length".into(),
1030                pos: pos(),
1031                span: span(0, 6),
1032            }),
1033            args: vec![projection],
1034            pos: pos(),
1035            span: span(0, 16),
1036        };
1037
1038        assert_eq!(expr.render(), "length (this.note)");
1039    }
1040
1041    #[test]
1042    fn section_render_depends_on_section_side() {
1043        let expr_left = Expr::Section {
1044            op: "+".into(),
1045            operand: Some(Box::new(Expr::Var {
1046                qualifier: None,
1047                name: "x".into(),
1048                pos: pos(),
1049                span: span(0, 1),
1050            })),
1051            side: SectionSide::Left,
1052            pos: pos(),
1053            span: span(0, 4),
1054        };
1055        let expr_right = Expr::Section {
1056            op: "+".into(),
1057            operand: Some(Box::new(Expr::Lit {
1058                kind: LitKind::Int,
1059                text: "1".to_string(),
1060                pos: pos(),
1061                span: span(0, 1),
1062            })),
1063            side: SectionSide::Right,
1064            pos: pos(),
1065            span: span(0, 4),
1066        };
1067
1068        assert_eq!(expr_left.render(), "(x +)");
1069        assert_eq!(expr_right.render(), "(+ 1)");
1070    }
1071
1072    #[test]
1073    fn pat_render_preserves_collection_shape() {
1074        let pat = Pat::Tuple {
1075            items: vec![
1076                Pat::Var {
1077                    name: "owner".into(),
1078                    pos: pos(),
1079                    span: span(1, 6),
1080                },
1081                Pat::List {
1082                    items: Vec::new(),
1083                    pos: pos(),
1084                    span: span(8, 10),
1085                },
1086            ],
1087            pos: pos(),
1088            span: span(0, 11),
1089        };
1090
1091        assert_eq!(pat.render(), "(owner, [])");
1092    }
1093}