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