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`].
127pub fn render_from_ast(
128    source: &str,
129    module: &Module,
130    trivia: &[Trivia],
131) -> Result<String, AstSpanError> {
132    check_nesting(module)?;
133    tile(source, module, trivia)
134}
135
136/// V2 — every child span is contained in its parent, and sibling spans are
137/// ordered and disjoint. Validates the whole node set (module container
138/// included) as a laminar family via a containment stack.
139fn check_nesting(module: &Module) -> Result<(), AstSpanError> {
140    let mut spans: Vec<Span> = Vec::new();
141    collect_module(module, &mut spans);
142    for span in &spans {
143        if !span.is_valid() {
144            return Err(AstSpanError::InvalidSpan {
145                start: span.start,
146                end: span.end,
147            });
148        }
149    }
150    spans.retain(|s| !s.is_empty());
151    // Outer-first: earlier start, then later end.
152    spans.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
153
154    let mut stack: Vec<Span> = Vec::new();
155    for span in spans {
156        // Pop ancestors that end at/before this span starts (its left siblings).
157        while stack.last().is_some_and(|top| top.end <= span.start) {
158            stack.pop();
159        }
160        // Whatever remains on top must contain this span; otherwise the two
161        // overlap without nesting (a sibling that starts before the previous
162        // one ended, or a child that spills past its parent).
163        if let Some(parent) = stack.last() {
164            if !parent.contains(&span) {
165                return Err(AstSpanError::OverlappingSpans {
166                    start: span.start,
167                    end: span.end,
168                    parent_start: parent.start,
169                    parent_end: parent.end,
170                });
171            }
172        }
173        stack.push(span);
174    }
175    Ok(())
176}
177
178/// V1/V3 — tile the file from content spans + non-blank trivia and reconstruct.
179/// "Content" excludes the whole-module container (which would cover everything
180/// trivially) but includes the `module … where` header. Any gap between spans
181/// must be whitespace-only; a non-whitespace gap is a real token no node claims,
182/// i.e. content the AST dropped.
183fn tile(source: &str, module: &Module, trivia: &[Trivia]) -> Result<String, AstSpanError> {
184    let mut content: Vec<Span> = Vec::new();
185    collect_module(module, &mut content);
186    let container = module.span;
187    content.retain(|s| !(s.is_empty() || (s.start == container.start && s.end == container.end)));
188
189    let mut items: Vec<(usize, usize)> = content.iter().map(|s| (s.start, s.end)).collect();
190    // Blank-line trivia carry no bytes; comment/CPP trivia fill the gaps that
191    // are legitimately not AST nodes.
192    items.extend(
193        trivia
194            .iter()
195            .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
196            .map(|t| (t.start, t.end)),
197    );
198    // Outer-first, like `check_nesting`: when intervals share a start, emit the
199    // broader parent before contained children so child spans can be skipped as
200    // already-covered tiles.
201    items.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
202
203    let mut out = String::with_capacity(source.len());
204    let mut prev = 0usize;
205    for (start, end) in items {
206        validate_interval(source, start, end)?;
207        if start < prev {
208            // Nested AST child spans and contained trivia are already covered
209            // by their parent tile. A partial overlap that extends past `prev`
210            // cannot be tiled losslessly and means the interval set is invalid.
211            if end <= prev {
212                continue;
213            }
214            return Err(AstSpanError::OverlappingTile {
215                start,
216                end,
217                previous_end: prev,
218            });
219        }
220        let gap = &source[prev..start];
221        if !gap.chars().all(char::is_whitespace) {
222            return Err(AstSpanError::UncoveredBytes {
223                start: prev,
224                end: start,
225                text: gap.to_string(),
226            });
227        }
228        out.push_str(gap);
229        out.push_str(&source[start..end]);
230        prev = end;
231    }
232    let tail = &source[prev..];
233    if !tail.chars().all(char::is_whitespace) {
234        return Err(AstSpanError::UncoveredTail {
235            start: prev,
236            text: tail.to_string(),
237        });
238    }
239    out.push_str(tail);
240
241    if out != source {
242        return Err(AstSpanError::ReconstructionMismatch {
243            reconstructed_len: out.len(),
244            source_len: source.len(),
245        });
246    }
247    Ok(out)
248}
249
250const fn validate_interval(source: &str, start: usize, end: usize) -> Result<(), AstSpanError> {
251    if start > end {
252        return Err(AstSpanError::IntervalStartAfterEnd { start, end });
253    }
254    if end > source.len() {
255        return Err(AstSpanError::IntervalExceedsSource {
256            start,
257            end,
258            source_len: source.len(),
259        });
260    }
261    if !source.is_char_boundary(start) || !source.is_char_boundary(end) {
262        return Err(AstSpanError::IntervalNotUtf8Boundary { start, end });
263    }
264    Ok(())
265}
266
267// ----- span collection ---------------------------------------------------
268
269fn collect_module(module: &Module, spans: &mut Vec<Span>) {
270    spans.push(module.span);
271    if !module.header.is_empty() {
272        spans.push(module.header);
273    }
274    for import in &module.imports {
275        spans.push(import.span);
276    }
277    for decl in &module.decls {
278        collect_decl(decl, spans);
279    }
280}
281
282fn collect_decl(decl: &Decl, spans: &mut Vec<Span>) {
283    match decl {
284        Decl::Template(template) => {
285            spans.push(template.span);
286            for field in &template.fields {
287                spans.push(field.span);
288            }
289            for body_decl in &template.body {
290                collect_tbody(body_decl, spans);
291            }
292        }
293        Decl::Interface(interface) => {
294            spans.push(interface.span);
295            for method in &interface.methods {
296                spans.push(method.span);
297            }
298            for choice in &interface.choices {
299                collect_choice(choice, spans);
300            }
301        }
302        Decl::Function(function) => {
303            // `function.span` is the equations' extent (contiguous); the signature,
304            // which may sit apart, is a separate sibling span.
305            spans.push(function.span);
306            for equation in &function.equations {
307                collect_eq(equation, spans);
308            }
309            if let Some(sig) = function.sig_span {
310                spans.push(sig);
311            }
312        }
313        Decl::TypeDef { span, .. } | Decl::Unknown { span, .. } => spans.push(*span),
314    }
315}
316
317fn collect_tbody(template_body_decl: &TemplateBodyDecl, spans: &mut Vec<Span>) {
318    match template_body_decl {
319        TemplateBodyDecl::Signatory { parties, span, .. }
320        | TemplateBodyDecl::Observer { parties, span, .. } => {
321            spans.push(*span);
322            for party in parties {
323                collect_expr(party, spans);
324            }
325        }
326        TemplateBodyDecl::Ensure { expr, span, .. }
327        | TemplateBodyDecl::Maintainer { expr, span, .. }
328        | TemplateBodyDecl::Key { expr, span, .. } => {
329            spans.push(*span);
330            collect_expr(expr, spans);
331        }
332        TemplateBodyDecl::Choice(choice) => collect_choice(choice, spans),
333        TemplateBodyDecl::InterfaceInstance(interface_instance) => {
334            spans.push(interface_instance.span);
335            for method in &interface_instance.methods {
336                collect_binding(method, spans);
337            }
338        }
339        TemplateBodyDecl::Other { span, .. } => spans.push(*span),
340    }
341}
342
343fn collect_choice(choice: &ChoiceDecl, spans: &mut Vec<Span>) {
344    spans.push(choice.span);
345    for param in &choice.params {
346        spans.push(param.span);
347    }
348    for controller in &choice.controllers {
349        collect_expr(controller, spans);
350    }
351    for observer in &choice.observers {
352        collect_expr(observer, spans);
353    }
354    if let Some(body) = &choice.body {
355        collect_expr(body, spans);
356    }
357}
358
359fn collect_eq(equation: &Equation, spans: &mut Vec<Span>) {
360    spans.push(equation.span);
361    for param in &equation.params {
362        collect_pat(param, spans);
363    }
364    collect_expr(&equation.body, spans);
365    for (guard, body) in &equation.guards {
366        collect_expr(guard, spans);
367        collect_expr(body, spans);
368    }
369    for where_binding in &equation.where_bindings {
370        collect_binding(where_binding, spans);
371    }
372}
373
374fn collect_binding(binding: &Binding, spans: &mut Vec<Span>) {
375    spans.push(binding.span);
376    collect_pat(&binding.pat, spans);
377    for param in &binding.params {
378        collect_pat(param, spans);
379    }
380    collect_expr(&binding.expr, spans);
381}
382
383fn collect_pat(pattern: &Pat, spans: &mut Vec<Span>) {
384    spans.push(pattern.span());
385    match pattern {
386        Pat::Con { args, .. } => {
387            for arg in args {
388                collect_pat(arg, spans);
389            }
390        }
391        Pat::Tuple { items, .. } | Pat::List { items, .. } => {
392            for item in items {
393                collect_pat(item, spans);
394            }
395        }
396        Pat::As { pat, .. } => collect_pat(pat, spans),
397        Pat::Var { .. } | Pat::Wild { .. } | Pat::Lit { .. } | Pat::Other { .. } => {}
398    }
399}
400
401fn collect_expr(expr: &Expr, spans: &mut Vec<Span>) {
402    spans.push(expr.span());
403    match expr {
404        Expr::App { func, args, .. } => {
405            collect_expr(func, spans);
406            for arg in args {
407                collect_expr(arg, spans);
408            }
409        }
410        Expr::BinOp { lhs, rhs, .. } => {
411            collect_expr(lhs, spans);
412            collect_expr(rhs, spans);
413        }
414        Expr::Neg { expr, .. } => collect_expr(expr, spans),
415        Expr::Lambda { params, body, .. } => {
416            for param in params {
417                collect_pat(param, spans);
418            }
419            collect_expr(body, spans);
420        }
421        Expr::If {
422            cond,
423            then_branch,
424            else_branch,
425            ..
426        } => {
427            collect_expr(cond, spans);
428            collect_expr(then_branch, spans);
429            collect_expr(else_branch, spans);
430        }
431        Expr::Case {
432            scrutinee, alts, ..
433        } => {
434            collect_expr(scrutinee, spans);
435            for alt in alts {
436                collect_alt(alt, spans);
437            }
438        }
439        Expr::Do { stmts, .. } => {
440            for stmt in stmts {
441                collect_dostmt(stmt, spans);
442            }
443        }
444        Expr::LetIn { bindings, body, .. } => {
445            for binding in bindings {
446                collect_binding(binding, spans);
447            }
448            collect_expr(body, spans);
449        }
450        Expr::Record { base, fields, .. } => {
451            collect_expr(base, spans);
452            for field in fields {
453                collect_field_assign(field, spans);
454            }
455        }
456        Expr::Tuple { items, .. } | Expr::List { items, .. } => {
457            for item in items {
458                collect_expr(item, spans);
459            }
460        }
461        Expr::Try { body, handlers, .. } => {
462            collect_expr(body, spans);
463            for handler in handlers {
464                collect_alt(handler, spans);
465            }
466        }
467        Expr::Section {
468            operand: Some(operand),
469            ..
470        } => collect_expr(operand, spans),
471        Expr::Var { .. }
472        | Expr::Con { .. }
473        | Expr::Lit { .. }
474        | Expr::Section { operand: None, .. }
475        | Expr::Error { .. } => {}
476    }
477}
478
479fn collect_alt(alt: &Alt, spans: &mut Vec<Span>) {
480    spans.push(alt.span);
481    collect_pat(&alt.pat, spans);
482    collect_expr(&alt.body, spans);
483}
484
485fn collect_field_assign(field_assign: &FieldAssign, spans: &mut Vec<Span>) {
486    spans.push(field_assign.span);
487    if let Some(value) = &field_assign.value {
488        collect_expr(value, spans);
489    }
490}
491
492fn collect_dostmt(do_stmt: &DoStmt, spans: &mut Vec<Span>) {
493    match do_stmt {
494        DoStmt::Bind {
495            pat, expr, span, ..
496        } => {
497            spans.push(*span);
498            collect_pat(pat, spans);
499            collect_expr(expr, spans);
500        }
501        DoStmt::Let { bindings, span, .. } => {
502            spans.push(*span);
503            for binding in bindings {
504                collect_binding(binding, spans);
505            }
506        }
507        DoStmt::Expr { expr, span, .. } => {
508            spans.push(*span);
509            collect_expr(expr, spans);
510        }
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::lexer::{Pos, Trivia};
518
519    #[test]
520    fn tile_reports_overlapping_intervals() {
521        let source = "module M where\n-- comment\n";
522        let module = Module {
523            name: "M".to_string(),
524            pos: Pos { line: 1, column: 1 },
525            span: Span::new(0, source.len()),
526            header: Span::new(0, "module M where".len()),
527            imports: Vec::new(),
528            decls: Vec::new(),
529        };
530        let trivia = vec![Trivia {
531            kind: TriviaKind::LineComment,
532            text: "-- comment".to_string(),
533            pos: Pos { line: 2, column: 1 },
534            start: "module M wher".len(),
535            end: "module M where\n-- comment".len(),
536        }];
537
538        let err = tile(source, &module, &trivia).unwrap_err();
539        assert!(
540            err.to_string().contains("overlaps previous tile"),
541            "overlap should fail loudly, got: {err}"
542        );
543    }
544
545    #[test]
546    fn validate_interval_reports_malformed_bounds() {
547        assert_eq!(
548            validate_interval("abc", 2, 1),
549            Err(AstSpanError::IntervalStartAfterEnd { start: 2, end: 1 })
550        );
551        assert_eq!(
552            validate_interval("abc", 0, 4),
553            Err(AstSpanError::IntervalExceedsSource {
554                start: 0,
555                end: 4,
556                source_len: 3
557            })
558        );
559        assert_eq!(
560            validate_interval("é", 1, 2),
561            Err(AstSpanError::IntervalNotUtf8Boundary { start: 1, end: 2 })
562        );
563    }
564
565    #[test]
566    fn tile_reports_uncovered_non_whitespace_bytes() {
567        let source = "module M where\nmissing\n";
568        let module = Module {
569            name: "M".to_string(),
570            pos: Pos { line: 1, column: 1 },
571            span: Span::new(0, source.len()),
572            header: Span::new(0, "module M where".len()),
573            imports: Vec::new(),
574            decls: Vec::new(),
575        };
576
577        assert_eq!(
578            tile(source, &module, &[]),
579            Err(AstSpanError::UncoveredTail {
580                start: "module M where".len(),
581                text: "\nmissing\n".to_string(),
582            })
583        );
584    }
585
586    #[test]
587    fn render_from_ast_roundtrips_header_only_module() {
588        let source = "module M where\n";
589        let module = Module {
590            name: "M".to_string(),
591            pos: Pos { line: 1, column: 1 },
592            span: Span::new(0, source.len()),
593            header: Span::new(0, "module M where".len()),
594            imports: Vec::new(),
595            decls: Vec::new(),
596        };
597
598        assert_eq!(render_from_ast(source, &module, &[]).as_deref(), Ok(source));
599    }
600}