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