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/// Reconstruct `source` from the AST's byte spans plus the lexer's `trivia`.
17///
18/// The AST-level mirror of `lexer::render_lossless`: it checks the spans nest
19/// (V2), then tiles the file from every *content* node span merged with the
20/// non-blank trivia spans (V1/V3). `Ok(reconstruction)` is byte-identical to
21/// `source`; `Err` names the first nesting violation or the first run of bytes
22/// no span covers (content the AST dropped).
23///
24/// Obtain `trivia` from [`crate::lexer::lex_with_trivia`].
25pub fn render_from_ast(source: &str, module: &Module, trivia: &[Trivia]) -> Result<String, String> {
26    check_nesting(module)?;
27    tile(source, module, trivia)
28}
29
30/// V2 — every child span is contained in its parent, and sibling spans are
31/// ordered and disjoint. Validates the whole node set (module container
32/// included) as a laminar family via a containment stack.
33fn check_nesting(module: &Module) -> Result<(), String> {
34    let mut spans: Vec<Span> = Vec::new();
35    collect_module(module, &mut spans);
36    for span in &spans {
37        if !span.is_valid() {
38            return Err(format!("invalid span [{}, {})", span.start, span.end));
39        }
40    }
41    spans.retain(|s| !s.is_empty());
42    // Outer-first: earlier start, then later end.
43    spans.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
44
45    let mut stack: Vec<Span> = Vec::new();
46    for span in spans {
47        // Pop ancestors that end at/before this span starts (its left siblings).
48        while stack.last().is_some_and(|top| top.end <= span.start) {
49            stack.pop();
50        }
51        // Whatever remains on top must contain this span; otherwise the two
52        // overlap without nesting (a sibling that starts before the previous
53        // one ended, or a child that spills past its parent).
54        if let Some(parent) = stack.last() {
55            if !parent.contains(&span) {
56                return Err(format!(
57                    "span [{}, {}) overlaps [{}, {}) without nesting",
58                    span.start, span.end, parent.start, parent.end
59                ));
60            }
61        }
62        stack.push(span);
63    }
64    Ok(())
65}
66
67/// V1/V3 — tile the file from content spans + non-blank trivia and reconstruct.
68/// "Content" excludes the whole-module container (which would cover everything
69/// trivially) but includes the `module … where` header. Any gap between spans
70/// must be whitespace-only; a non-whitespace gap is a real token no node claims,
71/// i.e. content the AST dropped.
72fn tile(source: &str, module: &Module, trivia: &[Trivia]) -> Result<String, String> {
73    let mut content: Vec<Span> = Vec::new();
74    collect_module(module, &mut content);
75    let container = module.span;
76    content.retain(|s| !(s.is_empty() || (s.start == container.start && s.end == container.end)));
77
78    let mut items: Vec<(usize, usize)> = content.iter().map(|s| (s.start, s.end)).collect();
79    // Blank-line trivia carry no bytes; comment/CPP trivia fill the gaps that
80    // are legitimately not AST nodes.
81    items.extend(
82        trivia
83            .iter()
84            .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
85            .map(|t| (t.start, t.end)),
86    );
87    // Outer-first, like `check_nesting`: when intervals share a start, emit the
88    // broader parent before contained children so child spans can be skipped as
89    // already-covered tiles.
90    items.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
91
92    let mut out = String::with_capacity(source.len());
93    let mut prev = 0usize;
94    for (start, end) in items {
95        validate_interval(source, start, end)?;
96        if start < prev {
97            // Nested AST child spans and contained trivia are already covered
98            // by their parent tile. A partial overlap that extends past `prev`
99            // cannot be tiled losslessly and means the interval set is invalid.
100            if end <= prev {
101                continue;
102            }
103            return Err(format!(
104                "span/trivia interval [{start}, {end}) overlaps previous tile ending at {prev}"
105            ));
106        }
107        let gap = &source[prev..start];
108        if !gap.chars().all(char::is_whitespace) {
109            return Err(format!(
110                "bytes {prev}..{start} not covered by any node or trivia span: {gap:?}"
111            ));
112        }
113        out.push_str(gap);
114        out.push_str(&source[start..end]);
115        prev = end;
116    }
117    let tail = &source[prev..];
118    if !tail.chars().all(char::is_whitespace) {
119        return Err(format!("bytes {prev}.. lost at EOF: {tail:?}"));
120    }
121    out.push_str(tail);
122
123    if out != source {
124        return Err(format!(
125            "reconstruction differs from source ({} vs {} bytes)",
126            out.len(),
127            source.len()
128        ));
129    }
130    Ok(out)
131}
132
133fn validate_interval(source: &str, start: usize, end: usize) -> Result<(), String> {
134    if start > end {
135        return Err(format!(
136            "span/trivia interval [{start}, {end}) has start after end"
137        ));
138    }
139    if end > source.len() {
140        return Err(format!(
141            "span/trivia interval [{start}, {end}) exceeds source length {}",
142            source.len()
143        ));
144    }
145    if !source.is_char_boundary(start) || !source.is_char_boundary(end) {
146        return Err(format!(
147            "span/trivia interval [{start}, {end}) does not align with UTF-8 boundaries"
148        ));
149    }
150    Ok(())
151}
152
153// ----- span collection ---------------------------------------------------
154
155fn collect_module(m: &Module, out: &mut Vec<Span>) {
156    out.push(m.span);
157    if !m.header.is_empty() {
158        out.push(m.header);
159    }
160    for imp in &m.imports {
161        out.push(imp.span);
162    }
163    for d in &m.decls {
164        collect_decl(d, out);
165    }
166}
167
168fn collect_decl(d: &Decl, out: &mut Vec<Span>) {
169    match d {
170        Decl::Template(t) => {
171            out.push(t.span);
172            for f in &t.fields {
173                out.push(f.span);
174            }
175            for b in &t.body {
176                collect_tbody(b, out);
177            }
178        }
179        Decl::Interface(i) => {
180            out.push(i.span);
181            for m in &i.methods {
182                out.push(m.span);
183            }
184            for c in &i.choices {
185                collect_choice(c, out);
186            }
187        }
188        Decl::Function(f) => {
189            // `f.span` is the equations' extent (contiguous); the signature,
190            // which may sit apart, is a separate sibling span.
191            out.push(f.span);
192            for eq in &f.equations {
193                collect_eq(eq, out);
194            }
195            if let Some(sig) = f.sig_span {
196                out.push(sig);
197            }
198        }
199        Decl::TypeDef { span, .. } | Decl::Unknown { span, .. } => out.push(*span),
200    }
201}
202
203fn collect_tbody(b: &TemplateBodyDecl, out: &mut Vec<Span>) {
204    match b {
205        TemplateBodyDecl::Signatory { parties, span, .. }
206        | TemplateBodyDecl::Observer { parties, span, .. } => {
207            out.push(*span);
208            for e in parties {
209                collect_expr(e, out);
210            }
211        }
212        TemplateBodyDecl::Ensure { expr, span, .. }
213        | TemplateBodyDecl::Maintainer { expr, span, .. }
214        | TemplateBodyDecl::Key { expr, span, .. } => {
215            out.push(*span);
216            collect_expr(expr, out);
217        }
218        TemplateBodyDecl::Choice(c) => collect_choice(c, out),
219        TemplateBodyDecl::InterfaceInstance(ii) => {
220            out.push(ii.span);
221            for m in &ii.methods {
222                collect_binding(m, out);
223            }
224        }
225        TemplateBodyDecl::Other { span, .. } => out.push(*span),
226    }
227}
228
229fn collect_choice(c: &ChoiceDecl, out: &mut Vec<Span>) {
230    out.push(c.span);
231    for p in &c.params {
232        out.push(p.span);
233    }
234    for e in &c.controllers {
235        collect_expr(e, out);
236    }
237    for e in &c.observers {
238        collect_expr(e, out);
239    }
240    if let Some(b) = &c.body {
241        collect_expr(b, out);
242    }
243}
244
245fn collect_eq(eq: &Equation, out: &mut Vec<Span>) {
246    out.push(eq.span);
247    for p in &eq.params {
248        collect_pat(p, out);
249    }
250    collect_expr(&eq.body, out);
251    for (g, b) in &eq.guards {
252        collect_expr(g, out);
253        collect_expr(b, out);
254    }
255    for wb in &eq.where_bindings {
256        collect_binding(wb, out);
257    }
258}
259
260fn collect_binding(b: &Binding, out: &mut Vec<Span>) {
261    out.push(b.span);
262    collect_pat(&b.pat, out);
263    for p in &b.params {
264        collect_pat(p, out);
265    }
266    collect_expr(&b.expr, out);
267}
268
269fn collect_pat(p: &Pat, out: &mut Vec<Span>) {
270    out.push(p.span());
271    match p {
272        Pat::Con { args, .. } => {
273            for a in args {
274                collect_pat(a, out);
275            }
276        }
277        Pat::Tuple { items, .. } | Pat::List { items, .. } => {
278            for it in items {
279                collect_pat(it, out);
280            }
281        }
282        Pat::As { pat, .. } => collect_pat(pat, out),
283        _ => {}
284    }
285}
286
287fn collect_expr(e: &Expr, out: &mut Vec<Span>) {
288    out.push(e.span());
289    match e {
290        Expr::App { func, args, .. } => {
291            collect_expr(func, out);
292            for a in args {
293                collect_expr(a, out);
294            }
295        }
296        Expr::BinOp { lhs, rhs, .. } => {
297            collect_expr(lhs, out);
298            collect_expr(rhs, out);
299        }
300        Expr::Neg { expr, .. } => collect_expr(expr, out),
301        Expr::Lambda { params, body, .. } => {
302            for p in params {
303                collect_pat(p, out);
304            }
305            collect_expr(body, out);
306        }
307        Expr::If {
308            cond,
309            then_branch,
310            else_branch,
311            ..
312        } => {
313            collect_expr(cond, out);
314            collect_expr(then_branch, out);
315            collect_expr(else_branch, out);
316        }
317        Expr::Case {
318            scrutinee, alts, ..
319        } => {
320            collect_expr(scrutinee, out);
321            for a in alts {
322                collect_alt(a, out);
323            }
324        }
325        Expr::Do { stmts, .. } => {
326            for s in stmts {
327                collect_dostmt(s, out);
328            }
329        }
330        Expr::LetIn { bindings, body, .. } => {
331            for b in bindings {
332                collect_binding(b, out);
333            }
334            collect_expr(body, out);
335        }
336        Expr::Record { base, fields, .. } => {
337            collect_expr(base, out);
338            for f in fields {
339                collect_field_assign(f, out);
340            }
341        }
342        Expr::Tuple { items, .. } | Expr::List { items, .. } => {
343            for it in items {
344                collect_expr(it, out);
345            }
346        }
347        Expr::Try { body, handlers, .. } => {
348            collect_expr(body, out);
349            for h in handlers {
350                collect_alt(h, out);
351            }
352        }
353        Expr::Section {
354            operand: Some(o), ..
355        } => collect_expr(o, out),
356        _ => {}
357    }
358}
359
360fn collect_alt(a: &Alt, out: &mut Vec<Span>) {
361    out.push(a.span);
362    collect_pat(&a.pat, out);
363    collect_expr(&a.body, out);
364}
365
366fn collect_field_assign(f: &FieldAssign, out: &mut Vec<Span>) {
367    out.push(f.span);
368    if let Some(v) = &f.value {
369        collect_expr(v, out);
370    }
371}
372
373fn collect_dostmt(s: &DoStmt, out: &mut Vec<Span>) {
374    match s {
375        DoStmt::Bind {
376            pat, expr, span, ..
377        } => {
378            out.push(*span);
379            collect_pat(pat, out);
380            collect_expr(expr, out);
381        }
382        DoStmt::Let { bindings, span, .. } => {
383            out.push(*span);
384            for b in bindings {
385                collect_binding(b, out);
386            }
387        }
388        DoStmt::Expr { expr, span, .. } => {
389            out.push(*span);
390            collect_expr(expr, out);
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::lexer::{Pos, Trivia};
399
400    #[test]
401    fn tile_reports_overlapping_intervals() {
402        let source = "module M where\n-- comment\n";
403        let module = Module {
404            name: "M".to_string(),
405            pos: Pos { line: 1, column: 1 },
406            span: Span::new(0, source.len()),
407            header: Span::new(0, "module M where".len()),
408            imports: Vec::new(),
409            decls: Vec::new(),
410        };
411        let trivia = vec![Trivia {
412            kind: TriviaKind::LineComment,
413            text: "-- comment".to_string(),
414            pos: Pos { line: 2, column: 1 },
415            start: "module M wher".len(),
416            end: "module M where\n-- comment".len(),
417        }];
418
419        let err = tile(source, &module, &trivia).unwrap_err();
420        assert!(
421            err.contains("overlaps previous tile"),
422            "overlap should fail loudly, got: {err}"
423        );
424    }
425}