Skip to main content

cel_parser/
fmt.rs

1//! Pretty-prints a [`crate::Expr`] tree back to CEL source text: precedence-aware
2//! parenthesization (added only where required, not exhaustively), single-space-around-operator
3//! normalization, and a 100-column layout target. Short expressions stay inline; longer lists,
4//! records and closure bodies nest by four spaces, with leading binary continuation operators.
5//! Unbreakable tokens and comments may exceed the target. Literal leaves are re-emitted via
6//! [`proc_macro2::Span::source_text`] rather than synthesized from [`crate::Literal`], so exact
7//! original notation (`1920.0` vs `1920.0f64`, a byte literal's spelling) round-trips.
8//!
9//! Typed arrays round-trip with their postfix `: Type` ascriptions, including recursive array
10//! types such as `[]: [[i32]]` and reusable tuple syntax such as `[]: [(i32, f64)]`. The
11//! formatter preserves complete tuple, array, and `Fn((A,), R)` callable annotations,
12//! including the comma that distinguishes a singleton tuple from grouping.
13
14use crate::ast::{Expr, LogicalOp};
15use crate::trivia::{Comment, GapPiece, scan_gap, take_block_comment};
16
17/// Owns one character-position index for a borrowed source.
18///
19/// Line starts index a shared character-to-byte boundary table.
20struct Source<'a> {
21    text: &'a str,
22    lines: Vec<usize>,
23    bytes: Vec<usize>,
24}
25
26impl<'a> Source<'a> {
27    /// Indexes every source position once in O(n) time and space for n source bytes.
28    fn new(text: &'a str) -> Self {
29        let mut lines = vec![0];
30        let mut bytes = Vec::new();
31        // Record each character boundary in its owning line.
32        for (character, (byte, ch)) in text.char_indices().enumerate() {
33            bytes.push(byte);
34            if ch == '\n' {
35                lines.push(character + 1);
36            }
37        }
38        bytes.push(text.len());
39        Self { text, lines, bytes }
40    }
41
42    /// Returns the byte boundary for a position in this source.
43    ///
44    /// - Precondition: `position` belongs to this source.
45    fn byte(&self, position: proc_macro2::LineColumn) -> usize {
46        debug_assert!((1..=self.lines.len()).contains(&position.line));
47        let character = self.lines[position.line - 1] + position.column;
48        debug_assert!(
49            character
50                < self
51                    .lines
52                    .get(position.line)
53                    .copied()
54                    .unwrap_or(self.bytes.len())
55        );
56        self.bytes[character]
57    }
58
59    /// Borrows the source between two positions, or an empty slice for synthetic source.
60    ///
61    /// - Precondition: nonempty source owns both positions.
62    fn between(&self, from: proc_macro2::LineColumn, to: proc_macro2::LineColumn) -> &'a str {
63        if self.text.is_empty() {
64            return "";
65        }
66        self.text.get(self.byte(from)..self.byte(to)).unwrap_or("")
67    }
68}
69
70/// An immutable Wadler document; construction never lays out a child expression.
71type Doc = pretty::RcDoc<'static, ()>;
72
73/// Preserves a lexical token's internal newlines and indentation independently of layout nesting.
74///
75/// - Complexity: O(n) in the token's text length.
76fn verbatim_doc(text: String) -> Doc {
77    let mut lines = text.split('\n');
78    let first = Doc::text(lines.next().unwrap_or_default().to_owned());
79    lines.fold(first, |doc, line| {
80        let line = Doc::text(line.to_owned());
81        doc.append(Doc::nesting(move |indent| {
82            let offset = isize::try_from(indent)
83                .expect("document indentation fits isize")
84                .checked_neg()
85                .expect("nonnegative indentation can be negated");
86            Doc::hardline().append(line.clone()).nest(offset)
87        }))
88    })
89}
90
91/// Lifts recovered trivia into a document with mandatory line-comment boundaries.
92///
93/// - Complexity: O(n) in the recovered text length.
94fn trivia_doc(text: String) -> Doc {
95    trivia_doc_at(text, 4)
96}
97
98/// Lifts recovered trivia with continuation nesting supplied by its grammar owner.
99///
100/// - Complexity: O(n) in the recovered text length.
101fn trivia_doc_at(text: String, continuation: isize) -> Doc {
102    let mut lines = text.split('\n');
103    let first = Doc::text(lines.next().unwrap_or_default().to_owned());
104    lines.fold(first, |doc, line| {
105        doc.append(
106            Doc::hardline()
107                .append(Doc::text(
108                    line.strip_prefix("    ").unwrap_or(line).to_owned(),
109                ))
110                .nest(continuation),
111        )
112    })
113}
114
115/// Builds a gap document, with a soft boundary before binary operators or after commas.
116///
117/// - Complexity: O(n) in the gap's text length.
118fn gap_doc(pieces: &[GapPiece], spacing: Spacing) -> Doc {
119    if pieces
120        .iter()
121        .any(|piece| matches!(piece, GapPiece::Comment(_)))
122        && let Some(index) = pieces
123            .iter()
124            .position(|piece| matches!(piece, GapPiece::Punct(_)))
125        && matches!(spacing, Spacing::Around | Spacing::CommaAfter)
126    {
127        let GapPiece::Punct(token) = pieces[index] else {
128            unreachable!()
129        };
130        let before = emit_gap(&pieces[..index], Spacing::None, 0);
131        let after = emit_gap(&pieces[index + 1..], Spacing::None, 0);
132        let after_empty = after.is_empty();
133        let before_line = matches!(
134            pieces.get(index.wrapping_sub(1)),
135            Some(GapPiece::Comment(Comment::Line(_)))
136        );
137        let after_line = matches!(pieces.last(), Some(GapPiece::Comment(Comment::Line(_))));
138        let prefix = if before_line {
139            trivia_doc_at(before, 0)
140        } else {
141            trivia_doc_at(before.trim_end().to_owned(), 0)
142        };
143        let suffix = if after_line {
144            trivia_doc_at(after, 0)
145        } else {
146            trivia_doc_at(after.trim_end().to_owned(), 0)
147        };
148        return match spacing {
149            Spacing::Around => prefix
150                .append(if before_line {
151                    Doc::nil()
152                } else {
153                    Doc::softline()
154                })
155                .append(token)
156                .append(if after_empty {
157                    Doc::space()
158                } else {
159                    suffix.append(if after_line { Doc::nil() } else { Doc::space() })
160                }),
161            Spacing::CommaAfter => prefix.append(token).append(if after_empty {
162                Doc::line()
163            } else {
164                suffix.append(if after_line { Doc::nil() } else { Doc::line() })
165            }),
166            Spacing::None => unreachable!(),
167        };
168    }
169    if pieces
170        .iter()
171        .all(|piece| matches!(piece, GapPiece::Punct(_)))
172    {
173        Doc::concat(pieces.iter().map(|piece| {
174            let GapPiece::Punct(token) = piece else {
175                unreachable!()
176            };
177            match spacing {
178                Spacing::Around => Doc::line().append(*token).append(" "),
179                Spacing::CommaAfter => Doc::text(*token).append(Doc::line()),
180                Spacing::None => Doc::text(*token),
181            }
182        }))
183    } else {
184        trivia_doc(emit_gap(pieces, spacing, 0))
185    }
186}
187
188/// Builds an indented leading-operator continuation without nesting the left operand.
189///
190/// - Complexity: O(n) in the gap's text length.
191fn binary_doc(lhs: Doc, gap: &[GapPiece], rhs: Doc) -> Doc {
192    lhs.append(gap_doc(gap, Spacing::Around).append(rhs).nest(4))
193        .group()
194}
195
196/// Ends an opening-brace gap with a soft boundary, preserving mandatory comment boundaries.
197///
198/// `trim_start` removes spacing already supplied by the preceding closing-brace boundary.
199///
200/// - Complexity: O(n) in the recovered gap's text length.
201fn body_gap_doc(pieces: &[GapPiece], trim_start: bool) -> Doc {
202    let text = emit_gap(pieces, Spacing::Around, 0);
203    let text = if trim_start { text.trim_start() } else { &text };
204    if matches!(pieces.last(), Some(GapPiece::Comment(Comment::Line(_)))) {
205        trivia_doc_at(text.to_owned(), 0)
206    } else {
207        trivia_doc_at(text.trim_end().to_owned(), 0).append(Doc::line())
208    }
209}
210
211/// Binding-strength level, loosest first, mirroring `lib.rs`'s grammar chain from
212/// `range_expression` (via `expression = range_expression`) through `primary_expression`.
213#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
214struct Level(u8);
215
216impl Level {
217    const RANGE: Level = Level(0);
218    const OR: Level = Level(1);
219    const AND: Level = Level(2);
220    const COMPARISON: Level = Level(3);
221    const BIT_OR: Level = Level(4);
222    const BIT_XOR: Level = Level(5);
223    const BIT_AND: Level = Level(6);
224    const SHIFT: Level = Level(7);
225    const ADDITIVE: Level = Level(8);
226    const MULTIPLICATIVE: Level = Level(9);
227    const CAST: Level = Level(10);
228    const UNARY: Level = Level(11);
229    const POSTFIX: Level = Level(12);
230    const PRIMARY: Level = Level(13);
231
232    /// The next level up (strictly tighter-binding than `self`).
233    fn tighter(self) -> Level {
234        Level(self.0 + 1)
235    }
236}
237
238/// 4 spaces per nesting level (mirrors `adam-lang::fmt::indent`).
239///
240/// - Complexity: O(depth).
241fn indent(depth: usize) -> String {
242    "    ".repeat(depth)
243}
244
245/// How a gap's expected `Punct` tokens are spaced against their operands.
246#[derive(Clone, Copy)]
247enum Spacing {
248    /// A single space on each side of the token: `a + b`.
249    Around,
250    /// No surrounding spaces: `a..b`, or a bare delimiter.
251    None,
252    /// The token then one space: `a, b`.
253    CommaAfter,
254}
255
256/// Renders the pieces of one scanned gap into the text that goes *between* two already-rendered
257/// operands. A `Comment::Block` is inlined space-separated; a `Comment::Line` ends its line and
258/// resumes at `indent(depth + 1)` (the wrap continuation), since a `//` comment runs to
259/// end-of-line. A `Punct` is spaced per `spacing`.
260///
261/// - Postcondition: the returned string starts and ends exactly as `spacing` dictates for the
262///   comment-free case (e.g. `Spacing::Around` with a lone `Punct` returns `" + "`), so a
263///   comment-free gap reprints identically to the pre-`scan_gap` formatter.
264///
265/// - Complexity: O(n) in `pieces.len()` plus their text lengths.
266fn emit_gap(pieces: &[GapPiece], spacing: Spacing, depth: usize) -> String {
267    let cont = indent(depth + 1);
268    let mut out = String::new();
269    for piece in pieces {
270        match piece {
271            GapPiece::Punct(tok) => {
272                match spacing {
273                    Spacing::Around => {
274                        // Skip the leading space only when the previous piece already left one
275                        // (a block comment's trailing `" */ "`, or a line comment's continuation
276                        // indent) — otherwise this token opens the gap and needs its own.
277                        if !out.ends_with(' ') {
278                            out.push(' ');
279                        }
280                        out.push_str(tok);
281                        out.push(' ');
282                    }
283                    Spacing::None => out.push_str(tok),
284                    Spacing::CommaAfter => {
285                        // Unlike `Around`, a comma never gets a leading space of its own -- it
286                        // glues directly to whatever precedes it, whether that's the element text
287                        // itself (the comment-free case) or a comment's trailing `" */ "` (a block
288                        // comment written between the element and its `,`, e.g.
289                        // `1i32 /* c */, 2i32`). A line comment's trailing continuation *indent*
290                        // is different: that space is meaningful (it's `indent(depth + 1)`, not
291                        // filler), so it must be left alone -- only the incidental single-space
292                        // filler a block comment or a prior `Around` punct leaves behind is
293                        // trimmed.
294                        if out.ends_with(' ') && !out.ends_with(&cont) {
295                            out.pop();
296                        }
297                        out.push_str(tok);
298                        out.push(' ');
299                    }
300                }
301            }
302            GapPiece::Comment(Comment::Block(text)) => {
303                // Separate from whatever came before (or, when this is the first piece, from the
304                // already-rendered operand this gap is appended to) with exactly one space,
305                // unless the previous piece already left a trailing space.
306                if !out.ends_with(' ') {
307                    out.push(' ');
308                }
309                out.push_str("/* ");
310                out.push_str(text);
311                out.push_str(" */ ");
312            }
313            GapPiece::Comment(Comment::Line(text)) => {
314                // A `//` comment runs to end of line, so anything after it must start on the next
315                // line; drop any trailing space this gap had accumulated before appending it.
316                let continuation = out
317                    .rsplit_once('\n')
318                    .is_some_and(|(_, tail)| tail.trim().is_empty());
319                if continuation {
320                    out.push_str("// ");
321                } else {
322                    let trimmed_len = out.trim_end().len();
323                    out.truncate(trimmed_len);
324                    out.push_str(" // ");
325                }
326                out.push_str(text);
327                out.push('\n');
328                out.push_str(&cont);
329            }
330        }
331    }
332    out
333}
334
335/// Returns the binding-strength level of a binary (two-operand) operator.
336///
337/// - Precondition: `name` is one of the binary operator tokens `lib.rs`'s grammar recognizes,
338///   excluding `"range"`/`"range_inclusive"` — those, and every other range-family operator
339///   (`"range_from"`/`"range_to"`/`"range_to_inclusive"`/`"range_full"`), are rendered by their
340///   own dedicated [`render`] match arms before any operator ever reaches this function, since
341///   their printed form (`x..y`, no surrounding spaces) doesn't fit this function's shared
342///   `"{lhs} {name} {rhs}"` shape the way every other binary operator here does.
343fn binary_op_level(name: &str) -> Level {
344    match name {
345        "|" => Level::BIT_OR,
346        "^" => Level::BIT_XOR,
347        "&" => Level::BIT_AND,
348        "<<" | ">>" => Level::SHIFT,
349        "+" | "-" => Level::ADDITIVE,
350        "*" | "/" | "%" => Level::MULTIPLICATIVE,
351        "==" | "!=" | "<" | ">" | "<=" | ">=" => Level::COMPARISON,
352        other => unreachable!("binary_op_level called with unknown operator `{other}`"),
353    }
354}
355
356/// Re-emits a literal's exact original text via its span, falling back to an empty string when
357/// none is recoverable (spans built without a live source file — never a real parse; see the
358/// module doc).
359fn render_literal(span: crate::ExprSpan) -> String {
360    span.start.source_text().unwrap_or_default()
361}
362
363/// Scans the source gap between two adjacent AST positions for comments interleaved with
364/// `expected` tokens. Returns just the expected tokens (no comments) when `source` is empty (a
365/// hand-built `Expr`), so a source-less format still works.
366///
367/// - Precondition: when `source` is non-empty, it is the exact text `expr` was parsed from —
368///   `prev_end`/`next_start` must resolve within it.
369///
370/// - Complexity: O(g + e) time and space for g gap bytes and e expected-token bytes.
371fn gap_between(
372    source: &Source<'_>,
373    prev_end: proc_macro2::Span,
374    next_start: proc_macro2::Span,
375    expected: &[&'static str],
376) -> Vec<GapPiece> {
377    scan_gap(source.between(prev_end.end(), next_start.start()), expected)
378}
379
380/// Recovers member-dot comments across closing receiver groups omitted from the AST.
381///
382/// - Precondition: non-empty `source` is the receiver's original parsed text, and both spans
383///   resolve within it.
384/// - Postcondition: grouping delimiters are omitted; comments and the dot retain source order.
385/// - Complexity: O(g) time and space for g gap bytes.
386fn member_gap(
387    source: &Source<'_>,
388    receiver_end: proc_macro2::Span,
389    name_start: proc_macro2::Span,
390) -> Vec<GapPiece> {
391    grouping_gap(source, receiver_end.end(), name_start.start(), &["."])
392}
393
394/// Renders a postfix base, retaining leading group comments inside one owning group.
395///
396/// - Precondition: `start` is the postfix node's original start in `source`, or source is empty.
397/// - Postcondition: the base's leading comments appear once, before its first rendered token.
398/// - Complexity: O(n + g) document space for n base nodes and g recovered gap bytes.
399fn format_postfix_base(
400    base: &Expr,
401    source: &Source<'_>,
402    depth: usize,
403    start: proc_macro2::LineColumn,
404) -> Doc {
405    let leading = grouping_gap(source, start, base.span().start.start(), &[]);
406    if leading.is_empty() {
407        format_at(base, source, depth, Level::POSTFIX)
408    } else {
409        let (text, _) = render(base, source, depth);
410        let prefix = emit_gap(&leading, Spacing::None, 0);
411        Doc::text("(")
412            .append(trivia_doc(prefix.trim_start().to_owned()))
413            .append(text)
414            .append(")")
415    }
416}
417
418/// Renders a comma-separated element list with per-gap comment recovery: the gap before the first
419/// element (after `bounds.0`, the opening delimiter's position, e.g. a `(` or `[`), each
420/// inter-element `,` gap, and the gap after the last element (before `bounds.1`, the position
421/// right past the closing delimiter, e.g. a `)` or `]`). `delims` is `(open, close)`, the
422/// delimiter tokens. When `trailing_comma` is set (a 1-tuple), a `,` is emitted after the sole
423/// element.
424///
425/// `bounds` holds raw [`proc_macro2::LineColumn`] positions (not `Span`s, unlike [`gap_between`]'s
426/// `prev_end`/`next_start`) because `Expr::Apply`'s, `Expr::Tuple`'s, and `Expr::Array`'s own
427/// recorded delimiter span covers the *whole* `(...)`/`[...]` group rather than a single
428/// delimiter character: `AstContext::apply_op`'s `"()"` arm, `is_tuple_or_group`, and
429/// `is_array_expression` all capture `self.last_span` from a `Token::CloseDelim`, and
430/// `lex_lexer`'s flattening gives every `OpenDelim`/`CloseDelim` the *enclosing `Group`'s* span
431/// (see `LexLexer::next`), so a group span's `.start()` lands on the opening delimiter and its
432/// `.end()` lands just past the closing delimiter regardless of which delimiter "produced" it.
433/// Resolving which of `.start()`/`.end()` is the right boundary is the caller's job (it knows
434/// whether it's holding a whole-group span or an ordinary leaf span, e.g. a callee's own end);
435/// this helper just consumes the two positions it's given.
436///
437/// - Complexity: O(n) in the number of elements plus their gap lengths.
438fn emit_list(
439    source: &Source<'_>,
440    depth: usize,
441    delims: (&'static str, &'static str),
442    bounds: (proc_macro2::LineColumn, proc_macro2::LineColumn),
443    elements: &[Expr],
444    trailing_comma: bool,
445) -> Doc {
446    let (open, close) = delims;
447    let (open_pos, close_pos) = bounds;
448    // Scans the raw gap between two already-resolved positions, mirroring `gap_between` but
449    // taking `LineColumn`s directly instead of deriving them from a fixed `prev_end.end()` /
450    // `next_start.start()` pairing (which doesn't fit `open_pos`/`close_pos`; see above).
451    let scan = |from: proc_macro2::LineColumn,
452                to: proc_macro2::LineColumn,
453                expected: &[&'static str]|
454     -> Vec<GapPiece> { scan_gap(source.between(from, to), expected) };
455
456    if elements.is_empty() {
457        // Whole gap between the delimiters: `open`, comments, `close`.
458        let pieces = scan(open_pos, close_pos, &[open, close]);
459        let whole = emit_gap(&pieces, Spacing::None, 0);
460        return trivia_doc(glue_before_close(&glue_after_open(&whole, open), close));
461    }
462    // Opening delimiter + any comment before the first element. `emit_gap`'s comment rendering
463    // always pads a leading space before a comment that opens a gap, which is right when the gap
464    // follows an already-rendered operand (`x /* c */ as i32`) but wrong right after a delimiter
465    // -- `glue_after_open` corrects it back to `f(/* a */ 1i32)`.
466    let mut pre = grouping_gap(source, open_pos, elements[0].span().start.start(), &[open]);
467    if open == "(" {
468        pre.insert(0, GapPiece::Punct("("));
469    }
470    let mut out = trivia_doc(glue_after_open(&emit_gap(&pre, Spacing::None, 0), open));
471    let mut body = Doc::line_();
472    for (i, el) in elements.iter().enumerate() {
473        body = body.append(format_at(el, source, depth, Level::OR));
474        if i + 1 < elements.len() {
475            let pieces = grouping_gap(
476                source,
477                el.span().end.end(),
478                elements[i + 1].span().start.start(),
479                &[","],
480            );
481            body = body.append(gap_doc(&pieces, Spacing::CommaAfter));
482        }
483    }
484    if trailing_comma {
485        body = body.append(",");
486    }
487    // Any comment before the closing delimiter, then the delimiter; symmetric to the opening gap
488    // above (`glue_before_close` drops the trailing space before `)` a comment would otherwise
489    // leave: `f(1i32 /* end */)`, not `f(1i32 /* end */ )`).
490    let mut post = grouping_gap(
491        source,
492        elements[elements.len() - 1].span().end.end(),
493        close_pos,
494        &[close],
495    );
496    if close == ")" {
497        post.push(GapPiece::Punct(")"));
498    }
499    out = out.append(body.nest(4));
500    let tail = glue_before_close(&emit_gap(&post, Spacing::None, 0), close);
501    out.append(Doc::line_()).append(trivia_doc(tail)).group()
502}
503
504/// Strips the one space [`emit_gap`] pads before a comment that immediately opens a gap, when
505/// that gap begins right after `open` — appropriate between an operand and an operator, but not
506/// right after an opening delimiter, which glues directly to a following comment.
507fn glue_after_open(gap: &str, open: &'static str) -> String {
508    match gap.strip_prefix(&format!("{open} ")) {
509        Some(rest) => format!("{open}{rest}"),
510        None => gap.to_string(),
511    }
512}
513
514/// Symmetric to [`glue_after_open`]: strips the one space [`emit_gap`] pads after a comment that
515/// immediately precedes `close`, so a trailing comment glues directly to the closing delimiter.
516fn glue_before_close(gap: &str, close: &'static str) -> String {
517    if let Some((_, last_line)) = gap.rsplit_once('\n') {
518        // `gap` wrapped after a `//` comment; `last_line` is the continuation line `emit_gap`
519        // started with its own indent. When that line holds nothing but indentation before
520        // `close` (no block comment glued onto the same line), those spaces are the
521        // continuation's own indent, not `emit_gap`'s one-space comment padding — leave them
522        // alone instead of stripping one as if it were the padding space.
523        if let Some(rest) = last_line.strip_suffix(close)
524            && rest.chars().all(|c| c == ' ')
525        {
526            return gap.to_string();
527        }
528    }
529    match gap.strip_suffix(&format!(" {close}")) {
530        Some(rest) => format!("{rest}{close}"),
531        None => gap.to_string(),
532    }
533}
534
535/// Returns the `'static` source spelling of a binary operator name, for `scan_gap`'s expected-token
536/// list (which needs `&'static str`). The spelling equals `name` for every binary operator this
537/// formatter's `binary_op_level` accepts.
538fn binary_op_token(name: &str) -> &'static str {
539    match name {
540        "|" => "|",
541        "^" => "^",
542        "&" => "&",
543        "<<" => "<<",
544        ">>" => ">>",
545        "+" => "+",
546        "-" => "-",
547        "*" => "*",
548        "/" => "/",
549        "%" => "%",
550        "==" => "==",
551        "!=" => "!=",
552        "<" => "<",
553        ">" => ">",
554        "<=" => "<=",
555        ">=" => ">=",
556        other => unreachable!("binary_op_token called with unknown operator `{other}`"),
557    }
558}
559
560/// Returns the `'static` source spelling of a unary operator name (`"-"` or `"!"`).
561fn unary_op_token(name: &str) -> &'static str {
562    match name {
563        "-" => "-",
564        "!" => "!",
565        other => unreachable!("unary_op_token called with unknown operator `{other}`"),
566    }
567}
568
569/// Renders one unresolved array-annotation type expression.
570///
571/// Named leaves are emitted verbatim, so built-in names and host-registered custom names format
572/// identically. Recursive arrays keep their bracket nesting, and tuple type expressions keep the
573/// same reusable syntax the parser accepts for future typed CEL surfaces.
574///
575/// - Complexity: O(t + n + b) time for t inspected type-source bytes, n type nodes and
576///   b emitted bytes; O(b) output and O(d) auxiliary stack space for type depth d.
577fn render_type_expr(type_expr: &crate::TypeExpr, source: &Source<'_>) -> String {
578    let span = type_expr.span();
579    let original = source.between(span.start.start(), span.end.end());
580    if original.contains("/*") || original.contains("//") {
581        return original.to_owned();
582    }
583    let mut output = String::new();
584    write_type_expr(type_expr, &mut output);
585    output
586}
587
588/// Appends canonical type syntax without materializing intermediate child strings.
589///
590/// - Complexity: O(n + b) time and O(d) stack space for n nodes, b emitted bytes and depth d.
591fn write_type_expr(type_expr: &crate::TypeExpr, output: &mut String) {
592    match type_expr {
593        crate::TypeExpr::Named { name, args, .. } => {
594            output.push_str(name);
595            if !args.is_empty() {
596                write_type_list(args, output, false);
597            }
598        }
599        crate::TypeExpr::Array { element, .. } => {
600            output.push('[');
601            write_type_expr(element, output);
602            output.push(']');
603        }
604        crate::TypeExpr::Tuple { elements, .. } => {
605            write_type_list(elements, output, true);
606        }
607    }
608}
609
610/// Appends parenthesized type arguments or tuple elements with committed separators.
611///
612/// - Complexity: O(n + b) time and O(d) stack space for n descendant nodes, b bytes and depth d.
613fn write_type_list(elements: &[crate::TypeExpr], output: &mut String, tuple: bool) {
614    output.push('(');
615    // Emit each child directly into the shared output.
616    for (index, element) in elements.iter().enumerate() {
617        if index != 0 {
618            output.push_str(", ");
619        }
620        write_type_expr(element, output);
621    }
622    if tuple && elements.len() == 1 {
623        output.push(',');
624    }
625    output.push(')');
626}
627
628/// Builds `expr`'s document alongside its binding-strength level, so the
629/// caller ([`format_at`]) can decide whether the context it's being placed in requires parens.
630///
631/// `source` is the text `expr` was parsed from (or `""` for a hand-built `Expr`).
632/// Children contribute documents, never independently formatted strings.
633///
634/// - Complexity: O(v + g + b) time and space for v node visits, g inspected source-slice bytes
635///   and b document-text bytes.
636fn render(expr: &Expr, source: &Source<'_>, depth: usize) -> (Doc, Level) {
637    match expr {
638        Expr::Struct {
639            name,
640            name_span,
641            fields,
642            span,
643        } => {
644            let mut out = Doc::text(name.clone());
645            let mut body = Doc::nil();
646            if let Some(first) = fields.first() {
647                let open = grouping_gap(
648                    source,
649                    name_span.end.end(),
650                    first.name_span.start.start(),
651                    &["{"],
652                );
653                body = body.append(body_gap_doc(&open, false));
654                for (index, field) in fields.iter().enumerate() {
655                    body = body.append(Doc::text(field.name.clone()));
656                    let colon = grouping_gap(
657                        source,
658                        field.name_span.end.end(),
659                        field.value.span().start.start(),
660                        &[":"],
661                    );
662                    body = body.append(trivia_doc(emit_gap(&colon, Spacing::CommaAfter, 0)));
663                    body = body.append(format_at(&field.value, source, depth, Level::RANGE));
664                    if let Some(next) = fields.get(index + 1) {
665                        let comma = grouping_gap(
666                            source,
667                            field.value.span().end.end(),
668                            next.name_span.start.start(),
669                            &[","],
670                        );
671                        body = body.append(gap_doc(&comma, Spacing::CommaAfter));
672                    }
673                }
674                let last = fields.last().expect("non-empty fields");
675                let mut tail = grouping_gap(
676                    source,
677                    last.value.span().end.end(),
678                    span.end.end(),
679                    &[",", "}"],
680                );
681                tail.retain(|piece| !matches!(piece, GapPiece::Punct(",")));
682                let closing = emit_gap(&tail, Spacing::Around, 0);
683                out = out
684                    .append(body.nest(4))
685                    .append(Doc::line())
686                    .append(trivia_doc(closing.trim().to_owned()))
687                    .group();
688            } else {
689                let tail = grouping_gap(source, name_span.end.end(), span.end.end(), &["{", "}"]);
690                out = out
691                    .append(" ")
692                    .append(trivia_doc(emit_gap(&tail, Spacing::None, 0)));
693            }
694            (out, Level::PRIMARY)
695        }
696        Expr::Literal { span, .. } => (verbatim_doc(render_literal(*span)), Level::PRIMARY),
697        // The parser records the full-range operator as an identifier; its source token
698        // distinguishes syntax from a genuine value binding with the same lookup name.
699        Expr::Ident { name, span }
700            if name == "range_full"
701                && span
702                    .start
703                    .source_text()
704                    .is_some_and(|text| matches!(text.as_str(), "." | "..")) =>
705        {
706            (Doc::text(".."), Level::RANGE)
707        }
708        Expr::Ident { name, .. } => (Doc::text(name.clone()), Level::PRIMARY),
709        Expr::Logical { op, lhs, rhs, .. } => {
710            let level = match op {
711                LogicalOp::Or => Level::OR,
712                LogicalOp::And => Level::AND,
713            };
714            let op_static: &'static str = match op {
715                LogicalOp::Or => "||",
716                LogicalOp::And => "&&",
717            };
718            let lhs_s = format_at(lhs, source, depth, level);
719            let rhs_s = format_at(rhs, source, depth, level.tighter());
720            let pieces = gap_between(source, lhs.span().end, rhs.span().start, &[op_static]);
721            (binary_doc(lhs_s, &pieces, rhs_s), level)
722        }
723        Expr::Op { operands, .. } if operands.is_empty() => {
724            // Defensive only: a real parse never produces an arity-0 `Expr::Op` (see the
725            // `Expr::Ident` arm above for how `"range_full"` actually arrives), but a hand-built
726            // tree using this representation still round-trips correctly, matching this file's
727            // existing convention of also handling non-parser-producible tree shapes (e.g.
728            // `nested_comparison_needs_parens_on_both_sides`).
729            (Doc::text(".."), Level::RANGE)
730        }
731        Expr::Op {
732            name,
733            operands,
734            span,
735        } if operands.len() == 1
736            && matches!(
737                name.as_str(),
738                "range_from" | "range_to" | "range_to_inclusive"
739            ) =>
740        {
741            // Range's own endpoints are always `or_expression`s (never chained further range
742            // expressions — see `is_range_expression`'s doc comment), so an operand only needs
743            // rendering strictly tighter than Range itself, the same non-chaining treatment
744            // `Level::COMPARISON` gets below.
745            let operand_s = format_at(&operands[0], source, depth, Level::RANGE.tighter());
746            let text = match name.as_str() {
747                "range_from" => {
748                    let pieces = gap_between(source, operands[0].span().end, span.end, &[".."]);
749                    operand_s.append(gap_doc(&pieces, Spacing::None))
750                }
751                "range_to" => {
752                    // `span.start` is the `".."` token's own span; a `gap_between` call always
753                    // scans strictly *after* its `prev_end` argument's end, so `".."` itself can
754                    // never appear inside the scanned gap here (unlike the operand-then-operator
755                    // arms below, where the operator genuinely sits inside the scanned range) —
756                    // it's rendered directly instead, and only a comment is scanned for.
757                    let pieces = gap_between(source, span.start, operands[0].span().start, &[]);
758                    Doc::text("..")
759                        .append(gap_doc(&pieces, Spacing::None))
760                        .append(operand_s)
761                }
762                "range_to_inclusive" => {
763                    let pieces = gap_between(source, span.start, operands[0].span().start, &[]);
764                    Doc::text("..=")
765                        .append(gap_doc(&pieces, Spacing::None))
766                        .append(operand_s)
767                }
768                _ => unreachable!("guarded by the outer match arm"),
769            };
770            (text, Level::RANGE)
771        }
772        Expr::Op {
773            name,
774            operands,
775            span,
776        } if operands.len() == 1 => {
777            let operand_s = format_at(&operands[0], source, depth, Level::UNARY);
778            let op_static = unary_op_token(name);
779            // Same reasoning as the `range_to`/`range_to_inclusive` arm above: the operator
780            // precedes the operand, so it can never appear inside the scanned gap; only a
781            // comment is scanned for, and the operator is rendered directly.
782            let pieces = gap_between(source, span.start, operands[0].span().start, &[]);
783            // Preserve the existing "- -1" disambiguating space when there are no comments.
784            let default_sep = if matches!(&operands[0], Expr::Op { name, operands, .. }
785                if operands.len() == 1 && matches!(name.as_str(), "-" | "!"))
786            {
787                " "
788            } else {
789                ""
790            };
791            let gap = if pieces.iter().any(|p| matches!(p, GapPiece::Comment(_))) {
792                emit_gap(&pieces, Spacing::None, 0)
793            } else {
794                default_sep.to_string()
795            };
796            (
797                Doc::text(op_static)
798                    .append(trivia_doc(gap))
799                    .append(operand_s),
800                Level::UNARY,
801            )
802        }
803        Expr::Op { name, operands, .. } if name == "range" || name == "range_inclusive" => {
804            // Same non-chaining reasoning as the arity-1 range arm above: both endpoints render
805            // strictly tighter than Range.
806            let lhs_s = format_at(&operands[0], source, depth, Level::RANGE.tighter());
807            let rhs_s = format_at(&operands[1], source, depth, Level::RANGE.tighter());
808            let op_static: &'static str = if name == "range_inclusive" {
809                "..="
810            } else {
811                ".."
812            };
813            let pieces = gap_between(
814                source,
815                operands[0].span().end,
816                operands[1].span().start,
817                &[op_static],
818            );
819            (
820                lhs_s
821                    .append(
822                        Doc::softline_()
823                            .append(gap_doc(&pieces, Spacing::None))
824                            .append(rhs_s)
825                            .nest(4),
826                    )
827                    .group(),
828                Level::RANGE,
829            )
830        }
831        Expr::Op { name, operands, .. } => {
832            let level = binary_op_level(name);
833            // Comparison can't chain — the grammar parses at most one per comparison_expression —
834            // so both operands must be strictly tighter than Comparison itself, unlike the other,
835            // left-associative (chaining) binary levels below, where only the right operand does.
836            let (lhs_min, rhs_min) = if level == Level::COMPARISON {
837                (level.tighter(), level.tighter())
838            } else {
839                (level, level.tighter())
840            };
841            let lhs_s = format_at(&operands[0], source, depth, lhs_min);
842            let rhs_s = format_at(&operands[1], source, depth, rhs_min);
843            // The operator token itself has no span; it lives in the gap between the two operands.
844            let op_static = binary_op_token(name);
845            let pieces = gap_between(
846                source,
847                operands[0].span().end,
848                operands[1].span().start,
849                &[op_static],
850            );
851            (binary_doc(lhs_s, &pieces, rhs_s), level)
852        }
853        Expr::Cast {
854            expr,
855            type_name,
856            span,
857        } => {
858            // Left-associative, like multiplicative/additive: the operand only needs to be at
859            // least as tight as Cast itself, so a chain like `x as i32 as f64` reprints without
860            // extra parens.
861            let expr_s = format_at(expr, source, depth, Level::CAST);
862            // The type name has no span of its own, so the gap scanned here runs from the operand's
863            // end through the whole cast node's end (which includes both `as` and the type name);
864            // only `"as"` is expected inside it, and the type name is appended afterward.
865            let pieces = gap_between(source, expr.span().end, span.end, &["as"]);
866            let text = binary_doc(expr_s, &pieces, Doc::text(type_name.clone()));
867            (text, Level::CAST)
868        }
869        Expr::Apply { callee, args, span } => {
870            let mut callee_s = format_postfix_base(callee, source, depth, span.start.start());
871            // Without parentheses a bare member followed by `()` becomes direct dispatch,
872            // rather than an ordinary call of the member's result.
873            if matches!(callee.as_ref(), Expr::Member { args: None, .. }) {
874                callee_s = Doc::text("(").append(callee_s).append(")");
875            }
876            let tail = grouping_gap(source, callee.span().end.end(), span.end.start(), &[]);
877            callee_s = callee_s.append(gap_doc(&tail, Spacing::None));
878            // The call's end span covers the complete argument group, so neither boundary
879            // includes closing callee groups omitted from the AST.
880            let list = emit_list(
881                source,
882                depth,
883                ("(", ")"),
884                (span.end.start(), span.end.end()),
885                args,
886                false,
887            );
888            (callee_s.append(list), Level::POSTFIX)
889        }
890        Expr::Member {
891            receiver,
892            name,
893            name_span,
894            args,
895            span,
896        } => {
897            let receiver_s = format_postfix_base(receiver, source, depth, span.start.start());
898            let gap = member_gap(source, receiver.span().end, name_span.start);
899            let member_gap = if gap.iter().all(|piece| matches!(piece, GapPiece::Punct(_))) {
900                Doc::line_().append(".")
901            } else if gap
902                .iter()
903                .any(|piece| matches!(piece, GapPiece::Comment(Comment::Line(_))))
904            {
905                trivia_doc_at(emit_gap(&gap, Spacing::None, 0), 0)
906            } else {
907                verbatim_doc(emit_gap(&gap, Spacing::None, 0))
908            };
909            let mut text = receiver_s
910                .append(member_gap.append(Doc::text(name.clone())).nest(4))
911                .group();
912            if let Some(args) = args {
913                text = text.append(emit_list(
914                    source,
915                    depth,
916                    ("(", ")"),
917                    (name_span.end.end(), span.end.end()),
918                    args,
919                    false,
920                ));
921            }
922            (text, Level::POSTFIX)
923        }
924        Expr::Tuple { elements, span } => {
925            // `span.start` and `span.end` are both the same whole-group span here (see
926            // `emit_list`'s doc comment): `.start()` is the position of `(`, `.end()` the
927            // position right after `)`.
928            let list = emit_list(
929                source,
930                depth,
931                ("(", ")"),
932                (span.start.start(), span.end.end()),
933                elements,
934                elements.len() == 1,
935            );
936            (list, Level::PRIMARY)
937        }
938        Expr::Array {
939            elements,
940            annotation_span,
941            type_annotation,
942            span,
943        } => {
944            // `span.start` is the `[` token. The closing `]` sits either at `span.end` for an
945            // unannotated array or immediately before `annotation_span` when a `: Type`
946            // ascription follows. Array literals never take a trailing comma (the grammar is
947            // LL(1) and rejects one outright), unlike the one-tuple special case above.
948            let close_pos = annotation_span
949                .map(|annotation_span| annotation_span.start.start())
950                .unwrap_or_else(|| span.end.end());
951            let mut list = emit_list(
952                source,
953                depth,
954                ("[", "]"),
955                (span.start.start(), close_pos),
956                elements,
957                false,
958            );
959            if let Some(type_annotation) = type_annotation {
960                let comments = annotation_span
961                    .map(|annotation| {
962                        gap_between(source, annotation.start, type_annotation.span().start, &[])
963                    })
964                    .unwrap_or_default();
965                let gap = emit_gap(&comments, Spacing::None, 0);
966                list = list
967                    .append(":")
968                    .append(trivia_doc(if gap.is_empty() {
969                        " ".to_owned()
970                    } else {
971                        gap
972                    }))
973                    .append(verbatim_doc(render_type_expr(type_annotation, source)));
974            }
975            (list, Level::PRIMARY)
976        }
977        Expr::TupleIndex { base, index, span } => (
978            format_postfix_base(base, source, depth, span.start.start())
979                .append(gap_doc(
980                    &grouping_gap(source, base.span().end.end(), span.end.start(), &["."]),
981                    Spacing::None,
982                ))
983                .append(Doc::text(index.to_string())),
984            Level::POSTFIX,
985        ),
986        Expr::If {
987            cond,
988            then_branch,
989            else_branch,
990            span,
991        } => {
992            // `if` <cond> `{` <then_branch> `}` [ `else` ( `{` <else_branch> `}` | <else-if> ) ].
993            // None of `if`/`{`/`}`/`else` carry their own span (each is a fixed keyword/delimiter
994            // consumed structurally by the parser, never recorded as its own node), so every gap
995            // between two real spans is scanned for its expected fixed token(s) alongside any
996            // interleaved comment. `Spacing::Around` is used throughout (rather than `None`) so
997            // that a gap holding more than one fixed token (`"}"`, `"else"`, and — for a braced
998            // else — `"{"`) still gets a single space between each token and the next, matching
999            // the pre-`scan_gap` literal `" } else { "` spacing exactly in the comment-free case.
1000            let cond_s = restricted_doc(
1001                cond,
1002                source,
1003                depth,
1004                crate::ExpressionRestriction::NoStructLiteral,
1005            );
1006            let then_s = format_at(then_branch, source, depth, Level::OR);
1007            let if_gap = grouping_gap(source, span.start.start(), header_start(cond), &["if"]);
1008            let open_gap = grouping_gap(
1009                source,
1010                header_end(cond),
1011                then_branch.span().start.start(),
1012                &["{"],
1013            );
1014            let mut text = Doc::nil();
1015            // Only the very first gap is `trim_start`'d: it opens the whole node's text, so any
1016            // leading space `Spacing::Around` would otherwise add before `"if"` must be dropped.
1017            text = text.append(trivia_doc(
1018                emit_gap(&if_gap, Spacing::Around, 0)
1019                    .trim_start()
1020                    .to_owned(),
1021            ));
1022            text = text.append(cond_s);
1023            text = text.append(body_gap_doc(&open_gap, false).append(then_s).nest(4));
1024            match else_branch {
1025                None => {
1026                    // No `else`: the gap between the then-branch and the node's own end holds
1027                    // just the closing `}` (plus any comment before it).
1028                    let close_gap = gap_between(source, then_branch.span().end, span.end, &["}"]);
1029                    // `trim_end`'d because this is the node's last piece of text: a trailing
1030                    // space here would otherwise survive into the caller's output (or, wrapped in
1031                    // parens by `format_at`, land right before the close-paren).
1032                    text = text.append(Doc::line()).append(trivia_doc(
1033                        emit_gap(&close_gap, Spacing::Around, 0).trim().to_owned(),
1034                    ));
1035                }
1036                Some(else_branch) => {
1037                    let is_chain = matches!(else_branch.as_ref(), Expr::If { .. });
1038                    // An `else if` chain has no braces of its own around the nested `if`; a plain
1039                    // `else` block does.
1040                    let expected: &[&'static str] = if is_chain {
1041                        &["}", "else"]
1042                    } else {
1043                        &["}", "else", "{"]
1044                    };
1045                    // One scan across the whole then-branch-to-else-branch gap (rather than one
1046                    // scan per token) so a comment sitting anywhere in it -- before `}`, between
1047                    // `}` and `else`, or between `else` and `{` -- is recovered in source order.
1048                    let else_gap = gap_between(
1049                        source,
1050                        then_branch.span().end,
1051                        else_branch.span().start,
1052                        expected,
1053                    );
1054                    text = text.append(Doc::line());
1055                    if is_chain {
1056                        // The nested `if` recurses through `render`, which lays out its own
1057                        // `if`/`{`/`}`/`else` gaps (including its own trailing `trim_end`), so
1058                        // nothing further is appended here.
1059                        let (else_s, _) = render(else_branch, source, depth);
1060                        text = text.append(body_gap_doc(&else_gap, true)).append(else_s);
1061                    } else {
1062                        let else_s = format_at(else_branch, source, depth, Level::OR);
1063                        text = text.append(body_gap_doc(&else_gap, true).append(else_s).nest(4));
1064                        let final_close_gap =
1065                            gap_between(source, else_branch.span().end, span.end, &["}"]);
1066                        text = text.append(Doc::line()).append(trivia_doc(
1067                            emit_gap(&final_close_gap, Spacing::Around, 0)
1068                                .trim()
1069                                .to_owned(),
1070                        ));
1071                    }
1072                }
1073            }
1074            (text.group(), Level::PRIMARY)
1075        }
1076        Expr::Closure { params, body, span } => {
1077            let body_s = format_at(body, source, depth, Level::OR);
1078            let header = if params.is_empty() {
1079                Doc::text("||")
1080            } else {
1081                let leading = gap_between(source, span.start, params[0].name_span.start, &[]);
1082                let mut parameters =
1083                    trivia_doc(emit_gap(&leading, Spacing::None, 0).trim_start().to_owned());
1084                for (index, param) in params.iter().enumerate() {
1085                    if index > 0 {
1086                        let comma = gap_between(
1087                            source,
1088                            params[index - 1].type_expr.span().end,
1089                            param.name_span.start,
1090                            &[","],
1091                        );
1092                        parameters = parameters.append(gap_doc(&comma, Spacing::CommaAfter));
1093                    }
1094                    let colon = gap_between(
1095                        source,
1096                        param.name_span.end,
1097                        param.type_expr.span().start,
1098                        &[":"],
1099                    );
1100                    parameters = parameters
1101                        .append(Doc::text(param.name.clone()))
1102                        .append(trivia_doc(emit_gap(&colon, Spacing::CommaAfter, 0)))
1103                        .append(verbatim_doc(render_type_expr(&param.type_expr, source)));
1104                }
1105                Doc::text("|")
1106                    .append(Doc::line_().append(parameters).nest(4))
1107                    .append(Doc::line_())
1108                    .append("|")
1109                    .group()
1110            };
1111            let (tail_start, expected): (proc_macro2::Span, &[&'static str]) = match params.last() {
1112                Some(last) => (last.type_expr.span().end, &["|"]),
1113                None => (span.start, &[]),
1114            };
1115            let pieces = gap_between(source, tail_start, body.span().start, expected);
1116            let comments: Vec<GapPiece> = pieces
1117                .into_iter()
1118                .filter(|p| matches!(p, GapPiece::Comment(_)))
1119                .collect();
1120            let gap = emit_gap(&comments, Spacing::None, 0);
1121            // A `//` comment among `comments` made `emit_gap` end `gap` with a line wrap (`\n` +
1122            // continuation indent); the body must continue right there, so `gap` is used verbatim
1123            // (already correctly spaced/wrapped) rather than trimmed — trimming would collapse the
1124            // wrap and glue the body onto the comment's own line, commenting it out.
1125            let has_line_comment = comments
1126                .iter()
1127                .any(|p| matches!(p, GapPiece::Comment(Comment::Line(_))));
1128            let sep = if has_line_comment {
1129                trivia_doc_at(gap, 0)
1130            } else if gap.trim().is_empty() {
1131                Doc::line()
1132            } else {
1133                trivia_doc(format!(" {} ", gap.trim()))
1134            };
1135            // Closures are complete expressions; operator and postfix operands require grouping.
1136            (
1137                header.append(sep.append(body_s).nest(4)).group(),
1138                Level::RANGE,
1139            )
1140        }
1141    }
1142}
1143
1144/// Returns the first rendered header token's position, excluding omitted operand groups.
1145///
1146/// Prefix/postfix operators and explicit expression delimiters retain their starting positions;
1147/// postfix rendering owns any leading receiver-group trivia.
1148///
1149/// - Complexity: O(h) in the visited leftmost AST path, with O(h) stack space.
1150fn header_start(expr: &Expr) -> proc_macro2::LineColumn {
1151    match expr {
1152        Expr::Op { name, operands, .. }
1153            if operands.len() == 2 || (name == "range_from" && operands.len() == 1) =>
1154        {
1155            header_start(&operands[0])
1156        }
1157        Expr::Logical { lhs, .. } => header_start(lhs),
1158        Expr::Cast { expr, .. } => header_start(expr),
1159        _ => expr.span().start.start(),
1160    }
1161}
1162
1163/// Returns the last rendered header token's position, excluding omitted operand/body groups.
1164///
1165/// Postfix operators and explicit expression delimiters retain their own ending positions.
1166///
1167/// - Complexity: O(h) in the visited rightmost AST path, with O(h) stack space.
1168fn header_end(expr: &Expr) -> proc_macro2::LineColumn {
1169    match expr {
1170        Expr::Op { name, operands, .. } if name != "range_from" && !operands.is_empty() => {
1171            header_end(operands.last().expect("non-empty operands"))
1172        }
1173        Expr::Logical { rhs, .. } => header_end(rhs),
1174        Expr::Closure { body, .. } => header_end(body),
1175        _ => expr.span().end.end(),
1176    }
1177}
1178
1179/// Recovers a field or header gap across grouping parentheses omitted from the AST.
1180///
1181/// - Precondition: positions refer to `source`, or `source` is empty.
1182/// - Postcondition: comments and expected tokens retain source order without grouping tokens.
1183/// - Complexity: O(g + e) time and space for g gap bytes and e expected-token bytes.
1184fn grouping_gap(
1185    source: &Source<'_>,
1186    from: proc_macro2::LineColumn,
1187    to: proc_macro2::LineColumn,
1188    expected: &[&'static str],
1189) -> Vec<GapPiece> {
1190    let gap = source.between(from, to);
1191    let mut rest = gap;
1192    let mut tokens = Vec::new();
1193    let mut next = 0;
1194    loop {
1195        rest = rest.trim_start();
1196        if let Some(after) = rest.strip_prefix("//") {
1197            rest = &after[after.find('\n').unwrap_or(after.len())..];
1198        } else if let Some((_, after)) = take_block_comment(rest) {
1199            rest = after;
1200        } else if let Some(after) = rest.strip_prefix('(') {
1201            tokens.push("(");
1202            rest = after;
1203        } else if let Some(after) = rest.strip_prefix(')') {
1204            tokens.push(")");
1205            rest = after;
1206        } else if next < expected.len() && rest.starts_with(expected[next]) {
1207            tokens.push(expected[next]);
1208            rest = &rest[expected[next].len()..];
1209            next += 1;
1210        } else {
1211            break;
1212        }
1213    }
1214    tokens.extend_from_slice(&expected[next..]);
1215    scan_gap(gap, &tokens)
1216        .into_iter()
1217        .filter(|piece| !matches!(piece, GapPiece::Punct("(" | ")")))
1218        .collect()
1219}
1220
1221/// Detects record syntax reached without an explicit expression boundary in a header.
1222///
1223/// - Complexity: O(n) in the number of visited AST nodes.
1224fn header_contains_record(expr: &Expr) -> bool {
1225    match expr {
1226        Expr::Struct { .. } => true,
1227        Expr::Op { operands, .. } => operands.iter().any(header_contains_record),
1228        Expr::Logical { lhs, rhs, .. } => {
1229            header_contains_record(lhs) || header_contains_record(rhs)
1230        }
1231        Expr::Cast { expr, .. } | Expr::Closure { body: expr, .. } => header_contains_record(expr),
1232        Expr::Member { receiver, .. } => header_contains_record(receiver),
1233        Expr::TupleIndex { base, .. } => header_contains_record(base),
1234        Expr::Apply { callee, .. } => header_contains_record(callee),
1235        Expr::Literal { .. }
1236        | Expr::Ident { .. }
1237        | Expr::Tuple { .. }
1238        | Expr::Array { .. }
1239        | Expr::If { .. } => false,
1240    }
1241}
1242
1243/// Formats an embedded expression while preserving a header's brace reservation.
1244///
1245/// Groups the complete header when an otherwise unparenthesized record would be ambiguous.
1246/// Ordinary delimiters and nested branch bodies retain their normal formatting.
1247///
1248/// - Precondition: `source` is the parsed source, or empty for a hand-built AST.
1249/// - Complexity: O(s + v + g + b) construction time and space for s source bytes,
1250///   v node visits, g inspected source-slice bytes and b document-text bytes, plus pretty layout.
1251///
1252/// # Examples
1253/// ```
1254/// use cel_parser::{AstContext, ExpressionRestriction, OpLookup, Parser, format_expr_restricted};
1255/// let source = "(Point { x: 1.0 }).x";
1256/// let ast = Parser::<AstContext>::new(OpLookup::new()).parse_str_ast(source).unwrap();
1257/// let text = format_expr_restricted(&ast, source, 0, ExpressionRestriction::NoStructLiteral);
1258/// assert_eq!(text, "(Point { x: 1.0 }.x)");
1259/// ```
1260pub fn format_expr_restricted(
1261    expr: &Expr,
1262    source: &str,
1263    depth: usize,
1264    restriction: crate::ExpressionRestriction,
1265) -> String {
1266    render_document(
1267        restricted_doc(expr, &Source::new(source), depth, restriction),
1268        depth,
1269        depth * 4,
1270        100,
1271        0,
1272    )
1273}
1274
1275/// Builds a brace-reserving expression without independently laying out its children.
1276///
1277/// - Complexity: O(v + g + b) time and space for v node visits, g inspected source-slice bytes
1278///   and b document-text bytes.
1279fn restricted_doc(
1280    expr: &Expr,
1281    source: &Source<'_>,
1282    depth: usize,
1283    restriction: crate::ExpressionRestriction,
1284) -> Doc {
1285    let text = format_at(expr, source, depth, Level::RANGE);
1286    if restriction == crate::ExpressionRestriction::NoStructLiteral && header_contains_record(expr)
1287    {
1288        Doc::text("(").append(text).append(")")
1289    } else {
1290        text
1291    }
1292}
1293
1294/// Formats a brace-reserving header and its surrounding comments within explicit boundaries.
1295///
1296/// `bounds` runs from just after the introducing keyword to just before the body brace.
1297/// Grouping parentheses omitted from the AST do not interrupt comment recovery.
1298///
1299/// - Precondition: `source` is the parsed source and `bounds` encloses `expr`, or source is empty.
1300/// - Postcondition: boundary comments appear exactly once in source order.
1301/// - Complexity: O(s + v + g + b) construction time and space for s source bytes,
1302///   v node visits, g inspected source-slice bytes and b document-text bytes, plus pretty layout.
1303///
1304/// # Examples
1305/// ```
1306/// use cel_parser::{AstContext, OpLookup, Parser, format_expr_header};
1307/// let source = "(Point { x: 1.0 }) /* keep */";
1308/// let expr = Parser::<AstContext>::new(OpLookup::new()).parse_str_ast(source).unwrap();
1309/// let text = format_expr_header(&expr, source, 0, (
1310///     proc_macro2::LineColumn { line: 1, column: 0 },
1311///     proc_macro2::LineColumn { line: 1, column: source.len() },
1312/// ));
1313/// assert_eq!(text, "(Point { x: 1.0 }) /* keep */");
1314/// ```
1315pub fn format_expr_header(
1316    expr: &Expr,
1317    source: &str,
1318    depth: usize,
1319    bounds: (proc_macro2::LineColumn, proc_macro2::LineColumn),
1320) -> String {
1321    format_expr_header_with_layout(expr, source, depth, bounds, depth * 4, 100, 0)
1322}
1323
1324/// Formats a brace-reserving header at `column`, reserving `suffix_columns` on its last line.
1325///
1326/// The target width includes the already-written prefix and following punctuation. Continuations
1327/// nest four spaces from `depth`; unbreakable tokens and comments may exceed `width`.
1328///
1329/// - Precondition: `source` is the parsed source and `bounds` encloses `expr`, or source is empty.
1330/// - Complexity: O(s + v + g + b) construction time and space for s source bytes,
1331///   v node visits, g inspected source-slice bytes and b document-text bytes, plus pretty layout.
1332///
1333/// # Examples
1334/// ```
1335/// use cel_parser::{AstContext, OpLookup, Parser, format_expr_header_with_layout, LineColumn};
1336/// let source = "value";
1337/// let expr = Parser::<AstContext>::new(OpLookup::new()).parse_str_ast(source).unwrap();
1338/// assert_eq!(format_expr_header_with_layout(&expr, source, 1,
1339///     (LineColumn { line: 1, column: 0 }, LineColumn { line: 1, column: 5 }),
1340///     16, 100, 2), "value");
1341/// ```
1342pub fn format_expr_header_with_layout(
1343    expr: &Expr,
1344    source: &str,
1345    depth: usize,
1346    bounds: (proc_macro2::LineColumn, proc_macro2::LineColumn),
1347    column: usize,
1348    width: usize,
1349    suffix_columns: usize,
1350) -> String {
1351    let source = &Source::new(source);
1352    let leading = grouping_gap(source, bounds.0, header_start(expr), &[]);
1353    let trailing = grouping_gap(source, header_end(expr), bounds.1, &[]);
1354    let mut text = trivia_doc(emit_gap(&leading, Spacing::None, 0).trim_start().to_owned());
1355    text = text.append(restricted_doc(
1356        expr,
1357        source,
1358        depth,
1359        crate::ExpressionRestriction::NoStructLiteral,
1360    ));
1361    let tail = emit_gap(&trailing, Spacing::None, 0);
1362    if matches!(trailing.last(), Some(GapPiece::Comment(Comment::Line(_)))) {
1363        text = text.append(trivia_doc(tail));
1364    } else {
1365        text = text.append(trivia_doc(tail.trim_end().to_owned()));
1366    }
1367    render_document(text, depth, column, width, suffix_columns)
1368}
1369
1370/// Renders `expr`, wrapping it in parens if its own level is looser than `min_level` requires.
1371///
1372/// `source` and `depth` are forwarded to [`render`] unchanged; see its doc comment.
1373///
1374/// - Complexity: O(v + g + b) time and space for v node visits, g inspected source-slice bytes
1375///   and b document-text bytes.
1376fn format_at(expr: &Expr, source: &Source<'_>, depth: usize, min_level: Level) -> Doc {
1377    let (text, level) = render(expr, source, depth);
1378    if level < min_level {
1379        Doc::text("(").append(text).append(")").group()
1380    } else {
1381        text
1382    }
1383}
1384
1385/// Pretty-prints `expr` back to CEL source text — see the module doc for the printing rules.
1386///
1387/// `source` is the text `expr` was parsed from (or `""` for a hand-built `Expr`); `depth` supplies
1388/// the base indentation for wrapped continuations and the assumed initial output column.
1389/// Postfix bases retain leading group comments, and member-dot comments survive omitted groups.
1390/// Closure bases stay grouped so following postfixes cannot move into their bodies.
1391///
1392/// - Complexity: O(s + v + g + b) construction time and space for s source bytes,
1393///   v node visits, g inspected source-slice bytes and b document-text bytes, plus pretty layout.
1394///
1395/// # Examples
1396///
1397/// ```
1398/// use cel_parser::{AstContext, OpLookup, Parser, format_expr};
1399///
1400/// let mut parser = Parser::<AstContext>::new(OpLookup::new());
1401/// let expr = parser.parse_str_ast("(1i32 + 2i32) * 3i32").unwrap();
1402/// assert_eq!(format_expr(&expr, "(1i32 + 2i32) * 3i32", 0), "(1i32 + 2i32) * 3i32");
1403/// ```
1404pub fn format_expr(expr: &Expr, source: &str, depth: usize) -> String {
1405    format_expr_with_layout(expr, source, depth, depth * 4, 100, 0)
1406}
1407
1408/// Formats an expression at the actual output `column` with a target `width`.
1409///
1410/// Reserves `suffix_columns` for following punctuation on the final line. Short expressions remain
1411/// inline. Lists, records and closure bodies nest four spaces; binary continuations lead with their
1412/// operator. Unbreakable tokens and comments may exceed the target. The result excludes the prefix,
1413/// suffix and initial indentation; continuation indentation starts at `depth * 4`.
1414/// When an indented continuation has more room and the first fragment cannot fit after the prefix,
1415/// the expression starts on a new line indented four spaces beyond `depth`.
1416///
1417/// - Precondition: `source` is the expression's parsed source, or empty for a hand-built AST.
1418/// - Complexity: O(s + v + g + b) construction time and space for s source bytes,
1419///   v node visits, g inspected source-slice bytes and b document-text bytes, plus pretty layout.
1420///
1421/// # Examples
1422/// ```
1423/// use cel_parser::{AstContext, OpLookup, Parser, format_expr_with_layout};
1424/// let source = "first + second";
1425/// let expr = Parser::<AstContext>::new(OpLookup::new()).parse_str_ast(source).unwrap();
1426/// assert_eq!(format_expr_with_layout(&expr, source, 1, 12, 26, 1),
1427///     "first\n        + second");
1428/// ```
1429pub fn format_expr_with_layout(
1430    expr: &Expr,
1431    source: &str,
1432    depth: usize,
1433    column: usize,
1434    width: usize,
1435    suffix_columns: usize,
1436) -> String {
1437    render_document(
1438        format_at(expr, &Source::new(source), depth, Level::RANGE),
1439        depth,
1440        column,
1441        width,
1442        suffix_columns,
1443    )
1444}
1445
1446/// Lays out one complete expression document, accounting for both embedding boundaries.
1447///
1448/// - Complexity: O(n + column + suffix_columns) space; layout uses the document library's group
1449///   fit traversal and never renders subtrees to measure them.
1450fn render_document(
1451    doc: Doc,
1452    depth: usize,
1453    column: usize,
1454    width: usize,
1455    suffix_columns: usize,
1456) -> String {
1457    let prefix = " ".repeat(column);
1458    let suffix = " ".repeat(suffix_columns);
1459    let doc = if column > depth * 4 + 4 {
1460        // Group only the boundary: fitting inspects the following document in broken mode,
1461        // stopping at its first existing break instead of demanding a flat whole expression.
1462        Doc::hardline()
1463            .append("    ")
1464            .flat_alt(Doc::nil())
1465            .group()
1466            .append(Doc::column(move |current| {
1467                if current == column {
1468                    doc.clone()
1469                } else {
1470                    doc.clone().nest(4)
1471                }
1472            }))
1473    } else {
1474        doc
1475    };
1476    let document = Doc::text(prefix)
1477        .append(doc.nest((depth * 4) as isize))
1478        .append(Doc::text(suffix));
1479    let mut out = String::new();
1480    document
1481        .render_fmt(width, &mut out)
1482        .expect("writing to String is infallible");
1483    out.truncate(out.len() - suffix_columns);
1484    out.drain(..column);
1485    out
1486}
1487
1488#[cfg(test)]
1489mod tests {
1490    use super::*;
1491    use crate::{AstContext, OpLookup, Parser};
1492
1493    /// Long binary expressions wrap before their operators and reparse unchanged.
1494    #[test]
1495    fn wraps_long_expressions_with_leading_operators() {
1496        let source = std::iter::repeat_n("long_operand_name", 8)
1497            .collect::<Vec<_>>()
1498            .join(" + ");
1499        let text = fmt(&source);
1500        assert!(text.contains("\n    + "), "{text}");
1501        assert!(text.lines().all(|line| line.len() <= 100), "{text}");
1502        assert_eq!(fmt(&text), text);
1503    }
1504
1505    /// Binary ranges retain compact spelling and expose a zero-width leading-operator boundary.
1506    #[test]
1507    fn wraps_binary_ranges_before_their_operators() {
1508        for operator in ["..", "..="] {
1509            let source = format!("{}{}{}", "a".repeat(45), operator, "b".repeat(45));
1510            let expr = parse(&source);
1511            let text = format_expr_with_layout(&expr, &source, 1, 13, 100, 1);
1512            assert_eq!(
1513                text,
1514                format!("{}\n        {operator}{}", "a".repeat(45), "b".repeat(45))
1515            );
1516            assert_eq!(
1517                format_expr_with_layout(&parse(&text), &text, 1, 13, 100, 1),
1518                text
1519            );
1520            assert_eq!(
1521                fmt(&format!("left{operator}right")),
1522                format!("left{operator}right")
1523            );
1524            let call = format!("call({source})");
1525            let formatted = fmt(&call);
1526            assert!(
1527                formatted.lines().all(|line| line.len() <= 100),
1528                "{formatted}"
1529            );
1530            assert_eq!(fmt(&formatted), formatted);
1531        }
1532    }
1533
1534    /// An empty closure's header can move with its body when the embedding prefix fills the line.
1535    #[test]
1536    fn empty_closure_can_break_before_its_header() {
1537        let source = "|| predicate";
1538        let expr = parse(source);
1539        let text = format_expr_with_layout(&expr, source, 1, 100, 100, 1);
1540        assert_eq!(text, "\n        || predicate");
1541        assert_eq!(
1542            format_expr_with_layout(&parse(&text), &text, 1, 100, 100, 1),
1543            text
1544        );
1545    }
1546
1547    /// Nested grammar-owned lists break without introducing unsupported trailing commas.
1548    #[test]
1549    fn wraps_nested_calls_records_arrays_and_closures() {
1550        let name = "a_long_argument_name_that_occupies_thirty_columns";
1551        for source in [
1552            format!("outer(inner({name}, {name}), [{name}, {name}])"),
1553            format!("Record {{ first: ({name}, {name}), second: [{name}, {name}] }}"),
1554            format!("map([{name}, {name}], |value: i32| value + {name} + {name})"),
1555        ] {
1556            let text = fmt(&source);
1557            assert!(text.contains('\n'), "{text}");
1558            assert!(text.lines().all(|line| line.len() <= 100), "{text}");
1559            assert_eq!(fmt(&text), text);
1560        }
1561    }
1562
1563    /// Parameter lists wrap and retain comments on both sides of their separators.
1564    #[test]
1565    fn wraps_closure_parameters_without_losing_comments() {
1566        let source = "|/* first */ a_long_parameter: i32 /* second */, another_long_parameter: i32, third_long_parameter: i32, fourth_long_parameter: i32| a_long_parameter";
1567        let text = fmt(source);
1568        for label in ["first", "second"] {
1569            assert_eq!(text.matches(label).count(), 1, "{text}");
1570        }
1571        assert!(text.lines().all(|line| line.len() <= 100), "{text}");
1572        assert_eq!(fmt(&text), text);
1573    }
1574
1575    /// Block comments do not disable leading-operator break opportunities.
1576    #[test]
1577    fn wraps_commented_binary_gaps() {
1578        let operand = "a".repeat(45);
1579        let source = format!("{operand} /* keep */ + {operand} + {operand}");
1580        let text = fmt(&source);
1581        assert!(text.lines().all(|line| line.len() <= 100), "{text}");
1582        assert_eq!(text.matches("keep").count(), 1);
1583        assert_eq!(fmt(&text), text);
1584    }
1585
1586    /// Consecutive line comments keep separate mandatory line boundaries.
1587    #[test]
1588    fn consecutive_line_comments_remain_separate() {
1589        let text = fmt("1 + // first\n// second\n2");
1590        assert_eq!(text, "1 + // first\n    // second\n    2");
1591        assert_eq!(fmt(&text), text);
1592    }
1593
1594    /// A comment after an opening brace cannot consume the first field or branch expression.
1595    #[test]
1596    fn brace_opening_line_comments_keep_their_mandatory_breaks() {
1597        for source in [
1598            "Record { // first\n value: 1 }",
1599            "if condition { // first\n 1 } else { // second\n 2 }",
1600        ] {
1601            let text = fmt(source);
1602            assert!(text.contains("// first\n"), "{text}");
1603            assert_eq!(fmt(&text), text);
1604        }
1605    }
1606
1607    /// Width includes the actual prefix and suffix, not an independently reduced child budget.
1608    #[test]
1609    fn embedding_reserves_exact_prefix_and_suffix_columns() {
1610        let source = "first + second";
1611        let expr = parse(source);
1612        assert_eq!(format_expr_with_layout(&expr, source, 1, 12, 27, 1), source);
1613        assert_eq!(
1614            format_expr_with_layout(&expr, source, 1, 12, 26, 1),
1615            "first\n        + second"
1616        );
1617        let bounds = (
1618            proc_macro2::LineColumn { line: 1, column: 0 },
1619            proc_macro2::LineColumn {
1620                line: 1,
1621                column: source.len(),
1622            },
1623        );
1624        assert_eq!(
1625            format_expr_header_with_layout(&expr, source, 1, bounds, 12, 28, 2),
1626            source
1627        );
1628        assert_eq!(
1629            format_expr_header_with_layout(&expr, source, 1, bounds, 12, 27, 2),
1630            "first\n        + second"
1631        );
1632    }
1633
1634    /// Literal-internal newlines retain their spelling rather than inheriting expression nesting.
1635    #[test]
1636    fn multiline_literal_spelling_is_independent_of_layout_nesting() {
1637        let source = "call(r#\"first\n  second\"#, 0xffi32)";
1638        let expr = parse(source);
1639        let text = format_expr_with_layout(&expr, source, 2, 20, 30, 1);
1640        assert!(text.contains("r#\"first\n  second\"#"), "{text}");
1641        assert!(text.contains("0xffi32"));
1642        assert_eq!(
1643            format_expr_with_layout(&parse(&text), &text, 2, 20, 30, 1),
1644            text
1645        );
1646    }
1647
1648    /// Layout retains grammar, literal spelling, and precedence across every compound surface.
1649    #[test]
1650    fn narrow_layout_preserves_expression_semantics_and_reaches_a_fixed_point() {
1651        for source in [
1652            "-(first_long_name + second_long_name) as i32",
1653            "first_long_name - (second_long_name - third_long_name)",
1654            "(|value: i32| value + first_long_name)(second_long_name)",
1655            "(holder.long_member_name)(first_long_name, second_long_name)",
1656            "first_long_name..=second_long_name",
1657            "[first_long_name, second_long_name]: [i32]",
1658            "(first_long_name + second_long_name,)",
1659            "[(first_long_name, second_long_name), (third_long_name, fourth_long_name)]",
1660            "if (Record { value: first_long_name }).value { 0xffi32 } else { 1i32 }",
1661            "first_long_name /* keep\n block */ + second_long_name",
1662            "call(first_long_name, // keep line\n second_long_name)",
1663            "first_long_name + // first\n // second\n second_long_name",
1664        ] {
1665            let expr = parse(source);
1666            let text = format_expr_with_layout(&expr, source, 0, 0, 40, 0);
1667            let parsed = parse(&text);
1668            assert_eq!(
1669                format_expr_with_layout(&parsed, &text, 0, 0, 40, 0),
1670                text,
1671                "{source}"
1672            );
1673            assert_eq!(
1674                format_expr_with_layout(&parsed, &text, 0, 0, 10_000, 0),
1675                format_expr_with_layout(&expr, source, 0, 0, 10_000, 0),
1676                "{source} -> {text}"
1677            );
1678        }
1679        for (source, expected) in [
1680            ("-(10 + 3) as i32", -13),
1681            ("10 - (3 - 1)", 8),
1682            ("(|value: i32| value + 5)(7)", 12),
1683            ("(1 + 2,).0", 3),
1684            ("if true && false { 1 / 0 } else { 42 }", 42),
1685        ] {
1686            let text = format_expr_with_layout(&parse(source), source, 0, 0, 12, 0);
1687            let mut segment = crate::CELParser::new(OpLookup::new())
1688                .parse_str(&text)
1689                .unwrap();
1690            assert_eq!(
1691                segment.call0::<i32>().unwrap(),
1692                expected,
1693                "{source} -> {text}"
1694            );
1695        }
1696    }
1697
1698    /// Maps empty, Unicode, newline, and final-line positions through one reusable index.
1699    #[test]
1700    fn source_index_preserves_character_boundaries() {
1701        let source = Source::new("é\nβ\n");
1702        assert_eq!(
1703            source.byte(proc_macro2::LineColumn { line: 1, column: 1 }),
1704            2
1705        );
1706        assert_eq!(
1707            source.byte(proc_macro2::LineColumn { line: 2, column: 1 }),
1708            5
1709        );
1710        assert_eq!(
1711            source.byte(proc_macro2::LineColumn { line: 3, column: 0 }),
1712            6
1713        );
1714        assert_eq!(
1715            Source::new("").byte(proc_macro2::LineColumn { line: 1, column: 0 }),
1716            0
1717        );
1718    }
1719
1720    /// Parses one complete expression for formatter contracts.
1721    fn parse(source: &str) -> Expr {
1722        Parser::<AstContext>::new(OpLookup::new())
1723            .parse_str_ast(source)
1724            .unwrap()
1725    }
1726
1727    /// Formats one parsed expression with the default layout.
1728    fn fmt(source: &str) -> String {
1729        format_expr(&parse(source), source, 0)
1730    }
1731
1732    #[test]
1733    fn additive_and_multiplicative_reprint_without_extra_parens() {
1734        assert_eq!(fmt("1i32 + 2i32 * 3i32"), "1i32 + 2i32 * 3i32");
1735    }
1736
1737    #[test]
1738    fn explicit_grouping_that_changes_precedence_keeps_its_parens() {
1739        assert_eq!(fmt("(1i32 + 2i32) * 3i32"), "(1i32 + 2i32) * 3i32");
1740    }
1741
1742    #[test]
1743    fn left_associative_chain_at_the_same_precedence_has_no_parens() {
1744        assert_eq!(fmt("1i32 - 2i32 - 3i32"), "1i32 - 2i32 - 3i32");
1745    }
1746
1747    #[test]
1748    fn a_right_leaning_tree_at_the_same_precedence_needs_parens() {
1749        // Not producible by real parsing (the grammar's additive_expression loop is always
1750        // left-associative) — built by hand to prove the printer round-trips a tree shape it
1751        // didn't itself produce. Uses Ident operands (rendered from `name`, not a span) so the
1752        // assertion reads as real text rather than the no-source-text fallback.
1753        fn ident(name: &str) -> Expr {
1754            Expr::Ident {
1755                name: name.to_string(),
1756                span: point(),
1757            }
1758        }
1759        fn point() -> crate::ExprSpan {
1760            crate::ExprSpan {
1761                start: proc_macro2::Span::call_site(),
1762                end: proc_macro2::Span::call_site(),
1763            }
1764        }
1765        let expr = Expr::Op {
1766            name: "-".to_string(),
1767            operands: vec![
1768                ident("a"),
1769                Expr::Op {
1770                    name: "-".to_string(),
1771                    operands: vec![ident("b"), ident("c")],
1772                    span: point(),
1773                },
1774            ],
1775            span: point(),
1776        };
1777        assert_eq!(format_expr(&expr, "", 0), "a - (b - c)");
1778    }
1779
1780    #[test]
1781    fn nested_comparison_needs_parens_on_both_sides() {
1782        // Also not producible by real parsing (comparison_expression allows at most one
1783        // comparison, never a nested one) — proves format_expr stays reparseable even for a
1784        // hand-built tree shape the grammar itself can't emit.
1785        fn ident(name: &str) -> Expr {
1786            Expr::Ident {
1787                name: name.to_string(),
1788                span: crate::ExprSpan {
1789                    start: proc_macro2::Span::call_site(),
1790                    end: proc_macro2::Span::call_site(),
1791                },
1792            }
1793        }
1794        let inner = Expr::Op {
1795            name: "==".to_string(),
1796            operands: vec![ident("a"), ident("b")],
1797            span: crate::ExprSpan {
1798                start: proc_macro2::Span::call_site(),
1799                end: proc_macro2::Span::call_site(),
1800            },
1801        };
1802        let expr = Expr::Op {
1803            name: "==".to_string(),
1804            operands: vec![inner, ident("c")],
1805            span: crate::ExprSpan {
1806                start: proc_macro2::Span::call_site(),
1807                end: proc_macro2::Span::call_site(),
1808            },
1809        };
1810        assert_eq!(format_expr(&expr, "", 0), "(a == b) == c");
1811    }
1812
1813    #[test]
1814    fn literal_notation_is_preserved_exactly() {
1815        assert_eq!(fmt("1920.0"), "1920.0");
1816        assert_eq!(fmt("1920.0f64"), "1920.0f64");
1817        assert_eq!(fmt("1i32"), "1i32");
1818    }
1819
1820    #[test]
1821    fn unary_minus_of_a_binary_expression_needs_parens() {
1822        assert_eq!(fmt("-(1i32 + 2i32)"), "-(1i32 + 2i32)");
1823    }
1824
1825    #[test]
1826    fn double_unary_minus_keeps_a_separating_space() {
1827        assert_eq!(fmt("- -1i32"), "- -1i32");
1828    }
1829
1830    #[test]
1831    fn cast_chain_reprints_without_extra_parens() {
1832        assert_eq!(fmt("x as i32 as f64"), "x as i32 as f64");
1833    }
1834
1835    #[test]
1836    fn unary_minus_before_a_cast_needs_no_parens() {
1837        // Matches Rust: `-x as f64` parses as `(-x) as f64` - unary already binds tighter than
1838        // Cast, so the printer doesn't need to add parens to preserve that grouping.
1839        assert_eq!(fmt("-x as f64"), "-x as f64");
1840    }
1841
1842    #[test]
1843    fn explicit_grouping_before_a_cast_keeps_its_parens() {
1844        // `as` binds tighter than `+`, so without parens `(a + b) as i32` would reprint as
1845        // `a + b as i32` - a different expression (`a + (b as i32)`). The parens must survive.
1846        assert_eq!(fmt("(a + b) as i32"), "(a + b) as i32");
1847    }
1848
1849    #[test]
1850    fn cast_operand_of_an_additive_expression_needs_no_parens() {
1851        // `a + b as i32` already parses as `a + (b as i32)` (`as` binds tighter than `+`), so no
1852        // parens are needed around the cast when reprinting.
1853        assert_eq!(fmt("a + b as i32"), "a + b as i32");
1854    }
1855
1856    #[test]
1857    fn explicit_grouping_around_a_cast_before_multiplicative_is_redundant_and_dropped() {
1858        // `(x as i32) * y` parses to the exact same tree as `x as i32 * y` (cast already binds
1859        // tighter than `*`), so the now-redundant parens are dropped on reprint - matching the
1860        // module doc's "parens added only where required, not exhaustively".
1861        assert_eq!(fmt("(x as i32) * y"), "x as i32 * y");
1862    }
1863
1864    #[test]
1865    fn one_tuple_keeps_its_trailing_comma() {
1866        assert_eq!(fmt("(1i32,)"), "(1i32,)");
1867    }
1868
1869    #[test]
1870    fn multi_element_tuple_has_no_trailing_comma() {
1871        assert_eq!(fmt("(1i32, 2i32)"), "(1i32, 2i32)");
1872    }
1873
1874    #[test]
1875    fn arrays_format_with_brackets_and_normalized_commas() {
1876        assert_eq!(fmt("[1i32,2i32]"), "[1i32, 2i32]");
1877        assert_eq!(fmt("[[1i32], [2i32]]"), "[[1i32], [2i32]]");
1878    }
1879
1880    #[test]
1881    fn typed_arrays_format_with_a_type_ascription() {
1882        assert_eq!(fmt("[1i32,2i32]:[i32]"), "[1i32, 2i32]: [i32]");
1883        assert_eq!(fmt("[]:[[i32]]"), "[]: [[i32]]");
1884    }
1885
1886    #[test]
1887    fn format_array_annotation_with_a_generic_builtin_element_type() {
1888        let source = "[]: [RangeInclusive(f64)]";
1889        let expr = parse(source);
1890        let formatted = format_expr(&expr, source, 0);
1891        assert_eq!(formatted, source);
1892    }
1893
1894    #[test]
1895    fn array_comments_remain_in_source_order() {
1896        assert_eq!(
1897            fmt("[1i32 /* first */, /* second */ 2i32]"),
1898            "[1i32 /* first */, /* second */ 2i32]"
1899        );
1900    }
1901
1902    #[test]
1903    fn comments_in_a_call_argument_list_are_preserved() {
1904        assert_eq!(
1905            fmt("f(/* a */ 1i32, /* b */ 2i32)"),
1906            "f(/* a */ 1i32, /* b */ 2i32)"
1907        );
1908    }
1909
1910    #[test]
1911    fn a_comment_before_a_closing_call_paren_is_preserved() {
1912        assert_eq!(fmt("f(1i32 /* end */)"), "f(1i32 /* end */)");
1913    }
1914
1915    #[test]
1916    fn comments_in_a_tuple_are_preserved() {
1917        assert_eq!(fmt("(1i32, /* mid */ 2i32)"), "(1i32, /* mid */ 2i32)");
1918    }
1919
1920    #[test]
1921    fn comment_free_calls_and_tuples_are_unchanged() {
1922        assert_eq!(fmt("f(1i32, 2i32)"), "f(1i32, 2i32)");
1923        assert_eq!(fmt("(1i32, 2i32)"), "(1i32, 2i32)");
1924        assert_eq!(fmt("(1i32,)"), "(1i32,)");
1925        assert_eq!(fmt("(1i32, 2i32).1"), "(1i32, 2i32).1");
1926    }
1927
1928    #[test]
1929    fn if_without_else_omits_the_else_clause() {
1930        assert_eq!(fmt("if true { 1i32 }"), "if true { 1i32 }");
1931    }
1932
1933    #[test]
1934    fn if_else_reprints_both_branches() {
1935        assert_eq!(
1936            fmt("if true { 1i32 } else { 2i32 }"),
1937            "if true { 1i32 } else { 2i32 }"
1938        );
1939    }
1940
1941    #[test]
1942    fn else_if_chain_has_no_braces_around_the_nested_if() {
1943        let source = "if true { 1i32 } else if false { 2i32 } else { 3i32 }";
1944        assert_eq!(fmt(source), source);
1945    }
1946
1947    #[test]
1948    fn a_comment_after_if_condition_is_preserved() {
1949        assert_eq!(fmt("if a /* c */ { 1i32 }"), "if a /* c */ { 1i32 }");
1950    }
1951
1952    #[test]
1953    fn a_comment_before_else_is_preserved() {
1954        assert_eq!(
1955            fmt("if a { 1i32 } /* e */ else { 2i32 }"),
1956            "if a { 1i32 } /* e */ else { 2i32 }"
1957        );
1958    }
1959
1960    #[test]
1961    fn logical_or_and_and_are_not_desugared_and_need_no_extra_parens() {
1962        assert_eq!(fmt("a || b && c"), "a || b && c");
1963    }
1964
1965    #[test]
1966    fn a_comment_around_a_logical_operator_is_preserved() {
1967        assert_eq!(fmt("a /* x */ || b"), "a /* x */ || b");
1968        assert_eq!(fmt("a && /* y */ b"), "a && /* y */ b");
1969    }
1970
1971    #[test]
1972    fn format_is_idempotent_through_a_reparse() {
1973        let source = "(1i32 + 2i32) * 3i32 - -4i32";
1974        let once = fmt(source);
1975        let twice = format_expr(&parse(&once), &once, 0);
1976        assert_eq!(once, twice);
1977    }
1978
1979    #[test]
1980    fn typed_array_annotation_round_trips_through_reparse() {
1981        let source = "[1.0, 42.5]: [f64]";
1982        let once = fmt(source);
1983        let twice = format_expr(&parse(&once), &once, 0);
1984        assert_eq!(once, twice);
1985    }
1986
1987    #[test]
1988    fn closure_with_one_param_reprints_with_its_type() {
1989        assert_eq!(fmt("|x: i32| x + 1i32"), "|x: i32| x + 1i32");
1990    }
1991
1992    #[test]
1993    fn closure_with_no_params_reprints_with_double_pipe() {
1994        assert_eq!(fmt("|| 1i32"), "|| 1i32");
1995    }
1996
1997    #[test]
1998    fn closure_with_multiple_params_joins_them_with_commas() {
1999        assert_eq!(fmt("|x: i32, y: i32| x + y"), "|x: i32, y: i32| x + y");
2000    }
2001
2002    #[test]
2003    fn closure_with_a_tuple_typed_param_reprints_the_tuple_type() {
2004        assert_eq!(fmt("|x: (i32, f64)| x.0"), "|x: (i32, f64)| x.0");
2005    }
2006
2007    #[test]
2008    fn a_comment_before_a_closure_body_is_preserved() {
2009        assert_eq!(
2010            fmt("|x: i32| /* b */ x + 1i32"),
2011            "|x: i32| /* b */ x + 1i32"
2012        );
2013    }
2014
2015    #[test]
2016    fn a_line_comment_before_a_closure_body_wraps_the_expression() {
2017        // A `//` comment forces a line wrap; the body must land on the continuation line, not get
2018        // glued onto the comment's own line (which would comment it out and make the output
2019        // non-reparseable).
2020        let source = "|x: i32| // c\n x + 1i32";
2021        let once = fmt(source);
2022        assert!(once.contains("// c"), "comment must survive: {once:?}");
2023        assert!(once.contains("x + 1i32"), "body must survive: {once:?}");
2024        let twice = format_expr(&parse(&once), &once, 0);
2025        assert_eq!(once, twice, "formatting must be idempotent");
2026    }
2027
2028    #[test]
2029    fn range_inclusive_reprints_without_spaces() {
2030        assert_eq!(fmt("1i32..=5i32"), "1i32..=5i32");
2031    }
2032
2033    #[test]
2034    fn range_reprints_without_spaces() {
2035        assert_eq!(fmt("1i32..5i32"), "1i32..5i32");
2036    }
2037
2038    #[test]
2039    fn range_from_reprints_without_spaces() {
2040        assert_eq!(fmt("1i32.."), "1i32..");
2041    }
2042
2043    #[test]
2044    fn range_to_reprints_without_spaces() {
2045        assert_eq!(fmt("..5i32"), "..5i32");
2046    }
2047
2048    #[test]
2049    fn range_to_inclusive_reprints_without_spaces() {
2050        assert_eq!(fmt("..=5i32"), "..=5i32");
2051    }
2052
2053    #[test]
2054    fn range_full_reprints_as_two_dots() {
2055        assert_eq!(fmt(".."), "..");
2056    }
2057
2058    #[test]
2059    fn range_endpoints_that_are_comparisons_need_no_parens() {
2060        // From `is_range_expression`'s own doc comment: `a == b..c == d` parses as
2061        // `(a == b)..(c == d)` -- comparison already binds tighter than range, so the printer
2062        // doesn't need parens to preserve that grouping.
2063        assert_eq!(fmt("a == b..c == d"), "a == b..c == d");
2064    }
2065
2066    #[test]
2067    fn range_endpoints_that_are_arithmetic_need_no_parens() {
2068        // From the same doc comment: `1 + 2..3 * 4` parses as `(1 + 2)..(3 * 4)` -- arithmetic
2069        // binds well inside range's own endpoints, so no parens are needed on reprint either.
2070        assert_eq!(fmt("1i32 + 2i32..3i32 * 4i32"), "1i32 + 2i32..3i32 * 4i32");
2071    }
2072
2073    #[test]
2074    fn emit_gap_around_a_plain_operator_uses_single_spaces() {
2075        let pieces = vec![GapPiece::Punct("+")];
2076        assert_eq!(emit_gap(&pieces, Spacing::Around, 0), " + ");
2077    }
2078
2079    #[test]
2080    fn emit_gap_none_spacing_glues_a_range_operator() {
2081        let pieces = vec![GapPiece::Punct("..")];
2082        assert_eq!(emit_gap(&pieces, Spacing::None, 0), "..");
2083    }
2084
2085    #[test]
2086    fn emit_gap_comma_after_puts_one_trailing_space() {
2087        let pieces = vec![GapPiece::Punct(",")];
2088        assert_eq!(emit_gap(&pieces, Spacing::CommaAfter, 0), ", ");
2089    }
2090
2091    #[test]
2092    fn emit_gap_inlines_a_block_comment_before_an_operator() {
2093        let pieces = vec![
2094            GapPiece::Comment(Comment::Block("a".to_string())),
2095            GapPiece::Punct("+"),
2096        ];
2097        assert_eq!(emit_gap(&pieces, Spacing::Around, 0), " /* a */ + ");
2098    }
2099
2100    #[test]
2101    fn emit_gap_inlines_a_block_comment_after_an_operator() {
2102        let pieces = vec![
2103            GapPiece::Punct("+"),
2104            GapPiece::Comment(Comment::Block("b".to_string())),
2105        ];
2106        assert_eq!(emit_gap(&pieces, Spacing::Around, 0), " + /* b */ ");
2107    }
2108
2109    #[test]
2110    fn emit_gap_wraps_after_a_line_comment_to_the_continuation_indent() {
2111        let pieces = vec![
2112            GapPiece::Punct("+"),
2113            GapPiece::Comment(Comment::Line("why".to_string())),
2114        ];
2115        // depth 0 => continuation at depth 1 (4 spaces). The operator keeps its leading space; the
2116        // line comment ends the line, and the next operand resumes at the continuation indent.
2117        assert_eq!(emit_gap(&pieces, Spacing::Around, 0), " + // why\n    ");
2118    }
2119
2120    #[test]
2121    fn emit_gap_comma_after_a_line_comment_keeps_the_full_continuation_indent() {
2122        // A comma following a line comment must NOT lose one space of the continuation indent --
2123        // that indent is meaningful (it's `indent(depth + 1)`), unlike the single incidental
2124        // space a block comment or an `Around` punct leaves before a comma.
2125        let pieces = vec![
2126            GapPiece::Comment(Comment::Line("why".to_string())),
2127            GapPiece::Punct(","),
2128        ];
2129        assert_eq!(emit_gap(&pieces, Spacing::CommaAfter, 0), " // why\n    , ");
2130    }
2131
2132    #[test]
2133    fn a_block_comment_before_a_binary_operator_is_preserved() {
2134        let source = "1i32 /* a */ + 2i32";
2135        assert_eq!(fmt(source), "1i32 /* a */ + 2i32");
2136    }
2137
2138    #[test]
2139    fn a_block_comment_after_a_binary_operator_is_preserved() {
2140        let source = "1i32 + /* b */ 2i32";
2141        assert_eq!(fmt(source), "1i32 + /* b */ 2i32");
2142    }
2143
2144    #[test]
2145    fn comments_on_both_sides_of_a_binary_operator_are_preserved() {
2146        let source = "1i32 /* a */ + /* b */ 2i32";
2147        assert_eq!(fmt(source), "1i32 /* a */ + /* b */ 2i32");
2148    }
2149
2150    #[test]
2151    fn a_comment_after_unary_minus_is_preserved() {
2152        assert_eq!(fmt("- /* neg */ 1i32"), "- /* neg */ 1i32");
2153    }
2154
2155    #[test]
2156    fn a_comment_around_a_cast_operator_is_preserved() {
2157        assert_eq!(fmt("x /* c */ as i32"), "x /* c */ as i32");
2158    }
2159
2160    #[test]
2161    fn a_comment_inside_a_range_is_preserved() {
2162        assert_eq!(fmt("1i32 /* r */ ..5i32"), "1i32 /* r */ ..5i32");
2163    }
2164
2165    #[test]
2166    fn comment_free_unary_cast_and_range_are_unchanged() {
2167        assert_eq!(fmt("-1i32"), "-1i32");
2168        assert_eq!(fmt("x as i32"), "x as i32");
2169        assert_eq!(fmt("1i32..5i32"), "1i32..5i32");
2170    }
2171
2172    #[test]
2173    fn a_line_comment_after_a_cast_operator_wraps_the_expression() {
2174        // A `//` comment forces a line wrap; `i32` must land on the continuation line, not get
2175        // glued onto the comment's own line (which would comment it out and make the output
2176        // non-reparseable).
2177        let source = "x as // c\n i32";
2178        let once = fmt(source);
2179        assert!(once.contains("// c"), "comment must survive: {once:?}");
2180        assert!(once.contains("i32"), "type name must survive: {once:?}");
2181        let twice = format_expr(&parse(&once), &once, 0);
2182        assert_eq!(once, twice, "formatting must be idempotent");
2183    }
2184
2185    #[test]
2186    fn issue_201_line_comment_inside_an_expression_round_trips() {
2187        // The exact repro from stlab/cel-rs#201.
2188        let source = "1i32 +\n    // why 2\n    2i32";
2189        let once = fmt(source);
2190        assert_eq!(once, "1i32 + // why 2\n    2i32");
2191        assert!(once.contains("// why 2"), "comment must survive: {once:?}");
2192        let twice = format_expr(&parse(&once), &once, 0);
2193        assert_eq!(once, twice, "formatting must be idempotent");
2194    }
2195
2196    #[test]
2197    fn the_full_inline_comment_example_round_trips() {
2198        // From the design discussion: block comments on both operands and around the operator,
2199        // plus a trailing line comment. The leading `/* p */` and trailing `/* r */` sit outside
2200        // the expression's own span (before its first token, after its last), which the design
2201        // doc explicitly carves out of `format_expr`'s scope: "`Expr`'s own outer boundary ... is
2202        // not `format_expr`'s to own ... that boundary belongs to whatever embeds the expression"
2203        // (adam-lang's Part A2, not yet implemented) -- so only the two in-scope, inter-operand
2204        // comments are expected to survive here.
2205        let source = "/* p */ 1i32 /* q */ + // hello\n2i32 /* r */";
2206        let once = fmt(source);
2207        assert!(
2208            once.contains("/* q */") && once.contains("// hello"),
2209            "the in-scope operand/operator comments survive: {once:?}"
2210        );
2211        let twice = format_expr(&parse(&once), &once, 0);
2212        assert_eq!(once, twice);
2213    }
2214}