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