Skip to main content

bynk_fmt/
fmt.rs

1//! Bynk source formatter.
2//!
3//! Re-parses the source into an AST and re-prints it in canonical form per
4//! the style rules in `design/bynk-lsp-spec.md` §3.5:
5//!
6//! - Tabs by default (one tab per nesting level).
7//! - K&R brace style: opening brace on the same line as the construct header.
8//! - Trailing commas in multi-line record / sum / parameter / argument lists.
9//! - One blank line between top-level declarations.
10//! - No blank lines between fields within a record or arms within a match.
11//! - Doc blocks immediately above their declaration, no blank line between.
12//! - One space around binary operators, after commas, no space inside parens.
13//! - Soft 100-column line width — see below.
14//!
15//! Line width (#963). Every fit test measures the *whole* line: the column the
16//! construct starts at, its own width, and the width of whatever the caller
17//! will still emit after it (a `-> Ret {` signature tail, a closing `)`, a
18//! match arm's `,`). A construct that does not fit is re-emitted vertically,
19//! breaking only where the grammar tolerates a newline:
20//!
21//! - Record constructions, list literals, `exports`/`enum` bodies, and agent
22//!   scheme configuration take one entry per line, with a trailing comma.
23//! - Parameter and argument lists take one entry per line **without** a
24//!   trailing comma — the grammar rejects one there.
25//! - A `&&` / `||` / `implies` run breaks before each operator. Arithmetic and
26//!   comparison operators never break: a continuation line opening with `+`
27//!   does not re-attach to the line above on re-parse.
28//! - A `.`-chain of two or more calls breaks before each call, unless the
29//!   overflow belongs to a trailing argument that can open its body on the
30//!   chain's own line (`xs.fold(init, (acc, x) => match acc {`).
31//! - An `if` sends both branches vertical; a block sends its statements
32//!   vertical.
33//!
34//! The target is soft: a construct with no break point inside it — a long
35//! string literal, a `Matches("…")` regex — is left over-long rather than
36//! mangled. Every layout choice is a function of the AST and the current
37//! column, so the result is stable under re-formatting.
38//!
39//! The formatter is idempotent: format → format yields the same text.
40//!
41//! Comments (v1.1): line comments are preserved through the lexer-to-parser
42//! trivia pipeline (lexer emits `Comment` tokens, parser attaches them to
43//! AST declarations and statements). The formatter re-emits leading
44//! comments above each node and a trailing comment, if any, on the same
45//! line as the node's last token. Comments inside expression sub-trees
46//! are not yet attached to individual operands; they are folded into the
47//! enclosing statement's leading trivia. When even that would lose a
48//! comment, [`format_source`] refuses with a `bynk.fmt.comment_loss`
49//! diagnostic instead of dropping user text (#523) — the file is left
50//! unchanged. See `design/bynk-lsp-spec.md` §3.5 for the canonical
51//! comment-placement rules.
52
53use bynk_syntax::ast::*;
54use bynk_syntax::error::CompileError;
55use bynk_syntax::lexer::{Token, TokenKind, tokenize};
56use bynk_syntax::parser::{parse_units, parse_units_with_drain_check};
57use bynk_syntax::span::Span;
58
59/// Indentation style: tabs or spaces. Mirrors the LSP spec's `[fmt].indent`
60/// setting.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum IndentStyle {
63    #[default]
64    Tab,
65    Spaces(u8),
66}
67
68/// Formatter options. All fields have spec-defined defaults.
69///
70/// `Copy` (#972): three scalar fields, and the config layering passes them by
71/// value through per-file resolution — a `clone()` at each hop would be noise.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct FormatOptions {
74    pub indent: IndentStyle,
75    pub max_line_width: u32,
76    pub trailing_comma: bool,
77}
78
79impl Default for FormatOptions {
80    fn default() -> Self {
81        Self {
82            indent: IndentStyle::Tab,
83            max_line_width: 100,
84            trailing_comma: true,
85        }
86    }
87}
88
89/// Error returned when formatting fails. The formatter cannot format code
90/// that does not parse, so all failure modes here surface as parse errors.
91#[derive(Debug, Clone)]
92pub struct FormatError {
93    pub errors: Vec<CompileError>,
94}
95
96/// Format a Bynk source string. On parse failure, returns the original
97/// source unchanged is *not* this function's responsibility — callers (LSP,
98/// CLI) decide how to handle parse failure. Here we surface the errors so
99/// the caller can do so.
100pub fn format_source(source: &str, opts: &FormatOptions) -> Result<String, FormatError> {
101    let tokens = tokenize(source).map_err(|e| FormatError { errors: vec![e] })?;
102    // v0.113: a file may hold more than one top-level unit (an atomic
103    // `commons` + `suite` file, DECISION S). Format each and join with a blank
104    // line. Each unit's output already ends in exactly one newline, so joining
105    // with `"\n"` inserts one blank line between units and leaves a single-unit
106    // file byte-identical.
107    let (units, _warnings, fully_drained) =
108        parse_units_with_drain_check(&tokens, source).map_err(|errors| FormatError { errors })?;
109    let output = render_units(&units, opts);
110    // #523/#66 guard: trivia is only attached at declaration/statement
111    // granularity, so a comment inside an expression subtree can be silently
112    // dropped. Losing user text is worse than leaving a file unformatted — when
113    // the output holds fewer comments than the input, refuse with a diagnostic
114    // pointing at the first comment that would vanish. `fully_drained` is the
115    // same parse's own answer to "did every comment reach a `Trivia` field?";
116    // when it's `true`, nothing could have been lost and `comment_loss`'s own
117    // re-tokenize-and-diff of `output` would only ever confirm that, so it is
118    // skipped outright — the common case for a file with no comment sitting
119    // inside a `match`/list/record/binop.
120    if !fully_drained && let Some(error) = comment_loss(source, &tokens, &output) {
121        return Err(FormatError {
122            errors: vec![error],
123        });
124    }
125    // #735 guard: the printer is hand-written and dodges several parse traps by
126    // convention (a tail `()` re-attaching as a call, a trailing comma making a
127    // param list unparseable). A shape the corpus misses that the printer
128    // mis-renders would otherwise be written straight over the user's file with
129    // exit 0. Before returning, re-parse the output and compare its *code*
130    // structure — every comment stripped from both sides, so trivia re-flow is
131    // ignored — against the input. When the output fails to re-parse, or a
132    // shape round-trips to a different AST, refuse rather than corrupt.
133    if let Some(error) = roundtrip_divergence(&tokens, source, &output, opts) {
134        return Err(FormatError {
135            errors: vec![error],
136        });
137    }
138    Ok(output)
139}
140
141/// Format every top-level unit and join with a blank line. A file may hold more
142/// than one top-level unit (v0.113, an atomic `commons` + `suite` file,
143/// DECISION S). Each unit's output already ends in exactly one newline, so
144/// joining with `"\n"` inserts one blank line between units and leaves a
145/// single-unit file byte-identical.
146fn render_units(units: &[SourceUnit], opts: &FormatOptions) -> String {
147    let parts: Vec<String> = units
148        .iter()
149        .map(|unit| {
150            let mut f = Formatter::new(opts);
151            f.format_unit(unit);
152            f.finish()
153        })
154        .collect();
155    parts.join("\n")
156}
157
158/// #735: the comment-free canonical rendering of `source` — every `Comment`
159/// token dropped before parsing, so the result carries no trivia and reflects
160/// only the code structure. Because the parser strips comments the same way
161/// ([`split_trivia`]), pre-filtering them changes nothing structural; it just
162/// leaves the trivia fields empty so the formatter emits pure code. Returns the
163/// parse errors when `source` does not tokenize or parse.
164fn code_only_canonical(source: &str, opts: &FormatOptions) -> Result<String, Vec<CompileError>> {
165    let tokens = tokenize(source).map_err(|e| vec![e])?;
166    code_only_canonical_from_tokens(&tokens, source, opts)
167}
168
169/// [`code_only_canonical`], given an already-tokenized `source` — finding #66:
170/// `format_source` tokenizes `source` once up front for the real render;
171/// `roundtrip_divergence` reuses that token list here instead of tokenizing
172/// `source` from scratch a second time.
173fn code_only_canonical_from_tokens(
174    tokens: &[Token],
175    source: &str,
176    opts: &FormatOptions,
177) -> Result<String, Vec<CompileError>> {
178    let code: Vec<Token> = tokens
179        .iter()
180        .filter(|t| t.kind != TokenKind::Comment)
181        .cloned()
182        .collect();
183    let units = parse_units(&code, source)?;
184    Ok(render_units(&units, opts))
185}
186
187/// #735: refuse when the formatter's `output` does not round-trip to the same
188/// code as `source`. Both sides are reduced to their comment-free canonical
189/// form ([`code_only_canonical`]) and compared: the formatter only re-flows
190/// whitespace and trivia, so for a faithful render the two canonical strings
191/// are byte-identical. A mismatch means either the output no longer parses (the
192/// data-loss vector) or the printer altered the AST. `None` when the output is
193/// safe to write.
194///
195/// Note: this guard assumes the formatter is idempotent on comment-free code —
196/// i.e. `render(parse(strip(x)))` is a stable canonical form. That invariant is
197/// held by the corpus/property idempotency tests. Were a future formatter
198/// change to break it on some shape, this guard would *refuse* an otherwise
199/// valid file rather than corrupt it — it fails safe (file unchanged + a
200/// diagnostic), but the surprise would be a formatter bug to fix upstream.
201fn roundtrip_divergence(
202    tokens: &[Token],
203    source: &str,
204    output: &str,
205    opts: &FormatOptions,
206) -> Option<CompileError> {
207    // The output MUST re-parse to the same structure; a failure here is the
208    // core corruption vector this guard exists to stop.
209    let canon_out = match code_only_canonical(output, opts) {
210        Ok(canon) => canon,
211        Err(_) => {
212            return Some(roundtrip_error(
213                "the formatter produced output that no longer parses",
214            ));
215        }
216    };
217    // The input already tokenized (and parsed) in `format_source` — `tokens` is
218    // that same token list, reused rather than tokenizing `source` a second
219    // time (finding #66). If its canonical form unexpectedly fails to compute,
220    // do not block a valid format on our own guard failing — leave the file
221    // writable.
222    let canon_in = code_only_canonical_from_tokens(tokens, source, opts).ok()?;
223    (canon_in != canon_out)
224        .then(|| roundtrip_error("the formatter's output does not round-trip to the same code"))
225}
226
227/// Build the `bynk.fmt.roundtrip` diagnostic shared by both failure modes of
228/// [`roundtrip_divergence`]. The span is deliberately `Span::default()` (the
229/// start of the file): the message points at neither the source nor the
230/// output — it is a generic "this is a formatter bug" — and the failing branch
231/// carries an *output*-relative span, which the caller renders against the
232/// *source* string. When the mis-rendered output is longer than the source,
233/// that span is out of range for the buffer ariadne is given (a misplaced
234/// caret, or a byte-index panic in the very formatter-bug path this guard
235/// exists to handle gracefully). A zero span is always in range and buys the
236/// message nothing to lose.
237fn roundtrip_error(what: &str) -> CompileError {
238    CompileError {
239        category: "bynk.fmt.roundtrip",
240        span: Span::default(),
241        message: format!("{what} — the file was left unchanged"),
242        labels: Vec::new(),
243        notes: vec![
244            "this is a formatter bug, not a problem with your source; please report it \
245             with the file that triggered it"
246                .to_string(),
247        ],
248        suggestions: Vec::new(),
249    }
250}
251
252/// #523: compare the comment population of `source` (already tokenized as
253/// `tokens`) against `output`. Returns a `bynk.fmt.comment_loss` error naming
254/// the first lost comment when the output would hold fewer comments, `None`
255/// when every comment survives. Comments may legitimately *move* (expression
256/// trivia folds into the enclosing statement's leading block), so the
257/// comparison is by body multiset, not position.
258fn comment_loss(source: &str, tokens: &[Token], output: &str) -> Option<CompileError> {
259    use bynk_syntax::lexer::comment_body;
260    let in_comments: Vec<Span> = tokens
261        .iter()
262        .filter(|t| t.kind == TokenKind::Comment)
263        .map(|t| t.span)
264        .collect();
265    if in_comments.is_empty() {
266        return None;
267    }
268    // The formatter's own output must tokenize; treat a failure as "all
269    // comments lost" rather than silently accepting the write.
270    let mut out_bodies: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
271    if let Ok(out_tokens) = tokenize(output) {
272        for t in &out_tokens {
273            if t.kind == TokenKind::Comment {
274                *out_bodies
275                    .entry(comment_body(output, t.span).trim().to_string())
276                    .or_insert(0) += 1;
277            }
278        }
279    }
280    let mut lost = 0usize;
281    let mut first_lost: Option<Span> = None;
282    for span in &in_comments {
283        let body = comment_body(source, *span).trim().to_string();
284        match out_bodies.get_mut(&body) {
285            Some(n) if *n > 0 => *n -= 1,
286            _ => {
287                lost += 1;
288                first_lost.get_or_insert(*span);
289            }
290        }
291    }
292    let span = first_lost?;
293    Some(CompileError {
294        category: "bynk.fmt.comment_loss",
295        span,
296        message: format!(
297            "formatting would lose {lost} comment{} — the file was left unchanged",
298            if lost == 1 { "" } else { "s" }
299        ),
300        labels: vec![(
301            span,
302            "this comment sits where the formatter cannot yet re-attach it".to_string(),
303        )],
304        notes: vec![
305            "comments inside expression subtrees are not yet preserved; move the comment onto \
306             its own line before the enclosing statement to format this file"
307                .to_string(),
308        ],
309        suggestions: Vec::new(),
310    })
311}
312
313// -- Internal formatter state --
314
315struct Formatter<'a> {
316    opts: &'a FormatOptions,
317    out: String,
318    indent_level: u32,
319    /// True when the formatter has just emitted a newline and is at the
320    /// start of a fresh line. Used to gate indent emission.
321    at_line_start: bool,
322}
323
324impl<'a> Formatter<'a> {
325    fn new(opts: &'a FormatOptions) -> Self {
326        Self {
327            opts,
328            out: String::new(),
329            indent_level: 0,
330            at_line_start: true,
331        }
332    }
333
334    fn finish(mut self) -> String {
335        // Single trailing newline.
336        while self.out.ends_with("\n\n") {
337            self.out.pop();
338        }
339        if !self.out.ends_with('\n') {
340            self.out.push('\n');
341        }
342        self.out
343    }
344
345    fn indent_unit(&self) -> String {
346        match self.opts.indent {
347            IndentStyle::Tab => "\t".to_string(),
348            IndentStyle::Spaces(n) => " ".repeat(n as usize),
349        }
350    }
351
352    fn emit_indent(&mut self) {
353        let unit = self.indent_unit();
354        for _ in 0..self.indent_level {
355            self.out.push_str(&unit);
356        }
357    }
358
359    fn push(&mut self, s: &str) {
360        if self.at_line_start && !s.starts_with('\n') {
361            self.emit_indent();
362            self.at_line_start = false;
363        }
364        if s.contains('\n') {
365            self.push_reindented(s);
366        } else {
367            self.out.push_str(s);
368        }
369    }
370
371    /// Append a multi-line string, re-applying the current indent to every
372    /// continuation line. Multi-line strings come from the single-line
373    /// expression renderers (`expr_to_string` and friends), which build their
374    /// internal structure assuming column zero — they embed `\n` plus relative
375    /// tabs but know nothing about the current nesting depth. Without this an
376    /// argument-position `match` (or any embedded multi-line expression) would
377    /// print its arms and trailing brace at column one regardless of how deeply
378    /// it is nested. The first line is emitted as-is (its indent, if any, was
379    /// handled by `push`); blank lines are left empty rather than padded.
380    fn push_reindented(&mut self, s: &str) {
381        let prefix = self.indent_unit().repeat(self.indent_level as usize);
382        for (i, line) in s.split('\n').enumerate() {
383            if i > 0 {
384                self.out.push('\n');
385                if !line.is_empty() {
386                    self.out.push_str(&prefix);
387                }
388            }
389            self.out.push_str(line);
390        }
391    }
392
393    fn newline(&mut self) {
394        self.out.push('\n');
395        self.at_line_start = true;
396    }
397
398    #[allow(dead_code)]
399    fn blank_line(&mut self) {
400        if !self.out.ends_with('\n') {
401            self.out.push('\n');
402        }
403        if !self.out.ends_with("\n\n") {
404            self.out.push('\n');
405        }
406        self.at_line_start = true;
407    }
408
409    fn indented<F: FnOnce(&mut Self)>(&mut self, f: F) {
410        self.indent_level += 1;
411        f(self);
412        self.indent_level -= 1;
413    }
414
415    /// Render `body` into a detached formatter positioned at this one's exact
416    /// column and indent, and commit it only if every line it produces stays
417    /// inside the budget (the last line counting `reserve` further columns for
418    /// whatever the caller will append). Returns whether it was committed.
419    ///
420    /// A candidate layout is often only judgeable once rendered: whether a
421    /// `.`-chain needs breaking at its dots depends on how its arguments wrap,
422    /// which depends on the column they start at. Measuring a real rendering
423    /// beats predicting one (#963).
424    fn try_layout<F: FnOnce(&mut Self)>(&mut self, reserve: usize, body: F) -> bool {
425        self.try_layout_if(reserve, body, || true)
426    }
427
428    /// [`Self::try_layout`] with a second condition, evaluated after the
429    /// rendering, for a caller that judges the candidate on more than its
430    /// width — a chain that fits but only by exploding an argument list.
431    fn try_layout_if<F: FnOnce(&mut Self), A: FnOnce() -> bool>(
432        &mut self,
433        reserve: usize,
434        body: F,
435        accept: A,
436    ) -> bool {
437        let line_start = self.out.rfind('\n').map_or(0, |i| i + 1);
438        let mut sub = Formatter {
439            opts: self.opts,
440            out: self.out[line_start..].to_string(),
441            indent_level: self.indent_level,
442            at_line_start: self.at_line_start,
443        };
444        let produced_from = sub.out.len();
445        body(&mut sub);
446        if !sub.every_line_within_budget(reserve) || !accept() {
447            return false;
448        }
449        // Splice the raw text: `sub` already emitted its own indentation, so
450        // going through `push` would double it.
451        self.out.push_str(&sub.out[produced_from..]);
452        self.at_line_start = sub.at_line_start;
453        true
454    }
455
456    /// Does every line of this (detached) formatter's buffer fit, with
457    /// `reserve` columns still free on the last one?
458    fn every_line_within_budget(&self, reserve: usize) -> bool {
459        let tab = self.indent_width();
460        let mut lines = self.out.split('\n').peekable();
461        let mut last = "";
462        while let Some(line) = lines.next() {
463            if lines.peek().is_none() {
464                last = line;
465                break;
466            }
467            if display_width(line, tab) > self.opts.max_line_width as usize {
468                return false;
469            }
470        }
471        let last_width = if self.at_line_start {
472            // The trailing indent is not in the buffer yet; `push` adds it.
473            self.indent_level as usize * tab
474        } else {
475            display_width(last, tab)
476        };
477        last_width + reserve <= self.opts.max_line_width as usize
478    }
479
480    // -- Doc block --
481
482    /// Emit a doc block immediately above a declaration. The content is
483    /// already normalised (common leading indent stripped) when stored in
484    /// the AST; we re-emit with the current indent applied per line.
485    fn emit_doc(&mut self, doc: &str) {
486        self.push("---");
487        self.newline();
488        for line in doc.lines() {
489            if line.is_empty() {
490                self.newline();
491            } else {
492                self.push(line);
493                self.newline();
494            }
495        }
496        self.push("---");
497        self.newline();
498    }
499
500    // -- Line-comment trivia (v1.1) --
501
502    /// Emit a sequence of leading line-comments, each on its own line at
503    /// the current indent. Group has no blank lines between entries.
504    fn emit_leading_comments(&mut self, comments: &[String]) {
505        for body in comments {
506            self.push("--");
507            self.push(body);
508            self.newline();
509        }
510    }
511
512    /// Emit a trailing comment on the same line as the just-emitted token.
513    /// The spec uses two spaces between code and comment for readability.
514    fn emit_trailing_comment(&mut self, body: Option<&str>) {
515        if let Some(body) = body {
516            // Ensure we're on the same line as the preceding tokens —
517            // strip any newline we just emitted.
518            while self.out.ends_with('\n') {
519                self.out.pop();
520            }
521            self.out.push_str("  --");
522            self.out.push_str(body);
523            self.newline();
524        }
525    }
526
527    // -- Top level --
528
529    fn format_unit(&mut self, unit: &SourceUnit) {
530        match unit {
531            SourceUnit::Commons(c) => self.format_commons(c),
532            SourceUnit::Context(c) => self.format_context(c),
533            SourceUnit::Suite(t) => self.format_test(t),
534            SourceUnit::Adapter(a) => self.format_adapter(a),
535        }
536    }
537
538    fn format_adapter(&mut self, a: &AdapterDecl) {
539        self.emit_leading_comments(&a.trivia.leading);
540        if let Some(doc) = &a.documentation {
541            self.emit_doc(doc);
542        }
543        let header = format!("adapter {}", a.name.joined());
544        match a.form {
545            CommonsForm::Brace => {
546                self.push(&header);
547                self.push(" {");
548                self.newline();
549                self.indented(|f| {
550                    f.format_adapter_body(a);
551                });
552                self.push("}");
553                self.newline();
554            }
555            CommonsForm::Fragment => {
556                self.push(&header);
557                self.newline();
558                self.newline();
559                self.format_adapter_body(a);
560            }
561        }
562    }
563
564    fn format_adapter_body(&mut self, a: &AdapterDecl) {
565        let mut any_header = false;
566        if let Some(b) = &a.binding {
567            self.emit_leading_comments(&b.trivia.leading);
568            self.push(&format!("binding {:?}", b.module));
569            if !b.requires.is_empty() {
570                let entries: Vec<String> = b
571                    .requires
572                    .iter()
573                    .map(|r| format!("{:?}: {:?}", r.package, r.range))
574                    .collect();
575                self.push(&format!(" requires {{ {} }}", entries.join(", ")));
576            }
577            self.emit_trailing_comment(b.trivia.trailing.as_deref());
578            if b.trivia.trailing.is_none() {
579                self.newline();
580            }
581            any_header = true;
582        }
583        for u in &a.uses {
584            self.emit_leading_comments(&u.trivia.leading);
585            self.push(&format!("uses {}", u.target.joined()));
586            self.emit_trailing_comment(u.trivia.trailing.as_deref());
587            if u.trivia.trailing.is_none() {
588                self.newline();
589            }
590            any_header = true;
591        }
592        for c in &a.consumes {
593            self.format_consumes(c);
594            any_header = true;
595        }
596        for e in &a.exports {
597            self.emit_leading_comments(&e.trivia.leading);
598            self.format_exports(e);
599            if e.trivia.trailing.is_some() {
600                self.emit_trailing_comment(e.trivia.trailing.as_deref());
601            }
602            any_header = true;
603        }
604        if any_header && !a.items.is_empty() {
605            self.newline();
606        }
607        let mut first = true;
608        for item in &a.items {
609            if !first {
610                self.newline();
611            }
612            self.format_item(item);
613            first = false;
614        }
615        if !a.trailing_comments.is_empty() {
616            if !a.items.is_empty() || any_header {
617                self.newline();
618            }
619            self.emit_leading_comments(&a.trailing_comments);
620        }
621    }
622
623    fn format_test(&mut self, t: &SuiteDecl) {
624        self.emit_leading_comments(&t.trivia.leading);
625        if let Some(doc) = &t.documentation {
626            self.emit_doc(doc);
627        }
628        let mut header = format!("suite {}", t.target.joined());
629        if let Some(tier) = t.tier {
630            header.push_str(&format!(" as {}", tier.as_str()));
631        }
632        match t.form {
633            CommonsForm::Brace => {
634                self.push(&header);
635                self.push(" {");
636                self.newline();
637                self.indented(|f| {
638                    f.format_test_body(
639                        &t.uses,
640                        &t.stubs,
641                        &t.cases,
642                        &t.properties,
643                        &t.trailing_comments,
644                    );
645                });
646                self.push("}");
647                self.newline();
648            }
649            CommonsForm::Fragment => {
650                self.push(&header);
651                self.newline();
652                self.format_test_body(
653                    &t.uses,
654                    &t.stubs,
655                    &t.cases,
656                    &t.properties,
657                    &t.trailing_comments,
658                );
659            }
660        }
661    }
662
663    fn format_test_body(
664        &mut self,
665        uses: &[UsesDecl],
666        stubs: &[StubClause],
667        cases: &[Case],
668        properties: &[PropertyDecl],
669        trailing_comments: &[String],
670    ) {
671        let mut first = true;
672        for u in uses {
673            if !first {
674                self.newline();
675            }
676            self.emit_leading_comments(&u.trivia.leading);
677            self.push(&format!("uses {}", u.target.joined()));
678            self.emit_trailing_comment(u.trivia.trailing.as_deref());
679            self.newline();
680            first = false;
681        }
682        for pv in stubs {
683            if !first {
684                self.newline();
685            }
686            self.format_stub_clause(pv);
687            first = false;
688        }
689        for c in cases {
690            if !first {
691                self.newline();
692            }
693            self.emit_leading_comments(&c.trivia.leading);
694            if let Some(doc) = &c.documentation {
695                self.emit_doc(doc);
696            }
697            let mut ch = format!("case \"{}\"", escape_string(&c.name));
698            if let Some(tier) = c.tier {
699                ch.push_str(&format!(" as {}", tier.as_str()));
700            }
701            ch.push(' ');
702            self.push(&ch);
703            self.format_case_block(&c.body, &c.stubs);
704            self.newline();
705            first = false;
706        }
707        for p in properties {
708            if !first {
709                self.newline();
710            }
711            self.emit_leading_comments(&p.trivia.leading);
712            if let Some(doc) = &p.documentation {
713                self.emit_doc(doc);
714            }
715            self.push(&format!("property \"{}\" {{", escape_string(&p.name)));
716            self.newline();
717            self.indented(|f| f.format_for_all(&p.forall));
718            self.push("}");
719            self.newline();
720            first = false;
721        }
722        for comment in trailing_comments {
723            self.push(&format!("--{comment}"));
724            self.newline();
725        }
726    }
727
728    /// v0.118: format a `stub` clause as a suite- or case-body line, with
729    /// its leading comments / doc and a terminating newline (testing track
730    /// slice 6).
731    fn format_stub_clause(&mut self, pv: &StubClause) {
732        self.emit_leading_comments(&pv.trivia.leading);
733        if let Some(doc) = &pv.documentation {
734            self.emit_doc(doc);
735        }
736        self.push(&stub_clause_to_string(pv));
737        self.emit_trailing_comment(pv.trivia.trailing.as_deref());
738        if pv.trivia.trailing.is_none() {
739            self.newline();
740        }
741    }
742
743    /// v0.118: format a `case` body, emitting its case-scoped `stub` clauses
744    /// as the leading lines inside the block, before the statements and tail.
745    /// With no case-scoped `stub` this is exactly [`Self::format_block`].
746    fn format_case_block(&mut self, b: &Block, stubs: &[StubClause]) {
747        if stubs.is_empty() {
748            self.format_block(b);
749            return;
750        }
751        self.push("{");
752        self.newline();
753        self.indented(|f| {
754            for pv in stubs {
755                f.format_stub_clause(pv);
756            }
757            for stmt in &b.statements {
758                let trivia = statement_trivia(stmt);
759                f.emit_leading_comments(&trivia.leading);
760                f.format_statement(stmt);
761                f.emit_trailing_comment(trivia.trailing.as_deref());
762                if trivia.trailing.is_none() {
763                    f.newline();
764                }
765            }
766            f.emit_leading_comments(&b.tail_leading_comments);
767            // See `format_block` / #981: any `()` tail is omitted, not just an
768            // implicit one.
769            if !omit_unit_tail(b) {
770                f.format_expr(&b.tail);
771                f.newline();
772            }
773        });
774        self.push("}");
775    }
776
777    /// v0.114: format a `for all <bindings> [where <pred>] { … }` binder — the
778    /// sole body of a `property`.
779    fn format_for_all(&mut self, fa: &ForAll) {
780        let bindings = fa
781            .bindings
782            .iter()
783            .map(|b| format!("{}: {}", b.name.name, type_ref_to_string(&b.type_ref)))
784            .collect::<Vec<_>>()
785            .join(", ");
786        let mut header = format!("for all {bindings}");
787        if let Some(w) = &fa.where_pred {
788            header.push_str(&format!(" where {}", expr_to_string(w)));
789        }
790        self.push(&format!("{header} "));
791        self.format_block(&fa.body);
792        self.newline();
793    }
794
795    fn format_commons(&mut self, c: &Commons) {
796        self.emit_leading_comments(&c.trivia.leading);
797        if let Some(doc) = &c.documentation {
798            self.emit_doc(doc);
799        }
800        let header = format!("commons {}", c.name.joined());
801        match c.form {
802            CommonsForm::Brace => {
803                self.push(&header);
804                self.push(" {");
805                self.newline();
806                self.indented(|f| {
807                    f.format_commons_body(&c.uses, &c.items, &c.trailing_comments);
808                });
809                self.push("}");
810                self.newline();
811            }
812            CommonsForm::Fragment => {
813                self.push(&header);
814                self.newline();
815                self.newline();
816                self.format_commons_body(&c.uses, &c.items, &c.trailing_comments);
817            }
818        }
819    }
820
821    fn format_commons_body(
822        &mut self,
823        uses: &[UsesDecl],
824        items: &[CommonsItem],
825        trailing_comments: &[String],
826    ) {
827        let mut any_uses = false;
828        for u in uses {
829            self.emit_leading_comments(&u.trivia.leading);
830            self.push(&format!("uses {}", u.target.joined()));
831            self.emit_trailing_comment(u.trivia.trailing.as_deref());
832            if u.trivia.trailing.is_none() {
833                self.newline();
834            }
835            any_uses = true;
836        }
837        if any_uses && !items.is_empty() {
838            self.newline();
839        }
840        let mut first = true;
841        for item in items {
842            if !first {
843                self.newline();
844            }
845            self.format_item(item);
846            first = false;
847        }
848        if !trailing_comments.is_empty() {
849            // One blank line before trailing-file comments if anything
850            // came before them.
851            if !items.is_empty() || any_uses {
852                self.newline();
853            }
854            self.emit_leading_comments(trailing_comments);
855        }
856    }
857
858    fn format_context(&mut self, c: &Context) {
859        self.emit_leading_comments(&c.trivia.leading);
860        if let Some(doc) = &c.documentation {
861            self.emit_doc(doc);
862        }
863        let header = format!("context {}", c.name.joined());
864        match c.form {
865            CommonsForm::Brace => {
866                self.push(&header);
867                self.push(" {");
868                self.newline();
869                self.indented(|f| {
870                    f.format_context_body(
871                        &c.uses,
872                        &c.consumes,
873                        &c.exports,
874                        &c.items,
875                        &c.trailing_comments,
876                    );
877                });
878                self.push("}");
879                self.newline();
880            }
881            CommonsForm::Fragment => {
882                self.push(&header);
883                self.newline();
884                self.newline();
885                self.format_context_body(
886                    &c.uses,
887                    &c.consumes,
888                    &c.exports,
889                    &c.items,
890                    &c.trailing_comments,
891                );
892            }
893        }
894    }
895
896    /// Print one `consumes` clause in any of its three forms: whole-unit,
897    /// aliased, or braced capability selection (v0.17 §3.3 — previously the
898    /// braced form was silently dropped, a semantic-changing format).
899    fn format_consumes(&mut self, c: &ConsumesDecl) {
900        self.emit_leading_comments(&c.trivia.leading);
901        match (&c.alias, &c.selected) {
902            (Some(alias), _) => {
903                self.push(&format!("consumes {} as {}", c.target.joined(), alias.name))
904            }
905            (None, Some(selected)) if selected.is_empty() => {
906                self.push(&format!("consumes {} {{ }}", c.target.joined()));
907            }
908            (None, Some(selected)) => {
909                let names: Vec<&str> = selected.iter().map(|i| i.name.as_str()).collect();
910                self.push(&format!(
911                    "consumes {} {{ {} }}",
912                    c.target.joined(),
913                    names.join(", ")
914                ));
915            }
916            (None, None) => self.push(&format!("consumes {}", c.target.joined())),
917        }
918        self.emit_trailing_comment(c.trivia.trailing.as_deref());
919        if c.trivia.trailing.is_none() {
920            self.newline();
921        }
922    }
923
924    fn format_context_body(
925        &mut self,
926        uses: &[UsesDecl],
927        consumes: &[ConsumesDecl],
928        exports: &[ExportsDecl],
929        items: &[CommonsItem],
930        trailing_comments: &[String],
931    ) {
932        let mut any_header = false;
933        for u in uses {
934            self.emit_leading_comments(&u.trivia.leading);
935            self.push(&format!("uses {}", u.target.joined()));
936            self.emit_trailing_comment(u.trivia.trailing.as_deref());
937            if u.trivia.trailing.is_none() {
938                self.newline();
939            }
940            any_header = true;
941        }
942        for c in consumes {
943            self.format_consumes(c);
944            any_header = true;
945        }
946        for e in exports {
947            self.emit_leading_comments(&e.trivia.leading);
948            self.format_exports(e);
949            // exports may emit multi-line — the trailing comment goes on
950            // its last line. Since format_exports already terminates with
951            // a newline, splice the comment before it if present.
952            if e.trivia.trailing.is_some() {
953                self.emit_trailing_comment(e.trivia.trailing.as_deref());
954            }
955            any_header = true;
956        }
957        if any_header && !items.is_empty() {
958            self.newline();
959        }
960        let mut first = true;
961        for item in items {
962            if !first {
963                self.newline();
964            }
965            self.format_item(item);
966            first = false;
967        }
968        if !trailing_comments.is_empty() {
969            if !items.is_empty() || any_header {
970                self.newline();
971            }
972            self.emit_leading_comments(trailing_comments);
973        }
974    }
975
976    fn format_exports(&mut self, e: &ExportsDecl) {
977        let vis = match e.kind {
978            ExportKind::Type(Visibility::Opaque) => "opaque",
979            ExportKind::Type(Visibility::Transparent) => "transparent",
980            ExportKind::Capability => "capability",
981        };
982        if e.names.is_empty() {
983            self.push(&format!("exports {} {{}}", vis));
984            self.newline();
985            return;
986        }
987        // Single-line form if it fits.
988        let oneline = format!(
989            "exports {} {{ {} }}",
990            vis,
991            e.names
992                .iter()
993                .map(|n| n.name.as_str())
994                .collect::<Vec<_>>()
995                .join(", ")
996        );
997        if self.fits(&oneline, 0) {
998            self.push(&oneline);
999            self.newline();
1000            return;
1001        }
1002        // Multi-line form.
1003        self.push(&format!("exports {} {{", vis));
1004        self.newline();
1005        self.indented(|f| {
1006            for (i, n) in e.names.iter().enumerate() {
1007                f.push(&n.name);
1008                if i + 1 < e.names.len() || f.opts.trailing_comma {
1009                    f.push(",");
1010                }
1011                f.newline();
1012            }
1013        });
1014        self.push("}");
1015        self.newline();
1016    }
1017
1018    /// The rendered width of one indent level. A tab is counted as four
1019    /// columns for width estimation (the file stores one byte; editors render
1020    /// it at the reader's chosen width, so any fixed number is an estimate).
1021    fn indent_width(&self) -> usize {
1022        match self.opts.indent {
1023            IndentStyle::Tab => 4,
1024            IndentStyle::Spaces(n) => n as usize,
1025        }
1026    }
1027
1028    /// The column the next character pushed would land on. Everything already
1029    /// emitted on the current line counts — #963: measuring only
1030    /// `indent_level` (as this did before) made every fit test blind to the
1031    /// prefix its caller had already printed, so a body measured as "fits"
1032    /// while sitting behind `fn name(params) -> Ret ` routinely overflowed.
1033    fn current_column(&self) -> usize {
1034        if self.at_line_start {
1035            // `push` has yet to emit this line's indent; account for it here.
1036            return self.indent_level as usize * self.indent_width();
1037        }
1038        let line = match self.out.rfind('\n') {
1039            Some(i) => &self.out[i + 1..],
1040            None => self.out.as_str(),
1041        };
1042        display_width(line, self.indent_width())
1043    }
1044
1045    /// Does `candidate`, emitted at the current column, leave `reserve`
1046    /// further columns inside the line budget? `reserve` is the width of text
1047    /// the caller knows will follow on the same line — a closing `)`, a
1048    /// `-> Ret {` suffix, an arm's `,`. A multi-line candidate never "fits":
1049    /// its own line breaks are the decision the caller is trying to make.
1050    fn fits(&self, candidate: &str, reserve: usize) -> bool {
1051        if candidate.contains('\n') {
1052            return false;
1053        }
1054        let column =
1055            self.current_column() + display_width(candidate, self.indent_width()) + reserve;
1056        column <= self.opts.max_line_width as usize
1057    }
1058
1059    fn format_item(&mut self, item: &CommonsItem) {
1060        match item {
1061            CommonsItem::Type(t) => self.format_type_decl(t),
1062            CommonsItem::Fn(f) => self.format_fn_decl(f),
1063            CommonsItem::Capability(c) => self.format_capability(c),
1064            CommonsItem::Provider(p) => self.format_provider(p),
1065            CommonsItem::Service(s) => self.format_service(s),
1066            CommonsItem::Agent(a) => self.format_agent(a),
1067            CommonsItem::Actor(a) => self.format_actor(a),
1068            CommonsItem::Messages(m) => self.format_messages(m),
1069            CommonsItem::Event(e) => self.format_event_decl(e),
1070        }
1071    }
1072
1073    fn format_event_decl(&mut self, e: &EventDecl) {
1074        self.emit_leading_comments(&e.trivia.leading);
1075        if let Some(doc) = &e.documentation {
1076            self.emit_doc(doc);
1077        }
1078        self.push(&format!("event {}", e.name.name));
1079        // Events slice 3b (#978): an optional `@schema(N)` (and any other
1080        // future event annotation) — same loop as `format_messages`'s.
1081        for ann in &e.annotations {
1082            self.push(" ");
1083            self.push(&annotation_to_string(ann));
1084        }
1085        self.push(" = ");
1086        self.format_record_body(&e.body);
1087        self.emit_trailing_comment(e.trivia.trailing.as_deref());
1088        if e.trivia.trailing.is_none() {
1089            self.newline();
1090        }
1091    }
1092
1093    fn format_messages(&mut self, m: &MessagesDecl) {
1094        self.emit_leading_comments(&m.trivia.leading);
1095        if let Some(doc) = &m.documentation {
1096            self.emit_doc(doc);
1097        }
1098        self.push(&format!("messages \"{}\"", escape_string(&m.tag)));
1099        for ann in &m.annotations {
1100            self.push(" ");
1101            self.push(&annotation_to_string(ann));
1102        }
1103        self.push(" {");
1104        self.newline();
1105        self.indented(|f| {
1106            for entry in &m.entries {
1107                f.push(&format!(
1108                    "\"{}\" => \"{}\"",
1109                    escape_string(&entry.code),
1110                    escape_string(&entry.template)
1111                ));
1112                f.newline();
1113            }
1114        });
1115        self.push("}");
1116        self.emit_trailing_comment(m.trivia.trailing.as_deref());
1117        if m.trivia.trailing.is_none() {
1118            self.newline();
1119        }
1120    }
1121
1122    // -- Type declarations --
1123
1124    fn format_type_decl(&mut self, t: &TypeDecl) {
1125        self.emit_leading_comments(&t.trivia.leading);
1126        if let Some(doc) = &t.documentation {
1127            self.emit_doc(doc);
1128        }
1129        // v0.157 (ADR 0183): `[A, B]` type parameters, spelled as on a function.
1130        let params = if t.type_params.is_empty() {
1131            String::new()
1132        } else {
1133            let names: Vec<&str> = t
1134                .type_params
1135                .iter()
1136                .map(|tp| tp.name.name.as_str())
1137                .collect();
1138            format!("[{}]", names.join(", "))
1139        };
1140        self.push(&format!("type {}{} = ", t.name.name, params));
1141        self.format_type_body(&t.body);
1142        self.emit_trailing_comment(t.trivia.trailing.as_deref());
1143        if t.trivia.trailing.is_none() {
1144            self.newline();
1145        }
1146    }
1147
1148    fn format_type_body(&mut self, body: &TypeBody) {
1149        match body {
1150            TypeBody::Refined {
1151                base, refinement, ..
1152            } => {
1153                self.push(base.name());
1154                if let Some(r) = refinement {
1155                    self.push(" where ");
1156                    self.format_refinement(r);
1157                }
1158            }
1159            TypeBody::Opaque {
1160                base, refinement, ..
1161            } => {
1162                self.push("opaque ");
1163                self.push(base.name());
1164                if let Some(r) = refinement {
1165                    self.push(" where ");
1166                    self.format_refinement(r);
1167                }
1168            }
1169            TypeBody::Record(r) => self.format_record_body(r),
1170            TypeBody::Sum(s) => self.format_sum_body(s),
1171        }
1172    }
1173
1174    fn format_refinement(&mut self, r: &Refinement) {
1175        for (i, p) in r.predicates.iter().enumerate() {
1176            if i > 0 {
1177                self.push(" && ");
1178            }
1179            self.format_pred(p);
1180        }
1181    }
1182
1183    fn format_pred(&mut self, p: &RefinementPred) {
1184        match &p.kind {
1185            PredKind::Matches(re) => self.push(&format!("Matches(\"{}\")", escape_string(re))),
1186            PredKind::InRange(a, b) => self.push(&format!("InRange({}, {})", a.value, b.value)),
1187            PredKind::InRangeF(a, b) => self.push(&format!("InRange({}, {})", a.lexeme, b.lexeme)),
1188            PredKind::MinLength(n) => self.push(&format!("MinLength({n})")),
1189            PredKind::MaxLength(n) => self.push(&format!("MaxLength({n})")),
1190            PredKind::Length(n) => self.push(&format!("Length({n})")),
1191            PredKind::NonNegative => self.push("NonNegative"),
1192            PredKind::Positive => self.push("Positive"),
1193            PredKind::NonEmpty => self.push("NonEmpty"),
1194        }
1195    }
1196
1197    fn format_record_body(&mut self, r: &RecordBody) {
1198        if r.fields.is_empty() {
1199            self.push("{}");
1200            return;
1201        }
1202        // Try single-line first.
1203        let oneline_fields: Vec<String> = r
1204            .fields
1205            .iter()
1206            .map(|f| self.format_record_field_oneline(f))
1207            .collect();
1208        let oneline = format!("{{ {} }}", oneline_fields.join(", "));
1209        if self.fits(&oneline, 0) {
1210            self.push(&oneline);
1211            return;
1212        }
1213        // Multi-line.
1214        self.push("{");
1215        self.newline();
1216        self.indented(|f| {
1217            for (i, field) in r.fields.iter().enumerate() {
1218                f.format_record_field(field);
1219                if i + 1 < r.fields.len() || f.opts.trailing_comma {
1220                    f.push(",");
1221                }
1222                f.newline();
1223            }
1224        });
1225        self.push("}");
1226    }
1227
1228    fn format_record_field(&mut self, field: &RecordField) {
1229        self.push(&format!("{}: ", field.name.name));
1230        self.format_type_ref(&field.type_ref);
1231        if let Some(r) = &field.refinement {
1232            self.push(" where ");
1233            self.format_refinement(r);
1234        }
1235        if let Some(init) = &field.init {
1236            self.push(" = ");
1237            self.format_expr(init);
1238        }
1239    }
1240
1241    fn format_record_field_oneline(&self, field: &RecordField) -> String {
1242        let mut out = format!("{}: ", field.name.name);
1243        out.push_str(&type_ref_to_string(&field.type_ref));
1244        if let Some(r) = &field.refinement {
1245            out.push_str(" where ");
1246            out.push_str(&refinement_to_string(r));
1247        }
1248        if let Some(init) = &field.init {
1249            out.push_str(" = ");
1250            out.push_str(&expr_to_string(init));
1251        }
1252        out
1253    }
1254
1255    fn format_sum_body(&mut self, s: &SumBody) {
1256        // Two surface forms exist; we render the pipe form (clearest for both
1257        // variants with and without payload). enum form is only meaningful for
1258        // payloadless variants — round-trip preserves semantics either way.
1259        let any_payload = s.variants.iter().any(|v| !v.payload.is_empty());
1260        if !any_payload {
1261            // Enum-style.
1262            let names: Vec<&str> = s.variants.iter().map(|v| v.name.name.as_str()).collect();
1263            let oneline = format!("enum {{ {} }}", names.join(", "));
1264            if self.fits(&oneline, 0) {
1265                self.push(&oneline);
1266                return;
1267            }
1268            self.push("enum {");
1269            self.newline();
1270            self.indented(|f| {
1271                for (i, v) in s.variants.iter().enumerate() {
1272                    f.push(&v.name.name);
1273                    if i + 1 < s.variants.len() || f.opts.trailing_comma {
1274                        f.push(",");
1275                    }
1276                    f.newline();
1277                }
1278            });
1279            self.push("}");
1280            return;
1281        }
1282        // Pipe form, multi-line.
1283        for (i, v) in s.variants.iter().enumerate() {
1284            if i > 0 {
1285                self.newline();
1286            }
1287            self.push("| ");
1288            self.push(&v.name.name);
1289            if !v.payload.is_empty() {
1290                self.push("(");
1291                let parts: Vec<String> = v
1292                    .payload
1293                    .iter()
1294                    .map(|p| format!("{}: {}", p.name.name, type_ref_to_string(&p.type_ref)))
1295                    .collect();
1296                self.push(&parts.join(", "));
1297                self.push(")");
1298            }
1299        }
1300        // v0.154 (ADR 0178): the trailing `embeds E as V, …` clause, on its own
1301        // line under the variants.
1302        if !s.embeds.is_empty() {
1303            self.newline();
1304            let parts: Vec<String> = s
1305                .embeds
1306                .iter()
1307                .map(|e| {
1308                    format!(
1309                        "{} as {}",
1310                        type_ref_to_string(&e.source_type),
1311                        e.variant.name
1312                    )
1313                })
1314                .collect();
1315            self.push(&format!("embeds {}", parts.join(", ")));
1316        }
1317    }
1318
1319    fn format_type_ref(&mut self, t: &TypeRef) {
1320        self.push(&type_ref_to_string(t));
1321    }
1322
1323    // -- Function declarations --
1324
1325    fn format_fn_decl(&mut self, f: &FnDecl) {
1326        self.emit_leading_comments(&f.trivia.leading);
1327        if let Some(doc) = &f.documentation {
1328            self.emit_doc(doc);
1329        }
1330        self.push("fn ");
1331        self.push(&f.name.display());
1332        // v0.20a: `[A, B]` type parameters.
1333        if !f.type_params.is_empty() {
1334            let names: Vec<&str> = f
1335                .type_params
1336                .iter()
1337                .map(|tp| tp.name.name.as_str())
1338                .collect();
1339            self.push(&format!("[{}]", names.join(", ")));
1340        }
1341        // The signature tail that shares the parameter list's line: ` -> Ret`
1342        // plus the body's ` {` (a contract clause moves the body to its own
1343        // line, so only the return type counts then).
1344        let tail = format!(" -> {}", type_ref_to_string(&f.return_type));
1345        let reserve = if f.requires.is_empty() && f.ensures.is_empty() {
1346            tail.chars().count() + " {".len()
1347        } else {
1348            tail.chars().count()
1349        };
1350        self.format_params(&f.params, f.has_self, reserve);
1351        self.push(" -> ");
1352        self.format_type_ref(&f.return_type);
1353        // v0.115: contract clauses on their own indented lines between the
1354        // return type and the body (`requires`/`ensures <name>: <pred>`).
1355        if f.requires.is_empty() && f.ensures.is_empty() {
1356            self.push(" ");
1357        } else {
1358            self.newline();
1359            self.indented(|f2| {
1360                for c in &f.requires {
1361                    f2.push(&format!(
1362                        "requires {}: {}",
1363                        c.name.name,
1364                        expr_to_string(&c.predicate)
1365                    ));
1366                    f2.newline();
1367                }
1368                for c in &f.ensures {
1369                    f2.push(&format!(
1370                        "ensures {}: {}",
1371                        c.name.name,
1372                        expr_to_string(&c.predicate)
1373                    ));
1374                    f2.newline();
1375                }
1376            });
1377        }
1378        self.format_block(&f.body);
1379        self.emit_trailing_comment(f.trivia.trailing.as_deref());
1380        if f.trivia.trailing.is_none() {
1381            self.newline();
1382        }
1383    }
1384
1385    /// Emit a parameter list. `reserve` is the width of the signature tail that
1386    /// will follow it on the same line — `-> Ret`, any `by`/`given` clauses,
1387    /// and the body's opening brace — so a list that only "fits" by ignoring
1388    /// what comes after it wraps instead (#963).
1389    fn format_params(&mut self, params: &[Param], has_self: bool, reserve: usize) {
1390        let mut rendered: Vec<String> = Vec::new();
1391        if has_self {
1392            rendered.push("self".to_string());
1393        }
1394        // `params` never includes `self` — it is tracked separately via the
1395        // `has_self` flag (see parser.rs parse_fn_decl).
1396        for p in params {
1397            rendered.push(format!(
1398                "{}: {}",
1399                p.name.name,
1400                type_ref_to_string(&p.type_ref)
1401            ));
1402        }
1403        let oneline = format!("({})", rendered.join(", "));
1404        // An empty list has nothing to wrap onto; `()` always stays put.
1405        if rendered.is_empty() || self.fits(&oneline, reserve) {
1406            self.push(&oneline);
1407            return;
1408        }
1409        // Wrapping moves the reserved tail onto the `)` line. When that line
1410        // would overflow anyway — a `given` list long enough on its own — the
1411        // wrap costs lines and buys nothing, so keep the single-line form.
1412        let closing_line = self.indent_level as usize * self.indent_width() + 1 + reserve;
1413        if closing_line > self.opts.max_line_width as usize {
1414            self.push(&oneline);
1415            return;
1416        }
1417        // Multi-line params.
1418        self.push("(");
1419        self.newline();
1420        self.indented(|f| {
1421            for (i, r) in rendered.iter().enumerate() {
1422                f.push(r);
1423                // Parameter lists — unlike records, enum/sum variants, agent
1424                // state fields and exports — do NOT accept a trailing comma in
1425                // the grammar, so never emit one here regardless of the
1426                // `trailing_comma` option, or the wrapped output fails to
1427                // re-parse.
1428                if i + 1 < rendered.len() {
1429                    f.push(",");
1430                }
1431                f.newline();
1432            }
1433        });
1434        self.push(")");
1435    }
1436
1437    // -- Capability / provider / service / agent (v0.5) --
1438
1439    fn format_capability(&mut self, c: &CapabilityDecl) {
1440        self.emit_leading_comments(&c.trivia.leading);
1441        if let Some(doc) = &c.documentation {
1442            self.emit_doc(doc);
1443        }
1444        self.push(&format!("capability {} {{", c.name.name));
1445        self.newline();
1446        self.indented(|f| {
1447            for op in &c.ops {
1448                f.emit_leading_comments(&op.trivia.leading);
1449                if let Some(doc) = &op.documentation {
1450                    f.emit_doc(doc);
1451                }
1452                f.push("fn ");
1453                f.push(&op.name.name);
1454                // #926: `[T, …]` type parameters on the op itself.
1455                if !op.type_params.is_empty() {
1456                    let names: Vec<&str> = op
1457                        .type_params
1458                        .iter()
1459                        .map(|tp| tp.name.name.as_str())
1460                        .collect();
1461                    f.push(&format!("[{}]", names.join(", ")));
1462                }
1463                let reserve = 4 + type_ref_to_string(&op.return_type).chars().count();
1464                f.format_params(&op.params, false, reserve);
1465                f.push(" -> ");
1466                f.format_type_ref(&op.return_type);
1467                f.emit_trailing_comment(op.trivia.trailing.as_deref());
1468                if op.trivia.trailing.is_none() {
1469                    f.newline();
1470                }
1471            }
1472        });
1473        self.push("}");
1474        self.emit_trailing_comment(c.trivia.trailing.as_deref());
1475        if c.trivia.trailing.is_none() {
1476            self.newline();
1477        }
1478    }
1479
1480    fn format_provider(&mut self, p: &ProviderDecl) {
1481        self.emit_leading_comments(&p.trivia.leading);
1482        if let Some(doc) = &p.documentation {
1483            self.emit_doc(doc);
1484        }
1485        self.push(&format!(
1486            "provides {} = {}",
1487            p.capability.name, p.provider_name.name
1488        ));
1489        if !p.given.is_empty() {
1490            self.push(" given ");
1491            let names: Vec<String> = p.given.iter().map(cap_ref_src).collect();
1492            self.push(&names.join(", "));
1493        }
1494        // v0.17: an external provider (inside an adapter) has no body.
1495        if p.external {
1496            self.emit_trailing_comment(p.trivia.trailing.as_deref());
1497            if p.trivia.trailing.is_none() {
1498                self.newline();
1499            }
1500            return;
1501        }
1502        self.push(" {");
1503        self.newline();
1504        self.indented(|f| {
1505            for (i, op) in p.ops.iter().enumerate() {
1506                if i > 0 {
1507                    f.newline();
1508                }
1509                f.emit_leading_comments(&op.trivia.leading);
1510                f.push("fn ");
1511                f.push(&op.name.name);
1512                let reserve = 4 + type_ref_to_string(&op.return_type).chars().count() + 2;
1513                f.format_params(&op.params, false, reserve);
1514                f.push(" -> ");
1515                f.format_type_ref(&op.return_type);
1516                f.push(" ");
1517                f.format_block(&op.body);
1518                f.emit_trailing_comment(op.trivia.trailing.as_deref());
1519                if op.trivia.trailing.is_none() {
1520                    f.newline();
1521                }
1522            }
1523        });
1524        self.push("}");
1525        self.emit_trailing_comment(p.trivia.trailing.as_deref());
1526        if p.trivia.trailing.is_none() {
1527            self.newline();
1528        }
1529    }
1530
1531    fn format_service(&mut self, s: &ServiceDecl) {
1532        self.emit_leading_comments(&s.trivia.leading);
1533        if let Some(doc) = &s.documentation {
1534            self.emit_doc(doc);
1535        }
1536        let from = match &s.protocol {
1537            ServiceProtocol::Call => String::new(),
1538            ServiceProtocol::Http => " from http".to_string(),
1539            ServiceProtocol::Cron => " from cron".to_string(),
1540            ServiceProtocol::Queue { name } => {
1541                format!(" from queue(\"{}\")", escape_string(name))
1542            }
1543            ServiceProtocol::WebSocket { in_type, out_type } => {
1544                format!(
1545                    " from websocket(in: {}, out: {})",
1546                    type_ref_to_string(in_type),
1547                    type_ref_to_string(out_type)
1548                )
1549            }
1550            ServiceProtocol::Events {
1551                event_type,
1552                pattern,
1553                schema_dispatch,
1554            } => {
1555                let header = match pattern {
1556                    Some(p) => format!(
1557                        " from Events({} {})",
1558                        type_ref_to_string(event_type),
1559                        event_pattern_src(p)
1560                    ),
1561                    None => format!(" from Events({})", type_ref_to_string(event_type)),
1562                };
1563                match schema_dispatch {
1564                    Some(d) => format!("{header} {}", schema_dispatch_src(d)),
1565                    None => header,
1566                }
1567            }
1568        };
1569        // v0.155: the optional service-level `by`/`given` defaults follow the
1570        // protocol on the header, `by` first — the ambient contract every handler
1571        // inherits unless it declares its own.
1572        let mut header = format!("service {}{}", s.name.name, from);
1573        if let Some(by) = &s.default_by {
1574            header.push_str(&format!(" {}", by_clause_src(by)));
1575        }
1576        if !s.default_given.is_empty() {
1577            let names: Vec<String> = s.default_given.iter().map(cap_ref_src).collect();
1578            header.push_str(&format!(" given {}", names.join(", ")));
1579        }
1580        self.push(&format!("{header} {{"));
1581        self.newline();
1582        self.indented(|f| {
1583            // v0.131/v0.141/v0.142: the CORS, security, and limits policies are
1584            // header-position sections, before the handlers (mirroring the agent
1585            // phase order). A canonical order — `cors`, then `security`, then
1586            // `limits` — with a blank line between each section.
1587            if let Some(cors) = &s.cors {
1588                f.format_cors_policy(cors);
1589                if s.security.is_some() || s.limits.is_some() || !s.handlers.is_empty() {
1590                    f.newline();
1591                }
1592            }
1593            if let Some(security) = &s.security {
1594                f.format_security_policy(security);
1595                if s.limits.is_some() || !s.handlers.is_empty() {
1596                    f.newline();
1597                }
1598            }
1599            if let Some(limits) = &s.limits {
1600                f.format_limits_policy(limits);
1601                if !s.handlers.is_empty() {
1602                    f.newline();
1603                }
1604            }
1605            for (i, h) in s.handlers.iter().enumerate() {
1606                if i > 0 {
1607                    f.newline();
1608                }
1609                f.format_handler(h);
1610            }
1611        });
1612        self.push("}");
1613        self.emit_trailing_comment(s.trivia.trailing.as_deref());
1614        if s.trivia.trailing.is_none() {
1615            self.newline();
1616        }
1617    }
1618
1619    /// Format a `cors { }` policy section (v0.131). One `name: value` field per
1620    /// line, with a trailing comma, mirroring a record construction.
1621    fn format_cors_policy(&mut self, cors: &CorsPolicy) {
1622        self.emit_leading_comments(&cors.trivia.leading);
1623        self.push("cors {");
1624        self.newline();
1625        self.indented(|f| {
1626            for field in &cors.fields {
1627                f.push(&format!("{}: ", field.name.name));
1628                f.format_expr_at(&field.value, 0, 1);
1629                f.push(",");
1630                f.newline();
1631            }
1632        });
1633        self.push("}");
1634        self.newline();
1635    }
1636
1637    /// Format a `security { }` policy section (v0.141). One `name: value` field
1638    /// per line, with a trailing comma, mirroring `format_cors_policy`.
1639    fn format_security_policy(&mut self, security: &SecurityPolicy) {
1640        self.emit_leading_comments(&security.trivia.leading);
1641        self.push("security {");
1642        self.newline();
1643        self.indented(|f| {
1644            for field in &security.fields {
1645                f.push(&format!("{}: ", field.name.name));
1646                f.format_expr_at(&field.value, 0, 1);
1647                f.push(",");
1648                f.newline();
1649            }
1650        });
1651        self.push("}");
1652        self.newline();
1653    }
1654
1655    /// Format a `limits { }` policy section (v0.142). One `name: value` field per
1656    /// line, with a trailing comma, mirroring `format_cors_policy`. A `maxBody`
1657    /// value keeps its as-written `_` digit separators (the `IntLit` lexeme).
1658    fn format_limits_policy(&mut self, limits: &LimitsPolicy) {
1659        self.emit_leading_comments(&limits.trivia.leading);
1660        self.push("limits {");
1661        self.newline();
1662        self.indented(|f| {
1663            for field in &limits.fields {
1664                f.push(&format!("{}: ", field.name.name));
1665                f.format_expr_at(&field.value, 0, 1);
1666                f.push(",");
1667                f.newline();
1668            }
1669        });
1670        self.push("}");
1671        self.newline();
1672    }
1673
1674    fn format_agent(&mut self, a: &AgentDecl) {
1675        self.emit_leading_comments(&a.trivia.leading);
1676        if let Some(doc) = &a.documentation {
1677            self.emit_doc(doc);
1678        }
1679        self.push(&format!("agent {} {{", a.name.name));
1680        self.newline();
1681        self.indented(|f| {
1682            // key
1683            f.push(&format!(
1684                "key {}: {}",
1685                a.key_name.name,
1686                type_ref_to_string(&a.key_type)
1687            ));
1688            f.newline();
1689            f.newline();
1690            // storage (v0.81, storage track): the agent's `store` fields.
1691            for sf in &a.store_fields {
1692                f.format_store_field(sf);
1693                f.newline();
1694            }
1695            // v0.80: invariants form a phase between the storage fields and the
1696            // handlers.
1697            for inv in &a.invariants {
1698                f.newline();
1699                f.format_invariant(inv);
1700            }
1701            // v0.116: step invariants form part of the same phase, beside the
1702            // snapshot invariants.
1703            for tr in &a.transitions {
1704                f.newline();
1705                f.format_transition(tr);
1706            }
1707            // handlers
1708            for h in &a.handlers {
1709                f.newline();
1710                f.format_handler(h);
1711            }
1712        });
1713        self.push("}");
1714        self.emit_trailing_comment(a.trivia.trailing.as_deref());
1715        if a.trivia.trailing.is_none() {
1716            self.newline();
1717        }
1718    }
1719
1720    /// Format a `store` field (v0.81): `store <name>: <Kind> [= <init>]`, with
1721    /// its leading comments / doc and trailing comment. The enclosing loop adds
1722    /// the line break.
1723    fn format_store_field(&mut self, sf: &StoreField) {
1724        self.emit_leading_comments(&sf.trivia.leading);
1725        if let Some(doc) = &sf.documentation {
1726            self.emit_doc(doc);
1727        }
1728        self.push(&format!(
1729            "store {}: {}",
1730            sf.name.name,
1731            store_kind_to_string(&sf.kind)
1732        ));
1733        // v0.85 (ADR 0111): annotations follow the kind, one space-separated each.
1734        for ann in &sf.annotations {
1735            self.push(&format!(" {}", annotation_to_string(ann)));
1736        }
1737        if let Some(init) = &sf.init {
1738            self.push(" = ");
1739            self.format_expr(init);
1740        }
1741        self.emit_trailing_comment(sf.trivia.trailing.as_deref());
1742    }
1743
1744    /// Format an agent invariant (v0.80): the name on one line, the predicate
1745    /// indented beneath, matching the §14 worked examples.
1746    fn format_invariant(&mut self, inv: &Invariant) {
1747        self.emit_leading_comments(&inv.trivia.leading);
1748        if let Some(doc) = &inv.documentation {
1749            self.emit_doc(doc);
1750        }
1751        self.push(&format!("invariant {}:", inv.name.name));
1752        self.newline();
1753        self.indented(|f| {
1754            f.format_expr(&inv.predicate);
1755        });
1756        self.emit_trailing_comment(inv.trivia.trailing.as_deref());
1757        if inv.trivia.trailing.is_none() {
1758            self.newline();
1759        }
1760    }
1761
1762    /// Format an agent step invariant (v0.116): `transition <name>:` with the
1763    /// `old`/`new` predicate indented beneath, mirroring [`format_invariant`].
1764    fn format_transition(&mut self, tr: &Transition) {
1765        self.emit_leading_comments(&tr.trivia.leading);
1766        if let Some(doc) = &tr.documentation {
1767            self.emit_doc(doc);
1768        }
1769        self.push(&format!("transition {}:", tr.name.name));
1770        self.newline();
1771        self.indented(|f| {
1772            f.format_expr(&tr.predicate);
1773        });
1774        self.emit_trailing_comment(tr.trivia.trailing.as_deref());
1775        if tr.trivia.trailing.is_none() {
1776            self.newline();
1777        }
1778    }
1779
1780    fn format_actor(&mut self, a: &ActorDecl) {
1781        self.emit_leading_comments(&a.trivia.leading);
1782        if let Some(doc) = &a.documentation {
1783            self.emit_doc(doc);
1784        }
1785        if let Some(r) = &a.refinement {
1786            // Reserved refinement form: `actor Name = Base where <predicate>`.
1787            self.push(&format!(
1788                "actor {} = {} where {}",
1789                a.name.name,
1790                r.base.name,
1791                expr_to_string(&r.predicate)
1792            ));
1793        } else {
1794            // Normal form: `actor Name { auth = Scheme(, identity = Type)? }`.
1795            let auth = a.auth.as_ref().map(|i| i.name.as_str()).unwrap_or("None");
1796            let args: Vec<String> = a
1797                .auth_config
1798                .iter()
1799                .map(|arg| match &arg.value {
1800                    bynk_syntax::ast::SchemeArgValue::Str(s) => {
1801                        format!("{} = \"{}\"", arg.key.name, escape_string(s))
1802                    }
1803                    bynk_syntax::ast::SchemeArgValue::Int(n) => {
1804                        format!("{} = {n}", arg.key.name)
1805                    }
1806                })
1807                .collect();
1808            let config = if args.is_empty() {
1809                String::new()
1810            } else {
1811                format!("({})", args.join(", "))
1812            };
1813            let identity = a
1814                .identity
1815                .as_ref()
1816                .map(|id| format!(", identity = {}", type_ref_to_string(id)))
1817                .unwrap_or_default();
1818            let oneline = format!(
1819                "actor {} {{ auth = {auth}{config}{identity} }}",
1820                a.name.name
1821            );
1822            if args.is_empty() || self.fits(&oneline, 0) {
1823                self.push(&oneline);
1824            } else {
1825                // An OIDC-style scheme carries issuer / audience / JWKS URLs
1826                // that blow past any line budget on one line (#963): open the
1827                // braces and give each scheme argument its own line.
1828                self.push(&format!("actor {} {{", a.name.name));
1829                self.newline();
1830                self.indented(|f| {
1831                    f.push(&format!("auth = {auth}("));
1832                    f.newline();
1833                    f.indented(|f2| {
1834                        for (i, arg) in args.iter().enumerate() {
1835                            f2.push(arg);
1836                            if i + 1 < args.len() {
1837                                f2.push(",");
1838                            }
1839                            f2.newline();
1840                        }
1841                    });
1842                    f.push(")");
1843                    if !identity.is_empty() {
1844                        // `identity` is a sibling of `auth`, so its comma stays
1845                        // with `auth`'s closing paren and it starts a new line.
1846                        f.push(",");
1847                        f.newline();
1848                        f.push(identity.trim_start_matches(", "));
1849                    }
1850                    f.newline();
1851                });
1852                self.push("}");
1853            }
1854        }
1855        self.emit_trailing_comment(a.trivia.trailing.as_deref());
1856        if a.trivia.trailing.is_none() {
1857            self.newline();
1858        }
1859    }
1860
1861    fn format_handler(&mut self, h: &Handler) {
1862        self.emit_leading_comments(&h.trivia.leading);
1863        if let Some(doc) = &h.documentation {
1864            self.emit_doc(doc);
1865        }
1866        // v0.140 (ADR 0163): handler-position annotations (`@cache(…)`) print one
1867        // per line above the `on`, mirroring how decorators read in source. Each is
1868        // rendered by the shared `annotation_to_string` used for `store` fields.
1869        for ann in &h.annotations {
1870            self.push(&annotation_to_string(ann));
1871            self.newline();
1872        }
1873        // The handler kind prefix: `on call`, `on http METHOD "path"`, or
1874        // `on cron("expr")`. Agent `on call` handlers carry a method name.
1875        match &h.kind {
1876            HandlerKind::Call => {
1877                self.push("on call");
1878                if let Some(m) = &h.method_name {
1879                    self.push(&format!(" {}", m.name));
1880                }
1881            }
1882            HandlerKind::Http { method, path } => {
1883                // Trailing space: the path string is followed by the param list,
1884                // which reads better separated (`… "/path" (params)`).
1885                self.push(&format!(
1886                    "on {}(\"{}\") ",
1887                    method.as_str(),
1888                    escape_string(path)
1889                ));
1890            }
1891            HandlerKind::Cron { expr } => {
1892                self.push(&format!("on schedule(\"{}\") ", escape_string(expr)));
1893            }
1894            HandlerKind::Message => {
1895                self.push("on message");
1896            }
1897            HandlerKind::Open => {
1898                self.push("on open");
1899            }
1900            HandlerKind::Close => {
1901                self.push("on close");
1902            }
1903            HandlerKind::Event => {
1904                self.push("on event");
1905            }
1906        }
1907        // The param list follows the kind prefix directly — `on call(params)`,
1908        // `on open(params)` — while the Http/Cron prefixes already emit a trailing
1909        // space (`on GET("/x") (params)`). (v0.155: the `by` clause no longer sits
1910        // here, so no separating space is needed.)
1911        // Everything from `-> Ret` to the body's `{` shares the parameter
1912        // list's line and none of it can wrap (the `given` list in particular
1913        // is newline-sensitive), so the whole tail is reserved up front and the
1914        // parameters are what gives (#963).
1915        let mut tail = format!(" -> {}", type_ref_to_string(&h.return_type));
1916        if let Some(by) = &h.by_clause {
1917            tail.push_str(&format!(" {}", by_clause_src(by)));
1918        }
1919        if !h.given.is_empty() {
1920            let names: Vec<String> = h.given.iter().map(cap_ref_src).collect();
1921            tail.push_str(&format!(" given {}", names.join(", ")));
1922        }
1923        self.format_params(&h.params, false, tail.chars().count() + " {".len());
1924        self.push(&tail);
1925        self.push(" ");
1926        self.format_block(&h.body);
1927        self.emit_trailing_comment(h.trivia.trailing.as_deref());
1928        if h.trivia.trailing.is_none() {
1929            self.newline();
1930        }
1931    }
1932
1933    // -- Blocks, statements, expressions --
1934
1935    fn format_block(&mut self, b: &Block) {
1936        self.format_block_with_reserve(b, 0);
1937    }
1938
1939    /// Format a block, knowing that `reserve` columns of text will follow its
1940    /// closing brace on the same line (` else {` on an `if`'s then-branch, an
1941    /// arm's `,`). Only a block that fits *including* that tail stays inline.
1942    fn format_block_with_reserve(&mut self, b: &Block, reserve: usize) {
1943        // A block with no statements, no trivia, and a simple tail
1944        // expression can be emitted inline if it fits; otherwise multi-line.
1945        let tail_oneline = expr_to_string(&b.tail);
1946        let any_stmt_trivia = b.statements.iter().any(|s| !statement_trivia(s).is_empty());
1947        if b.statements.is_empty()
1948            && b.tail_leading_comments.is_empty()
1949            && !any_stmt_trivia
1950            && self.fits(&format!("{{ {tail_oneline} }}"), reserve)
1951        {
1952            self.push("{ ");
1953            self.push(&tail_oneline);
1954            self.push(" }");
1955            return;
1956        }
1957        self.format_block_multiline(b);
1958    }
1959
1960    /// The multi-line block form: brace, one statement per indented line, the
1961    /// tail expression, closing brace. Split out of [`Self::format_block`] so
1962    /// the wrapped-expression printer can force it (#963) for an `if`/lambda
1963    /// body whose single-line form would overflow.
1964    fn format_block_multiline(&mut self, b: &Block) {
1965        self.push("{");
1966        self.newline();
1967        self.indented(|f| {
1968            for stmt in &b.statements {
1969                let trivia = statement_trivia(stmt);
1970                f.emit_leading_comments(&trivia.leading);
1971                f.format_statement(stmt);
1972                f.emit_trailing_comment(trivia.trailing.as_deref());
1973                if trivia.trailing.is_none() {
1974                    f.newline();
1975                }
1976            }
1977            f.emit_leading_comments(&b.tail_leading_comments);
1978            if !omit_unit_tail(b) {
1979                f.format_expr(&b.tail);
1980                f.newline();
1981            }
1982        });
1983        self.push("}");
1984    }
1985
1986    fn format_statement(&mut self, s: &Statement) {
1987        match s {
1988            Statement::Let(l) => {
1989                self.push("let ");
1990                self.push(&l.name.name);
1991                if let Some(t) = &l.type_annot {
1992                    self.push(": ");
1993                    self.format_type_ref(t);
1994                }
1995                self.push(" = ");
1996                self.format_expr(&l.value);
1997            }
1998            Statement::EffectLet(l) => {
1999                self.push("let ");
2000                self.push(&l.name.name);
2001                if let Some(t) = &l.type_annot {
2002                    self.push(": ");
2003                    self.format_type_ref(t);
2004                }
2005                self.push(" <- ");
2006                // The `by <Actor>` clause trails the value on the same line.
2007                let principal = l
2008                    .principal
2009                    .as_ref()
2010                    .map(|p| format!(" {}", call_site_actor_src(p)));
2011                let reserve = principal.as_deref().map_or(0, |p| p.chars().count());
2012                self.format_expr_at(&l.value, 0, reserve);
2013                if let Some(principal) = principal {
2014                    self.push(&principal);
2015                }
2016            }
2017            Statement::Expect(a) => {
2018                self.push("expect ");
2019                self.format_expr(&a.value);
2020            }
2021            Statement::Send(s) => {
2022                self.push("~> ");
2023                self.format_expr(&s.value);
2024            }
2025            Statement::Do(d) => {
2026                self.push("do ");
2027                self.format_expr(&d.value);
2028            }
2029            Statement::Assign(a) => {
2030                self.push(&a.target.name);
2031                self.push(" := ");
2032                self.format_expr(&a.value);
2033            }
2034        }
2035    }
2036
2037    fn format_expr(&mut self, e: &Expr) {
2038        self.format_expr_at(e, 0, 0);
2039    }
2040
2041    /// Emit `e` at the current column, breaking it across lines when its
2042    /// single-line form would overrun the line budget (#963).
2043    ///
2044    /// `parent_prec` is the enclosing operator's precedence, exactly as in
2045    /// [`expr_with_prec`] — it decides parenthesisation. `reserve` is the width
2046    /// of text the caller will emit after this expression on the same line (a
2047    /// closing `)`, an arm's `,`, ` else {`), so a sub-expression is not judged
2048    /// to fit on the strength of a line it does not in fact end.
2049    ///
2050    /// The flat form always wins when it fits: this only ever *adds* line
2051    /// breaks, and only at points the grammar accepts (verified by the
2052    /// round-trip guard in [`format_source`]).
2053    fn format_expr_at(&mut self, e: &Expr, parent_prec: u8, reserve: usize) {
2054        // `match` renders multi-line unconditionally, so it must go through the
2055        // indent-aware emitter rather than `expr_to_string` — the latter builds
2056        // a flat string with hardcoded single-tab arms that ignores the current
2057        // nesting depth (the closing brace and every arm would land at column
2058        // one regardless of how deeply the `match` is nested).
2059        if let ExprKind::Match { discriminant, arms } = &e.kind {
2060            self.format_match(discriminant, arms);
2061            return;
2062        }
2063        let flat = expr_with_prec(e, parent_prec);
2064        if self.fits(&flat, reserve) {
2065            self.push(&flat);
2066            return;
2067        }
2068        // Too wide. Re-emit broken across lines — inside the parentheses the
2069        // flat form would have added, if precedence calls for them.
2070        if needs_parens(e, parent_prec) {
2071            self.push("(");
2072            self.format_expr_broken(e, reserve + 1);
2073            self.push(")");
2074        } else {
2075            self.format_expr_broken(e, reserve);
2076        }
2077    }
2078
2079    /// The multi-line rendering of an expression that does not fit. Each arm
2080    /// breaks at a point the grammar tolerates a newline; anything with no such
2081    /// point (a long string literal, an identifier) falls through to the flat
2082    /// form, which simply overruns — the 100-column target is soft.
2083    fn format_expr_broken(&mut self, e: &Expr, reserve: usize) {
2084        match &e.kind {
2085            // `T { field: value, … }` — one field per line.
2086            ExprKind::RecordConstruction { type_name, fields } if !fields.is_empty() => {
2087                self.push(&format!("{} {{", type_name.name));
2088                self.format_field_inits(fields.iter(), None);
2089            }
2090            // `T { ...base, field: value, … }` — the spread first, then the
2091            // overrides, one per line.
2092            ExprKind::RecordSpread {
2093                type_name,
2094                base,
2095                overrides,
2096            } => {
2097                match type_name {
2098                    Some(tn) => self.push(&format!("{} {{", tn.name)),
2099                    None => self.push("{"),
2100                }
2101                let spread = format!("...{}", expr_with_prec(base, 0));
2102                self.format_field_inits(overrides.iter(), Some(&spread));
2103            }
2104            // A call's arguments, one per line. Unlike a record body an
2105            // argument list does NOT accept a trailing comma (the grammar
2106            // rejects it), so the wrapped form never emits one.
2107            ExprKind::Call {
2108                name,
2109                type_args,
2110                args,
2111            } if !args.is_empty() => {
2112                self.push(&format!("{}{}(", name.name, type_args_src(type_args)));
2113                self.format_arg_list(args, reserve);
2114            }
2115            ExprKind::ConstructorCall {
2116                type_name,
2117                method,
2118                args,
2119            } if !args.is_empty() => {
2120                self.push(&format!("{}.{}(", type_name.name, method.name));
2121                self.format_arg_list(args, reserve);
2122            }
2123            ExprKind::Val { type_ref, args } if !args.is_empty() => {
2124                self.push(&format!("Val[{}](", type_ref_to_string(type_ref)));
2125                self.format_arg_list(args, reserve);
2126            }
2127            // A `.`-chain: `receiver.a().b()` — kept on one line where the
2128            // overflow is an argument's, broken before each call where it is
2129            // the chain's own (see `format_chain`).
2130            ExprKind::MethodCall { .. } | ExprKind::FieldAccess { .. } => {
2131                self.format_chain(e, reserve);
2132            }
2133            // `[a, b, c]` — one element per line.
2134            ExprKind::ListLit(elems) if !elems.is_empty() => {
2135                self.push("[");
2136                self.newline();
2137                self.indented(|f| {
2138                    for (i, elem) in elems.iter().enumerate() {
2139                        let last = i + 1 == elems.len();
2140                        f.format_expr_at(elem, 0, if last { 0 } else { 1 });
2141                        if !last || f.opts.trailing_comma {
2142                            f.push(",");
2143                        }
2144                        f.newline();
2145                    }
2146                });
2147                self.push("]");
2148            }
2149            // A run of `&&` / `||` / `implies` breaks before each operator, per
2150            // the spec's "wraps at `&&`/`||` boundaries". Only these: a
2151            // continuation line starting with an arithmetic or comparison
2152            // operator does not re-attach to the line above on re-parse.
2153            ExprKind::BinOp(op, ..) if is_logical(*op) => {
2154                let prec = binop_prec(*op);
2155                let mut operands = Vec::new();
2156                flatten_binop(e, *op, &mut operands);
2157                self.format_expr_at(operands[0], prec, 0);
2158                self.indented(|f| {
2159                    for (i, operand) in operands.iter().enumerate().skip(1) {
2160                        f.newline();
2161                        f.push(&format!("{} ", op.name()));
2162                        let last = i + 1 == operands.len();
2163                        f.format_expr_at(operand, prec + 1, if last { reserve } else { 0 });
2164                    }
2165                });
2166            }
2167            // Any other binary operator stays on one line, but its operands may
2168            // still break internally (a record or call on either side).
2169            ExprKind::BinOp(op, lhs, rhs) => {
2170                let prec = binop_prec(*op);
2171                let tail = format!(" {} {}", op.name(), expr_with_prec(rhs, prec + 1));
2172                // The right-hand side shares the operator's line whenever it is
2173                // itself unbroken, so charge it to the left-hand side's budget.
2174                let lhs_reserve = if tail.contains('\n') {
2175                    0
2176                } else {
2177                    tail.chars().count() + reserve
2178                };
2179                self.format_expr_at(lhs, prec, lhs_reserve);
2180                self.push(&format!(" {} ", op.name()));
2181                self.format_expr_at(rhs, prec + 1, reserve);
2182            }
2183            ExprKind::Is { value, pattern } => {
2184                let pat = format!(" is {}", pattern_to_string(pattern));
2185                self.format_expr_at(value, 4, pat.chars().count() + reserve);
2186                self.push(&pat);
2187            }
2188            // `if cond { … } else { … }` — both branches go vertical. Once the
2189            // one-line form is over budget, splitting only one branch leaves a
2190            // lopsided line that is no easier to read.
2191            ExprKind::If {
2192                cond,
2193                then_block,
2194                else_block,
2195            } => {
2196                self.push("if ");
2197                self.format_expr_at(cond, 0, 2);
2198                self.push(" ");
2199                self.format_block_multiline(then_block);
2200                // v0.146 (ADR 0170): an `if` with no `else` carries a
2201                // synthesised unit else-branch — omit it, as the flat form does.
2202                if !else_block.is_synth_unit() {
2203                    self.push(" else ");
2204                    self.format_block_multiline(else_block);
2205                }
2206            }
2207            ExprKind::Block(b) => self.format_block_multiline(b),
2208            ExprKind::Lambda(lambda) => {
2209                let params: Vec<String> = lambda
2210                    .params
2211                    .iter()
2212                    .map(|p| match &p.type_ref {
2213                        Some(tr) => format!("{}: {}", p.name.name, type_ref_to_string(tr)),
2214                        None => p.name.name.clone(),
2215                    })
2216                    .collect();
2217                self.push(&format!("({}) => ", params.join(", ")));
2218                self.format_expr_at(&lambda.body, 0, reserve);
2219            }
2220            // Single-argument wrappers: nothing to break at the wrapper itself,
2221            // so recurse and let the payload wrap inside the parentheses.
2222            ExprKind::Ok(v) => self.wrap_call("Ok(", v, reserve),
2223            ExprKind::Err(v) => self.wrap_call("Err(", v, reserve),
2224            ExprKind::Some(v) => self.wrap_call("Some(", v, reserve),
2225            ExprKind::EffectPure(v) => self.wrap_call("Effect.pure(", v, reserve),
2226            ExprKind::Wire(v) => self.wrap_call("Wire(", v, reserve),
2227            ExprKind::Paren(v) => self.wrap_call("(", v, reserve),
2228            ExprKind::Question(v) => {
2229                self.format_expr_at(v, 8, reserve + 1);
2230                self.push("?");
2231            }
2232            ExprKind::Expect(v) => {
2233                self.push("expect ");
2234                self.format_expr_at(v, 0, reserve);
2235            }
2236            // Nothing breakable — a literal, an identifier, an interpolated
2237            // string. Emit it as-is and overrun.
2238            _ => self.push(&expr_with_prec(e, 0)),
2239        }
2240    }
2241
2242    /// `<head><inner>)` where `inner` wraps inside the parentheses.
2243    fn wrap_call(&mut self, head: &str, inner: &Expr, reserve: usize) {
2244        self.push(head);
2245        self.format_expr_at(inner, 0, reserve + 1);
2246        self.push(")");
2247    }
2248
2249    /// The body of a wrapped record construction or spread: one `name: value`
2250    /// per indented line, then the closing brace. `spread` is the leading
2251    /// `...base` entry, when there is one. The brace and any type name are the
2252    /// caller's to emit.
2253    fn format_field_inits<'f, I>(&mut self, fields: I, spread: Option<&str>)
2254    where
2255        I: ExactSizeIterator<Item = &'f FieldInit>,
2256    {
2257        let total = fields.len() + usize::from(spread.is_some());
2258        self.newline();
2259        self.indented(|f| {
2260            let mut emitted = 0usize;
2261            if let Some(spread) = spread {
2262                f.push(spread);
2263                emitted += 1;
2264                if emitted < total || f.opts.trailing_comma {
2265                    f.push(",");
2266                }
2267                f.newline();
2268            }
2269            for field in fields {
2270                f.push(&field.name.name);
2271                if let Some(v) = &field.value {
2272                    f.push(": ");
2273                    f.format_expr_at(v, 0, 1);
2274                }
2275                emitted += 1;
2276                if emitted < total || f.opts.trailing_comma {
2277                    f.push(",");
2278                }
2279                f.newline();
2280            }
2281        });
2282        self.push("}");
2283    }
2284
2285    /// The arguments of a wrapped call plus its closing `)` — the caller has
2286    /// already emitted the `name(` head.
2287    ///
2288    /// A trailing argument hugs the call — the earlier arguments stay on the
2289    /// call's line and it opens its own body there, so
2290    /// `xs.fold(init, (acc, x) => match acc {` reads as one construct instead
2291    /// of being pushed down a level. Hugging is attempted for a sole argument
2292    /// (there is no sibling for it to misalign against) and, past that, only
2293    /// for a trailing lambda / record / block / `match` / `if`, whose opening
2294    /// line is short. It is taken only if every line it produces fits;
2295    /// otherwise each argument goes on its own indented line.
2296    ///
2297    /// Never a trailing comma — the grammar rejects one in an argument list,
2298    /// and the wrapped output has to re-parse.
2299    fn format_arg_list(&mut self, args: &[Expr], reserve: usize) -> bool {
2300        if let Some((last, leading)) = args.split_last()
2301            && (leading.is_empty() || is_block_like(last))
2302            && self.try_layout(reserve, |f| {
2303                for arg in leading {
2304                    f.push(&expr_with_prec(arg, 0));
2305                    f.push(", ");
2306                }
2307                f.format_expr_at(last, 0, 1);
2308                f.push(")");
2309            })
2310        {
2311            return false;
2312        }
2313        self.newline();
2314        self.indented(|f| {
2315            for (i, arg) in args.iter().enumerate() {
2316                let last = i + 1 == args.len();
2317                f.format_expr_at(arg, 0, if last { 0 } else { 1 });
2318                if !last {
2319                    f.push(",");
2320                }
2321                f.newline();
2322            }
2323        });
2324        self.push(")");
2325        true
2326    }
2327
2328    /// Emit a `.`-chain (`receiver.a(…).b(…).c`) broken across lines.
2329    ///
2330    /// A single-call chain never breaks at its `.` — there is no pipeline to
2331    /// read and the overflow belongs to the argument list. A multi-call chain
2332    /// prefers to stay on one line too, and breaks before each call only when
2333    /// staying inline would strand an exploded argument list mid-chain.
2334    fn format_chain(&mut self, e: &Expr, reserve: usize) {
2335        let (base, links) = flatten_chain(e);
2336        let calls = links
2337            .iter()
2338            .filter(|l| matches!(l, ChainLink::Method { .. }))
2339            .count();
2340        if calls < 2 {
2341            self.format_chain_inline(base, &links, reserve);
2342            return;
2343        }
2344        // Keeping a multi-call chain intact is preferable when the overflow
2345        // belongs to an argument rather than to the chain — the
2346        // `xs.fold(init, (acc, x) => match acc {` shape, where a body opens on
2347        // the chain's own line. That reading survives only while every step
2348        // either fits or *hugs*. A step forced to put its arguments one per
2349        // line strands a bare `)` mid-chain, at which point the chain itself is
2350        // what is too long, and breaking at its dots reads better.
2351        let exploded = std::cell::Cell::new(false);
2352        if self.try_layout_if(
2353            reserve,
2354            |f| exploded.set(f.format_chain_inline(base, &links, reserve)),
2355            || !exploded.get(),
2356        ) {
2357            return;
2358        }
2359        // Break before each *call*, not before each `.`. A field access is part
2360        // of whatever it qualifies: a leading `msg.params` belongs to the
2361        // receiver, and a `.rows.count()` reads as one step, so a line never
2362        // opens with a bare `.field`.
2363        let first_call = links
2364            .iter()
2365            .position(|l| matches!(l, ChainLink::Method { .. }))
2366            .expect("a chain with two calls has one");
2367        self.format_expr_at(base, 8, 0);
2368        for link in &links[..first_call] {
2369            self.format_chain_link(link, 0);
2370        }
2371        self.indented(|f| {
2372            let mut i = first_call;
2373            while i < links.len() {
2374                let mut end = i;
2375                while end < links.len() && matches!(links[end], ChainLink::Field(_)) {
2376                    end += 1;
2377                }
2378                // …and the call those field accesses qualify, if any.
2379                if end < links.len() {
2380                    end += 1;
2381                }
2382                f.newline();
2383                for (offset, link) in links[i..end].iter().enumerate() {
2384                    let is_last = end == links.len() && i + offset + 1 == links.len();
2385                    f.format_chain_link(link, if is_last { reserve } else { 0 });
2386                }
2387                i = end;
2388            }
2389        });
2390    }
2391
2392    /// The chain on one line: the receiver, then every `.`-step in place. Any
2393    /// step whose arguments do not fit wraps them, but no break is introduced
2394    /// at a `.`. Reports whether any step had to put its arguments one per
2395    /// line, which is what tells [`Self::format_chain`] this layout is a poor
2396    /// fit for the chain.
2397    fn format_chain_inline(
2398        &mut self,
2399        base: &Expr,
2400        links: &[ChainLink<'_>],
2401        reserve: usize,
2402    ) -> bool {
2403        self.format_expr_at(base, 8, 0);
2404        let mut exploded = false;
2405        for (i, link) in links.iter().enumerate() {
2406            exploded |=
2407                self.format_chain_link(link, if i + 1 == links.len() { reserve } else { 0 });
2408        }
2409        exploded
2410    }
2411
2412    /// One `.field` or `.method(args)` step of a chain, wrapping the argument
2413    /// list when the step does not fit on the current line. Reports whether
2414    /// that wrapping was the one-argument-per-line form.
2415    fn format_chain_link(&mut self, link: &ChainLink<'_>, reserve: usize) -> bool {
2416        match link {
2417            ChainLink::Field(name) => {
2418                self.push(&format!(".{name}"));
2419                false
2420            }
2421            ChainLink::Method {
2422                method,
2423                type_args,
2424                args,
2425            } => {
2426                let head = format!(".{}{}", method, type_args_src(type_args));
2427                let flat = format!(
2428                    "{head}({})",
2429                    args.iter()
2430                        .map(|a| expr_with_prec(a, 0))
2431                        .collect::<Vec<_>>()
2432                        .join(", ")
2433                );
2434                if args.is_empty() || self.fits(&flat, reserve) {
2435                    self.push(&flat);
2436                    return false;
2437                }
2438                self.push(&head);
2439                self.push("(");
2440                self.format_arg_list(args, reserve)
2441            }
2442        }
2443    }
2444
2445    /// Emit a `match` expression at the current indent level. Arms sit one
2446    /// level deeper than the `match`/`}`; block-bodied arms recurse through
2447    /// `format_block` so their statements indent correctly in turn.
2448    fn format_match(&mut self, discriminant: &Expr, arms: &[MatchArm]) {
2449        self.push("match ");
2450        self.format_expr_at(discriminant, 0, " {".len());
2451        self.push(" {");
2452        self.newline();
2453        self.indented(|f| {
2454            for arm in arms {
2455                f.push(&pattern_to_string(&arm.pattern));
2456                // ADR 0169: render an optional `if <guard>` before `=>`.
2457                if let Some(guard) = &arm.guard {
2458                    f.push(" if ");
2459                    f.format_expr_at(guard, 0, " => ".len());
2460                }
2461                f.push(" => ");
2462                // Every arm ends in a `,`, which counts against its budget.
2463                match &arm.body {
2464                    MatchBody::Expr(e) => f.format_expr_at(e, 0, 1),
2465                    MatchBody::Block(b) => f.format_block_with_reserve(b, 1),
2466                }
2467                f.push(",");
2468                f.newline();
2469            }
2470        });
2471        self.push("}");
2472    }
2473}
2474
2475/// One step of a `.`-chain, as collected by [`flatten_chain`].
2476enum ChainLink<'e> {
2477    Field(&'e str),
2478    Method {
2479        method: &'e str,
2480        type_args: &'e [TypeRef],
2481        args: &'e [Expr],
2482    },
2483}
2484
2485/// Split `receiver.a(…).b.c(…)` into its innermost receiver and the `.`-steps
2486/// applied to it, outermost last. A non-chain expression yields itself and an
2487/// empty list.
2488fn flatten_chain(e: &Expr) -> (&Expr, Vec<ChainLink<'_>>) {
2489    let mut links = Vec::new();
2490    let mut cur = e;
2491    loop {
2492        match &cur.kind {
2493            ExprKind::FieldAccess { receiver, field } => {
2494                links.push(ChainLink::Field(field.name.as_str()));
2495                cur = receiver;
2496            }
2497            ExprKind::MethodCall {
2498                receiver,
2499                method,
2500                type_args,
2501                args,
2502            } => {
2503                links.push(ChainLink::Method {
2504                    method: method.name.as_str(),
2505                    type_args,
2506                    args,
2507                });
2508                cur = receiver;
2509            }
2510            _ => break,
2511        }
2512    }
2513    links.reverse();
2514    (cur, links)
2515}
2516
2517/// The `[T, U]` type-argument suffix on a call, or the empty string.
2518fn type_args_src(type_args: &[TypeRef]) -> String {
2519    if type_args.is_empty() {
2520        return String::new();
2521    }
2522    format!(
2523        "[{}]",
2524        type_args
2525            .iter()
2526            .map(type_ref_to_string)
2527            .collect::<Vec<_>>()
2528            .join(", ")
2529    )
2530}
2531
2532/// The operators a wrapped expression may break *before*. A continuation line
2533/// opening with `&&`, `||`, or `implies` re-attaches to the line above on
2534/// re-parse; one opening with `+` or `==` does not.
2535fn is_logical(op: BinOp) -> bool {
2536    matches!(op, BinOp::And | BinOp::Or | BinOp::Implies)
2537}
2538
2539/// Collect the operands of a left-nested run of the same operator, so
2540/// `a && b && c` breaks into three lines rather than nesting two levels deep.
2541fn flatten_binop<'e>(e: &'e Expr, op: BinOp, out: &mut Vec<&'e Expr>) {
2542    if let ExprKind::BinOp(inner_op, lhs, rhs) = &e.kind
2543        && *inner_op == op
2544    {
2545        flatten_binop(lhs, op, out);
2546        out.push(rhs);
2547        return;
2548    }
2549    out.push(e);
2550}
2551
2552/// An expression whose wrapped form opens with a short header and a brace, so
2553/// it reads correctly as the sole argument of a call it shares a line with —
2554/// `xs.forEach((x) => {`, `Fetch.send(Request {`. Excludes anything whose first
2555/// wrapped line is as long as the construct itself (a call, a `.`-chain), which
2556/// would just move the overflow rather than remove it.
2557fn is_block_like(e: &Expr) -> bool {
2558    matches!(
2559        e.kind,
2560        ExprKind::Lambda(_)
2561            | ExprKind::Block(_)
2562            | ExprKind::RecordConstruction { .. }
2563            | ExprKind::RecordSpread { .. }
2564            | ExprKind::Match { .. }
2565            | ExprKind::If { .. }
2566    )
2567}
2568
2569/// Whether [`expr_with_prec`] would parenthesise `e` in a `parent_prec`
2570/// context. The wrapped printer emits those parentheses itself, since it
2571/// bypasses the flat renderer that would otherwise add them.
2572fn needs_parens(e: &Expr, parent_prec: u8) -> bool {
2573    match &e.kind {
2574        ExprKind::BinOp(op, ..) => binop_prec(*op) < parent_prec,
2575        ExprKind::UnaryOp(..) => parent_prec > 7,
2576        _ => false,
2577    }
2578}
2579
2580/// Borrow the trivia attached to a statement variant.
2581/// Render a `given`-clause capability reference back to source: a bare name
2582/// for a local capability, or `prefix.Name` for a cross-context one (v0.15).
2583fn cap_ref_src(c: &CapRef) -> String {
2584    match &c.context {
2585        Some(prefix) => format!("{}.{}", prefix.joined(), c.name.name),
2586        None => c.name.name.clone(),
2587    }
2588}
2589
2590/// Render a `by` clause back to source: `by <Actor>` (binder-less), `by <b>: <Actor>`
2591/// (captured identity), or an ordered sum `by <b>: A | B` (v0.52). Shared by handler
2592/// and service-header (v0.155) formatting.
2593fn by_clause_src(by: &ByClause) -> String {
2594    let actors = by
2595        .actors
2596        .iter()
2597        .map(|a| a.name.as_str())
2598        .collect::<Vec<_>>()
2599        .join(" | ");
2600    match &by.binder {
2601        Some(b) => format!("by {}: {actors}", b.name),
2602        None => format!("by {actors}"),
2603    }
2604}
2605
2606/// Render an events subscription pattern (Events track slice 1, spine
2607/// #936): `{ field: value, .. }`. The trailing `..` is mandatory whenever any
2608/// field is listed, so it always renders — there is no pattern-less `Some`
2609/// case to omit it for (a pattern-less subscription is `pattern: None` on
2610/// `ServiceProtocol::Events`, handled by the caller before this is reached).
2611fn event_pattern_src(p: &EventPattern) -> String {
2612    let fields: Vec<String> = p
2613        .fields
2614        .iter()
2615        .map(|f| format!("{}: {}", f.name.name, event_pattern_value_src(&f.value)))
2616        .collect();
2617    format!("{{ {}, .. }}", fields.join(", "))
2618}
2619
2620fn event_pattern_value_src(v: &EventPatternValue) -> String {
2621    match v {
2622        EventPatternValue::Literal { value, .. } => match value {
2623            LiteralValue::Int(n) => n.to_string(),
2624            LiteralValue::Str(s) => format!("\"{}\"", escape_string(s)),
2625            LiteralValue::Bool(b) => b.to_string(),
2626        },
2627        EventPatternValue::Variant {
2628            type_name, variant, ..
2629        } => match type_name {
2630            Some(t) => format!("{}.{}", t.name, variant.name),
2631            None => variant.name.clone(),
2632        },
2633    }
2634}
2635
2636/// Render a `via schema(N)` dispatch clause (Events track slice 4, spine
2637/// #936), written after the `from Events(...)` header's closing `)`.
2638fn schema_dispatch_src(d: &SchemaDispatch) -> String {
2639    match &d.pattern {
2640        SchemaVersionPattern::Literal(n) => format!("via schema({n})"),
2641    }
2642}
2643
2644/// v0.182 (#664): render a call-site actor clause — `by User("bob")` or the
2645/// unit-identity `by Visitor`.
2646fn call_site_actor_src(p: &CallSiteActor) -> String {
2647    match &p.identity {
2648        Some(id) => format!("by {}({})", p.actor.name, expr_with_prec(id, 0)),
2649        None => format!("by {}", p.actor.name),
2650    }
2651}
2652
2653/// Render a storage kind: `Cell[Int]`, `Map[K, V]`, or a bare head (v0.81).
2654fn store_kind_to_string(k: &StoreKind) -> String {
2655    if k.args.is_empty() {
2656        k.head.name.clone()
2657    } else {
2658        format!(
2659            "{}[{}]",
2660            k.head.name,
2661            k.args
2662                .iter()
2663                .map(type_ref_to_string)
2664                .collect::<Vec<_>>()
2665                .join(", ")
2666        )
2667    }
2668}
2669
2670/// Render a storage annotation (v0.85; ADR 0111): `@name`, or `@name(arg, …)`
2671/// where each argument is an optional `label: ` then the value expression.
2672/// Render a storage annotation as a single source-syntax token: `@indexed(by:
2673/// id)`, `@bounded(10000)`, `@ttl(5.minutes)`, or a bare `@retain`. Public so
2674/// the LSP's agent-state hover (ADR 0161) can render a `store` field's
2675/// annotations without re-deriving them.
2676pub fn annotation_to_string(ann: &Annotation) -> String {
2677    if ann.args.is_empty() {
2678        return format!("@{}", ann.name.name);
2679    }
2680    let args = ann
2681        .args
2682        .iter()
2683        .map(|a| match &a.label {
2684            Some(l) => format!("{}: {}", l.name, expr_with_prec(&a.value, 0)),
2685            None => expr_with_prec(&a.value, 0),
2686        })
2687        .collect::<Vec<_>>()
2688        .join(", ");
2689    format!("@{}({})", ann.name.name, args)
2690}
2691
2692/// v0.118: render a `stub` clause head-to-tail as a single source line:
2693/// `stub <capability>.<method>(<args>) <rhs>` (testing track slice 6).
2694fn stub_clause_to_string(pv: &StubClause) -> String {
2695    let args = pv
2696        .args
2697        .iter()
2698        .map(|a| match a {
2699            ArgPattern::Any(_) => "_".to_string(),
2700            ArgPattern::Value(e) => expr_to_string(e),
2701        })
2702        .collect::<Vec<_>>()
2703        .join(", ");
2704    let rhs = match &pv.rhs {
2705        StubRhs::Returns(e) => format!("returns {}", expr_to_string(e)),
2706        StubRhs::Fails(_) => "fails".to_string(),
2707        StubRhs::ReturnsEach(outcomes, _) => {
2708            let items = outcomes
2709                .iter()
2710                .map(|o| match o {
2711                    SeqOutcome::Value(e) => expr_to_string(e),
2712                    SeqOutcome::Fails(_) => "fails".to_string(),
2713                })
2714                .collect::<Vec<_>>()
2715                .join(", ");
2716            format!("returns each [{items}]")
2717        }
2718    };
2719    format!(
2720        "stub {}.{}({}) {}",
2721        pv.capability.name, pv.method.name, args, rhs
2722    )
2723}
2724
2725fn statement_trivia(s: &Statement) -> &Trivia {
2726    match s {
2727        Statement::Let(l) | Statement::EffectLet(l) => &l.trivia,
2728        Statement::Expect(a) => &a.trivia,
2729        Statement::Send(s) => &s.trivia,
2730        Statement::Do(d) => &d.trivia,
2731        Statement::Assign(a) => &a.trivia,
2732    }
2733}
2734
2735// -- String-rendering helpers (used by inline single-line emission) --
2736
2737/// The rendered width of a single line: one column per character, except a
2738/// tab, which advances to the next multiple of `tab`. Counting `char`s rather
2739/// than bytes keeps a non-ASCII identifier or string literal from being
2740/// over-measured and wrapped for no reason.
2741fn display_width(line: &str, tab: usize) -> usize {
2742    let mut col = 0usize;
2743    for ch in line.chars() {
2744        if ch == '\t' {
2745            col += tab - (col % tab);
2746        } else {
2747            col += 1;
2748        }
2749    }
2750    col
2751}
2752
2753fn type_ref_to_string(t: &TypeRef) -> String {
2754    match t {
2755        TypeRef::Base(b, _) => b.name().to_string(),
2756        TypeRef::Named(id) => id.name.clone(),
2757        TypeRef::Result(a, b, _) => format!(
2758            "Result[{}, {}]",
2759            type_ref_to_string(a),
2760            type_ref_to_string(b)
2761        ),
2762        TypeRef::Option(t, _) => format!("Option[{}]", type_ref_to_string(t)),
2763        TypeRef::Effect(t, _) => format!("Effect[{}]", type_ref_to_string(t)),
2764        TypeRef::HttpResult(t, _) => format!("HttpResult[{}]", type_ref_to_string(t)),
2765        TypeRef::QueueResult(_) => "QueueResult".to_string(),
2766        TypeRef::List(t, _) => format!("List[{}]", type_ref_to_string(t)),
2767        TypeRef::Query(t, _) => format!("Query[{}]", type_ref_to_string(t)),
2768        TypeRef::Stream(t, _) => format!("Stream[{}]", type_ref_to_string(t)),
2769        TypeRef::Connection(t, _) => format!("Connection[{}]", type_ref_to_string(t)),
2770        TypeRef::History(t, _) => format!("History[{}]", type_ref_to_string(t)),
2771        TypeRef::Map(k, v, _) => {
2772            format!("Map[{}, {}]", type_ref_to_string(k), type_ref_to_string(v))
2773        }
2774        TypeRef::ValidationError(_) => "ValidationError".to_string(),
2775        TypeRef::JsonError(_) => "JsonError".to_string(),
2776        TypeRef::Unit(_) => "()".to_string(),
2777        // v0.157 (ADR 0183): a user generic-type application, as written.
2778        TypeRef::App { name, args, .. } => format!(
2779            "{}[{}]",
2780            name.name,
2781            args.iter()
2782                .map(type_ref_to_string)
2783                .collect::<Vec<_>>()
2784                .join(", ")
2785        ),
2786        TypeRef::Fn(params, ret, _) => {
2787            let lhs = match params.len() {
2788                0 => "()".to_string(),
2789                1 if !matches!(params[0], TypeRef::Fn(..)) => type_ref_to_string(&params[0]),
2790                _ => format!(
2791                    "({})",
2792                    params
2793                        .iter()
2794                        .map(type_ref_to_string)
2795                        .collect::<Vec<_>>()
2796                        .join(", ")
2797                ),
2798            };
2799            format!("{lhs} -> {}", type_ref_to_string(ret))
2800        }
2801    }
2802}
2803
2804pub fn refinement_to_string(r: &Refinement) -> String {
2805    let mut s = String::new();
2806    for (i, p) in r.predicates.iter().enumerate() {
2807        if i > 0 {
2808            s.push_str(" && ");
2809        }
2810        s.push_str(&pred_to_string(p));
2811    }
2812    s
2813}
2814
2815fn pred_to_string(p: &RefinementPred) -> String {
2816    match &p.kind {
2817        PredKind::Matches(re) => format!("Matches(\"{}\")", escape_string(re)),
2818        PredKind::InRange(a, b) => format!("InRange({}, {})", a.value, b.value),
2819        PredKind::InRangeF(a, b) => format!("InRange({}, {})", a.lexeme, b.lexeme),
2820        PredKind::MinLength(n) => format!("MinLength({n})"),
2821        PredKind::MaxLength(n) => format!("MaxLength({n})"),
2822        PredKind::Length(n) => format!("Length({n})"),
2823        PredKind::NonNegative => "NonNegative".to_string(),
2824        PredKind::Positive => "Positive".to_string(),
2825        PredKind::NonEmpty => "NonEmpty".to_string(),
2826    }
2827}
2828
2829pub fn escape_string(s: &str) -> String {
2830    let mut out = String::with_capacity(s.len());
2831    for ch in s.chars() {
2832        match ch {
2833            '\\' => out.push_str("\\\\"),
2834            '"' => out.push_str("\\\""),
2835            '\n' => out.push_str("\\n"),
2836            '\t' => out.push_str("\\t"),
2837            c => out.push(c),
2838        }
2839    }
2840    out
2841}
2842
2843pub fn expr_to_string(e: &Expr) -> String {
2844    expr_with_prec(e, 0)
2845}
2846
2847// Operator precedences (smaller = binds looser):
2848//   1: || 2: && 3: == != 4: < <= > >= 5: + - 6: * / 7: unary ! - 8: postfix . () ?
2849fn binop_prec(op: BinOp) -> u8 {
2850    match op {
2851        // v0.80: `implies` is the lowest-precedence binary operator (below `||`).
2852        BinOp::Implies => 0,
2853        BinOp::Or => 1,
2854        BinOp::And => 2,
2855        BinOp::Eq | BinOp::NotEq => 3,
2856        BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq => 4,
2857        BinOp::Add | BinOp::Sub => 5,
2858        BinOp::Mul | BinOp::Div => 6,
2859    }
2860}
2861
2862fn expr_with_prec(e: &Expr, parent_prec: u8) -> String {
2863    match &e.kind {
2864        // v0.142 (ADR 0166): the stored lexeme verbatim — formatting must not
2865        // normalise away the author's `_` digit separators.
2866        ExprKind::IntLit { lexeme, .. } => lexeme.clone(),
2867        // v0.21: the stored lexeme verbatim — formatting must not normalise.
2868        ExprKind::FloatLit { lexeme, .. } => lexeme.clone(),
2869        // v0.86 (ADR 0112): a duration literal `<value>.<unit>`.
2870        ExprKind::DurationLit { value, unit, .. } => format!("{value}.{}", unit.name()),
2871        ExprKind::StrLit(s) => format!("\"{}\"", escape_string(s)),
2872        // v0.43: re-emit the interpolated string — chunks re-escaped, each
2873        // hole as `\(expr)`. Re-escaping a chunk's literal `\` to `\\` keeps a
2874        // source `\\(` (an escaped `\(`) round-tripping as text, not a hole.
2875        ExprKind::InterpStr(parts) => {
2876            let mut out = String::from("\"");
2877            for part in parts {
2878                match part {
2879                    InterpPart::Chunk(text) => out.push_str(&escape_string(text)),
2880                    InterpPart::Hole(hole) => {
2881                        out.push_str(&format!("\\({})", expr_with_prec(hole, 0)));
2882                    }
2883                }
2884            }
2885            out.push('"');
2886            out
2887        }
2888        ExprKind::BoolLit(b) => b.to_string(),
2889        ExprKind::UnitLit => "()".to_string(),
2890        ExprKind::Ident(id) => id.name.clone(),
2891        ExprKind::ListLit(elems) => format!(
2892            "[{}]",
2893            elems
2894                .iter()
2895                .map(expr_to_string)
2896                .collect::<Vec<_>>()
2897                .join(", ")
2898        ),
2899        ExprKind::Call {
2900            name,
2901            type_args,
2902            args,
2903        } => {
2904            let targs = if type_args.is_empty() {
2905                String::new()
2906            } else {
2907                format!(
2908                    "[{}]",
2909                    type_args
2910                        .iter()
2911                        .map(type_ref_to_string)
2912                        .collect::<Vec<_>>()
2913                        .join(", ")
2914                )
2915            };
2916            let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
2917            format!("{}{}({})", name.name, targs, parts.join(", "))
2918        }
2919        ExprKind::BinOp(op, l, r) => {
2920            let prec = binop_prec(*op);
2921            let inner = format!(
2922                "{} {} {}",
2923                expr_with_prec(l, prec),
2924                op.name(),
2925                expr_with_prec(r, prec + 1)
2926            );
2927            if prec < parent_prec {
2928                format!("({inner})")
2929            } else {
2930                inner
2931            }
2932        }
2933        ExprKind::UnaryOp(op, inner) => {
2934            // Unary binds tightly (prec 7).
2935            let s = format!("{}{}", op.name(), expr_with_prec(inner, 7));
2936            if parent_prec > 7 { format!("({s})") } else { s }
2937        }
2938        ExprKind::Paren(inner) => format!("({})", expr_with_prec(inner, 0)),
2939        // v0.20a: a lambda prints as `(params) => body`.
2940        ExprKind::Lambda(lambda) => {
2941            let params: Vec<String> = lambda
2942                .params
2943                .iter()
2944                .map(|p| match &p.type_ref {
2945                    Some(tr) => format!("{}: {}", p.name.name, type_ref_to_string(tr)),
2946                    None => p.name.name.clone(),
2947                })
2948                .collect();
2949            let body = match &lambda.body.kind {
2950                ExprKind::Block(b) => format_block_oneline(b),
2951                _ => expr_with_prec(&lambda.body, 0),
2952            };
2953            format!("({}) => {}", params.join(", "), body)
2954        }
2955        ExprKind::Block(b) => format_block_oneline(b),
2956        ExprKind::If {
2957            cond,
2958            then_block,
2959            else_block,
2960        } => {
2961            // v0.146 (ADR 0170): an `if` with no `else` carries a synthesised
2962            // unit else-branch — omit it so the else-less form round-trips.
2963            if else_block.is_synth_unit() {
2964                format!(
2965                    "if {} {}",
2966                    expr_with_prec(cond, 0),
2967                    format_block_oneline(then_block),
2968                )
2969            } else {
2970                format!(
2971                    "if {} {} else {}",
2972                    expr_with_prec(cond, 0),
2973                    format_block_oneline(then_block),
2974                    format_block_oneline(else_block),
2975                )
2976            }
2977        }
2978        ExprKind::Ok(v) => format!("Ok({})", expr_with_prec(v, 0)),
2979        ExprKind::Err(v) => format!("Err({})", expr_with_prec(v, 0)),
2980        ExprKind::Some(v) => format!("Some({})", expr_with_prec(v, 0)),
2981        ExprKind::None => "None".to_string(),
2982        ExprKind::Question(v) => format!("{}?", expr_with_prec(v, 8)),
2983        ExprKind::ConstructorCall {
2984            type_name,
2985            method,
2986            args,
2987        } => {
2988            let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
2989            format!("{}.{}({})", type_name.name, method.name, parts.join(", "))
2990        }
2991        ExprKind::RecordConstruction { type_name, fields } => {
2992            let parts: Vec<String> = fields
2993                .iter()
2994                .map(|f| match &f.value {
2995                    Some(v) => format!("{}: {}", f.name.name, expr_with_prec(v, 0)),
2996                    None => f.name.name.clone(),
2997                })
2998                .collect();
2999            if parts.is_empty() {
3000                format!("{} {{}}", type_name.name)
3001            } else {
3002                format!("{} {{ {} }}", type_name.name, parts.join(", "))
3003            }
3004        }
3005        ExprKind::FieldAccess { receiver, field } => {
3006            format!("{}.{}", expr_with_prec(receiver, 8), field.name)
3007        }
3008        ExprKind::MethodCall {
3009            receiver,
3010            method,
3011            type_args,
3012            args,
3013        } => {
3014            let targs = if type_args.is_empty() {
3015                String::new()
3016            } else {
3017                format!(
3018                    "[{}]",
3019                    type_args
3020                        .iter()
3021                        .map(type_ref_to_string)
3022                        .collect::<Vec<_>>()
3023                        .join(", ")
3024                )
3025            };
3026            let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
3027            format!(
3028                "{}.{}{targs}({})",
3029                expr_with_prec(receiver, 8),
3030                method.name,
3031                parts.join(", ")
3032            )
3033        }
3034        ExprKind::Match { discriminant, arms } => {
3035            let mut out = String::new();
3036            out.push_str("match ");
3037            out.push_str(&expr_with_prec(discriminant, 0));
3038            out.push_str(" {\n");
3039            for arm in arms {
3040                out.push('\t');
3041                out.push_str(&pattern_to_string(&arm.pattern));
3042                if let Some(guard) = &arm.guard {
3043                    out.push_str(" if ");
3044                    out.push_str(&expr_with_prec(guard, 0));
3045                }
3046                out.push_str(" => ");
3047                match &arm.body {
3048                    MatchBody::Expr(e) => out.push_str(&expr_with_prec(e, 0)),
3049                    MatchBody::Block(b) => out.push_str(&format_block_oneline(b)),
3050                }
3051                out.push_str(",\n");
3052            }
3053            out.push('}');
3054            out
3055        }
3056        ExprKind::Is { value, pattern } => {
3057            format!(
3058                "{} is {}",
3059                expr_with_prec(value, 4),
3060                pattern_to_string(pattern)
3061            )
3062        }
3063        ExprKind::RecordSpread {
3064            type_name,
3065            base,
3066            overrides,
3067        } => {
3068            let mut parts = vec![format!("...{}", expr_with_prec(base, 0))];
3069            for f in overrides {
3070                if let Some(v) = &f.value {
3071                    parts.push(format!("{}: {}", f.name.name, expr_with_prec(v, 0)));
3072                } else {
3073                    parts.push(f.name.name.clone());
3074                }
3075            }
3076            let body = parts.join(", ");
3077            match type_name {
3078                Some(tn) => format!("{} {{ {} }}", tn.name, body),
3079                None => format!("{{ {} }}", body),
3080            }
3081        }
3082        ExprKind::EffectPure(v) => format!("Effect.pure({})", expr_with_prec(v, 0)),
3083        ExprKind::Expect(v) => format!("expect {}", expr_with_prec(v, 0)),
3084        ExprKind::Val { type_ref, args } => {
3085            let t = type_ref_to_string(type_ref);
3086            if args.is_empty() {
3087                format!("Val[{t}]")
3088            } else {
3089                let a = args
3090                    .iter()
3091                    .map(|x| expr_with_prec(x, 0))
3092                    .collect::<Vec<_>>()
3093                    .join(", ");
3094                format!("Val[{t}]({a})")
3095            }
3096        }
3097        ExprKind::Wire(inner) => format!("Wire({})", expr_with_prec(inner, 0)),
3098        ExprKind::Trace { cap, op } => format!("trace({}.{})", cap.name, op.name),
3099        ExprKind::Observation(o) => {
3100            let subject = format!("{}.{}", o.cap.name, o.op.name);
3101            match &o.matcher {
3102                ObservationMatcher::NeverCalled => format!("{subject} never called"),
3103                ObservationMatcher::Before { cap, op } => {
3104                    format!("{subject} before {}.{}", cap.name, op.name)
3105                }
3106                ObservationMatcher::Called { count, with_pred } => {
3107                    let mut s = format!("{subject} called");
3108                    if let Some(c) = count {
3109                        if matches!(c.kind, ExprKind::IntLit { value: 1, .. }) {
3110                            s.push_str(" once");
3111                        } else {
3112                            s.push_str(&format!(" {} times", expr_with_prec(c, 0)));
3113                        }
3114                    }
3115                    if let Some(p) = with_pred {
3116                        s.push_str(&format!(" with {}", expr_with_prec(p, 0)));
3117                    }
3118                    s
3119                }
3120            }
3121        }
3122    }
3123}
3124
3125fn pattern_to_string(p: &Pattern) -> String {
3126    match p {
3127        Pattern::Wildcard(_) => "_".to_string(),
3128        // ADR 0169: a bare name binding renders as its identifier.
3129        Pattern::Binding(id) => id.name.clone(),
3130        // v0.130: literal patterns render as their source literal.
3131        Pattern::Literal { value, .. } => match value {
3132            LiteralValue::Int(n) => n.to_string(),
3133            LiteralValue::Str(s) => format!("\"{}\"", escape_string(s)),
3134            LiteralValue::Bool(b) => b.to_string(),
3135        },
3136        // #472: `p where predicate` — the inner pattern, then the predicate
3137        // list rendered the same way a `type X = Base where P` refinement is.
3138        Pattern::Refined {
3139            inner, predicate, ..
3140        } => format!(
3141            "{} where {}",
3142            pattern_to_string(inner),
3143            refinement_to_string(predicate)
3144        ),
3145        Pattern::Variant {
3146            type_name,
3147            variant,
3148            bindings,
3149            ..
3150        } => {
3151            let name_part = match type_name {
3152                Some(t) => format!("{}.{}", t.name, variant.name),
3153                None => variant.name.clone(),
3154            };
3155            if bindings.is_empty() {
3156                name_part
3157            } else {
3158                // ADR 0169: each payload binding is a full sub-pattern.
3159                let parts: Vec<String> = bindings
3160                    .iter()
3161                    .map(|b| match &b.kind {
3162                        PatternBindingKind::Positional { pattern } => pattern_to_string(pattern),
3163                        PatternBindingKind::Named { field, pattern } => {
3164                            format!("{}: {}", field.name, pattern_to_string(pattern))
3165                        }
3166                    })
3167                    .collect();
3168                format!("{}({})", name_part, parts.join(", "))
3169            }
3170        }
3171        // #474: an or-pattern renders as its alternatives joined by `|`.
3172        Pattern::Or(alts, _) => alts
3173            .iter()
3174            .map(pattern_to_string)
3175            .collect::<Vec<_>>()
3176            .join(" | "),
3177    }
3178}
3179
3180/// #981: whether a block's `()` tail should be omitted rather than printed.
3181///
3182/// A `()` tail — whether the parser synthesised it ([`Block::implicit_tail`])
3183/// or the user wrote it out explicitly — is exactly the block's default
3184/// value, so dropping it is loss-free: the parser re-derives the same
3185/// implicit unit tail either way (v0.7 / v0.146, ADR 0170).
3186///
3187/// Omitting it is not just an idempotency nicety, it is required for
3188/// correctness whenever anything precedes the tail (a statement, a `case`'s
3189/// `stub` clause): Bynk has no statement terminator, so a printed `()`
3190/// immediately after a preceding line's last token re-attaches to it as a
3191/// zero-arg call on re-parse (`x` / `()` → `x()`) rather than staying two
3192/// separate constructs. #735 only special-cased the *implicit*-tail shape;
3193/// #981 found the identical corruption for an *explicit* `()` tail (e.g. the
3194/// last statement of a `match` arm's block), which is exactly as dangerous
3195/// once anything comes before it. So this covers both, structurally, rather
3196/// than special-casing another syntactic position.
3197fn omit_unit_tail(b: &Block) -> bool {
3198    matches!(b.tail.kind, ExprKind::UnitLit) && b.tail_leading_comments.is_empty()
3199}
3200
3201fn format_block_oneline(b: &Block) -> String {
3202    if b.statements.is_empty() {
3203        // v0.146 (ADR 0170): an empty block with a synthesised `()` tail prints
3204        // as `{}` — printing `{ () }` would not round-trip against the parser's
3205        // implicit-tail synthesis.
3206        if b.implicit_tail {
3207            "{}".to_string()
3208        } else {
3209            format!("{{ {} }}", expr_with_prec(&b.tail, 0))
3210        }
3211    } else {
3212        // Multi-line block — render with newlines and tab indentation.
3213        let mut out = String::from("{\n");
3214        for stmt in &b.statements {
3215            out.push('\t');
3216            out.push_str(&stmt_to_string(stmt));
3217            out.push('\n');
3218        }
3219        if !omit_unit_tail(b) {
3220            out.push('\t');
3221            out.push_str(&expr_with_prec(&b.tail, 0));
3222            out.push('\n');
3223        }
3224        out.push('}');
3225        out
3226    }
3227}
3228
3229fn stmt_to_string(s: &Statement) -> String {
3230    match s {
3231        Statement::Let(l) => {
3232            let mut out = format!("let {}", l.name.name);
3233            if let Some(t) = &l.type_annot {
3234                out.push_str(&format!(": {}", type_ref_to_string(t)));
3235            }
3236            out.push_str(&format!(" = {}", expr_with_prec(&l.value, 0)));
3237            out
3238        }
3239        Statement::EffectLet(l) => {
3240            let mut out = format!("let {}", l.name.name);
3241            if let Some(t) = &l.type_annot {
3242                out.push_str(&format!(": {}", type_ref_to_string(t)));
3243            }
3244            out.push_str(&format!(" <- {}", expr_with_prec(&l.value, 0)));
3245            if let Some(p) = &l.principal {
3246                out.push_str(&format!(" {}", call_site_actor_src(p)));
3247            }
3248            out
3249        }
3250        Statement::Expect(a) => format!("expect {}", expr_with_prec(&a.value, 0)),
3251        Statement::Send(s) => format!("~> {}", expr_with_prec(&s.value, 0)),
3252        Statement::Do(d) => format!("do {}", expr_with_prec(&d.value, 0)),
3253        Statement::Assign(a) => format!("{} := {}", a.target.name, expr_with_prec(&a.value, 0)),
3254    }
3255}
3256
3257#[cfg(test)]
3258mod tests {
3259    use super::*;
3260
3261    fn fmt(src: &str) -> String {
3262        format_source(src, &FormatOptions::default()).expect("format failed")
3263    }
3264
3265    #[test]
3266    fn formats_minimal_commons() {
3267        let src = "commons fitness.units {}";
3268        let out = fmt(src);
3269        assert!(out.starts_with("commons fitness.units"));
3270        // Idempotency.
3271        let out2 = fmt(&out);
3272        assert_eq!(out, out2);
3273    }
3274
3275    #[test]
3276    fn formats_refined_type() {
3277        let src = "commons x { type Metres = Int where NonNegative }";
3278        let out = fmt(src);
3279        assert!(out.contains("type Metres = Int where NonNegative"));
3280        let out2 = fmt(&out);
3281        assert_eq!(out, out2);
3282    }
3283
3284    #[test]
3285    fn formats_function_decl() {
3286        let src = "commons x { fn add(a: Int, b: Int) -> Int { a + b } }";
3287        let out = fmt(src);
3288        assert!(out.contains("fn add(a: Int, b: Int) -> Int"));
3289        let out2 = fmt(&out);
3290        assert_eq!(out, out2);
3291    }
3292
3293    #[test]
3294    fn formats_record() {
3295        let src = "commons x { type Pt = { x: Int, y: Int } }";
3296        let out = fmt(src);
3297        let out2 = fmt(&out);
3298        assert_eq!(out, out2, "formatter not idempotent: {out}");
3299    }
3300
3301    #[test]
3302    fn formats_doc_block() {
3303        let src = "commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}";
3304        let out = fmt(src);
3305        assert!(out.contains("A descriptive doc."));
3306        let out2 = fmt(&out);
3307        assert_eq!(out, out2);
3308    }
3309
3310    // -- v1.1 comment preservation --
3311
3312    #[test]
3313    fn preserves_leading_line_comment_on_decl() {
3314        let src = "commons x {\n-- explain T\ntype T = Int where NonNegative\n}";
3315        let out = fmt(src);
3316        assert!(out.contains("-- explain T"), "comment dropped: {out}");
3317        // Idempotent.
3318        assert_eq!(out, fmt(&out));
3319    }
3320
3321    #[test]
3322    fn preserves_trailing_line_comment_on_decl() {
3323        let src = "commons x {\ntype T = Int where NonNegative  -- short\n}";
3324        let out = fmt(src);
3325        assert!(out.contains("-- short"));
3326        // The trailing comment must remain on the same line as the decl.
3327        assert!(
3328            out.lines()
3329                .any(|l| l.contains("type T") && l.contains("-- short")),
3330            "trailing comment not on same line: {out}"
3331        );
3332        assert_eq!(out, fmt(&out));
3333    }
3334
3335    #[test]
3336    fn preserves_grouped_leading_comments() {
3337        let src = "commons x {\n-- one\n-- two\ntype T = Int where Positive\n}";
3338        let out = fmt(src);
3339        assert!(out.contains("-- one"));
3340        assert!(out.contains("-- two"));
3341        // Adjacent — no blank line between the comments.
3342        let i1 = out.find("-- one").unwrap();
3343        let i2 = out.find("-- two").unwrap();
3344        let between = &out[i1..i2];
3345        assert_eq!(
3346            between.matches('\n').count(),
3347            1,
3348            "blank line inserted: {out}"
3349        );
3350        assert_eq!(out, fmt(&out));
3351    }
3352
3353    #[test]
3354    fn preserves_comment_before_block_tail() {
3355        let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
3356        let out = fmt(src);
3357        assert!(out.contains("-- result"), "tail comment dropped: {out}");
3358        assert_eq!(out, fmt(&out));
3359    }
3360
3361    #[test]
3362    fn preserves_comment_with_doc_block_above_decl() {
3363        let src = "commons x {\n-- TODO: rename\n---\nThe canonical T.\n---\ntype T = Int where Positive\n}";
3364        let out = fmt(src);
3365        assert!(out.contains("-- TODO: rename"));
3366        assert!(out.contains("The canonical T."));
3367        // Spec layout: comment, then doc block, then declaration.
3368        let ic = out.find("-- TODO: rename").unwrap();
3369        let id = out.find("The canonical T.").unwrap();
3370        let it = out.find("type T").unwrap();
3371        assert!(ic < id && id < it, "ordering wrong: {out}");
3372        assert_eq!(out, fmt(&out));
3373    }
3374
3375    #[test]
3376    fn preserves_trailing_file_comment() {
3377        let src = "commons x.y\n\ntype T = Int where Positive\n-- TODO\n";
3378        let out = fmt(src);
3379        assert!(out.contains("-- TODO"));
3380        assert_eq!(out, fmt(&out));
3381    }
3382
3383    // -- Finding #66: the drain-check fast path must not weaken the
3384    // comment-loss guard --
3385
3386    /// A comment strictly inside an expression subtree (here, a binop chain)
3387    /// is the one shape `TriviaTable` genuinely cannot drain — `fully_drained`
3388    /// must be `false` for it, so `format_source`'s fast path does not skip
3389    /// `comment_loss` and the file is still refused rather than silently
3390    /// losing the comment.
3391    #[test]
3392    fn expression_interior_comment_is_not_fully_drained_and_still_refused() {
3393        let src = "commons x {\n  fn f() -> Int {\n    1 + -- note\n    2\n  }\n}\n";
3394        let tokens = tokenize(src).unwrap();
3395        let (_, _, fully_drained) = parse_units_with_drain_check(&tokens, src)
3396            .expect("a comment inside an expression is still a valid parse");
3397        assert!(
3398            !fully_drained,
3399            "an expression-interior comment must leave the trivia table undrained"
3400        );
3401        let err = format_source(src, &FormatOptions::default())
3402            .expect_err("formatting must still refuse rather than lose the comment");
3403        assert_eq!(err.errors[0].category, "bynk.fmt.comment_loss");
3404    }
3405
3406    /// The mirror image: an ordinary file with no expression-interior comment
3407    /// (every comment sits before a declaration/statement, or trailing one) is
3408    /// `fully_drained`, so `format_source`'s fast path is what actually runs —
3409    /// and formatting must still succeed and preserve the comment.
3410    #[test]
3411    fn ordinary_comment_is_fully_drained_and_formats_normally() {
3412        let src = "commons x {\n-- note\ntype T = Int where Positive\n}\n";
3413        let tokens = tokenize(src).unwrap();
3414        let (_, _, fully_drained) =
3415            parse_units_with_drain_check(&tokens, src).expect("should parse");
3416        assert!(
3417            fully_drained,
3418            "a declaration-leading comment must be fully drained"
3419        );
3420        let out = fmt(src);
3421        assert!(out.contains("-- note"));
3422    }
3423
3424    // -- #981: a bare-identifier statement + trailing `()` must not merge
3425    // into a call expression.
3426
3427    #[test]
3428    fn match_arm_block_tail_unit_after_assign_does_not_reattach_as_a_call() {
3429        // The exact shape from #981: an Assign statement whose value is a
3430        // bare (capitalised, enum-variant-shaped) identifier, immediately
3431        // followed by the block's own explicit `()` tail. The formatter must
3432        // not print these adjacently — that reparses as `status := Paid()`,
3433        // a call, rather than the original two constructs.
3434        let src = "commons x { fn f(status: T) -> T {\n  match status {\n    Draft => {\n      status := Paid\n      ()\n    }\n    Paid => (),\n  }\n} }";
3435        let out = fmt(src);
3436        assert!(
3437            !out.contains("Paid()"),
3438            "the `()` tail must not re-attach to `Paid` as a call:\n{out}"
3439        );
3440        assert!(
3441            out.contains("status := Paid"),
3442            "the assignment must survive unmangled:\n{out}"
3443        );
3444        assert_eq!(out, fmt(&out), "must be idempotent");
3445    }
3446
3447    #[test]
3448    fn explicit_unit_tail_after_a_statement_is_omitted_like_an_implicit_one() {
3449        // Not just the implicit-tail shape #735 special-cased — an
3450        // *explicit* `()` written by the user right after a statement is
3451        // exactly as dangerous to print, so it is omitted the same way.
3452        let src = "commons x { fn f() -> Effect[()] {\n  let a = 1\n  ()\n} }";
3453        let out = fmt(src);
3454        let expected = "commons x {\n\tfn f() -> Effect[()] {\n\t\tlet a = 1\n\t}\n}\n";
3455        assert_eq!(
3456            out, expected,
3457            "an explicit unit tail after a statement must be omitted"
3458        );
3459        assert_eq!(out, fmt(&out), "must be idempotent");
3460    }
3461
3462    // -- #735 round-trip guard --
3463
3464    #[test]
3465    fn code_only_canonical_ignores_comments() {
3466        // Two sources whose only difference is comments must reduce to the same
3467        // comment-free canonical form — this is what lets the round-trip guard
3468        // compare structure while the formatter re-flows trivia freely.
3469        let opts = FormatOptions::default();
3470        let bare = "commons x { type T = Int where Positive }";
3471        let commented = "commons x {\n-- a note\ntype T = Int where Positive  -- trailing\n}";
3472        assert_eq!(
3473            code_only_canonical(bare, &opts).unwrap(),
3474            code_only_canonical(commented, &opts).unwrap(),
3475        );
3476    }
3477
3478    #[test]
3479    fn roundtrip_guard_accepts_faithful_output() {
3480        // The formatter's own output over a real source must round-trip.
3481        let opts = FormatOptions::default();
3482        let src = "commons x { fn add(a: Int, b: Int) -> Int { a + b } }";
3483        let out = format_source(src, &opts).unwrap();
3484        let tokens = tokenize(src).unwrap();
3485        assert!(roundtrip_divergence(&tokens, src, &out, &opts).is_none());
3486    }
3487
3488    #[test]
3489    fn roundtrip_guard_rejects_non_parsing_output() {
3490        // Simulate a printer that emitted garbage: the output no longer parses,
3491        // so the guard must fire rather than let it be written. The output here
3492        // is *longer* than the source and its parse error lands near its end —
3493        // the error's span must nonetheless stay within the source, because the
3494        // caller renders it against `source`, not the output (an out-of-range
3495        // primary span misplaces the caret or panics ariadne). See
3496        // `roundtrip_error`.
3497        let opts = FormatOptions::default();
3498        let src = "commons x { type T = Int where Positive }";
3499        let corrupt = "commons x { type T = Int where Positive } fn f(a: Int) -> Int { a +";
3500        assert!(
3501            corrupt.len() > src.len(),
3502            "output must be the longer buffer"
3503        );
3504        let tokens = tokenize(src).unwrap();
3505        let err = roundtrip_divergence(&tokens, src, corrupt, &opts)
3506            .expect("must reject non-parsing output");
3507        assert_eq!(err.category, "bynk.fmt.roundtrip");
3508        assert!(
3509            err.span.end <= src.len(),
3510            "roundtrip error span {:?} escapes the source it is rendered against (len {})",
3511            err.span,
3512            src.len(),
3513        );
3514    }
3515
3516    #[test]
3517    fn roundtrip_guard_rejects_structural_divergence() {
3518        // Simulate a printer that emitted parseable-but-wrong code: the output
3519        // parses, but to a different AST than the input. The guard must catch it.
3520        let opts = FormatOptions::default();
3521        let src = "commons x { type T = Int where Positive }";
3522        let wrong = "commons x { type T = Bool }";
3523        let tokens = tokenize(src).unwrap();
3524        let err = roundtrip_divergence(&tokens, src, wrong, &opts)
3525            .expect("must reject structural divergence");
3526        assert_eq!(err.category, "bynk.fmt.roundtrip");
3527        assert!(err.span.end <= src.len(), "span escapes the source buffer");
3528    }
3529
3530    #[test]
3531    fn roundtrip_error_renders_against_source_without_panicking() {
3532        // The guard's error is rendered against the *source* (`run_fmt` calls
3533        // `print_errors(&e.errors, &source, …)`). ariadne uses a primary span
3534        // unconditionally, so a span outside the source buffer misplaces the
3535        // caret or panics — exactly in the formatter-bug path this guard must
3536        // handle gracefully. Render the real diagnostic against a short source
3537        // and assert it produces output without panicking.
3538        let err = roundtrip_error("the formatter produced output that no longer parses");
3539        let source = "commons x {}";
3540        let rendered = bynk_render::render_errors(std::slice::from_ref(&err), source, "<test>");
3541        assert!(
3542            rendered.contains("bynk.fmt.roundtrip"),
3543            "diagnostic did not render: {rendered}"
3544        );
3545    }
3546
3547    #[test]
3548    fn unchanged_files_without_comments_format_identically() {
3549        let src = "commons x { type T = Int where NonNegative }";
3550        let out = fmt(src);
3551        // Sanity: the formatter still produces the canonical output for
3552        // existing fixtures (no spurious comment rendering).
3553        assert!(!out.contains("--"), "unexpected comment in output: {out}");
3554    }
3555
3556    // -- v0.81 storage track: `store` fields and the `:=` write --
3557
3558    #[test]
3559    fn formats_store_field_and_cell_write() {
3560        let src = "context shop {\nagent Counter {\nkey id: String\nstore count: Cell[Int] = 0\non call bump() -> Effect[()] {\ncount := count + 1\n()\n}\n}\n}";
3561        let out = fmt(src);
3562        assert!(
3563            out.contains("store count: Cell[Int] = 0"),
3564            "store field not formatted: {out}"
3565        );
3566        assert!(
3567            out.contains("count := count + 1"),
3568            "cell write not formatted: {out}"
3569        );
3570        assert_eq!(out, fmt(&out), "formatter not idempotent: {out}");
3571    }
3572
3573    #[test]
3574    fn formats_store_only_agent_without_state_block() {
3575        let src = "context shop {\nagent Counter {\nkey id: String\nstore count: Cell[Int] = 0\non call get() -> Effect[Int] {\ncount\n}\n}\n}";
3576        let out = fmt(src);
3577        // A `store`-only agent emits no empty `state { }` block.
3578        assert!(!out.contains("state {"), "spurious state block: {out}");
3579        assert!(out.contains("store count: Cell[Int] = 0"), "{out}");
3580        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3581    }
3582
3583    // -- #963: line-width-driven wrapping --
3584
3585    /// Every line of `out`, measured the way the formatter measures them.
3586    fn widths(out: &str) -> Vec<usize> {
3587        out.lines().map(|l| display_width(l, 4)).collect()
3588    }
3589
3590    fn assert_within_budget(out: &str) {
3591        let over: Vec<&str> = out.lines().filter(|l| display_width(l, 4) > 100).collect();
3592        assert!(over.is_empty(), "lines over 100 columns: {over:?}\n{out}");
3593    }
3594
3595    #[test]
3596    fn fit_test_counts_the_prefix_already_on_the_line() {
3597        // The body fits on its own but not behind the signature, which is what
3598        // the pre-#963 fit test measured.
3599        let src = "commons x {\nfn authorise(amount: Int, ceiling: Int, floor: Int) -> Result[Int, Error] { if amount > ceiling { Err(Declined) } else { Ok(amount) } }\n}";
3600        let out = fmt(src);
3601        assert_within_budget(&out);
3602        assert!(
3603            out.contains("-> Result[Int, Error] {\n"),
3604            "body not broken out: {out}"
3605        );
3606        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3607    }
3608
3609    #[test]
3610    fn wraps_a_long_record_construction_one_field_per_line() {
3611        let src = "commons x {\nfn make() -> R { R { alpha: \"first value here\", beta: \"second value here\", gamma: \"third value here\", delta: \"fourth value\" } }\n}";
3612        let out = fmt(src);
3613        assert_within_budget(&out);
3614        assert!(
3615            out.contains("\t\talpha: \"first value here\",\n"),
3616            "fields not one per line: {out}"
3617        );
3618        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3619    }
3620
3621    #[test]
3622    fn wraps_a_long_argument_list_without_a_trailing_comma() {
3623        // A parameter/argument list rejects a trailing comma in the grammar, so
3624        // the wrapped form must not emit one — `format_source` would refuse the
3625        // output on the round-trip guard if it did.
3626        let src = "commons x {\nfn go() -> Int { combine(firstOperandValue, secondOperandValue, thirdOperandValue, fourthOperandValue) }\n}";
3627        let out = fmt(src);
3628        assert_within_budget(&out);
3629        assert!(
3630            !out.contains(",\n\t\t)"),
3631            "trailing comma in an argument list: {out}"
3632        );
3633        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3634    }
3635
3636    #[test]
3637    fn wraps_long_parameter_lists_behind_the_return_type() {
3638        let src = "context x {\nservice api from http {\non POST(\"/reservations/confirm\") (identifier: String, body: Reservation) -> Effect[HttpResult[Reservation]] by Visitor { Ok(body) }\n}\n}";
3639        let out = fmt(src);
3640        assert_within_budget(&out);
3641        assert!(
3642            out.contains("\t\t\tidentifier: String,\n"),
3643            "params not wrapped: {out}"
3644        );
3645        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3646    }
3647
3648    #[test]
3649    fn breaks_a_long_chain_at_its_dots() {
3650        let src = "commons x {\nfn go(rows: List[Row]) -> List[Int] { rows.filterOnlyTheInteresting((r) => r.nights > 0).mapEachOntoItsValue((r) => r.nights * r.rate).collect() }\n}";
3651        let out = fmt(src);
3652        assert_within_budget(&out);
3653        assert!(
3654            out.contains("\n\t\t\t.collect()"),
3655            "chain not broken at the dots: {out}"
3656        );
3657        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3658    }
3659
3660    #[test]
3661    fn keeps_a_chain_intact_when_only_an_argument_needs_wrapping() {
3662        // The chain itself is short; the `match` inside is what spans lines.
3663        // Breaking at the dots here would be noise, so the chain stays put.
3664        let src = "commons x {\nfn join(parts: List[String]) -> String {\nlet init: Option[String] = None\nparts.fold(init, (acc, p) => match acc {\nSome(s) => Some(s.concat(p)),\nNone => Some(p),\n}).getOrElse(\"\")\n}\n}";
3665        let out = fmt(src);
3666        assert_within_budget(&out);
3667        assert!(
3668            out.contains("parts.fold(init, (acc, p) => match acc {"),
3669            "chain broken needlessly: {out}"
3670        );
3671        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3672    }
3673
3674    #[test]
3675    fn breaks_a_long_conjunction_before_each_operator() {
3676        let src = "commons x {\nfn ok(a: Int, b: Int, c: Int, d: Int) -> Bool { aSufficientlyLongPredicateName(a) && anotherRatherLongPredicate(b) && yetAnotherLongishPredicate(c) && theFinalPredicateHere(d) }\n}";
3677        let out = fmt(src);
3678        assert_within_budget(&out);
3679        assert!(
3680            out.lines().any(|l| l.trim_start().starts_with("&& ")),
3681            "conjunction not broken at the operators: {out}"
3682        );
3683        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3684    }
3685
3686    #[test]
3687    fn never_breaks_before_an_arithmetic_operator() {
3688        // A continuation line opening with `+` does not re-attach on re-parse,
3689        // so an arithmetic run stays on one line however long it gets.
3690        let src = "commons x {\nfn total(a: Int, b: Int, c: Int, d: Int) -> Int { someLongFunctionName(a) + anotherLongFunction(b) + aThirdLongFunction(c) + lastOne(d) }\n}";
3691        let out = fmt(src);
3692        assert!(
3693            !out.lines().any(|l| l.trim_start().starts_with("+ ")),
3694            "broke before `+`, which does not re-parse: {out}"
3695        );
3696        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3697    }
3698
3699    #[test]
3700    fn wraps_an_over_long_actor_auth_config() {
3701        let src = "context x {\nactor Partner { auth = Oidc(issuer = \"https://issuer.example.test\", audience = \"reservations-api\", jwks = \"https://issuer.example.test/jwks.json\"), identity = PartnerId }\n}";
3702        let out = fmt(src);
3703        assert_within_budget(&out);
3704        assert!(
3705            out.contains("\t\tissuer = "),
3706            "scheme args not wrapped: {out}"
3707        );
3708        assert!(out.contains("identity = PartnerId"), "identity lost: {out}");
3709        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3710    }
3711
3712    #[test]
3713    fn a_lone_block_like_argument_hugs_its_call() {
3714        let src = "commons x {\nfn go(items: List[Item]) -> Effect[()] { items.forEachInTurnAndOrder((item: Item) => { let _ <- store.put(item.identifier, item) }) }\n}";
3715        let out = fmt(src);
3716        assert_within_budget(&out);
3717        assert!(
3718            out.contains("((item: Item) => {"),
3719            "sole lambda argument did not hug its call: {out}"
3720        );
3721        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3722    }
3723
3724    #[test]
3725    fn an_unbreakable_line_is_left_long_rather_than_mangled() {
3726        // No break point exists inside a string literal; the 100-column target
3727        // is soft, so the line simply overruns.
3728        let long = "x".repeat(140);
3729        let src = format!("commons x {{\nfn go() -> String {{ \"{long}\" }}\n}}");
3730        let out = fmt(&src);
3731        assert!(
3732            widths(&out).iter().any(|w| *w > 100),
3733            "expected an over-long line: {out}"
3734        );
3735        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3736    }
3737
3738    #[test]
3739    fn wrapping_is_idempotent_and_structure_preserving_across_widths() {
3740        // The round-trip guard inside `format_source` already refuses output
3741        // that re-parses differently, so a successful format at each width is
3742        // itself the structural assertion.
3743        let src = "context x {\ntype R = { id: String, name: String, size: Int }\nfn build(id: String, name: String, size: Int) -> R { R { id: id, name: name, size: size } }\nfn pick(rows: List[R]) -> List[String] { rows.filter((r) => r.size > 0).map((r) => r.name).collect() }\n}";
3744        for width in [40u32, 60, 80, 100, 120] {
3745            let opts = FormatOptions {
3746                max_line_width: width,
3747                ..FormatOptions::default()
3748            };
3749            let out = format_source(src, &opts).unwrap_or_else(|e| {
3750                panic!("width {width}: format refused ({} errors)", e.errors.len())
3751            });
3752            let again = format_source(&out, &opts).unwrap_or_else(|e| {
3753                panic!(
3754                    "width {width}: reformat refused ({} errors)",
3755                    e.errors.len()
3756                )
3757            });
3758            assert_eq!(out, again, "width {width}: not idempotent:\n{out}");
3759        }
3760    }
3761
3762    // Events slice 3b (#978): `@schema(N)` on an event round-trips the same
3763    // way `messages "en" @reference { ... }`'s annotation already does.
3764    #[test]
3765    fn event_schema_annotation_formats_and_is_idempotent() {
3766        let src = "context commerce.order {\nevent PaymentConfirmed @schema(2) = {\norderId: String,\n}\n}";
3767        let out = fmt(src);
3768        assert!(
3769            out.contains("event PaymentConfirmed @schema(2) = {"),
3770            "{out}"
3771        );
3772        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3773    }
3774
3775    // An event with no annotation formats exactly as it did before this
3776    // slice — no stray space before `=`.
3777    #[test]
3778    fn event_with_no_annotation_formats_unchanged() {
3779        let src = "context commerce.order {\nevent PaymentConfirmed = {\norderId: String,\n}\n}";
3780        let out = fmt(src);
3781        assert!(out.contains("event PaymentConfirmed = {"), "{out}");
3782        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3783    }
3784
3785    // Events slice 4 (#985): `via schema(N)` on a subscription header
3786    // round-trips after the `from Events(...)` header's closing `)`.
3787    #[test]
3788    fn via_schema_dispatch_formats_and_is_idempotent() {
3789        let src = "context commerce.order {\nservice OnPayment from Events(PaymentConfirmed) via schema(2) {\non event(e: PaymentConfirmed) -> Effect[()] {\nEffect.pure(())\n}\n}\n}";
3790        let out = fmt(src);
3791        assert!(
3792            out.contains("from Events(PaymentConfirmed) via schema(2)"),
3793            "{out}"
3794        );
3795        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3796    }
3797
3798    // A payload pattern and a `via schema(...)` clause are independent and
3799    // combine on one header — neither one's presence should perturb the
3800    // other's rendering.
3801    #[test]
3802    fn via_schema_dispatch_combines_with_a_payload_pattern() {
3803        let src = "context commerce.order {\nservice OnPayment from Events(PaymentConfirmed { region: Domestic, .. }) via schema(2) {\non event(e: PaymentConfirmed) -> Effect[()] {\nEffect.pure(())\n}\n}\n}";
3804        let out = fmt(src);
3805        assert!(
3806            out.contains("from Events(PaymentConfirmed { region: Domestic, .. }) via schema(2)"),
3807            "{out}"
3808        );
3809        assert_eq!(out, fmt(&out), "not idempotent: {out}");
3810    }
3811}