Skip to main content

daml_parser/
ast_span.rs

1//! AST losslessness oracle for daml-fmt.
2//!
3//! Every AST node carries a byte `Span` (see `ast::Span`). This module proves
4//! those spans are faithful: it collects every node's span, checks they form a
5//! *laminar family* (each child contained in its parent, siblings ordered and
6//! disjoint — `ast::Span` invariants), and reconstructs the file by tiling the
7//! span forest with the verbatim source bytes that fall between sibling spans.
8//!
9//! If the spans nest correctly and the module span covers the file, the
10//! reconstruction is byte-identical to the source. A parser that dropped a
11//! token's bytes from every node span, or produced an overlap, fails here.
12
13use crate::ast::{
14    Alt, Binding, ChoiceDecl, Decl, DoStmt, Equation, Expr, FieldAssign, FixityDecl,
15    GuardQualifier, InterfaceInstanceBodyItem, Module, Pat, PatFieldAssign, Span, TemplateBodyDecl,
16    Type, TypeAnnotation,
17};
18use crate::lexer::{Trivia, TriviaKind};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum AstSpanError {
23    /// An AST span had `start > end`; both fields are byte offsets.
24    InvalidSpan {
25        /// Inclusive start byte offset.
26        start: usize,
27        /// Exclusive end byte offset.
28        end: usize,
29    },
30    /// Two AST spans overlapped without one containing the other.
31    OverlappingSpans {
32        /// Inclusive start byte offset of the offending span.
33        start: usize,
34        /// Exclusive end byte offset of the offending span.
35        end: usize,
36        /// Inclusive start byte offset of the active parent/sibling span.
37        parent_start: usize,
38        /// Exclusive end byte offset of the active parent/sibling span.
39        parent_end: usize,
40    },
41    /// A reconstruction tile overlapped bytes already emitted.
42    OverlappingTile {
43        /// Inclusive start byte offset of the overlapping tile.
44        start: usize,
45        /// Exclusive end byte offset of the overlapping tile.
46        end: usize,
47        /// Exclusive end byte offset of the previous tile.
48        previous_end: usize,
49    },
50    /// A non-empty source byte interval was not covered by AST or trivia.
51    UncoveredBytes {
52        /// Inclusive start byte offset of the uncovered interval.
53        start: usize,
54        /// Exclusive end byte offset of the uncovered interval.
55        end: usize,
56        /// Source text from the uncovered interval.
57        text: String,
58    },
59    /// Source bytes after the final AST/trivia interval were not covered.
60    UncoveredTail {
61        /// Inclusive start byte offset of the uncovered tail.
62        start: usize,
63        /// Source text from the uncovered tail.
64        text: String,
65    },
66    /// Reconstructed source length differed from the original source length.
67    ReconstructionMismatch {
68        /// Reconstructed byte length.
69        reconstructed_len: usize,
70        /// Original source byte length.
71        source_len: usize,
72    },
73    /// A token/trivia interval had `start > end`; both fields are byte offsets.
74    IntervalStartAfterEnd {
75        /// Inclusive start byte offset.
76        start: usize,
77        /// Exclusive end byte offset.
78        end: usize,
79    },
80    /// A token/trivia interval extended past `source_len`.
81    IntervalExceedsSource {
82        /// Inclusive start byte offset.
83        start: usize,
84        /// Exclusive end byte offset.
85        end: usize,
86        /// Original source byte length.
87        source_len: usize,
88    },
89    /// A token/trivia interval boundary was not a UTF-8 boundary in `source`.
90    IntervalNotUtf8Boundary {
91        /// Inclusive start byte offset.
92        start: usize,
93        /// Exclusive end byte offset.
94        end: usize,
95    },
96}
97
98impl std::fmt::Display for AstSpanError {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            Self::InvalidSpan { start, end } => write!(f, "invalid span [{start}, {end})"),
102            Self::OverlappingSpans {
103                start,
104                end,
105                parent_start,
106                parent_end,
107            } => write!(
108                f,
109                "span [{start}, {end}) overlaps [{parent_start}, {parent_end}) without nesting"
110            ),
111            Self::OverlappingTile {
112                start,
113                end,
114                previous_end,
115            } => write!(
116                f,
117                "span/trivia interval [{start}, {end}) overlaps previous tile ending at {previous_end}"
118            ),
119            Self::UncoveredBytes { start, end, text } => write!(
120                f,
121                "bytes {start}..{end} not covered by any node or trivia span: {text:?}"
122            ),
123            Self::UncoveredTail { start, text } => {
124                write!(f, "bytes {start}.. lost at EOF: {text:?}")
125            }
126            Self::ReconstructionMismatch {
127                reconstructed_len,
128                source_len,
129            } => write!(
130                f,
131                "reconstruction differs from source ({reconstructed_len} vs {source_len} bytes)"
132            ),
133            Self::IntervalStartAfterEnd { start, end } => {
134                write!(f, "span/trivia interval [{start}, {end}) has start after end")
135            }
136            Self::IntervalExceedsSource {
137                start,
138                end,
139                source_len,
140            } => write!(
141                f,
142                "span/trivia interval [{start}, {end}) exceeds source length {source_len}"
143            ),
144            Self::IntervalNotUtf8Boundary { start, end } => write!(
145                f,
146                "span/trivia interval [{start}, {end}) does not align with UTF-8 boundaries"
147            ),
148        }
149    }
150}
151
152impl std::error::Error for AstSpanError {}
153
154/// Reconstruct `source` from the AST's byte spans plus the lexer's `trivia`.
155///
156/// The AST-level mirror of `lexer::render_lossless`: it checks the spans nest
157/// (V2), then tiles the file from every *content* node span merged with the
158/// non-blank trivia spans (V1/V3). `Ok(reconstruction)` is byte-identical to
159/// `source`; `Err` names the first nesting violation or the first run of bytes
160/// no span covers (content the AST dropped).
161///
162/// Obtain `trivia` from [`crate::lexer::lex_with_trivia`].
163///
164/// # Errors
165///
166/// Returns [`AstSpanError`] when spans fail nesting checks or when bytes in
167/// `source` are not covered by any AST node or trivia span.
168#[must_use = "check and handle render failures"]
169pub fn render_from_ast(
170    source: &str,
171    module: &Module,
172    trivia: &[Trivia],
173) -> Result<String, AstSpanError> {
174    check_nesting(module)?;
175    tile(source, module, trivia)
176}
177
178/// V2 — every child span is contained in its parent, and sibling spans are
179/// ordered and disjoint. Validates the whole node set (module container
180/// included) as a laminar family via a containment stack.
181fn check_nesting(module: &Module) -> Result<(), AstSpanError> {
182    let mut spans: Vec<Span> = Vec::new();
183    collect_module(module, &mut spans);
184    for span in &spans {
185        if !span.is_valid() {
186            return Err(AstSpanError::InvalidSpan {
187                start: span.start_usize(),
188                end: span.end_usize(),
189            });
190        }
191    }
192    spans.retain(|s| !s.is_empty());
193    // Outer-first: earlier start, then later end.
194    spans.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
195
196    let mut stack: Vec<Span> = Vec::new();
197    for span in spans {
198        // Pop ancestors that end at/before this span starts (its left siblings).
199        while stack.last().is_some_and(|top| top.end <= span.start) {
200            stack.pop();
201        }
202        // Whatever remains on top must contain this span; otherwise the two
203        // overlap without nesting (a sibling that starts before the previous
204        // one ended, or a child that spills past its parent).
205        if let Some(parent) = stack.last() {
206            if !parent.contains(&span) {
207                return Err(AstSpanError::OverlappingSpans {
208                    start: span.start_usize(),
209                    end: span.end_usize(),
210                    parent_start: parent.start_usize(),
211                    parent_end: parent.end_usize(),
212                });
213            }
214        }
215        stack.push(span);
216    }
217    Ok(())
218}
219
220/// V1/V3 — tile the file from content spans + non-blank trivia and reconstruct.
221/// "Content" excludes the whole-module container (which would cover everything
222/// trivially) but includes the `module … where` header. Any gap between spans
223/// must be whitespace-only; a non-whitespace gap is a real token no node claims,
224/// i.e. content the AST dropped.
225fn tile(source: &str, module: &Module, trivia: &[Trivia]) -> Result<String, AstSpanError> {
226    let mut content: Vec<Span> = Vec::new();
227    collect_module(module, &mut content);
228    let container = module.span;
229    content.retain(|s| !(s.is_empty() || (s.start == container.start && s.end == container.end)));
230
231    let mut items: Vec<(usize, usize)> = content
232        .iter()
233        .map(|s| (s.start_usize(), s.end_usize()))
234        .collect();
235    // Blank-line trivia carry no bytes; comment/CPP trivia fill the gaps that
236    // are legitimately not AST nodes.
237    items.extend(
238        trivia
239            .iter()
240            .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
241            .map(|t| (t.start, t.end)),
242    );
243    // Outer-first, like `check_nesting`: when intervals share a start, emit the
244    // broader parent before contained children so child spans can be skipped as
245    // already-covered tiles.
246    items.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
247
248    let mut out = String::with_capacity(source.len());
249    let mut prev = 0usize;
250    for (start, end) in items {
251        validate_interval(source, start, end)?;
252        if start < prev {
253            // Nested AST child spans and contained trivia are already covered
254            // by their parent tile. A partial overlap that extends past `prev`
255            // cannot be tiled losslessly and means the interval set is invalid.
256            if end <= prev {
257                continue;
258            }
259            return Err(AstSpanError::OverlappingTile {
260                start,
261                end,
262                previous_end: prev,
263            });
264        }
265        let gap = &source[prev..start];
266        if !gap.chars().all(char::is_whitespace) {
267            return Err(AstSpanError::UncoveredBytes {
268                start: prev,
269                end: start,
270                text: gap.to_string(),
271            });
272        }
273        out.push_str(gap);
274        out.push_str(&source[start..end]);
275        prev = end;
276    }
277    let tail = &source[prev..];
278    if !tail.chars().all(char::is_whitespace) {
279        return Err(AstSpanError::UncoveredTail {
280            start: prev,
281            text: tail.to_string(),
282        });
283    }
284    out.push_str(tail);
285
286    if out != source {
287        return Err(AstSpanError::ReconstructionMismatch {
288            reconstructed_len: out.len(),
289            source_len: source.len(),
290        });
291    }
292    Ok(out)
293}
294
295const fn validate_interval(source: &str, start: usize, end: usize) -> Result<(), AstSpanError> {
296    if start > end {
297        return Err(AstSpanError::IntervalStartAfterEnd { start, end });
298    }
299    if end > source.len() {
300        return Err(AstSpanError::IntervalExceedsSource {
301            start,
302            end,
303            source_len: source.len(),
304        });
305    }
306    if !source.is_char_boundary(start) || !source.is_char_boundary(end) {
307        return Err(AstSpanError::IntervalNotUtf8Boundary { start, end });
308    }
309    Ok(())
310}
311
312// ----- span collection ---------------------------------------------------
313
314fn collect_module(module: &Module, spans: &mut Vec<Span>) {
315    spans.push(module.span);
316    if !module.header.is_empty() {
317        spans.push(module.header);
318    }
319    for import in &module.imports {
320        spans.push(import.span);
321        if let Some(label) = &import.package_label {
322            spans.push(label.span);
323        }
324    }
325    for decl in &module.decls {
326        collect_decl(decl, spans);
327    }
328}
329
330fn collect_decl(decl: &Decl, spans: &mut Vec<Span>) {
331    match decl {
332        Decl::Template(template) => {
333            spans.push(template.span);
334            for field in &template.fields {
335                spans.push(field.span);
336                if let TypeAnnotation::Present(ty) = &field.ty {
337                    collect_type(ty, spans);
338                }
339            }
340            for body_decl in &template.body {
341                collect_tbody(body_decl, spans);
342            }
343        }
344        Decl::Interface(interface) => {
345            spans.push(interface.span);
346            for method in &interface.methods {
347                spans.push(method.span);
348                if let TypeAnnotation::Present(ty) = &method.ty {
349                    collect_type(ty, spans);
350                }
351            }
352            for choice in &interface.choices {
353                collect_choice(choice, spans);
354            }
355        }
356        Decl::Function(function) => {
357            // `function.span` is the equations' extent (contiguous); the signature,
358            // which may sit apart, is a separate sibling span.
359            spans.push(function.span);
360            for equation in &function.equations {
361                collect_eq(equation, spans);
362            }
363            if let Some(sig) = function.sig_span {
364                spans.push(sig);
365            }
366            if let TypeAnnotation::Present(ty) = &function.ty {
367                collect_type(ty, spans);
368            }
369        }
370        Decl::TypeDef { span, .. }
371        | Decl::Unknown { span, .. }
372        | Decl::Fixity(FixityDecl { span, .. })
373        | Decl::UnsupportedSyntax { span, .. } => spans.push(*span),
374    }
375}
376
377fn collect_tbody(template_body_decl: &TemplateBodyDecl, spans: &mut Vec<Span>) {
378    match template_body_decl {
379        TemplateBodyDecl::Signatory { parties, span, .. }
380        | TemplateBodyDecl::Observer { parties, span, .. } => {
381            spans.push(*span);
382            for party in parties {
383                collect_expr(party, spans);
384            }
385        }
386        TemplateBodyDecl::Ensure { expr, span, .. }
387        | TemplateBodyDecl::Maintainer { expr, span, .. } => {
388            spans.push(*span);
389            collect_expr(expr, spans);
390        }
391        TemplateBodyDecl::Key { expr, span, ty, .. } => {
392            spans.push(*span);
393            collect_expr(expr, spans);
394            if let TypeAnnotation::Present(ty) = ty {
395                collect_type(ty, spans);
396            }
397        }
398        TemplateBodyDecl::Choice(choice) => collect_choice(choice, spans),
399        TemplateBodyDecl::InterfaceInstance(interface_instance) => {
400            spans.push(interface_instance.span);
401            for item in &interface_instance.items {
402                match item {
403                    InterfaceInstanceBodyItem::View { expr, span, .. } => {
404                        spans.push(*span);
405                        collect_expr(expr, spans);
406                    }
407                    InterfaceInstanceBodyItem::Method(method) => collect_binding(method, spans),
408                }
409            }
410        }
411        TemplateBodyDecl::Other { span, .. } => spans.push(*span),
412    }
413}
414
415fn collect_choice(choice: &ChoiceDecl, spans: &mut Vec<Span>) {
416    spans.push(choice.span);
417    for param in &choice.params {
418        spans.push(param.span);
419        if let TypeAnnotation::Present(ty) = &param.ty {
420            collect_type(ty, spans);
421        }
422    }
423    for controller in &choice.controllers {
424        collect_expr(controller, spans);
425    }
426    for observer in &choice.observers {
427        collect_expr(observer, spans);
428    }
429    for authority in &choice.authority_exprs {
430        collect_expr(authority, spans);
431    }
432    if let Some(body) = &choice.body {
433        collect_expr(body, spans);
434    }
435    if let TypeAnnotation::Present(return_ty) = &choice.return_ty {
436        collect_type(return_ty, spans);
437    }
438}
439
440fn collect_type(ty: &Type, spans: &mut Vec<Span>) {
441    spans.push(ty.span());
442    match ty {
443        Type::Con { .. } | Type::Var(_, _) | Type::Unit(_) | Type::Lit { .. } => {}
444        Type::App(head, args, _) => {
445            collect_type(head, spans);
446            for arg in args {
447                collect_type(arg, spans);
448            }
449        }
450        Type::List(inner, _) | Type::Constrained(inner, _) => collect_type(inner, spans),
451        Type::Tuple(elems, _) => {
452            for elem in elems {
453                collect_type(elem, spans);
454            }
455        }
456        Type::Fun(lhs, rhs, _) => {
457            collect_type(lhs, spans);
458            collect_type(rhs, spans);
459        }
460    }
461}
462
463fn collect_eq(equation: &Equation, spans: &mut Vec<Span>) {
464    spans.push(equation.span);
465    for param in &equation.params {
466        collect_pat(param, spans);
467    }
468    collect_expr(&equation.body, spans);
469    for (guard, body) in &equation.guards {
470        collect_expr(guard, spans);
471        collect_expr(body, spans);
472    }
473    for where_binding in &equation.where_bindings {
474        collect_binding(where_binding, spans);
475    }
476}
477
478fn collect_binding(binding: &Binding, spans: &mut Vec<Span>) {
479    spans.push(binding.span);
480    collect_pat(&binding.pat, spans);
481    for param in &binding.params {
482        collect_pat(param, spans);
483    }
484    collect_expr(&binding.expr, spans);
485}
486
487fn collect_pat(pattern: &Pat, spans: &mut Vec<Span>) {
488    spans.push(pattern.span());
489    match pattern {
490        Pat::Con { args, .. } => {
491            for arg in args {
492                collect_pat(arg, spans);
493            }
494        }
495        Pat::Record { fields, .. } => {
496            for field in fields {
497                spans.push(field.span());
498                if let PatFieldAssign::Assign { pat, .. } = field {
499                    collect_pat(pat, spans);
500                }
501            }
502        }
503        Pat::Tuple { items, .. } | Pat::List { items, .. } => {
504            for item in items {
505                collect_pat(item, spans);
506            }
507        }
508        Pat::As { pat, .. } => collect_pat(pat, spans),
509        Pat::Var { .. } | Pat::Wild { .. } | Pat::Lit { .. } | Pat::Other { .. } => {}
510    }
511}
512
513fn collect_expr(expr: &Expr, spans: &mut Vec<Span>) {
514    spans.push(expr.span());
515    match expr {
516        Expr::App { func, args, .. } => {
517            collect_expr(func, spans);
518            for arg in args {
519                collect_expr(arg, spans);
520            }
521        }
522        Expr::BinOp { lhs, rhs, .. } => {
523            collect_expr(lhs, spans);
524            collect_expr(rhs, spans);
525        }
526        Expr::Neg { expr, .. } => collect_expr(expr, spans),
527        Expr::Lambda { params, body, .. } => {
528            for param in params {
529                collect_pat(param, spans);
530            }
531            collect_expr(body, spans);
532        }
533        Expr::If {
534            cond,
535            then_branch,
536            else_branch,
537            ..
538        } => {
539            collect_expr(cond, spans);
540            collect_expr(then_branch, spans);
541            collect_expr(else_branch, spans);
542        }
543        Expr::Case {
544            scrutinee, alts, ..
545        } => {
546            collect_expr(scrutinee, spans);
547            for alt in alts {
548                collect_alt(alt, spans);
549            }
550        }
551        Expr::Do { stmts, .. } => {
552            for stmt in stmts {
553                collect_dostmt(stmt, spans);
554            }
555        }
556        Expr::LetIn { bindings, body, .. } => {
557            for binding in bindings {
558                collect_binding(binding, spans);
559            }
560            collect_expr(body, spans);
561        }
562        Expr::Record { base, fields, .. } => {
563            collect_expr(base, spans);
564            for field in fields {
565                collect_field_assign(field, spans);
566            }
567        }
568        Expr::Tuple { items, .. } | Expr::List { items, .. } => {
569            for item in items {
570                collect_expr(item, spans);
571            }
572        }
573        Expr::Try { body, handlers, .. } => {
574            collect_expr(body, spans);
575            for handler in handlers {
576                collect_alt(handler, spans);
577            }
578        }
579        Expr::LeftSection { operand, .. } | Expr::RightSection { operand, .. } => {
580            collect_expr(operand, spans);
581        }
582        Expr::Var { .. }
583        | Expr::Con { .. }
584        | Expr::Lit { .. }
585        | Expr::OperatorRef { .. }
586        | Expr::Error { .. } => {}
587    }
588}
589
590fn collect_guard_qualifier(guard: &GuardQualifier, spans: &mut Vec<Span>) {
591    spans.push(guard.span());
592    match guard {
593        GuardQualifier::Bool { expr, .. } => collect_expr(expr, spans),
594        GuardQualifier::Pattern { pat, expr, .. } => {
595            collect_pat(pat, spans);
596            collect_expr(expr, spans);
597        }
598    }
599}
600
601fn collect_alt(alt: &Alt, spans: &mut Vec<Span>) {
602    spans.push(alt.span);
603    collect_pat(&alt.pat, spans);
604    for branch in &alt.branches {
605        spans.push(branch.span);
606        for guard in &branch.guards {
607            collect_guard_qualifier(guard, spans);
608        }
609        collect_expr(&branch.body, spans);
610    }
611    for where_binding in &alt.where_bindings {
612        collect_binding(where_binding, spans);
613    }
614}
615
616fn collect_field_assign(field_assign: &FieldAssign, spans: &mut Vec<Span>) {
617    spans.push(field_assign.span());
618    if let FieldAssign::Assign { value, .. } = field_assign {
619        collect_expr(value, spans);
620    }
621}
622
623fn collect_dostmt(do_stmt: &DoStmt, spans: &mut Vec<Span>) {
624    match do_stmt {
625        DoStmt::Bind {
626            pat, expr, span, ..
627        } => {
628            spans.push(*span);
629            collect_pat(pat, spans);
630            collect_expr(expr, spans);
631        }
632        DoStmt::Let { bindings, span, .. } => {
633            spans.push(*span);
634            for binding in bindings {
635                collect_binding(binding, spans);
636            }
637        }
638        DoStmt::Expr { expr, span, .. } => {
639            spans.push(*span);
640            collect_expr(expr, spans);
641        }
642    }
643}
644
645// Tile-oracle interval validation and overlap detection for the ast_span phase.
646#[cfg(test)]
647#[allow(clippy::unwrap_used)]
648mod tests {
649    use super::*;
650    use crate::lexer::{Pos, Trivia};
651
652    #[test]
653    fn tile_reports_overlapping_intervals() {
654        let source = "module M where\n-- comment\n";
655        let module = Module {
656            name: "M".into(),
657            pos: Pos { line: 1, column: 1 },
658            span: Span::from_usize(0, source.len()),
659            header: Span::from_usize(0, "module M where".len()),
660            imports: Vec::new(),
661            decls: Vec::new(),
662        };
663        let trivia = vec![Trivia {
664            kind: TriviaKind::LineComment,
665            text: "-- comment".to_string(),
666            pos: Pos { line: 2, column: 1 },
667            start: "module M wher".len(),
668            end: "module M where\n-- comment".len(),
669        }];
670
671        let err = tile(source, &module, &trivia).unwrap_err();
672        assert!(
673            err.to_string().contains("overlaps previous tile"),
674            "overlap should fail loudly, got: {err}"
675        );
676    }
677
678    #[test]
679    fn validate_interval_reports_malformed_bounds() {
680        assert_eq!(
681            validate_interval("abc", 2, 1),
682            Err(AstSpanError::IntervalStartAfterEnd { start: 2, end: 1 })
683        );
684        assert_eq!(
685            validate_interval("abc", 0, 4),
686            Err(AstSpanError::IntervalExceedsSource {
687                start: 0,
688                end: 4,
689                source_len: 3
690            })
691        );
692        assert_eq!(
693            validate_interval("é", 1, 2),
694            Err(AstSpanError::IntervalNotUtf8Boundary { start: 1, end: 2 })
695        );
696    }
697
698    #[test]
699    fn tile_reports_uncovered_non_whitespace_bytes() {
700        let source = "module M where\nmissing\n";
701        let module = Module {
702            name: "M".into(),
703            pos: Pos { line: 1, column: 1 },
704            span: Span::from_usize(0, source.len()),
705            header: Span::from_usize(0, "module M where".len()),
706            imports: Vec::new(),
707            decls: Vec::new(),
708        };
709
710        assert_eq!(
711            tile(source, &module, &[]),
712            Err(AstSpanError::UncoveredTail {
713                start: "module M where".len(),
714                text: "\nmissing\n".to_string(),
715            })
716        );
717    }
718
719    #[test]
720    fn render_from_ast_roundtrips_header_only_module() {
721        let source = "module M where\n";
722        let module = Module {
723            name: "M".into(),
724            pos: Pos { line: 1, column: 1 },
725            span: Span::from_usize(0, source.len()),
726            header: Span::from_usize(0, "module M where".len()),
727            imports: Vec::new(),
728            decls: Vec::new(),
729        };
730
731        assert_eq!(render_from_ast(source, &module, &[]).as_deref(), Ok(source));
732    }
733}