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