Skip to main content

adam_lang/
fmt.rs

1//! Pretty-prints an [`crate::ast::Sheet`] back to adam-lang source text: 4-space indentation,
2//! opening braces on the same line, `leading_comment`/`blank_line_before` reproduced exactly as
3//! [`crate::trivia::attach_trivia`] recovered them (including a file-header-style comment
4//! preceding the `sheet` keyword itself, `Sheet.leading_comment`), and binding bodies/cell
5//! initializers/requirement bodies delegated to [`cel_parser::format_expr_with_layout`].
6//! Expression layout targets 100 columns including declaration prefixes and punctuation. Nested
7//! lists, records and closure bodies use four spaces; binary continuations lead with operators.
8//! Type annotations are re-emitted via `Span::source_text()` directly
9//! via `TypeExpr::span()`; parameter annotations borrow their complete source range so applied
10//! named types retain arguments and comments. Branch-match literals use `ConditionalBranch::literal_span` — see
11//! the design doc for why no `Literal` value is needed. Conditional branches have no comma
12//! separator or terminator.
13//!
14//! Never called on a sheet with any recorded syntax errors — see `adam-lsp`'s
15//! `textDocument/formatting` handler, which refuses to format in that case.
16//!
17//! Writer cost bounds use `v` for visited AST nodes, `b` for emitted bytes
18//! (including indentation), `g` for summed scanned source-region bytes, `c` for
19//! summed character-prefix coordinate conversions, and `p` for summed output-line
20//! prefix measurements. `T` and `A` denote delegated CEL layout time and peak
21//! temporary space; `h` denotes writer call depth. Repeated work counts repeatedly,
22//! including descendant writers. Bounds amortize growth of the output string.
23//! Sharing line starts saves index construction, not prefix scans or recursive
24//! region/layout work; these bounds do not promise globally linear formatting.
25
26use crate::ast;
27
28/// Borrows source text and owns its line-offset table for one formatting operation.
29///
30/// Every declaration-gap lookup shares this table; no source text is copied.
31struct FormattingSource<'a> {
32    /// Borrows the exact source whose character positions the AST records.
33    text: &'a str,
34    /// Stores byte offsets of source lines once per formatting operation.
35    line_starts: Vec<usize>,
36}
37
38impl<'a> FormattingSource<'a> {
39    /// Indexes source lines once in O(n) time and O(l) space for n bytes and l lines.
40    fn new(text: &'a str) -> Self {
41        Self {
42            text,
43            line_starts: cel_parser::trivia::line_start_byte_offsets(text),
44        }
45    }
46}
47
48impl std::ops::Deref for FormattingSource<'_> {
49    /// Exposes the original source text.
50    type Target = str;
51
52    /// Borrows the original source without copying.
53    fn deref(&self) -> &str {
54        self.text
55    }
56}
57
58/// Returns the current output column in Unicode scalar values.
59///
60/// - Complexity: O(k) time in the final line's length; O(1) space.
61fn output_column(out: &str) -> usize {
62    out.rsplit('\n').next().unwrap_or_default().chars().count()
63}
64
65/// Appends one expression using the complete emitted prefix and following punctuation budget.
66///
67/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
68///   using the module's writer cost dimensions for this expression.
69fn write_expression(
70    out: &mut String,
71    source: &FormattingSource<'_>,
72    expr: &cel_parser::Expr,
73    depth: usize,
74    suffix_columns: usize,
75) {
76    let text = cel_parser::format_expr_with_layout(
77        expr,
78        source,
79        depth,
80        output_column(out),
81        100,
82        suffix_columns,
83    );
84    append_expression_layout(out, &text);
85}
86
87/// Appends expression layout, removing prefix padding when layout starts on a new line.
88///
89/// - Complexity: O(n + k) time in expression bytes and the prefix's trailing padding;
90///   O(n) appended output space and O(1) other space.
91fn append_expression_layout(out: &mut String, text: &str) {
92    if text.starts_with('\n') {
93        out.truncate(out.trim_end_matches(' ').len());
94    }
95    out.push_str(text);
96}
97
98/// Reserves punctuation before the next clause, or the declaration's semicolon.
99fn cell_clause_suffix(
100    filter: Option<&ast::CellFilter>,
101    require: Option<&ast::RequireBlock>,
102) -> usize {
103    if filter.is_some() {
104        8
105    } else if require.is_some() {
106        10
107    } else {
108        1
109    }
110}
111
112/// Returns four spaces per nesting level.
113///
114/// - Complexity: O(depth) time and owned space.
115fn indent(depth: usize) -> String {
116    "    ".repeat(depth)
117}
118
119/// Writes `comment`'s text starting at the current write position — its first line gets no
120/// leading indent or prefix of its own, so the caller positions the cursor first (either after
121/// its own `indent(depth)`, for a comment on its own line, or inline after a declaration's own
122/// terminator) — continuing any further lines at `depth`'s indentation, and always ending with a
123/// newline: a `Comment::Line` as one `// ` line per stored line; a `Comment::Block` as
124/// `/* text */` on one line when its text has no internal `\n`, or a multi-line `/*`/`*/`-
125/// delimited block (one line per stored line, indented one level past `depth`) when it does.
126/// Shared by [`write_comment`] and [`write_line_end`].
127///
128/// - Complexity: O(t + b) time for payload bytes t and emitted bytes b, including
129///   per-line indentation; O(b + depth) output and temporary indentation space.
130fn write_comment_inline(out: &mut String, comment: &ast::Comment, depth: usize) {
131    match comment {
132        ast::Comment::Line(text) => {
133            let mut lines = text.split('\n');
134            if let Some(first) = lines.next() {
135                out.push_str("// ");
136                out.push_str(first);
137                out.push('\n');
138            }
139            for line in lines {
140                out.push_str(&indent(depth));
141                out.push_str("// ");
142                out.push_str(line);
143                out.push('\n');
144            }
145        }
146        ast::Comment::Block(text) => {
147            if text.contains('\n') {
148                out.push_str("/*\n");
149                for line in text.split('\n') {
150                    out.push_str(&indent(depth + 1));
151                    out.push_str(line);
152                    out.push('\n');
153                }
154                out.push_str(&indent(depth));
155                out.push_str("*/\n");
156            } else {
157                out.push_str("/* ");
158                out.push_str(text);
159                out.push_str(" */\n");
160            }
161        }
162    }
163}
164
165/// Writes one recovered `Comment` on its own line at `depth`'s indentation, via
166/// [`write_comment_inline`].
167///
168/// - Complexity: O(t + b) time for payload bytes t and emitted bytes b, including
169///   indentation; O(b + depth) output and temporary indentation space.
170fn write_comment(out: &mut String, comment: &ast::Comment, depth: usize) {
171    out.push_str(&indent(depth));
172    write_comment_inline(out, comment, depth);
173}
174
175/// Writes `doc_comment`'s lines (if present) as one `marker` line per stored line, at `depth`'s
176/// indentation. `marker` is `"///"` for a declaration's own doc comment or `"//!"` for the
177/// sheet's own (each stored line already includes the space rustdoc puts after the marker, e.g.
178/// `" the total"` for `/// the total`, so no extra space is inserted here).
179///
180/// - Complexity: O(t + b) time for documentation bytes t and emitted bytes b,
181///   including markers and per-line indentation; O(b + depth) output and temporary space.
182fn write_doc_comment(out: &mut String, marker: &str, doc_comment: Option<&str>, depth: usize) {
183    if let Some(doc) = doc_comment {
184        for line in doc.split('\n') {
185            out.push_str(&indent(depth));
186            out.push_str(marker);
187            out.push_str(line);
188            out.push('\n');
189        }
190    }
191}
192
193/// Emits `blank_line_before`/`leading_comment` ahead of an item, if either is present.
194///
195/// - Complexity: O(t + b) time for comment bytes t and emitted bytes b, including
196///   indentation; O(b + depth) output and temporary space.
197fn write_trivia(
198    out: &mut String,
199    blank_line_before: bool,
200    leading_comment: Option<&ast::Comment>,
201    depth: usize,
202) {
203    if blank_line_before {
204        out.push('\n');
205    }
206    if let Some(comment) = leading_comment {
207        write_comment(out, comment, depth);
208    }
209}
210
211/// Emits a container's trailing comment (honoring `blank_line_before_close`) immediately before
212/// its closing `}` is written, reusing [`write_comment`] so block-vs-line style is preserved
213/// exactly as for leading comments. See <https://github.com/stlab/cel-rs/issues/52>.
214///
215/// - Complexity: O(t + b) time for comment bytes t and emitted bytes b, including
216///   indentation; O(b + depth) output and temporary space.
217fn write_trailing_trivia(
218    out: &mut String,
219    blank_line_before_close: bool,
220    trailing_comment: Option<&ast::Comment>,
221    depth: usize,
222) {
223    if blank_line_before_close {
224        out.push('\n');
225    }
226    if let Some(comment) = trailing_comment {
227        write_comment(out, comment, depth);
228    }
229}
230
231/// Terminates a declaration's own output line with `terminator` (`;` or `}`), appending its
232/// same-line `trailing_line_comment` inline first when present — a `//` comment always ends the
233/// line it's on, and a `/* */` comment stays inline with what precedes it, so both are written
234/// before the newline rather than after it. See <https://github.com/stlab/cel-rs/issues/59>.
235///
236/// - Complexity: O(t + b) time for comment bytes t and emitted bytes b, including
237///   terminator and indentation; O(b + depth) output and temporary space.
238fn write_line_end(
239    out: &mut String,
240    terminator: &str,
241    trailing_line_comment: Option<&ast::Comment>,
242    depth: usize,
243) {
244    out.push_str(terminator);
245    match trailing_line_comment {
246        Some(comment) => {
247            out.push(' ');
248            write_comment_inline(out, comment, depth);
249        }
250        None => out.push('\n'),
251    }
252}
253
254/// Re-emits a literal's exact original text via its span, falling back to an empty string when
255/// none is recoverable — mirrors `cel_parser::fmt`'s identical fallback (see the module doc for
256/// why no `Literal` value is needed here).
257///
258/// - Complexity: O(t) time and owned space for t recovered source bytes.
259fn source_text_or_empty(span: ast::ExprSpan) -> String {
260    span.start.source_text().unwrap_or_default()
261}
262
263/// Borrows a source region, returning `None` for empty source or an unresolvable range.
264///
265/// - Precondition: non-synthetic positions refer to `source`.
266/// - Complexity: O(c) time in the two source-line prefixes' character counts; O(1) space.
267fn source_region<'a>(
268    source: &FormattingSource<'a>,
269    start: proc_macro2::LineColumn,
270    end: proc_macro2::LineColumn,
271) -> Option<&'a str> {
272    use cel_parser::trivia::line_column_to_byte;
273    if source.is_empty() {
274        return None;
275    }
276    let start = line_column_to_byte(source, &source.line_starts, start);
277    let end = line_column_to_byte(source, &source.line_starts, end);
278    source.text.get(start..end)
279}
280
281/// Emits the complete accepted parameter type and its comments, not only its first token.
282///
283/// Synthetic type spans use their stored source text.
284///
285/// - Precondition: spans refer to `source`, or `source` is empty.
286/// - Complexity: O(c + t) time for source-line prefixes and type text; O(t) output space.
287fn write_parameter_type(out: &mut String, source: &FormattingSource<'_>, ty: &ast::TypeExpr) {
288    let span = ty.span();
289    if let Some(text) = source_region(source, span.start.start(), span.end.end()) {
290        out.push_str(text);
291    } else {
292        out.push_str(&source_text_or_empty(span));
293    }
294}
295
296/// Emits the complete documented header prefix, retaining interspersed comments in source order.
297///
298/// Synthetic declarations use their stored documentation instead.
299///
300/// - Precondition: spans refer to `source`, or `source` is empty.
301/// - Complexity: O(c + r + b) time for converted character prefixes c, examined
302///   documentation-region bytes r, and emitted bytes b including indentation;
303///   O(b + depth) output and temporary indentation space.
304fn write_header_documentation(
305    out: &mut String,
306    source: &FormattingSource<'_>,
307    start: proc_macro2::Span,
308    keyword: ast::ExprSpan,
309    prefix: &str,
310    documentation: Option<&str>,
311    depth: usize,
312) {
313    if let Some(documentation) = documentation {
314        if let Some(region) = source_region(source, start.start(), keyword.start.start())
315            .filter(|region| !region.is_empty())
316        {
317            out.push_str(&indent(depth));
318            out.push_str(region.trim_end_matches([' ', '\t']));
319        } else {
320            write_doc_comment(out, prefix, Some(documentation), depth);
321        }
322    }
323}
324
325/// A single-point `ExprSpan` (start and end coincide), for a gap endpoint with no spanned AST
326/// node of its own — e.g. the terminating `;` following a `CellDecl`'s initializer, which has
327/// only `CellDecl.span.end` (the `;` token's own span) available, not a full node.
328fn point_span(span: proc_macro2::Span) -> ast::ExprSpan {
329    ast::ExprSpan {
330        start: span,
331        end: span,
332    }
333}
334
335/// Renders the source gap between two adam-lang declaration positions — any comments it holds,
336/// interleaved *in source order* with `expected`, the fixed literal tokens (e.g. `"="`, `":="`)
337/// the grammar guarantees appear there. Passing the operator through [`cel_parser::trivia::scan_gap`]
338/// (rather than having the caller append it afterward) is what preserves ordering: a comment that
339/// follows the `=` in `cell a: i32 = /* p */ 1;` stays after it, and one on each side of it
340/// (`cell a: i32 /* before */ = /* after */ 1;`) keeps its position relative to the operator.
341///
342/// Spacing mirrors `cel_parser::fmt::emit_gap`'s `Around` convention: an `expected` token is
343/// rendered space-separated, a `Comment::Block` stays inline, and a `Comment::Line` (which runs to
344/// end of line) wraps — ending the gap with a newline plus `indent(depth + 1)` so whatever the
345/// caller appends next starts on a fresh line rather than being swallowed into the comment.
346///
347/// - Postcondition: an *operator* gap (`expected` non-empty) emits every required `expected` token,
348///   even when `source` is empty or the positions don't resolve (a hand-built declaration with
349///   synthetic spans still gets its `=`/`:=`), and ends with a separating space so the caller's
350///   next token (the initializer) is not glued on. A *comment-only* gap (`expected` empty) returns
351///   `""` when it holds no comments, and never adds a trailing space, so the caller appends its own
352///   terminator (`;`) or keyword directly.
353///   Comment-free module paths and parameter/argument lists use compact punctuation.
354///   Closing-list gaps preserve every comment on either side of the closing parenthesis.
355///
356/// `depth` is the caller's own indent depth (the declaration's, not the gap's).
357///
358/// - Complexity: O(c + g + e + b) time and O(g + e + b) space for converted
359///   character prefixes c, scanned gap bytes g, expected-token count plus bytes e,
360///   and emitted bytes b including indentation. The enclosing operation shares line offsets.
361fn emit_decl_gap(
362    source: &FormattingSource<'_>,
363    prev_end: ast::ExprSpan,
364    next_start: ast::ExprSpan,
365    expected: &[&'static str],
366    depth: usize,
367) -> String {
368    use cel_parser::trivia::{GapPiece, line_column_to_byte, scan_gap};
369    // The scanner re-synthesizes required tokens even on an empty or non-resolving gap, so a
370    // hand-built declaration (empty `source`) still renders its operator.
371    let pieces = if source.is_empty() {
372        scan_gap("", expected)
373    } else {
374        let start = line_column_to_byte(source, &source.line_starts, prev_end.end.end());
375        let end = line_column_to_byte(source, &source.line_starts, next_start.start.start());
376        match source.get(start..end) {
377            Some(gap) => scan_gap(gap, expected),
378            None => scan_gap("", expected),
379        }
380    };
381    if pieces
382        .iter()
383        .all(|piece| matches!(piece, GapPiece::Punct(_)))
384    {
385        let compact = match expected {
386            ["("] => Some("("),
387            [","] => Some(", "),
388            [":"] => Some(": "),
389            ["::"] => Some("::"),
390            [")"] => Some(")"),
391            ["(", ")"] => Some("()"),
392            _ => None,
393        };
394        if let Some(compact) = compact {
395            return compact.to_owned();
396        }
397    }
398    let mut out = String::new();
399    for piece in pieces {
400        match piece {
401            GapPiece::Punct(tok) => {
402                if !out.ends_with(' ') {
403                    out.push(' ');
404                }
405                out.push_str(tok);
406                out.push(' ');
407            }
408            GapPiece::Comment(cel_parser::Comment::Block(text)) => {
409                if !out.ends_with(' ') {
410                    out.push(' ');
411                }
412                out.push_str("/* ");
413                out.push_str(&text);
414                out.push_str(" */");
415            }
416            GapPiece::Comment(cel_parser::Comment::Line(text)) => {
417                if !out.ends_with(' ') {
418                    out.push(' ');
419                }
420                out.push_str("// ");
421                out.push_str(&text);
422                out.push('\n');
423                out.push_str(&indent(depth + 1));
424            }
425        }
426    }
427    // An operator gap must end separated from the initializer the caller appends next; a
428    // comment-only gap must not (its terminator/keyword is appended with its own leading space).
429    if !expected.is_empty() && !out.is_empty() && !out.ends_with(' ') {
430        out.push(' ');
431    }
432    out
433}
434
435/// Writes one `a := ...;` / `(a, b) := ...;` binding, delegating its body to
436/// [`cel_parser::format_expr`].
437///
438/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
439///   using the module's writer cost dimensions for the complete binding.
440fn write_binding(
441    out: &mut String,
442    source: &FormattingSource<'_>,
443    binding: &ast::BindingDecl,
444    depth: usize,
445) {
446    write_trivia(
447        out,
448        binding.blank_line_before,
449        binding.leading_comment.as_ref(),
450        depth,
451    );
452    out.push_str(&indent(depth));
453    if binding.destructure {
454        out.push('(');
455        for (i, (name, _)) in binding.outputs.iter().enumerate() {
456            if i > 0 {
457                out.push_str(", ");
458            }
459            out.push_str(name);
460        }
461        if binding.outputs.len() == 1 {
462            out.push(',');
463        }
464        out.push(')');
465    } else {
466        debug_assert_eq!(
467            binding.outputs.len(),
468            1,
469            "a non-destructuring binding names exactly one output"
470        );
471        out.push_str(&binding.outputs[0].0);
472    }
473    // `:=` gap, rendered in-position so a comment on either side of it keeps its order.
474    let op_start = binding
475        .outputs
476        .last()
477        .map(|(_, span)| *span)
478        .unwrap_or(binding.span);
479    out.push_str(&emit_decl_gap(
480        source,
481        op_start,
482        binding.body.span(),
483        &[":="],
484        depth,
485    ));
486    write_expression(out, source, &binding.body, depth, 1);
487    // Comment between the binding's body and its terminating `;`.
488    out.push_str(&emit_decl_gap(
489        source,
490        binding.body.span(),
491        point_span(binding.span.end),
492        &[],
493        depth,
494    ));
495    write_line_end(out, ";", binding.trailing_line_comment.as_ref(), depth);
496}
497
498/// Writes one `relationship { ... }` declaration and its bindings, in declaration order.
499///
500/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
501///   including all binding writers, using the module's writer cost dimensions.
502fn write_relationship(
503    out: &mut String,
504    source: &FormattingSource<'_>,
505    rel: &ast::RelationshipDecl,
506    depth: usize,
507) {
508    write_trivia(
509        out,
510        rel.blank_line_before,
511        rel.leading_comment.as_ref(),
512        depth,
513    );
514    write_doc_comment(out, "///", rel.doc_comment.as_deref(), depth);
515    out.push_str(&indent(depth));
516    out.push_str("relationship");
517    // Comment between the `relationship` keyword and its `{` (the keyword's own span starts at
518    // `rel.span.start`; scan from just past it to the block's `{`).
519    out.push_str(&emit_decl_gap(
520        source,
521        point_span(rel.span.start),
522        rel.open_brace_span,
523        &[],
524        depth,
525    ));
526    out.push_str(" {\n");
527    for binding in &rel.bindings {
528        write_binding(out, source, binding, depth + 1);
529    }
530    write_trailing_trivia(
531        out,
532        rel.blank_line_before_close,
533        rel.trailing_comment.as_ref(),
534        depth + 1,
535    );
536    out.push_str(&indent(depth));
537    write_line_end(out, "}", rel.trailing_line_comment.as_ref(), depth);
538}
539
540/// Writes a `{ ... }` block of relationships, shared by both a named conditional branch and the
541/// default (`_ =>`) arm.
542///
543/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
544///   including every descendant writer, using the module's writer cost dimensions.
545fn write_branch_relationships(
546    out: &mut String,
547    source: &FormattingSource<'_>,
548    relationships: &[ast::BranchItem],
549    trailing_comment: Option<&ast::Comment>,
550    blank_line_before_close: bool,
551    line_trailing_comment: Option<&ast::Comment>,
552    depth: usize,
553) {
554    out.push_str("{\n");
555    for rel in relationships {
556        match rel {
557            ast::BranchItem::Relationship(rel) => write_relationship(out, source, rel, depth + 1),
558            ast::BranchItem::RelationshipUse(call) => {
559                write_relationship_use(out, source, call, depth + 1)
560            }
561        }
562    }
563    write_trailing_trivia(out, blank_line_before_close, trailing_comment, depth + 1);
564    out.push_str(&indent(depth));
565    write_line_end(out, "}", line_trailing_comment, depth);
566}
567
568/// Writes one `literal_pattern => { ... }` conditional branch, re-emitting the match literal via
569/// its span rather than the (unused) `Literal` value, with a leading `-` when
570/// [`ast::ConditionalBranch::negated`].
571///
572/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
573///   including the branch body, using the module's writer cost dimensions.
574fn write_branch(
575    out: &mut String,
576    source: &FormattingSource<'_>,
577    branch: &ast::ConditionalBranch,
578    depth: usize,
579) {
580    write_trivia(
581        out,
582        branch.blank_line_before,
583        branch.leading_comment.as_ref(),
584        depth,
585    );
586    out.push_str(&indent(depth));
587    if branch.negated {
588        out.push('-');
589    }
590    out.push_str(&source_text_or_empty(branch.literal_span));
591    // One gap from the literal through the `=>` to the branch body's `{`, recovering comments on
592    // either side of `=>` in source order.
593    out.push_str(&emit_decl_gap(
594        source,
595        branch.literal_span,
596        branch.open_brace_span,
597        &["=>"],
598        depth,
599    ));
600    write_branch_relationships(
601        out,
602        source,
603        &branch.relationships,
604        branch.trailing_comment.as_ref(),
605        branch.blank_line_before_close,
606        branch.trailing_line_comment.as_ref(),
607        depth,
608    );
609}
610
611/// Writes one `conditional <expr> { ... }` declaration: its branches in declaration
612/// order (dispatching on the match-subject expression), followed by its optional `_ => { ... }` default arm.
613///
614/// Preserves header-boundary comments across omitted groups while reserving the body brace.
615///
616/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
617///   including every branch, using the module's writer cost dimensions.
618fn write_conditional(
619    out: &mut String,
620    source: &FormattingSource<'_>,
621    cond: &ast::ConditionalDecl,
622    depth: usize,
623) {
624    write_trivia(
625        out,
626        cond.blank_line_before,
627        cond.leading_comment.as_ref(),
628        depth,
629    );
630    write_doc_comment(out, "///", cond.doc_comment.as_deref(), depth);
631    out.push_str(&indent(depth));
632    out.push_str("conditional ");
633    let header = cel_parser::format_expr_header_with_layout(
634        &cond.match_expr,
635        source,
636        depth,
637        (cond.span.start.end(), cond.open_brace_span.start.start()),
638        output_column(out),
639        100,
640        2,
641    );
642    append_expression_layout(out, &header);
643    out.push_str(" {\n");
644    for branch in &cond.branches {
645        write_branch(out, source, branch, depth + 1);
646    }
647    if let Some(default) = &cond.default {
648        write_trivia(
649            out,
650            default.blank_line_before,
651            default.leading_comment.as_ref(),
652            depth + 1,
653        );
654        out.push_str(&indent(depth + 1));
655        out.push('_');
656        // One gap from `_` through `=>` to the default body's `{`, recovering comments around `=>`.
657        out.push_str(&emit_decl_gap(
658            source,
659            point_span(default.span.start),
660            default.open_brace_span,
661            &["=>"],
662            depth + 1,
663        ));
664        write_branch_relationships(
665            out,
666            source,
667            &default.relationships,
668            default.trailing_comment.as_ref(),
669            default.blank_line_before_close,
670            default.trailing_line_comment.as_ref(),
671            depth + 1,
672        );
673    }
674    write_trailing_trivia(
675        out,
676        cond.blank_line_before_close,
677        cond.trailing_comment.as_ref(),
678        depth + 1,
679    );
680    out.push_str(&indent(depth));
681    write_line_end(out, "}", cond.trailing_line_comment.as_ref(), depth);
682}
683
684/// Writes one `cell name[: type][ = initializer][ filter body][ require { ... }];` declaration,
685/// delegating its type annotation to [`source_text_or_empty`] via `TypeExpr::span()` and its
686/// initializer/filter body to [`cel_parser::format_expr`].
687///
688/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
689///   including clauses and requirements, using the module's writer cost dimensions.
690fn write_cell(out: &mut String, source: &FormattingSource<'_>, cell: &ast::CellDecl, depth: usize) {
691    write_trivia(
692        out,
693        cell.blank_line_before,
694        cell.leading_comment.as_ref(),
695        depth,
696    );
697    write_doc_comment(out, "///", cell.doc_comment.as_deref(), depth);
698    out.push_str(&indent(depth));
699    out.push_str("cell ");
700    out.push_str(&cell.name);
701    if let Some(type_expr) = &cell.type_name {
702        out.push_str(": ");
703        out.push_str(&source_text_or_empty(type_expr.span()));
704    }
705    // `=` gap, rendered in-position so comments keep their order relative to the operator.
706    if let Some(expr) = &cell.initializer {
707        let op_start = cell
708            .type_name
709            .as_ref()
710            .map(ast::TypeExpr::span)
711            .unwrap_or(cell.name_span);
712        out.push_str(&emit_decl_gap(source, op_start, expr.span(), &["="], depth));
713        write_expression(
714            out,
715            source,
716            expr,
717            depth,
718            cell_clause_suffix(cell.filter.as_ref(), cell.require.as_ref()),
719        );
720    }
721    write_cell_clauses(
722        out,
723        source,
724        depth,
725        cell.initializer.as_ref().map(cel_parser::Expr::span),
726        cell.type_name.as_ref().map(ast::TypeExpr::span),
727        cell.name_span,
728        cell.filter.as_ref(),
729        cell.require.as_ref(),
730        cell.span.end,
731    );
732    write_line_end(out, ";", cell.trailing_line_comment.as_ref(), depth);
733}
734
735/// Writes a declaration's optional `filter`/`require` clauses and recovers any comment in each gap
736/// between adjacent segments (initializer → `filter` → `require` → `;`), shared by [`write_cell`],
737/// [`write_source`], and [`write_out`]. `prev_*` give the end span of whichever of
738/// initializer/type/name precedes the clauses; `terminator` is the declaration's own `;` span.
739///
740/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
741///   including all requirements, using the module's writer cost dimensions.
742#[allow(clippy::too_many_arguments)]
743fn write_cell_clauses(
744    out: &mut String,
745    source: &FormattingSource<'_>,
746    depth: usize,
747    initializer_span: Option<ast::ExprSpan>,
748    type_span: Option<ast::ExprSpan>,
749    name_span: ast::ExprSpan,
750    filter: Option<&ast::CellFilter>,
751    require: Option<&ast::RequireBlock>,
752    terminator: proc_macro2::Span,
753) {
754    let mut prev = initializer_span.or(type_span).unwrap_or(name_span);
755    if let Some(filter) = filter {
756        // One gap from the previous segment through the `filter` keyword to the filter body,
757        // recovering comments on either side of `filter` in source order.
758        out.push_str(&emit_decl_gap(
759            source,
760            prev,
761            filter.body.span(),
762            &["filter"],
763            depth,
764        ));
765        write_expression(
766            out,
767            source,
768            &filter.body,
769            depth,
770            if require.is_some() { 10 } else { 1 },
771        );
772        prev = filter.body.span();
773    }
774    if let Some(require) = require {
775        // One gap from the previous segment through the `require` keyword to the block's `{`,
776        // recovering comments on either side of `require` (the keyword itself has no stored span,
777        // but it lies inside this gap).
778        out.push_str(&emit_decl_gap(
779            source,
780            prev,
781            require.open_brace_span,
782            &["require"],
783            depth,
784        ));
785        write_require_block(out, source, require, depth);
786        prev = require.span;
787    }
788    // Comment between the last segment and the terminating `;`.
789    out.push_str(&emit_decl_gap(
790        source,
791        prev,
792        point_span(terminator),
793        &[],
794        depth,
795    ));
796}
797
798/// Writes one `[ "@" identifier " " ] ...;` requirement.
799///
800/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
801///   using the module's writer cost dimensions for the requirement and its expression.
802fn write_requirement(
803    out: &mut String,
804    source: &FormattingSource<'_>,
805    req: &ast::RequirementDecl,
806    depth: usize,
807) {
808    write_trivia(
809        out,
810        req.blank_line_before,
811        req.leading_comment.as_ref(),
812        depth,
813    );
814    out.push_str(&indent(depth));
815    if let Some(name) = &req.name {
816        out.push('@');
817        out.push_str(name);
818        // Recover any comment between the `@label` and the body (e.g. `@c /* keep */ w <= 10`).
819        if let Some(name_span) = req.name_span {
820            out.push_str(&emit_decl_gap(
821                source,
822                name_span,
823                req.body.span(),
824                &[],
825                depth,
826            ));
827        }
828        out.push(' ');
829    }
830    write_expression(out, source, &req.body, depth, 1);
831    write_line_end(out, ";", req.trailing_line_comment.as_ref(), depth);
832}
833
834/// Writes one `require` block's `{ ... }` body (no `require` keyword, no trailing `;`) — the
835/// caller emits the `require` keyword (and any comments around it) via [`emit_decl_gap`] so a
836/// comment between `require` and its `{` is preserved. Shared by [`write_cell`], [`write_source`],
837/// and [`write_out`].
838///
839/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
840///   including every requirement, using the module's writer cost dimensions.
841fn write_require_block(
842    out: &mut String,
843    source: &FormattingSource<'_>,
844    require: &ast::RequireBlock,
845    depth: usize,
846) {
847    out.push_str("{\n");
848    for req in &require.requirements {
849        write_requirement(out, source, req, depth + 1);
850    }
851    write_trailing_trivia(
852        out,
853        require.blank_line_before_close,
854        require.trailing_comment.as_ref(),
855        depth + 1,
856    );
857    out.push_str(&indent(depth));
858    out.push('}');
859}
860
861/// Writes one `out name[: type] := ...[ filter body][ require { ... } ];` declaration.
862///
863/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
864///   including clauses and requirements, using the module's writer cost dimensions.
865fn write_out(out: &mut String, source: &FormattingSource<'_>, decl: &ast::OutDecl, depth: usize) {
866    write_trivia(
867        out,
868        decl.blank_line_before,
869        decl.leading_comment.as_ref(),
870        depth,
871    );
872    write_doc_comment(out, "///", decl.doc_comment.as_deref(), depth);
873    out.push_str(&indent(depth));
874    out.push_str("out ");
875    out.push_str(&decl.name);
876    if let Some(type_expr) = &decl.type_name {
877        out.push_str(": ");
878        out.push_str(&source_text_or_empty(type_expr.span()));
879    }
880    // `:=` gap, rendered in-position so comments keep their order relative to the operator.
881    let op_start = decl
882        .type_name
883        .as_ref()
884        .map(ast::TypeExpr::span)
885        .unwrap_or(decl.name_span);
886    out.push_str(&emit_decl_gap(
887        source,
888        op_start,
889        decl.initializer.span(),
890        &[":="],
891        depth,
892    ));
893    write_expression(
894        out,
895        source,
896        &decl.initializer,
897        depth,
898        cell_clause_suffix(decl.filter.as_ref(), decl.require.as_ref()),
899    );
900    write_cell_clauses(
901        out,
902        source,
903        depth,
904        Some(decl.initializer.span()),
905        decl.type_name.as_ref().map(ast::TypeExpr::span),
906        decl.name_span,
907        decl.filter.as_ref(),
908        decl.require.as_ref(),
909        decl.span.end,
910    );
911    write_line_end(out, ";", decl.trailing_line_comment.as_ref(), depth);
912}
913
914/// Writes one `source name[: type][ = initializer][ filter body][ require { ... }];` declaration.
915/// Mirrors [`write_cell`] exactly.
916///
917/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
918///   including clauses and requirements, using the module's writer cost dimensions.
919fn write_source(
920    out: &mut String,
921    source: &FormattingSource<'_>,
922    decl: &ast::SourceDecl,
923    depth: usize,
924) {
925    write_trivia(
926        out,
927        decl.blank_line_before,
928        decl.leading_comment.as_ref(),
929        depth,
930    );
931    write_doc_comment(out, "///", decl.doc_comment.as_deref(), depth);
932    out.push_str(&indent(depth));
933    out.push_str("source ");
934    out.push_str(&decl.name);
935    if let Some(type_expr) = &decl.type_name {
936        out.push_str(": ");
937        out.push_str(&source_text_or_empty(type_expr.span()));
938    }
939    // `=` gap, rendered in-position so comments keep their order relative to the operator.
940    if let Some(expr) = &decl.initializer {
941        let op_start = decl
942            .type_name
943            .as_ref()
944            .map(ast::TypeExpr::span)
945            .unwrap_or(decl.name_span);
946        out.push_str(&emit_decl_gap(source, op_start, expr.span(), &["="], depth));
947        write_expression(
948            out,
949            source,
950            expr,
951            depth,
952            cell_clause_suffix(decl.filter.as_ref(), decl.require.as_ref()),
953        );
954    }
955    write_cell_clauses(
956        out,
957        source,
958        depth,
959        decl.initializer.as_ref().map(cel_parser::Expr::span),
960        decl.type_name.as_ref().map(ast::TypeExpr::span),
961        decl.name_span,
962        decl.filter.as_ref(),
963        decl.require.as_ref(),
964        decl.span.end,
965    );
966    write_line_end(out, ";", decl.trailing_line_comment.as_ref(), depth);
967}
968
969/// Dispatches to the writer for one top-level sheet item.
970///
971/// - Precondition: `item` is not `SheetItem::Error` — [`format_sheet`]'s own precondition
972///   (`sheet.errors.is_empty()`) guarantees no `Error` item ever reaches this function.
973/// - Complexity: the selected writer's complete subtree time and space, using the
974///   module's writer cost dimensions.
975fn write_sheet_item(
976    out: &mut String,
977    source: &FormattingSource<'_>,
978    item: &ast::SheetItem,
979    depth: usize,
980) {
981    match item {
982        ast::SheetItem::Cell(cell) => write_cell(out, source, cell, depth),
983        ast::SheetItem::Relationship(rel) => write_relationship(out, source, rel, depth),
984        ast::SheetItem::RelationshipUse(call) => write_relationship_use(out, source, call, depth),
985        ast::SheetItem::Conditional(cond) => write_conditional(out, source, cond, depth),
986        ast::SheetItem::Out(out_decl) => write_out(out, source, out_decl, depth),
987        ast::SheetItem::Source(decl) => write_source(out, source, decl, depth),
988        ast::SheetItem::Error { .. } => {
989            unreachable!("format_sheet is only called on a sheet with no recorded syntax errors")
990        }
991    }
992}
993
994/// Pretty-prints `sheet` back to adam-lang source text — see the module doc for the printing
995/// rules.
996///
997/// `source` is the text `sheet` was parsed from (or `""` for a programmatically built `Sheet`);
998/// it's forwarded to [`cel_parser::format_expr`] for every embedded expression.
999///
1000/// - Precondition: `sheet` has no recorded syntax errors (`sheet.errors.is_empty()`) — a sheet
1001///   with a `SheetItem::Error` placeholder cannot be printed back to valid source.
1002/// - Complexity: O(n + v + b + g + c + p) time plus delegated CEL layout, for source
1003///   bytes n, visited AST nodes v, emitted bytes b including indentation, summed
1004///   scanned region bytes g, converted character prefixes c, and measured output-line
1005///   prefixes p. O(l + b + g + h) space plus CEL layout storage, for source lines l
1006///   and writer call depth h. Repeated prefix/region work is not globally linear.
1007///
1008/// # Examples
1009///
1010/// ```
1011/// use adam_lang::{AdamAstParser, attach_trivia, format_sheet};
1012///
1013/// let source = "sheet s { cell x: i32 = 1; }";
1014/// let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1015/// attach_trivia(source, &mut sheet);
1016/// assert_eq!(format_sheet(&sheet, source), "sheet s {\n    cell x: i32 = 1;\n}\n");
1017/// ```
1018pub fn format_sheet(sheet: &ast::Sheet, source: &str) -> String {
1019    format_sheet_indexed(sheet, &FormattingSource::new(source))
1020}
1021
1022/// Formats one sheet using the enclosing operation's shared source index.
1023///
1024/// - Precondition: `sheet` contains no recorded syntax errors.
1025/// - Complexity: O(v + b + g + c + p + T) time and O(b + g + h + A) space,
1026///   including every item, using the module's writer cost dimensions. No index is rebuilt.
1027fn format_sheet_indexed(sheet: &ast::Sheet, source: &FormattingSource<'_>) -> String {
1028    debug_assert!(
1029        sheet.errors.is_empty(),
1030        "format_sheet's precondition: no recorded syntax errors"
1031    );
1032    let mut out = String::new();
1033    write_trivia(&mut out, false, sheet.leading_comment.as_ref(), 0);
1034    write_header_documentation(
1035        &mut out,
1036        source,
1037        sheet.span.start,
1038        sheet.keyword_span,
1039        "//!",
1040        sheet.doc_comment.as_deref(),
1041        0,
1042    );
1043    out.push_str("sheet");
1044    out.push_str(&emit_decl_gap(
1045        source,
1046        sheet.keyword_span,
1047        sheet.name_span,
1048        &[],
1049        0,
1050    ));
1051    out.push(' ');
1052    out.push_str(&sheet.name);
1053    let mut previous = sheet.name_span;
1054    if let Some(parameters) = &sheet.parameters {
1055        for (index, parameter) in parameters.iter().enumerate() {
1056            out.push_str(&emit_decl_gap(
1057                source,
1058                previous,
1059                parameter.name_span,
1060                if index == 0 { &["("] } else { &[","] },
1061                0,
1062            ));
1063            out.push_str(&parameter.name);
1064            out.push_str(&emit_decl_gap(
1065                source,
1066                parameter.name_span,
1067                parameter.type_name.span(),
1068                &[":"],
1069                0,
1070            ));
1071            write_parameter_type(&mut out, source, &parameter.type_name);
1072            previous = parameter.type_name.span();
1073        }
1074        out.push_str(&emit_decl_gap(
1075            source,
1076            previous,
1077            sheet.open_brace_span,
1078            if parameters.is_empty() {
1079                &["(", ")"]
1080            } else {
1081                &[")"]
1082            },
1083            0,
1084        ));
1085    } else {
1086        out.push_str(&emit_decl_gap(
1087            source,
1088            previous,
1089            sheet.open_brace_span,
1090            &[],
1091            0,
1092        ));
1093    }
1094    if !out.ends_with(' ') {
1095        out.push(' ');
1096    }
1097    out.push_str("{\n");
1098    for item in &sheet.items {
1099        write_sheet_item(&mut out, source, item, 1);
1100    }
1101    write_trailing_trivia(
1102        &mut out,
1103        sheet.blank_line_before_close,
1104        sheet.trailing_comment.as_ref(),
1105        1,
1106    );
1107    out.push_str("}\n");
1108    out
1109}
1110
1111/// Error returned by [`format_source`] when `source` can't be safely formatted.
1112///
1113/// # Examples
1114///
1115/// ```
1116/// use adam_lang::{FormatSourceError, format_source};
1117/// assert!(matches!(format_source("invalid"), Err(FormatSourceError::Parse(_))));
1118/// ```
1119#[derive(Debug)]
1120pub enum FormatSourceError {
1121    /// `source` failed to parse at all.
1122    Parse(cel_parser::ParseError),
1123    /// `source` parsed but recovered one or more syntax errors inside individual sheet items.
1124    Recovered(Vec<cel_parser::ParseError>),
1125}
1126
1127impl std::fmt::Display for FormatSourceError {
1128    /// Writes parse diagnostics in order, separating recovered errors with newlines.
1129    ///
1130    /// - Complexity: O(b) time and formatter output space for b diagnostic bytes.
1131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1132        match self {
1133            FormatSourceError::Parse(error) => write!(f, "{error}"),
1134            FormatSourceError::Recovered(errors) => {
1135                for (i, error) in errors.iter().enumerate() {
1136                    if i > 0 {
1137                        writeln!(f)?;
1138                    }
1139                    write!(f, "{error}")?;
1140                }
1141                Ok(())
1142            }
1143        }
1144    }
1145}
1146
1147impl std::error::Error for FormatSourceError {}
1148
1149/// Parses and formats `source` as a complete adam-lang module.
1150///
1151/// Mixed or detached comment runs in ordinary AST attachments normalize to line
1152/// comments while retaining payload order; see [`crate::attach_trivia`].
1153///
1154/// - Complexity: parsing plus [`crate::attach_module_trivia`] and [`format_module`]
1155///   time, including their repeated character-prefix and region scans and delegated
1156///   CEL layout. Space holds the parsed AST, recovered trivia, line indexes, output,
1157///   writer stack, and peak CEL layout temporaries. Sharing indexes does not make
1158///   the whole pipeline linear in source length.
1159///
1160/// # Errors
1161///
1162/// Returns [`FormatSourceError::Recovered`] for recovered syntax errors. Tokenization failures,
1163/// empty input, and input with no recognizable module declaration return
1164/// [`FormatSourceError::Parse`]. Refuses to format code it cannot fully understand.
1165///
1166/// # Examples
1167///
1168/// ```
1169/// use adam_lang::format_source;
1170///
1171/// assert_eq!(
1172///     format_source("sheet   s{cell x:i32=1;}").unwrap(),
1173///     "sheet s {\n    cell x: i32 = 1;\n}\n"
1174/// );
1175/// assert!(format_source("not a sheet at all").is_err());
1176/// ```
1177pub fn format_source(source: &str) -> Result<String, FormatSourceError> {
1178    let mut module = crate::AdamAstParser::new()
1179        .parse_module_str(source)
1180        .map_err(FormatSourceError::Parse)?;
1181    if module.items.is_empty() {
1182        return Err(FormatSourceError::Parse(cel_parser::ParseError::new(
1183            "expected `use` or `sheet`",
1184            proc_macro2::Span::call_site(),
1185        )));
1186    }
1187    if module
1188        .items
1189        .iter()
1190        .all(|item| matches!(item, ast::ModuleItem::Error(_)))
1191    {
1192        return Err(FormatSourceError::Parse(module.errors.remove(0)));
1193    }
1194    crate::attach_module_trivia(source, &mut module);
1195    format_module(&module, source)
1196}
1197
1198/// Writes an unresolved path, preserving comments between its segments.
1199///
1200/// - Complexity: O(v + b + g + c) time and O(b + g) space for path segments v,
1201///   emitted bytes b including indentation, scanned gap bytes g, and converted prefixes c.
1202fn write_path(
1203    out: &mut String,
1204    source: &FormattingSource<'_>,
1205    path: &[(String, ast::ExprSpan)],
1206    depth: usize,
1207) {
1208    for (index, (name, span)) in path.iter().enumerate() {
1209        if index > 0 {
1210            out.push_str(&emit_decl_gap(
1211                source,
1212                path[index - 1].1,
1213                *span,
1214                &["::"],
1215                depth,
1216            ));
1217        }
1218        out.push_str(name);
1219    }
1220}
1221
1222/// Writes a named component use through the existing declaration-gap and trivia writers.
1223///
1224/// - Complexity: O(v + b + g + c) time and O(b + g + h) space for arguments/path
1225///   segments v, emitted bytes b including indentation, scanned regions g, converted
1226///   prefixes c, and writer call depth h.
1227fn write_relationship_use(
1228    out: &mut String,
1229    source: &FormattingSource<'_>,
1230    call: &ast::RelationshipUse,
1231    depth: usize,
1232) {
1233    for comment in &call.opening_comments {
1234        write_trivia(out, false, Some(comment), depth);
1235    }
1236    write_trivia(
1237        out,
1238        call.blank_line_before,
1239        call.leading_comment.as_ref(),
1240        depth,
1241    );
1242    write_header_documentation(
1243        out,
1244        source,
1245        call.span.start,
1246        call.keyword_span,
1247        "///",
1248        call.doc_comment.as_deref(),
1249        depth,
1250    );
1251    out.push_str(&indent(depth));
1252    out.push_str("relationship");
1253    out.push_str(&emit_decl_gap(
1254        source,
1255        call.keyword_span,
1256        call.path[0].1,
1257        &[],
1258        depth,
1259    ));
1260    out.push(' ');
1261    write_path(out, source, &call.path, depth);
1262    let mut previous = call.path.last().expect("parsed paths are nonempty").1;
1263    for (index, argument) in call.arguments.iter().enumerate() {
1264        out.push_str(&emit_decl_gap(
1265            source,
1266            previous,
1267            argument.name_span,
1268            if index == 0 { &["("] } else { &[","] },
1269            depth,
1270        ));
1271        out.push_str(&argument.name);
1272        out.push_str(&emit_decl_gap(
1273            source,
1274            argument.name_span,
1275            argument.cell_span,
1276            &[":"],
1277            depth,
1278        ));
1279        out.push_str(&argument.cell);
1280        previous = argument.cell_span;
1281    }
1282    out.push_str(&emit_decl_gap(
1283        source,
1284        previous,
1285        point_span(call.span.end),
1286        if call.arguments.is_empty() {
1287            &["(", ")"]
1288        } else {
1289            &[")"]
1290        },
1291        depth,
1292    ));
1293    write_line_end(out, ";", call.trailing_line_comment.as_ref(), depth);
1294}
1295
1296/// Formats every module item in order, preserving attached file-boundary comments.
1297///
1298/// - Precondition: spans and attached trivia refer to `source`, or use synthetic spans with `""`.
1299/// - Complexity: O(n + v + b + g + c + p) time plus delegated CEL layout, for source
1300///   bytes n, visited AST nodes v, emitted bytes b including indentation, summed
1301///   scanned region bytes g, converted character prefixes c, and measured output-line
1302///   prefixes p. O(l + b + g + h) space plus CEL layout storage, for source lines l
1303///   and writer call depth h. Repeated prefix/region work is not globally linear.
1304///   Rejection also copies the recorded diagnostics in time and space proportional
1305///   to their count and owned text.
1306///
1307/// # Errors
1308///
1309/// Returns `Recovered` for any recorded syntax error or recovery placeholder.
1310///
1311/// # Examples
1312///
1313/// ```
1314/// let source = "sheet helper() {} sheet root {}";
1315/// let mut module = adam_lang::AdamAstParser::new().parse_module_str(source).unwrap();
1316/// adam_lang::attach_module_trivia(source, &mut module);
1317/// let formatted = adam_lang::format_module(&module, source).unwrap();
1318/// assert_eq!(adam_lang::format_source(&formatted).unwrap(), formatted);
1319/// ```
1320pub fn format_module(module: &ast::Module, source: &str) -> Result<String, FormatSourceError> {
1321    if !module.errors.is_empty() {
1322        return Err(FormatSourceError::Recovered(module.errors.clone()));
1323    }
1324    let source = &FormattingSource::new(source);
1325    let mut out = String::new();
1326    for (index, item) in module.items.iter().enumerate() {
1327        if let Some(comments) = module.comments.get(index) {
1328            for comment in comments {
1329                write_trivia(&mut out, false, Some(comment), 0);
1330            }
1331        }
1332        match item {
1333            ast::ModuleItem::Sheet(sheet)
1334                if sheet
1335                    .items
1336                    .iter()
1337                    .any(|item| matches!(item, ast::SheetItem::Error { .. })) =>
1338            {
1339                return Err(FormatSourceError::Recovered(vec![
1340                    cel_parser::ParseError::new("recovered sheet item", sheet.span.start),
1341                ]));
1342            }
1343            ast::ModuleItem::Sheet(sheet) if sheet.errors.is_empty() => {
1344                out.push_str(&format_sheet_indexed(sheet, source))
1345            }
1346            ast::ModuleItem::Use(import) => {
1347                write_header_documentation(
1348                    &mut out,
1349                    source,
1350                    import.span.start,
1351                    import.keyword_span,
1352                    "//!",
1353                    import.doc_comment.as_deref(),
1354                    0,
1355                );
1356                out.push_str("use");
1357                out.push_str(&emit_decl_gap(
1358                    source,
1359                    import.keyword_span,
1360                    import.path[0].1,
1361                    &[],
1362                    0,
1363                ));
1364                out.push(' ');
1365                write_path(&mut out, source, &import.path, 0);
1366                let mut previous = import.path.last().expect("parsed paths are nonempty").1;
1367                if let Some((alias, span)) = &import.alias {
1368                    out.push_str(&emit_decl_gap(source, previous, *span, &["as"], 0));
1369                    out.push_str(alias);
1370                    previous = *span;
1371                }
1372                out.push_str(&emit_decl_gap(
1373                    source,
1374                    previous,
1375                    point_span(import.span.end),
1376                    &[],
1377                    0,
1378                ));
1379                out.push_str(";\n");
1380            }
1381            ast::ModuleItem::Sheet(sheet) => {
1382                return Err(FormatSourceError::Recovered(sheet.errors.clone()));
1383            }
1384            ast::ModuleItem::Error(span) => {
1385                return Err(FormatSourceError::Recovered(vec![
1386                    cel_parser::ParseError::new("recovered module item", span.start),
1387                ]));
1388            }
1389        }
1390    }
1391    if let Some(comments) = module.comments.get(module.items.len()) {
1392        for comment in comments {
1393            write_trivia(&mut out, false, Some(comment), 0);
1394        }
1395    }
1396    Ok(out)
1397}
1398
1399#[cfg(test)]
1400mod tests {
1401    /// Measures the active line, counting Unicode scalars rather than UTF-8 bytes.
1402    #[test]
1403    fn output_column_tracks_the_active_line() {
1404        for (text, expected) in [
1405            ("", 0),
1406            ("cell é = ", 9),
1407            ("old prefix\n    := ", 7),
1408            ("line\n", 0),
1409        ] {
1410            assert_eq!(super::output_column(text), expected);
1411        }
1412    }
1413
1414    /// Verifies grouped restricted subjects retain boundary comments exactly once and in order.
1415    #[test]
1416    fn grouped_conditional_headers_preserve_comments() {
1417        for header in [
1418            "(Sample { value: 1.0 }) /* first */",
1419            "/* first */ (Sample { value: 1.0 })",
1420            "(/* first */ Sample { value: 1.0 } /* second */) /* third */",
1421            "(/* first */ (/* second */ Sample { value: 1.0 }) /* third */) /* fourth */",
1422            "(Sample { value: 1.0 }) /* first /* nested ) */ end */",
1423            "(/* first /* nested ( */ end */ Sample { value: 1.0 }) /* second */",
1424            "((Sample { value: 1.0 }) /* first */) /* second */",
1425            "(// first\n Sample { value: 1.0 } // second\n) // third\n",
1426            "// first\n (/* second */ Sample { value: 1.0 }) // third\n",
1427            "(/* first */ p) /* second */",
1428            "(/* first */ (Sample { value: 1.0 }).value) /* second */",
1429        ] {
1430            let source = format!("sheet s {{ conditional {header} {{ _ => {{}} }} }}");
1431            let formatted = super::format_source(&source).unwrap();
1432            let mut previous = 0;
1433            for label in ["first", "second", "third", "fourth"] {
1434                if source.contains(label) {
1435                    assert_eq!(
1436                        formatted.matches(label).count(),
1437                        1,
1438                        "{source} -> {formatted}"
1439                    );
1440                    let position = formatted.find(label).unwrap();
1441                    assert!(position >= previous, "{formatted}");
1442                    previous = position;
1443                }
1444            }
1445            assert_eq!(super::format_source(&formatted).unwrap(), formatted);
1446        }
1447    }
1448
1449    use super::*;
1450    use crate::AdamAstParser;
1451
1452    /// Formats a valid single-sheet fixture after recovering its trivia.
1453    ///
1454    /// - Precondition: `source` parses as a sheet.
1455    /// - Complexity: follows parsing, trivia recovery, and [`format_sheet`].
1456    fn format(source: &str) -> String {
1457        let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1458        crate::attach_trivia(source, &mut sheet);
1459        format_sheet(&sheet, source)
1460    }
1461
1462    /// Displays a parse failure or every recovered diagnostic in source order.
1463    #[test]
1464    fn source_error_display_preserves_diagnostics_and_order() {
1465        let first = cel_parser::ParseError::new("first diagnostic", proc_macro2::Span::call_site());
1466        let second =
1467            cel_parser::ParseError::new("second diagnostic", proc_macro2::Span::call_site());
1468        assert_eq!(
1469            FormatSourceError::Parse(first.clone()).to_string(),
1470            first.to_string()
1471        );
1472        assert_eq!(
1473            FormatSourceError::Recovered(vec![first.clone(), second.clone()]).to_string(),
1474            format!("{first}\n{second}")
1475        );
1476    }
1477
1478    /// Emits canonical braces and indentation for an empty sheet.
1479    #[test]
1480    fn formats_an_empty_sheet() {
1481        assert_eq!(format("sheet s {}"), "sheet s {\n}\n");
1482    }
1483
1484    /// Preserves a file-header comment before the sheet.
1485    #[test]
1486    fn preserves_a_leading_comment_before_the_sheet_itself() {
1487        assert_eq!(
1488            format("// file header\nsheet s { cell x: i32 = 1; }"),
1489            "// file header\nsheet s {\n    cell x: i32 = 1;\n}\n"
1490        );
1491    }
1492
1493    /// Omits header trivia when the source has none.
1494    #[test]
1495    fn no_leading_comment_before_the_sheet_emits_nothing_extra() {
1496        assert_eq!(
1497            format("sheet s { cell x: i32 = 1; }"),
1498            "sheet s {\n    cell x: i32 = 1;\n}\n"
1499        );
1500    }
1501
1502    /// Formats a cell's explicit type and initializer.
1503    #[test]
1504    fn formats_a_cell_with_type_and_initializer() {
1505        assert_eq!(
1506            format("sheet s { cell width: f64 = 1920.0; }"),
1507            "sheet s {\n    cell width: f64 = 1920.0;\n}\n"
1508        );
1509    }
1510
1511    /// Formats source declaration syntax.
1512    #[test]
1513    fn formats_a_source_decl() {
1514        assert_eq!(
1515            format("sheet s { source width: i32 = 4; }"),
1516            "sheet s {\n    source width: i32 = 4;\n}\n"
1517        );
1518    }
1519
1520    /// Formats an annotation-only cell.
1521    #[test]
1522    fn formats_a_cell_with_only_a_type_annotation() {
1523        assert_eq!(
1524            format("sheet s { cell area: f64; }"),
1525            "sheet s {\n    cell area: f64;\n}\n"
1526        );
1527    }
1528
1529    /// Formats an initializer-only cell.
1530    #[test]
1531    fn formats_a_cell_with_only_an_initializer() {
1532        assert_eq!(
1533            format("sheet s { cell mode = 0i32; }"),
1534            "sheet s {\n    cell mode = 0i32;\n}\n"
1535        );
1536    }
1537
1538    /// Preserves intentional declaration grouping without adding blank lines.
1539    #[test]
1540    fn packed_cells_stay_packed_and_a_blank_line_before_a_relationship_is_preserved() {
1541        let source = "sheet s {\n    cell a: i32 = 1;\n    cell b: i32 = 2;\n\n    relationship { b := a; }\n}";
1542        let expected = "sheet s {\n    cell a: i32 = 1;\n    cell b: i32 = 2;\n\n    relationship {\n        b := a;\n    }\n}\n";
1543        assert_eq!(format(source), expected);
1544    }
1545
1546    /// Collapses consecutive blank lines to one separator.
1547    #[test]
1548    fn a_run_of_blank_lines_collapses_to_one() {
1549        let source = "sheet s {\n    cell a: i32 = 1;\n\n\n\n    cell b: i32 = 2;\n}";
1550        let expected = "sheet s {\n    cell a: i32 = 1;\n\n    cell b: i32 = 2;\n}\n";
1551        assert_eq!(format(source), expected);
1552    }
1553
1554    /// Formats every binding in a relationship.
1555    #[test]
1556    fn formats_a_relationship_with_multiple_bindings() {
1557        let source = "sheet s {\n    relationship {\n        area := width * height;\n        width := area / height;\n    }\n}";
1558        let expected = "sheet s {\n    relationship {\n        area := width * height;\n        width := area / height;\n    }\n}\n";
1559        assert_eq!(format(source), expected);
1560    }
1561
1562    /// Retains parentheses around a multi-output destructure.
1563    #[test]
1564    fn formats_a_multi_output_destructuring_binding_with_parens() {
1565        let source =
1566            "sheet s {\n    relationship {\n        (sum, diff) := (a + b, a - b);\n    }\n}";
1567        let expected =
1568            "sheet s {\n    relationship {\n        (sum, diff) := (a + b, a - b);\n    }\n}\n";
1569        assert_eq!(format(source), expected);
1570    }
1571
1572    /// Retains the required comma of a singleton tuple destructure.
1573    #[test]
1574    fn formats_a_single_element_tuple_destructuring_binding_with_a_trailing_comma() {
1575        let source = "sheet s {\n    relationship {\n        (x,) := (a,);\n    }\n}";
1576        let expected = "sheet s {\n    relationship {\n        (x,) := (a,);\n    }\n}\n";
1577        assert_eq!(format(source), expected);
1578    }
1579
1580    /// Refuses to format parenthesized binding targets without the required first comma.
1581    #[test]
1582    fn rejects_a_parenthesized_binding_target_without_a_comma() {
1583        let source = "sheet s {\n    relationship {\n        (x) := a;\n    }\n}";
1584        assert!(format_source(source).is_err());
1585    }
1586
1587    /// Preserves a leading comment on a nested binding.
1588    #[test]
1589    fn preserves_a_comment_on_a_nested_binding() {
1590        let source = "sheet s {\n    relationship {\n        b := a;\n\n        // second\n        a := b;\n    }\n}";
1591        let expected = "sheet s {\n    relationship {\n        b := a;\n\n        // second\n        a := b;\n    }\n}\n";
1592        assert_eq!(format(source), expected);
1593    }
1594
1595    /// Formats named and default branches without optional commas.
1596    #[test]
1597    fn formats_a_conditional_with_branches_and_a_default_and_no_trailing_commas() {
1598        let source = "sheet s {\n    conditional p {\n        0i32 => { relationship { b := a; } }\n        _ => { relationship { a := b; } }\n    }\n}";
1599        let expected = "sheet s {\n    conditional p {\n        0i32 => {\n            relationship {\n                b := a;\n            }\n        }\n        _ => {\n            relationship {\n                a := b;\n            }\n        }\n    }\n}\n";
1600        assert_eq!(format(source), expected);
1601    }
1602
1603    /// Retains negation on a conditional branch literal.
1604    #[test]
1605    fn formats_a_conditional_branch_with_a_negated_literal_key() {
1606        let source = "sheet s {\n    conditional p {\n        -1i32 => { relationship { b := a; } }\n        _ => { relationship { a := b; } }\n    }\n}";
1607        let expected = "sheet s {\n    conditional p {\n        -1i32 => {\n            relationship {\n                b := a;\n            }\n        }\n        _ => {\n            relationship {\n                a := b;\n            }\n        }\n    }\n}\n";
1608        assert_eq!(format(source), expected);
1609    }
1610
1611    /// Formats an expression-valued conditional subject.
1612    #[test]
1613    fn formats_a_conditional_with_an_expression_match_subject() {
1614        let source = "sheet s {\n    conditional a && b {\n        _ => { relationship { d := c; } }\n    }\n}";
1615        let expected = "sheet s {\n    conditional a && b {\n        _ => {\n            relationship {\n                d := c;\n            }\n        }\n    }\n}\n";
1616        assert_eq!(format(source), expected);
1617    }
1618
1619    /// Preserves a conditional branch's leading comment.
1620    #[test]
1621    fn preserves_a_comment_on_a_conditional_branch() {
1622        let source = "sheet s {\n    conditional m {\n        0i32 => { relationship { b := a; } }\n        // one\n        1i32 => { relationship { b := a; } }\n    }\n}";
1623        let expected = "sheet s {\n    conditional m {\n        0i32 => {\n            relationship {\n                b := a;\n            }\n        }\n        // one\n        1i32 => {\n            relationship {\n                b := a;\n            }\n        }\n    }\n}\n";
1624        assert_eq!(format(source), expected);
1625    }
1626
1627    /// Preserves expression meaning through precedence-aware parenthesization.
1628    #[test]
1629    fn binding_body_delegates_precedence_aware_parenthesization_to_cel_parser() {
1630        let source = "sheet s { relationship { c := (a + b) * 2i32; } }";
1631        let expected = "sheet s {\n    relationship {\n        c := (a + b) * 2i32;\n    }\n}\n";
1632        assert_eq!(format(source), expected);
1633    }
1634
1635    /// Reaches a formatting fixed point after reparsing a sheet.
1636    #[test]
1637    fn format_is_idempotent_through_a_reparse() {
1638        let source = "sheet demo {\n    cell a: f64 = 2.0;\n    cell b: f64 = 3.0;\n\n    relationship {\n        c := a * b;\n    }\n}";
1639        let once = format(source);
1640        let twice = format(&once);
1641        assert_eq!(once, twice);
1642    }
1643
1644    /// Reaches a formatting fixed point for conditional declarations.
1645    #[test]
1646    fn format_is_idempotent_through_a_reparse_with_a_conditional() {
1647        let source = "sheet demo {\n    cell p: i32 = 0;\n    cell c: f64;\n\n    conditional p {\n        0i32 => {\n            relationship {\n                c := c;\n            }\n        }\n        _ => {\n            relationship {\n                c := c;\n            }\n        }\n    }\n}";
1648        let once = format(source);
1649        let twice = format(&once);
1650        assert_eq!(once, twice);
1651    }
1652
1653    /// Formats an explicitly typed output without requirements.
1654    #[test]
1655    fn formats_an_out_with_explicit_type_and_no_requirements() {
1656        let source = "sheet s {\n    out area: f64 := width * height;\n}";
1657        let expected = "sheet s {\n    out area: f64 := width * height;\n}\n";
1658        assert_eq!(format(source), expected);
1659    }
1660
1661    /// Formats an output without adding an annotation.
1662    #[test]
1663    fn formats_an_out_with_no_type_annotation() {
1664        let source = "sheet s {\n    out area := width;\n}";
1665        let expected = "sheet s {\n    out area := width;\n}\n";
1666        assert_eq!(format(source), expected);
1667    }
1668
1669    /// Preserves requirement order while formatting an output.
1670    #[test]
1671    fn formats_an_out_with_requirements_in_declaration_order() {
1672        let source = "sheet s {\n    out area: f64 := width * height require {\n        @max_area width * height <= max_area;\n    };\n}";
1673        let expected = "sheet s {\n    out area: f64 := width * height require {\n        @max_area width * height <= max_area;\n    };\n}\n";
1674        assert_eq!(format(source), expected);
1675    }
1676
1677    /// Formats an ordinary cell's requirement block.
1678    #[test]
1679    fn formats_a_cell_with_a_require_block() {
1680        let source =
1681            "sheet s {\n    cell x: i32 = 5 require {\n        @positive x > 0;\n    };\n}";
1682        assert_eq!(format(source), format!("{source}\n"));
1683    }
1684
1685    /// Formats a source cell's requirement block.
1686    #[test]
1687    fn formats_a_source_with_a_require_block() {
1688        let source =
1689            "sheet s {\n    source x: i32 = 5 require {\n        @positive x > 0;\n    };\n}";
1690        assert_eq!(format(source), format!("{source}\n"));
1691    }
1692
1693    /// Formats a source cell's filter clause.
1694    #[test]
1695    fn formats_a_source_with_a_filter_clause() {
1696        let source = "sheet s {\n    source x: i32 = 5 filter 0..=10;\n}";
1697        assert_eq!(format(source), format!("{source}\n"));
1698    }
1699
1700    /// Formats an output cell's filter clause.
1701    #[test]
1702    fn formats_an_out_with_a_filter_clause() {
1703        let source =
1704            "sheet s {\n    cell width: i32 = 4;\n    out area := width filter 0..=100;\n}";
1705        assert_eq!(format(source), format!("{source}\n"));
1706    }
1707
1708    /// Formats an explicit tuple cell annotation.
1709    #[test]
1710    fn formats_a_cell_with_an_explicit_tuple_type() {
1711        assert_eq!(
1712            format("sheet s { cell a: (i32, f64) = (1, 2.5); }"),
1713            "sheet s {\n    cell a: (i32, f64) = (1, 2.5);\n}\n"
1714        );
1715    }
1716
1717    /// Formats a recursive tuple cell annotation.
1718    #[test]
1719    fn formats_a_cell_with_a_nested_tuple_type() {
1720        assert_eq!(
1721            format("sheet s { cell a: (i32, (f64, String)); }"),
1722            "sheet s {\n    cell a: (i32, (f64, String));\n}\n"
1723        );
1724    }
1725
1726    /// Formats a tuple output annotation.
1727    #[test]
1728    fn formats_an_out_with_an_explicit_tuple_type() {
1729        assert_eq!(
1730            format("sheet s { out a: (i32, i32) := (x, x); }"),
1731            "sheet s {\n    out a: (i32, i32) := (x, x);\n}\n"
1732        );
1733    }
1734
1735    /// Reaches a formatting fixed point for tuple-valued cells.
1736    #[test]
1737    fn format_is_idempotent_through_a_reparse_with_a_tuple_cell() {
1738        let source = "sheet s {\n    cell a: (i32, f64) = (1, 2.5);\n}";
1739        let once = format(source);
1740        let twice = format(&once);
1741        assert_eq!(once, twice);
1742    }
1743
1744    /// Preserves the delimiters of a single block comment.
1745    #[test]
1746    fn formats_a_single_line_block_comment_preserving_its_style() {
1747        let source =
1748            "sheet s {\n    cell a: i32 = 1;\n    /* the total */\n    cell x: i32 = 1;\n}";
1749        let expected =
1750            "sheet s {\n    cell a: i32 = 1;\n    /* the total */\n    cell x: i32 = 1;\n}\n";
1751        assert_eq!(format(source), expected);
1752    }
1753
1754    /// Preserves a multiline block comment's style.
1755    #[test]
1756    fn formats_a_multi_line_block_comment_preserving_its_style() {
1757        let source = "sheet s {\n    cell a: i32 = 1;\n    /*\n        line one\n        line two\n    */\n    cell x: i32 = 1;\n}";
1758        let expected = "sheet s {\n    cell a: i32 = 1;\n    /*\n        line one\n        line two\n    */\n    cell x: i32 = 1;\n}\n";
1759        assert_eq!(format(source), expected);
1760    }
1761
1762    /// Retains the complete license-header regression fixture.
1763    #[test]
1764    fn formats_the_issue_105_license_header_repro_without_dropping_it() {
1765        let source =
1766            "/*\n    Copyright 2013 Adobe\n    ...\n*/\nsheet s {\n    cell a: i32 = 1;\n}";
1767        let expected =
1768            "/*\n    Copyright 2013 Adobe\n    ...\n*/\nsheet s {\n    cell a: i32 = 1;\n}\n";
1769        assert_eq!(format(source), expected);
1770    }
1771
1772    /// Reaches a formatting fixed point for block comments.
1773    #[test]
1774    fn block_comment_formatting_is_idempotent_through_a_reparse() {
1775        let source = "sheet s {\n    cell a: i32 = 1;\n    /*\n        line one\n        line two\n    */\n    cell x: i32 = 1;\n}";
1776        let once = format(source);
1777        let twice = format(&once);
1778        assert_eq!(once, twice);
1779    }
1780
1781    /// Formats outer documentation on a cell.
1782    #[test]
1783    fn formats_a_cell_with_a_doc_comment() {
1784        assert_eq!(
1785            format("sheet s { /// the total\n cell x: i32 = 1; }"),
1786            "sheet s {\n    /// the total\n    cell x: i32 = 1;\n}\n"
1787        );
1788    }
1789
1790    /// Formats inner documentation within a sheet.
1791    #[test]
1792    fn formats_a_sheet_level_doc_comment() {
1793        assert_eq!(
1794            format("//! module docs\nsheet s { cell x: i32 = 1; }"),
1795            "//! module docs\nsheet s {\n    cell x: i32 = 1;\n}\n"
1796        );
1797    }
1798
1799    /// Preserves the source order of plain and documentation comments.
1800    #[test]
1801    fn formats_a_plain_comment_and_doc_comment_together_in_source_order() {
1802        let source =
1803            "sheet s {\n    cell w: i32 = 0;\n    // TODO\n    /// docs\n    cell x: i32 = 1;\n}";
1804        let expected =
1805            "sheet s {\n    cell w: i32 = 0;\n    // TODO\n    /// docs\n    cell x: i32 = 1;\n}\n";
1806        assert_eq!(format(source), expected);
1807    }
1808
1809    /// Formats documentation on relationships, conditionals, and outputs.
1810    #[test]
1811    fn formats_doc_comments_on_a_relationship_conditional_and_out() {
1812        let source = "sheet s {\n    /// r\n    relationship { b := a; }\n\n    /// c\n    conditional true { true => { relationship { b := a; } } }\n\n    /// o\n    out area: f64 := w;\n}";
1813        let expected = "sheet s {\n    /// r\n    relationship {\n        b := a;\n    }\n\n    /// c\n    conditional true {\n        true => {\n            relationship {\n                b := a;\n            }\n        }\n    }\n\n    /// o\n    out area: f64 := w;\n}\n";
1814        assert_eq!(format(source), expected);
1815    }
1816
1817    /// Reaches a formatting fixed point for documentation comments.
1818    #[test]
1819    fn doc_comment_formatting_is_idempotent_through_a_reparse() {
1820        let source = "sheet s {\n    /// the total\n    cell x: i32 = 1;\n}";
1821        let once = format(source);
1822        let twice = format(&once);
1823        assert_eq!(once, twice);
1824    }
1825
1826    /// Preserves comments before a sheet's closing brace.
1827    #[test]
1828    fn formats_a_trailing_comment_before_the_sheets_closing_brace() {
1829        assert_eq!(
1830            format("sheet s {\n    cell x: i32 = 1;\n    // trailing\n}"),
1831            "sheet s {\n    cell x: i32 = 1;\n    // trailing\n}\n"
1832        );
1833    }
1834
1835    /// Preserves trailing comments in an otherwise empty relationship.
1836    #[test]
1837    fn formats_a_trailing_comment_in_an_empty_relationship() {
1838        assert_eq!(
1839            format("sheet s {\n    relationship {\n        // only this\n    }\n}"),
1840            "sheet s {\n    relationship {\n        // only this\n    }\n}\n"
1841        );
1842    }
1843
1844    /// Preserves comments before a relationship's closing brace.
1845    #[test]
1846    fn formats_a_trailing_comment_before_a_relationships_closing_brace() {
1847        let source =
1848            "sheet s {\n    relationship {\n        b := a;\n        // trailing\n    }\n}";
1849        let expected =
1850            "sheet s {\n    relationship {\n        b := a;\n        // trailing\n    }\n}\n";
1851        assert_eq!(format(source), expected);
1852    }
1853
1854    /// Preserves comments before a conditional's closing brace.
1855    #[test]
1856    fn formats_a_trailing_comment_before_a_conditionals_own_closing_brace() {
1857        let source = "sheet s {\n    conditional m {\n        0i32 => { relationship { b := a; } }\n        // trailing\n    }\n}";
1858        let expected = "sheet s {\n    conditional m {\n        0i32 => {\n            relationship {\n                b := a;\n            }\n        }\n        // trailing\n    }\n}\n";
1859        assert_eq!(format(source), expected);
1860    }
1861
1862    /// Preserves trailing comments within a default arm.
1863    #[test]
1864    fn formats_a_trailing_comment_in_a_default_arm() {
1865        let source = "sheet s {\n    conditional m {\n        _ => {\n            relationship { b := a; }\n            // trailing\n        }\n    }\n}";
1866        let expected = "sheet s {\n    conditional m {\n        _ => {\n            relationship {\n                b := a;\n            }\n            // trailing\n        }\n    }\n}\n";
1867        assert_eq!(format(source), expected);
1868    }
1869
1870    /// Preserves comments before an output requirement block closes.
1871    #[test]
1872    fn formats_a_trailing_comment_before_a_requires_closing_brace() {
1873        let source = "sheet s {\n    out area: f64 := w require {\n        @c w <= 10.0;\n        // trailing\n    };\n}";
1874        let expected = "sheet s {\n    out area: f64 := w require {\n        @c w <= 10.0;\n        // trailing\n    };\n}\n";
1875        assert_eq!(format(source), expected);
1876    }
1877
1878    /// Preserves comments before an ordinary cell's requirement block closes.
1879    #[test]
1880    fn formats_a_trailing_comment_before_a_cells_requires_closing_brace() {
1881        // Mirrors `formats_a_trailing_comment_before_a_requires_closing_brace`, but for a
1882        // `cell`'s own `require` block — regression test for the comment being silently dropped
1883        // (never attached, so never written back out) because `attach_trivia` only recovered
1884        // require-block trivia for `SheetItem::Out`, not `SheetItem::Cell`/`SheetItem::Source`.
1885        let source = "sheet s {\n    cell a: i32 = 1 require {\n        @r a > 0;\n        // trailing\n    };\n}";
1886        let expected = "sheet s {\n    cell a: i32 = 1 require {\n        @r a > 0;\n        // trailing\n    };\n}\n";
1887        assert_eq!(format(source), expected);
1888    }
1889
1890    /// Preserves comments before a source cell's requirement block closes.
1891    #[test]
1892    fn formats_a_trailing_comment_before_a_sources_requires_closing_brace() {
1893        // Mirrors `formats_a_trailing_comment_before_a_requires_closing_brace`, but for a
1894        // `source`'s own `require` block.
1895        let source = "sheet s {\n    source a: i32 = 1 require {\n        @r a > 0;\n        // trailing\n    };\n}";
1896        let expected = "sheet s {\n    source a: i32 = 1 require {\n        @r a > 0;\n        // trailing\n    };\n}\n";
1897        assert_eq!(format(source), expected);
1898    }
1899
1900    /// Reaches a formatting fixed point for trailing container trivia.
1901    #[test]
1902    fn trailing_trivia_formatting_is_idempotent_through_a_reparse() {
1903        let source = "sheet s {\n    cell a: i32 = 1;\n    // trailing\n}";
1904        let once = format(source);
1905        let twice = format(&once);
1906        assert_eq!(once, twice);
1907    }
1908
1909    /// Formats a cell's filter expression.
1910    #[test]
1911    fn formats_a_cell_with_a_filter() {
1912        assert_eq!(
1913            format("sheet s { cell a: i32 = 1 filter _; }"),
1914            "sheet s {\n    cell a: i32 = 1 filter _;\n}\n"
1915        );
1916    }
1917
1918    /// Formats a filter expression containing named cell references.
1919    #[test]
1920    fn formats_a_cell_with_a_filter_referencing_a_cell() {
1921        assert_eq!(
1922            format("sheet s { cell hi: i32 = 100; cell a: i32 = 1 filter min(_, hi); }"),
1923            "sheet s {\n    cell hi: i32 = 100;\n    cell a: i32 = 1 filter min(_, hi);\n}\n"
1924        );
1925    }
1926
1927    /// Reaches a formatting fixed point for filtered cells.
1928    #[test]
1929    fn format_is_idempotent_through_a_reparse_with_a_filter() {
1930        let source = "sheet s {\n    cell a: i32 = 1 filter _;\n}";
1931        let once = format(source);
1932        let twice = format(&once);
1933        assert_eq!(once, twice);
1934    }
1935
1936    /// Emits canonical filter clause spacing.
1937    #[test]
1938    fn formats_a_filter() {
1939        let source = "sheet s { cell x: i32 = 0 filter 0..=10; }";
1940        let sheet = AdamAstParser::new().parse_str(source).unwrap();
1941        assert_eq!(
1942            format_sheet(&sheet, source),
1943            "sheet s {\n    cell x: i32 = 0 filter 0..=10;\n}\n"
1944        );
1945    }
1946
1947    /// Formats an unlabeled requirement without inventing a label.
1948    #[test]
1949    fn formats_a_requirement_with_no_label() {
1950        let source = "sheet s {\n    cell x: i32 = 5 require {\n        x > 0;\n    };\n}";
1951        assert_eq!(format(source), format!("{source}\n"));
1952    }
1953
1954    /// Formats valid source through the public source API.
1955    #[test]
1956    fn format_source_formats_valid_source() {
1957        assert_eq!(
1958            format_source("sheet   s{cell x:i32=1;}").unwrap(),
1959            "sheet s {\n    cell x: i32 = 1;\n}\n"
1960        );
1961    }
1962
1963    /// Rejects source that fails before a sheet can be parsed.
1964    #[test]
1965    fn format_source_errors_on_source_that_fails_to_parse() {
1966        assert!(matches!(
1967            format_source("not a sheet at all"),
1968            Err(FormatSourceError::Parse(_))
1969        ));
1970    }
1971
1972    /// Refuses to format a tree containing recovered syntax errors.
1973    #[test]
1974    fn format_source_errors_on_a_recovered_syntax_error() {
1975        assert!(matches!(
1976            format_source("sheet s { cell x unknown_syntax }"),
1977            Err(FormatSourceError::Recovered(_))
1978        ));
1979    }
1980
1981    // Issue #59: a same-line trailing `//`/`/* */` comment stays on the declaration it trails,
1982    // instead of migrating onto its own line before the next declaration.
1983
1984    /// Keeps a cell's same-line line comment on that cell.
1985    #[test]
1986    fn formats_a_same_line_trailing_line_comment_on_a_cell() {
1987        let source =
1988            "sheet s {\n    cell a: f64 = 2.0; // end of line comment\n    cell b: f64 = 3.0;\n}";
1989        let expected =
1990            "sheet s {\n    cell a: f64 = 2.0; // end of line comment\n    cell b: f64 = 3.0;\n}\n";
1991        assert_eq!(format(source), expected);
1992    }
1993
1994    /// Keeps a cell's same-line block comment on that cell.
1995    #[test]
1996    fn formats_a_same_line_trailing_block_comment_on_a_cell() {
1997        let source = "sheet s {\n    cell a: f64 = 2.0; /* note */\n    cell b: f64 = 3.0;\n}";
1998        let expected = "sheet s {\n    cell a: f64 = 2.0; /* note */\n    cell b: f64 = 3.0;\n}\n";
1999        assert_eq!(format(source), expected);
2000    }
2001
2002    /// Retains a last item's same-line comment before the sheet closes.
2003    #[test]
2004    fn formats_a_same_line_trailing_comment_on_the_last_item_before_the_sheets_closing_brace() {
2005        let source = "sheet s {\n    cell a: i32 = 1; // note\n}";
2006        let expected = "sheet s {\n    cell a: i32 = 1; // note\n}\n";
2007        assert_eq!(format(source), expected);
2008    }
2009
2010    /// Retains a binding's same-line trailing comment.
2011    #[test]
2012    fn formats_a_same_line_trailing_comment_on_a_binding() {
2013        let source =
2014            "sheet s {\n    relationship {\n        b := a; // note\n        a := b;\n    }\n}";
2015        let expected =
2016            "sheet s {\n    relationship {\n        b := a; // note\n        a := b;\n    }\n}\n";
2017        assert_eq!(format(source), expected);
2018    }
2019
2020    /// Retains a block comment even when its entire gap is on one line.
2021    #[test]
2022    fn formats_a_same_line_block_comment_when_the_gap_has_no_newline_at_all() {
2023        // PR #202 review: previously the trailing `/* note */` was silently dropped, since
2024        // items+comment sharing one physical line with no `\n` anywhere in the gap fell into
2025        // the "drop the final fragment" path before the comment scanner ever ran.
2026        let source = "sheet s {\n    cell a: i32 = 1; /* note */ cell b: i32 = 2;\n}";
2027        let expected = "sheet s {\n    cell a: i32 = 1; /* note */\n    cell b: i32 = 2;\n}\n";
2028        assert_eq!(format(source), expected);
2029    }
2030
2031    /// Retains the last relationship's same-line comment within a named branch.
2032    #[test]
2033    fn formats_a_same_line_trailing_comment_before_a_conditional_branchs_own_closing_brace() {
2034        let source = "sheet s {\n    conditional m {\n        0i32 => {\n            relationship { b := a; } // note\n        }\n    }\n}";
2035        let expected = "sheet s {\n    conditional m {\n        0i32 => {\n            relationship {\n                b := a;\n            } // note\n        }\n    }\n}\n";
2036        assert_eq!(format(source), expected);
2037    }
2038
2039    /// Retains the last relationship's same-line comment within the default arm.
2040    #[test]
2041    fn formats_a_same_line_trailing_comment_before_a_default_arms_own_closing_brace() {
2042        let source = "sheet s {\n    conditional m {\n        _ => {\n            relationship { b := a; } // note\n        }\n    }\n}";
2043        let expected = "sheet s {\n    conditional m {\n        _ => {\n            relationship {\n                b := a;\n            } // note\n        }\n    }\n}\n";
2044        assert_eq!(format(source), expected);
2045    }
2046
2047    /// Retains the last named branch's same-line trailing comment.
2048    #[test]
2049    fn formats_a_same_line_trailing_comment_before_a_conditionals_own_closing_brace_with_only_branches()
2050     {
2051        let source = "sheet s {\n    conditional m {\n        0i32 => { relationship { b := a; } } // note\n    }\n}";
2052        let expected = "sheet s {\n    conditional m {\n        0i32 => {\n            relationship {\n                b := a;\n            }\n        } // note\n    }\n}\n";
2053        assert_eq!(format(source), expected);
2054    }
2055
2056    /// Retains the default arm's same-line trailing comment.
2057    #[test]
2058    fn formats_a_same_line_trailing_comment_before_a_conditionals_own_closing_brace_with_a_default()
2059    {
2060        let source = "sheet s {\n    conditional m {\n        _ => { relationship { b := a; } } // note\n    }\n}";
2061        let expected = "sheet s {\n    conditional m {\n        _ => {\n            relationship {\n                b := a;\n            }\n        } // note\n    }\n}\n";
2062        assert_eq!(format(source), expected);
2063    }
2064
2065    /// Retains the last requirement's same-line trailing comment.
2066    #[test]
2067    fn formats_a_same_line_trailing_comment_before_a_requires_closing_brace() {
2068        let source =
2069            "sheet s {\n    out area: f64 := w require {\n        @c w <= 10.0; // note\n    };\n}";
2070        let expected = "sheet s {\n    out area: f64 := w require {\n        @c w <= 10.0; // note\n    };\n}\n";
2071        assert_eq!(format(source), expected);
2072    }
2073
2074    /// Reaches a formatting fixed point for same-line trailing comments.
2075    #[test]
2076    fn same_line_trailing_comment_formatting_is_idempotent_through_a_reparse() {
2077        let source =
2078            "sheet s {\n    cell a: f64 = 2.0; // end of line comment\n    cell b: f64 = 3.0;\n}";
2079        let once = format(source);
2080        let twice = format(&once);
2081        assert_eq!(once, twice);
2082    }
2083
2084    // Issue #201: comments in gaps *within* one declaration's own tokens (as opposed to gaps
2085    // between sibling declarations, or before a container's closing brace — both already handled
2086    // by `trivia.rs`).
2087
2088    /// Retains an inline block comment after a cell initializer.
2089    #[test]
2090    fn preserves_a_block_comment_between_a_cell_initializer_and_its_semicolon() {
2091        let source = "sheet s {\n    cell a: i32 = 1 /* trailing */;\n}";
2092        assert_eq!(
2093            format(source),
2094            "sheet s {\n    cell a: i32 = 1 /* trailing */;\n}\n"
2095        );
2096    }
2097
2098    /// Retains an inline block comment after a cell annotation.
2099    #[test]
2100    fn preserves_a_block_comment_between_a_type_and_the_equals() {
2101        let source = "sheet s {\n    cell a: i32 /* t */ = 1;\n}";
2102        assert_eq!(
2103            format(source),
2104            "sheet s {\n    cell a: i32 /* t */ = 1;\n}\n"
2105        );
2106    }
2107
2108    /// Leaves comment-free declaration formatting unchanged.
2109    #[test]
2110    fn a_declaration_with_no_intra_token_comments_is_unchanged() {
2111        assert_eq!(
2112            format("sheet s { cell a: i32 = 1; }"),
2113            "sheet s {\n    cell a: i32 = 1;\n}\n"
2114        );
2115    }
2116
2117    /// Retains leading, interior, and trailing initializer comments at a fixed point.
2118    #[test]
2119    fn a_cell_initializer_with_leading_inline_and_trailing_comments_round_trips_losslessly() {
2120        let source = "sheet s {\n    cell a: i32 = /* p */ 1 /* q */ + 2 /* r */;\n}";
2121        let once = format_source(source).unwrap();
2122        // Every comment survives (none silently dropped).
2123        for needle in ["/* p */", "/* q */", "/* r */"] {
2124            assert!(once.contains(needle), "comment {needle} lost: {once:?}");
2125        }
2126        // And formatting is idempotent.
2127        let twice = format_source(&once).unwrap();
2128        assert_eq!(once, twice);
2129    }
2130
2131    /// Retains a line comment before the initializer's semicolon.
2132    #[test]
2133    fn preserves_a_line_comment_between_a_cell_initializer_and_its_semicolon() {
2134        // A `//` comment forces a line wrap; the terminating `;` must land on the continuation
2135        // line, not get glued onto the comment's own line, which would comment the `;` out and
2136        // make the output fail to reparse.
2137        let source = "sheet s {\n    cell a: i32 = 1 // c\n    ;\n}";
2138        let once = format_source(source).unwrap();
2139        assert!(once.contains("// c"), "comment must survive: {once:?}");
2140        assert!(
2141            once.trim_end().ends_with('}'),
2142            "the sheet's own closing brace must not have been commented out: {once:?}"
2143        );
2144        // Must still reparse (a stray `;` swallowed into the comment would fail this).
2145        let twice = format_source(&once).unwrap();
2146        assert_eq!(once, twice, "formatting must be idempotent");
2147    }
2148
2149    /// Retains a line comment between an annotation and assignment.
2150    #[test]
2151    fn preserves_a_line_comment_between_a_type_and_the_equals() {
2152        let source = "sheet s {\n    cell a: i32 // c\n    = 1;\n}";
2153        let once = format_source(source).unwrap();
2154        assert!(once.contains("// c"), "comment must survive: {once:?}");
2155        let twice = format_source(&once).unwrap();
2156        assert_eq!(once, twice, "formatting must be idempotent");
2157    }
2158
2159    /// Preserves the order of multiple comments after assignment.
2160    #[test]
2161    fn preserves_comment_order_after_the_equals() {
2162        let source = "sheet s {\n    cell a: i32 = /* after */ 1;\n}";
2163        let once = format_source(source).unwrap();
2164        let eq = once.find('=').expect("has =");
2165        let c = once.find("/* after */").expect("comment survives");
2166        assert!(c > eq, "comment must stay AFTER the =, got: {once:?}");
2167    }
2168
2169    /// Preserves comment order on both sides of assignment.
2170    #[test]
2171    fn preserves_comment_order_around_the_equals() {
2172        let source = "sheet s {\n    cell a: i32 /* before */ = /* after */ 1;\n}";
2173        let once = format_source(source).unwrap();
2174        let before = once.find("/* before */").expect("before survives");
2175        let eq = once.find('=').expect("has =");
2176        let after = once.find("/* after */").expect("after survives");
2177        assert!(before < eq && eq < after, "order lost: {once:?}");
2178    }
2179
2180    /// Retains comments preceding a filter clause.
2181    #[test]
2182    fn preserves_a_comment_before_the_filter_keyword() {
2183        let source = "sheet s {\n    cell a: i32 = 1 /* keep */ filter 0..=10;\n}";
2184        let once = format_source(source).unwrap();
2185        assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
2186    }
2187
2188    /// Retains comments following a filter expression.
2189    #[test]
2190    fn preserves_a_comment_after_the_filter_body() {
2191        let source = "sheet s {\n    cell a: i32 = 1 filter 0..=10 /* keep */;\n}";
2192        let once = format_source(source).unwrap();
2193        assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
2194    }
2195
2196    /// Retains comments preceding a requirement clause.
2197    #[test]
2198    fn preserves_a_comment_before_the_require_keyword() {
2199        let source = "sheet s {\n    out area: f64 := w /* keep */ require {\n        @c w <= 10.0;\n    };\n}";
2200        let once = format_source(source).unwrap();
2201        assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
2202    }
2203
2204    /// Checks marker preservation and fixed-point formatting for a valid comment-gap fixture.
2205    ///
2206    /// - Precondition: `source` is valid syntax containing `/* keep */`.
2207    /// - Complexity: follows two calls to [`format_source`].
2208    fn assert_comment_survives_and_is_idempotent(source: &str) {
2209        let once = format_source(source).unwrap();
2210        assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
2211        let twice = format_source(&once).unwrap();
2212        assert_eq!(once, twice, "formatting must be idempotent: {once:?}");
2213    }
2214
2215    /// Retains a filter-keyword gap comment at a formatting fixed point.
2216    #[test]
2217    fn preserves_a_comment_between_the_filter_keyword_and_body() {
2218        assert_comment_survives_and_is_idempotent(
2219            "sheet s {\n    cell a: i32 = 1 filter /* keep */ 0..=10;\n}",
2220        );
2221    }
2222
2223    /// Retains a requirement-opening gap comment at a formatting fixed point.
2224    #[test]
2225    fn preserves_a_comment_between_the_require_keyword_and_brace() {
2226        assert_comment_survives_and_is_idempotent(
2227            "sheet s {\n    out area: f64 := w require /* keep */ {\n        @c w <= 10.0;\n    };\n}",
2228        );
2229    }
2230
2231    /// Retains a branch-key gap comment at a formatting fixed point.
2232    #[test]
2233    fn preserves_a_comment_between_a_branch_literal_and_the_fat_arrow() {
2234        assert_comment_survives_and_is_idempotent(
2235            "sheet s {\n    conditional p {\n        0i32 /* keep */ => { relationship { b := a; } }\n    }\n}",
2236        );
2237    }
2238
2239    /// Retains a branch-body gap comment at a formatting fixed point.
2240    #[test]
2241    fn preserves_a_comment_between_the_fat_arrow_and_a_branch_body() {
2242        assert_comment_survives_and_is_idempotent(
2243            "sheet s {\n    conditional p {\n        0i32 => /* keep */ { relationship { b := a; } }\n    }\n}",
2244        );
2245    }
2246
2247    /// Retains a requirement-label gap comment at a formatting fixed point.
2248    #[test]
2249    fn preserves_a_comment_between_a_requirement_label_and_body() {
2250        assert_comment_survives_and_is_idempotent(
2251            "sheet s {\n    out area: f64 := w require {\n        @c /* keep */ w <= 10.0;\n    };\n}",
2252        );
2253    }
2254
2255    /// Retains a conditional-subject gap comment at a formatting fixed point.
2256    #[test]
2257    fn preserves_a_comment_between_a_conditional_subject_and_its_brace() {
2258        assert_comment_survives_and_is_idempotent(
2259            "sheet s {\n    conditional p /* keep */ {\n        0i32 => { relationship { b := a; } }\n    }\n}",
2260        );
2261    }
2262
2263    /// Retains a relationship-opening gap comment at a formatting fixed point.
2264    #[test]
2265    fn preserves_a_comment_between_the_relationship_keyword_and_its_brace() {
2266        assert_comment_survives_and_is_idempotent(
2267            "sheet s {\n    relationship /* keep */ {\n        b := a;\n    }\n}",
2268        );
2269    }
2270}