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 — long parameter lists wrap across lines.
14//!
15//! The formatter is idempotent: format → format yields the same text.
16//!
17//! Comments (v1.1): line comments are preserved through the lexer-to-parser
18//! trivia pipeline (lexer emits `Comment` tokens, parser attaches them to
19//! AST declarations and statements). The formatter re-emits leading
20//! comments above each node and a trailing comment, if any, on the same
21//! line as the node's last token. Comments inside expression sub-trees
22//! are not yet attached to individual operands; they are folded into the
23//! enclosing statement's leading trivia (or dropped if no such enclosing
24//! statement exists). See `design/bynk-lsp-spec.md` §3.5 for the canonical
25//! comment-placement rules.
26
27use bynk_syntax::ast::*;
28use bynk_syntax::error::CompileError;
29use bynk_syntax::lexer::tokenize;
30use bynk_syntax::parser::parse_units;
31
32/// Indentation style: tabs or spaces. Mirrors the LSP spec's `[fmt].indent`
33/// setting.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum IndentStyle {
36    #[default]
37    Tab,
38    Spaces(u8),
39}
40
41/// Formatter options. All fields have spec-defined defaults.
42#[derive(Debug, Clone)]
43pub struct FormatOptions {
44    pub indent: IndentStyle,
45    pub max_line_width: u32,
46    pub trailing_comma: bool,
47}
48
49impl Default for FormatOptions {
50    fn default() -> Self {
51        Self {
52            indent: IndentStyle::Tab,
53            max_line_width: 100,
54            trailing_comma: true,
55        }
56    }
57}
58
59/// Error returned when formatting fails. The formatter cannot format code
60/// that does not parse, so all failure modes here surface as parse errors.
61#[derive(Debug, Clone)]
62pub struct FormatError {
63    pub errors: Vec<CompileError>,
64}
65
66/// Format a Bynk source string. On parse failure, returns the original
67/// source unchanged is *not* this function's responsibility — callers (LSP,
68/// CLI) decide how to handle parse failure. Here we surface the errors so
69/// the caller can do so.
70pub fn format_source(source: &str, opts: &FormatOptions) -> Result<String, FormatError> {
71    let tokens = tokenize(source).map_err(|e| FormatError { errors: vec![e] })?;
72    // v0.113: a file may hold more than one top-level unit (an atomic
73    // `commons` + `suite` file, DECISION S). Format each and join with a blank
74    // line. Each unit's output already ends in exactly one newline, so joining
75    // with `"\n"` inserts one blank line between units and leaves a single-unit
76    // file byte-identical.
77    let units = parse_units(&tokens, source).map_err(|errors| FormatError { errors })?;
78    let parts: Vec<String> = units
79        .iter()
80        .map(|unit| {
81            let mut f = Formatter::new(opts);
82            f.format_unit(unit);
83            f.finish()
84        })
85        .collect();
86    Ok(parts.join("\n"))
87}
88
89// -- Internal formatter state --
90
91struct Formatter<'a> {
92    opts: &'a FormatOptions,
93    out: String,
94    indent_level: u32,
95    /// True when the formatter has just emitted a newline and is at the
96    /// start of a fresh line. Used to gate indent emission.
97    at_line_start: bool,
98}
99
100impl<'a> Formatter<'a> {
101    fn new(opts: &'a FormatOptions) -> Self {
102        Self {
103            opts,
104            out: String::new(),
105            indent_level: 0,
106            at_line_start: true,
107        }
108    }
109
110    fn finish(mut self) -> String {
111        // Single trailing newline.
112        while self.out.ends_with("\n\n") {
113            self.out.pop();
114        }
115        if !self.out.ends_with('\n') {
116            self.out.push('\n');
117        }
118        self.out
119    }
120
121    fn indent_unit(&self) -> String {
122        match self.opts.indent {
123            IndentStyle::Tab => "\t".to_string(),
124            IndentStyle::Spaces(n) => " ".repeat(n as usize),
125        }
126    }
127
128    fn emit_indent(&mut self) {
129        let unit = self.indent_unit();
130        for _ in 0..self.indent_level {
131            self.out.push_str(&unit);
132        }
133    }
134
135    fn push(&mut self, s: &str) {
136        if self.at_line_start && !s.starts_with('\n') {
137            self.emit_indent();
138            self.at_line_start = false;
139        }
140        if s.contains('\n') {
141            self.push_reindented(s);
142        } else {
143            self.out.push_str(s);
144        }
145    }
146
147    /// Append a multi-line string, re-applying the current indent to every
148    /// continuation line. Multi-line strings come from the single-line
149    /// expression renderers (`expr_to_string` and friends), which build their
150    /// internal structure assuming column zero — they embed `\n` plus relative
151    /// tabs but know nothing about the current nesting depth. Without this an
152    /// argument-position `match` (or any embedded multi-line expression) would
153    /// print its arms and trailing brace at column one regardless of how deeply
154    /// it is nested. The first line is emitted as-is (its indent, if any, was
155    /// handled by `push`); blank lines are left empty rather than padded.
156    fn push_reindented(&mut self, s: &str) {
157        let prefix = self.indent_unit().repeat(self.indent_level as usize);
158        for (i, line) in s.split('\n').enumerate() {
159            if i > 0 {
160                self.out.push('\n');
161                if !line.is_empty() {
162                    self.out.push_str(&prefix);
163                }
164            }
165            self.out.push_str(line);
166        }
167    }
168
169    fn newline(&mut self) {
170        self.out.push('\n');
171        self.at_line_start = true;
172    }
173
174    #[allow(dead_code)]
175    fn blank_line(&mut self) {
176        if !self.out.ends_with('\n') {
177            self.out.push('\n');
178        }
179        if !self.out.ends_with("\n\n") {
180            self.out.push('\n');
181        }
182        self.at_line_start = true;
183    }
184
185    fn indented<F: FnOnce(&mut Self)>(&mut self, f: F) {
186        self.indent_level += 1;
187        f(self);
188        self.indent_level -= 1;
189    }
190
191    // -- Doc block --
192
193    /// Emit a doc block immediately above a declaration. The content is
194    /// already normalised (common leading indent stripped) when stored in
195    /// the AST; we re-emit with the current indent applied per line.
196    fn emit_doc(&mut self, doc: &str) {
197        self.push("---");
198        self.newline();
199        for line in doc.lines() {
200            if line.is_empty() {
201                self.newline();
202            } else {
203                self.push(line);
204                self.newline();
205            }
206        }
207        self.push("---");
208        self.newline();
209    }
210
211    // -- Line-comment trivia (v1.1) --
212
213    /// Emit a sequence of leading line-comments, each on its own line at
214    /// the current indent. Group has no blank lines between entries.
215    fn emit_leading_comments(&mut self, comments: &[String]) {
216        for body in comments {
217            self.push("--");
218            self.push(body);
219            self.newline();
220        }
221    }
222
223    /// Emit a trailing comment on the same line as the just-emitted token.
224    /// The spec uses two spaces between code and comment for readability.
225    fn emit_trailing_comment(&mut self, body: Option<&str>) {
226        if let Some(body) = body {
227            // Ensure we're on the same line as the preceding tokens —
228            // strip any newline we just emitted.
229            while self.out.ends_with('\n') {
230                self.out.pop();
231            }
232            self.out.push_str("  --");
233            self.out.push_str(body);
234            self.newline();
235        }
236    }
237
238    // -- Top level --
239
240    fn format_unit(&mut self, unit: &SourceUnit) {
241        match unit {
242            SourceUnit::Commons(c) => self.format_commons(c),
243            SourceUnit::Context(c) => self.format_context(c),
244            SourceUnit::Suite(t) => self.format_test(t),
245            SourceUnit::Integration(i) => self.format_integration(i),
246            SourceUnit::Adapter(a) => self.format_adapter(a),
247        }
248    }
249
250    fn format_adapter(&mut self, a: &AdapterDecl) {
251        self.emit_leading_comments(&a.trivia.leading);
252        if let Some(doc) = &a.documentation {
253            self.emit_doc(doc);
254        }
255        let header = format!("adapter {}", a.name.joined());
256        match a.form {
257            CommonsForm::Brace => {
258                self.push(&header);
259                self.push(" {");
260                self.newline();
261                self.indented(|f| {
262                    f.format_adapter_body(a);
263                });
264                self.push("}");
265                self.newline();
266            }
267            CommonsForm::Fragment => {
268                self.push(&header);
269                self.newline();
270                self.newline();
271                self.format_adapter_body(a);
272            }
273        }
274    }
275
276    fn format_adapter_body(&mut self, a: &AdapterDecl) {
277        let mut any_header = false;
278        if let Some(b) = &a.binding {
279            self.emit_leading_comments(&b.trivia.leading);
280            self.push(&format!("binding {:?}", b.module));
281            if !b.requires.is_empty() {
282                let entries: Vec<String> = b
283                    .requires
284                    .iter()
285                    .map(|r| format!("{:?}: {:?}", r.package, r.range))
286                    .collect();
287                self.push(&format!(" requires {{ {} }}", entries.join(", ")));
288            }
289            self.emit_trailing_comment(b.trivia.trailing.as_deref());
290            if b.trivia.trailing.is_none() {
291                self.newline();
292            }
293            any_header = true;
294        }
295        for u in &a.uses {
296            self.emit_leading_comments(&u.trivia.leading);
297            self.push(&format!("uses {}", u.target.joined()));
298            self.emit_trailing_comment(u.trivia.trailing.as_deref());
299            if u.trivia.trailing.is_none() {
300                self.newline();
301            }
302            any_header = true;
303        }
304        for c in &a.consumes {
305            self.format_consumes(c);
306            any_header = true;
307        }
308        for e in &a.exports {
309            self.emit_leading_comments(&e.trivia.leading);
310            self.format_exports(e);
311            if e.trivia.trailing.is_some() {
312                self.emit_trailing_comment(e.trivia.trailing.as_deref());
313            }
314            any_header = true;
315        }
316        if any_header && !a.items.is_empty() {
317            self.newline();
318        }
319        let mut first = true;
320        for item in &a.items {
321            if !first {
322                self.newline();
323            }
324            self.format_item(item);
325            first = false;
326        }
327        if !a.trailing_comments.is_empty() {
328            if !a.items.is_empty() || any_header {
329                self.newline();
330            }
331            self.emit_leading_comments(&a.trailing_comments);
332        }
333    }
334
335    fn format_integration(&mut self, i: &IntegrationDecl) {
336        self.emit_leading_comments(&i.trivia.leading);
337        if let Some(doc) = &i.documentation {
338            self.emit_doc(doc);
339        }
340        let header = format!("suite integration \"{}\"", escape_string(&i.suite));
341        match i.form {
342            CommonsForm::Brace => {
343                self.push(&header);
344                self.push(" {");
345                self.newline();
346                self.indented(|f| {
347                    f.format_integration_body(i);
348                });
349                self.push("}");
350                self.newline();
351            }
352            CommonsForm::Fragment => {
353                self.push(&header);
354                self.newline();
355                self.newline();
356                self.format_integration_body(i);
357            }
358        }
359    }
360
361    fn format_integration_body(&mut self, i: &IntegrationDecl) {
362        let wires = i
363            .participants
364            .iter()
365            .map(|p| p.joined())
366            .collect::<Vec<_>>()
367            .join(", ");
368        self.push(&format!("wires {wires}"));
369        self.newline();
370        for u in &i.uses {
371            self.newline();
372            self.emit_leading_comments(&u.trivia.leading);
373            self.push(&format!("uses {}", u.target.joined()));
374            self.emit_trailing_comment(u.trivia.trailing.as_deref());
375            self.newline();
376        }
377        for c in &i.cases {
378            self.newline();
379            self.emit_leading_comments(&c.trivia.leading);
380            if let Some(doc) = &c.documentation {
381                self.emit_doc(doc);
382            }
383            self.push(&format!("case \"{}\" ", escape_string(&c.name)));
384            self.format_block(&c.body);
385            self.newline();
386        }
387        for comment in &i.trailing_comments {
388            self.push(&format!("--{comment}"));
389            self.newline();
390        }
391    }
392
393    fn format_test(&mut self, t: &SuiteDecl) {
394        self.emit_leading_comments(&t.trivia.leading);
395        if let Some(doc) = &t.documentation {
396            self.emit_doc(doc);
397        }
398        let header = format!("suite {}", t.target.joined());
399        match t.form {
400            CommonsForm::Brace => {
401                self.push(&header);
402                self.push(" {");
403                self.newline();
404                self.indented(|f| {
405                    f.format_test_body(
406                        &t.uses,
407                        &t.mocks,
408                        &t.cases,
409                        &t.properties,
410                        &t.trailing_comments,
411                    );
412                });
413                self.push("}");
414                self.newline();
415            }
416            CommonsForm::Fragment => {
417                self.push(&header);
418                self.newline();
419                self.format_test_body(
420                    &t.uses,
421                    &t.mocks,
422                    &t.cases,
423                    &t.properties,
424                    &t.trailing_comments,
425                );
426            }
427        }
428    }
429
430    fn format_test_body(
431        &mut self,
432        uses: &[UsesDecl],
433        mocks: &[MockDecl],
434        cases: &[Case],
435        properties: &[PropertyDecl],
436        trailing_comments: &[String],
437    ) {
438        let mut first = true;
439        for u in uses {
440            if !first {
441                self.newline();
442            }
443            self.emit_leading_comments(&u.trivia.leading);
444            self.push(&format!("uses {}", u.target.joined()));
445            self.emit_trailing_comment(u.trivia.trailing.as_deref());
446            self.newline();
447            first = false;
448        }
449        for m in mocks {
450            if !first {
451                self.newline();
452            }
453            self.emit_leading_comments(&m.trivia.leading);
454            if let Some(doc) = &m.documentation {
455                self.emit_doc(doc);
456            }
457            self.push(&format!(
458                "mocks {} = {} {{",
459                m.target_name.name, m.impl_name.name
460            ));
461            self.newline();
462            self.indented(|f| {
463                let mut first_op = true;
464                for op in &m.ops {
465                    if !first_op {
466                        f.newline();
467                    }
468                    let params = op
469                        .params
470                        .iter()
471                        .map(|p| format!("{}: {}", p.name.name, type_ref_to_string(&p.type_ref)))
472                        .collect::<Vec<_>>()
473                        .join(", ");
474                    f.push(&format!(
475                        "fn {}({params}) -> {} ",
476                        op.name.name,
477                        type_ref_to_string(&op.return_type)
478                    ));
479                    f.format_block(&op.body);
480                    f.newline();
481                    first_op = false;
482                }
483            });
484            self.push("}");
485            self.newline();
486            first = false;
487        }
488        for c in cases {
489            if !first {
490                self.newline();
491            }
492            self.emit_leading_comments(&c.trivia.leading);
493            if let Some(doc) = &c.documentation {
494                self.emit_doc(doc);
495            }
496            self.push(&format!("case \"{}\" ", escape_string(&c.name)));
497            self.format_block(&c.body);
498            self.newline();
499            first = false;
500        }
501        for p in properties {
502            if !first {
503                self.newline();
504            }
505            self.emit_leading_comments(&p.trivia.leading);
506            if let Some(doc) = &p.documentation {
507                self.emit_doc(doc);
508            }
509            self.push(&format!("property \"{}\" {{", escape_string(&p.name)));
510            self.newline();
511            self.indented(|f| f.format_for_all(&p.forall));
512            self.push("}");
513            self.newline();
514            first = false;
515        }
516        for comment in trailing_comments {
517            self.push(&format!("--{comment}"));
518            self.newline();
519        }
520    }
521
522    /// v0.114: format a `for all <bindings> [where <pred>] { … }` binder — the
523    /// sole body of a `property`.
524    fn format_for_all(&mut self, fa: &ForAll) {
525        let bindings = fa
526            .bindings
527            .iter()
528            .map(|b| format!("{}: {}", b.name.name, type_ref_to_string(&b.type_ref)))
529            .collect::<Vec<_>>()
530            .join(", ");
531        let mut header = format!("for all {bindings}");
532        if let Some(w) = &fa.where_pred {
533            header.push_str(&format!(" where {}", expr_to_string(w)));
534        }
535        self.push(&format!("{header} "));
536        self.format_block(&fa.body);
537        self.newline();
538    }
539
540    fn format_commons(&mut self, c: &Commons) {
541        self.emit_leading_comments(&c.trivia.leading);
542        if let Some(doc) = &c.documentation {
543            self.emit_doc(doc);
544        }
545        let header = format!("commons {}", c.name.joined());
546        match c.form {
547            CommonsForm::Brace => {
548                self.push(&header);
549                self.push(" {");
550                self.newline();
551                self.indented(|f| {
552                    f.format_commons_body(&c.uses, &c.items, &c.trailing_comments);
553                });
554                self.push("}");
555                self.newline();
556            }
557            CommonsForm::Fragment => {
558                self.push(&header);
559                self.newline();
560                self.newline();
561                self.format_commons_body(&c.uses, &c.items, &c.trailing_comments);
562            }
563        }
564    }
565
566    fn format_commons_body(
567        &mut self,
568        uses: &[UsesDecl],
569        items: &[CommonsItem],
570        trailing_comments: &[String],
571    ) {
572        let mut any_uses = false;
573        for u in uses {
574            self.emit_leading_comments(&u.trivia.leading);
575            self.push(&format!("uses {}", u.target.joined()));
576            self.emit_trailing_comment(u.trivia.trailing.as_deref());
577            if u.trivia.trailing.is_none() {
578                self.newline();
579            }
580            any_uses = true;
581        }
582        if any_uses && !items.is_empty() {
583            self.newline();
584        }
585        let mut first = true;
586        for item in items {
587            if !first {
588                self.newline();
589            }
590            self.format_item(item);
591            first = false;
592        }
593        if !trailing_comments.is_empty() {
594            // One blank line before trailing-file comments if anything
595            // came before them.
596            if !items.is_empty() || any_uses {
597                self.newline();
598            }
599            self.emit_leading_comments(trailing_comments);
600        }
601    }
602
603    fn format_context(&mut self, c: &Context) {
604        self.emit_leading_comments(&c.trivia.leading);
605        if let Some(doc) = &c.documentation {
606            self.emit_doc(doc);
607        }
608        let header = format!("context {}", c.name.joined());
609        match c.form {
610            CommonsForm::Brace => {
611                self.push(&header);
612                self.push(" {");
613                self.newline();
614                self.indented(|f| {
615                    f.format_context_body(
616                        &c.uses,
617                        &c.consumes,
618                        &c.exports,
619                        &c.items,
620                        &c.trailing_comments,
621                    );
622                });
623                self.push("}");
624                self.newline();
625            }
626            CommonsForm::Fragment => {
627                self.push(&header);
628                self.newline();
629                self.newline();
630                self.format_context_body(
631                    &c.uses,
632                    &c.consumes,
633                    &c.exports,
634                    &c.items,
635                    &c.trailing_comments,
636                );
637            }
638        }
639    }
640
641    /// Print one `consumes` clause in any of its three forms: whole-unit,
642    /// aliased, or braced capability selection (v0.17 §3.3 — previously the
643    /// braced form was silently dropped, a semantic-changing format).
644    fn format_consumes(&mut self, c: &ConsumesDecl) {
645        self.emit_leading_comments(&c.trivia.leading);
646        match (&c.alias, &c.selected) {
647            (Some(alias), _) => {
648                self.push(&format!("consumes {} as {}", c.target.joined(), alias.name))
649            }
650            (None, Some(selected)) if selected.is_empty() => {
651                self.push(&format!("consumes {} {{ }}", c.target.joined()));
652            }
653            (None, Some(selected)) => {
654                let names: Vec<&str> = selected.iter().map(|i| i.name.as_str()).collect();
655                self.push(&format!(
656                    "consumes {} {{ {} }}",
657                    c.target.joined(),
658                    names.join(", ")
659                ));
660            }
661            (None, None) => self.push(&format!("consumes {}", c.target.joined())),
662        }
663        self.emit_trailing_comment(c.trivia.trailing.as_deref());
664        if c.trivia.trailing.is_none() {
665            self.newline();
666        }
667    }
668
669    fn format_context_body(
670        &mut self,
671        uses: &[UsesDecl],
672        consumes: &[ConsumesDecl],
673        exports: &[ExportsDecl],
674        items: &[CommonsItem],
675        trailing_comments: &[String],
676    ) {
677        let mut any_header = false;
678        for u in uses {
679            self.emit_leading_comments(&u.trivia.leading);
680            self.push(&format!("uses {}", u.target.joined()));
681            self.emit_trailing_comment(u.trivia.trailing.as_deref());
682            if u.trivia.trailing.is_none() {
683                self.newline();
684            }
685            any_header = true;
686        }
687        for c in consumes {
688            self.format_consumes(c);
689            any_header = true;
690        }
691        for e in exports {
692            self.emit_leading_comments(&e.trivia.leading);
693            self.format_exports(e);
694            // exports may emit multi-line — the trailing comment goes on
695            // its last line. Since format_exports already terminates with
696            // a newline, splice the comment before it if present.
697            if e.trivia.trailing.is_some() {
698                self.emit_trailing_comment(e.trivia.trailing.as_deref());
699            }
700            any_header = true;
701        }
702        if any_header && !items.is_empty() {
703            self.newline();
704        }
705        let mut first = true;
706        for item in items {
707            if !first {
708                self.newline();
709            }
710            self.format_item(item);
711            first = false;
712        }
713        if !trailing_comments.is_empty() {
714            if !items.is_empty() || any_header {
715                self.newline();
716            }
717            self.emit_leading_comments(trailing_comments);
718        }
719    }
720
721    fn format_exports(&mut self, e: &ExportsDecl) {
722        let vis = match e.kind {
723            ExportKind::Type(Visibility::Opaque) => "opaque",
724            ExportKind::Type(Visibility::Transparent) => "transparent",
725            ExportKind::Capability => "capability",
726        };
727        if e.names.is_empty() {
728            self.push(&format!("exports {} {{}}", vis));
729            self.newline();
730            return;
731        }
732        // Single-line form if it fits.
733        let oneline = format!(
734            "exports {} {{ {} }}",
735            vis,
736            e.names
737                .iter()
738                .map(|n| n.name.as_str())
739                .collect::<Vec<_>>()
740                .join(", ")
741        );
742        if self.line_fits(&oneline) {
743            self.push(&oneline);
744            self.newline();
745            return;
746        }
747        // Multi-line form.
748        self.push(&format!("exports {} {{", vis));
749        self.newline();
750        self.indented(|f| {
751            for (i, n) in e.names.iter().enumerate() {
752                f.push(&n.name);
753                if i + 1 < e.names.len() || f.opts.trailing_comma {
754                    f.push(",");
755                }
756                f.newline();
757            }
758        });
759        self.push("}");
760        self.newline();
761    }
762
763    fn line_fits(&self, candidate: &str) -> bool {
764        let unit_len = match self.opts.indent {
765            IndentStyle::Tab => 4, // Approximate tab width for width estimation.
766            IndentStyle::Spaces(n) => n as usize,
767        };
768        let column = self.indent_level as usize * unit_len + candidate.len();
769        column as u32 <= self.opts.max_line_width
770    }
771
772    fn format_item(&mut self, item: &CommonsItem) {
773        match item {
774            CommonsItem::Type(t) => self.format_type_decl(t),
775            CommonsItem::Fn(f) => self.format_fn_decl(f),
776            CommonsItem::Capability(c) => self.format_capability(c),
777            CommonsItem::Provider(p) => self.format_provider(p),
778            CommonsItem::Service(s) => self.format_service(s),
779            CommonsItem::Agent(a) => self.format_agent(a),
780            CommonsItem::Actor(a) => self.format_actor(a),
781        }
782    }
783
784    // -- Type declarations --
785
786    fn format_type_decl(&mut self, t: &TypeDecl) {
787        self.emit_leading_comments(&t.trivia.leading);
788        if let Some(doc) = &t.documentation {
789            self.emit_doc(doc);
790        }
791        self.push(&format!("type {} = ", t.name.name));
792        self.format_type_body(&t.body);
793        self.emit_trailing_comment(t.trivia.trailing.as_deref());
794        if t.trivia.trailing.is_none() {
795            self.newline();
796        }
797    }
798
799    fn format_type_body(&mut self, body: &TypeBody) {
800        match body {
801            TypeBody::Refined {
802                base, refinement, ..
803            } => {
804                self.push(base.name());
805                if let Some(r) = refinement {
806                    self.push(" where ");
807                    self.format_refinement(r);
808                }
809            }
810            TypeBody::Opaque {
811                base, refinement, ..
812            } => {
813                self.push("opaque ");
814                self.push(base.name());
815                if let Some(r) = refinement {
816                    self.push(" where ");
817                    self.format_refinement(r);
818                }
819            }
820            TypeBody::Record(r) => self.format_record_body(r),
821            TypeBody::Sum(s) => self.format_sum_body(s),
822        }
823    }
824
825    fn format_refinement(&mut self, r: &Refinement) {
826        for (i, p) in r.predicates.iter().enumerate() {
827            if i > 0 {
828                self.push(" and ");
829            }
830            self.format_pred(p);
831        }
832    }
833
834    fn format_pred(&mut self, p: &RefinementPred) {
835        match &p.kind {
836            PredKind::Matches(re) => self.push(&format!("Matches(\"{}\")", escape_string(re))),
837            PredKind::InRange(a, b) => self.push(&format!("InRange({}, {})", a.value, b.value)),
838            PredKind::InRangeF(a, b) => self.push(&format!("InRange({}, {})", a.lexeme, b.lexeme)),
839            PredKind::MinLength(n) => self.push(&format!("MinLength({n})")),
840            PredKind::MaxLength(n) => self.push(&format!("MaxLength({n})")),
841            PredKind::Length(n) => self.push(&format!("Length({n})")),
842            PredKind::NonNegative => self.push("NonNegative"),
843            PredKind::Positive => self.push("Positive"),
844            PredKind::NonEmpty => self.push("NonEmpty"),
845        }
846    }
847
848    fn format_record_body(&mut self, r: &RecordBody) {
849        if r.fields.is_empty() {
850            self.push("{}");
851            return;
852        }
853        // Try single-line first.
854        let oneline_fields: Vec<String> = r
855            .fields
856            .iter()
857            .map(|f| self.format_record_field_oneline(f))
858            .collect();
859        let oneline = format!("{{ {} }}", oneline_fields.join(", "));
860        if self.line_fits(&oneline) && !oneline.contains('\n') {
861            self.push(&oneline);
862            return;
863        }
864        // Multi-line.
865        self.push("{");
866        self.newline();
867        self.indented(|f| {
868            for (i, field) in r.fields.iter().enumerate() {
869                f.format_record_field(field);
870                if i + 1 < r.fields.len() || f.opts.trailing_comma {
871                    f.push(",");
872                }
873                f.newline();
874            }
875        });
876        self.push("}");
877    }
878
879    fn format_record_field(&mut self, field: &RecordField) {
880        self.push(&format!("{}: ", field.name.name));
881        self.format_type_ref(&field.type_ref);
882        if let Some(r) = &field.refinement {
883            self.push(" where ");
884            self.format_refinement(r);
885        }
886        if let Some(init) = &field.init {
887            self.push(" = ");
888            self.format_expr(init);
889        }
890    }
891
892    fn format_record_field_oneline(&self, field: &RecordField) -> String {
893        let mut out = format!("{}: ", field.name.name);
894        out.push_str(&type_ref_to_string(&field.type_ref));
895        if let Some(r) = &field.refinement {
896            out.push_str(" where ");
897            out.push_str(&refinement_to_string(r));
898        }
899        if let Some(init) = &field.init {
900            out.push_str(" = ");
901            out.push_str(&expr_to_string(init));
902        }
903        out
904    }
905
906    fn format_sum_body(&mut self, s: &SumBody) {
907        // Two surface forms exist; we render the pipe form (clearest for both
908        // variants with and without payload). enum form is only meaningful for
909        // payloadless variants — round-trip preserves semantics either way.
910        let any_payload = s.variants.iter().any(|v| !v.payload.is_empty());
911        if !any_payload {
912            // Enum-style.
913            let names: Vec<&str> = s.variants.iter().map(|v| v.name.name.as_str()).collect();
914            let oneline = format!("enum {{ {} }}", names.join(", "));
915            if self.line_fits(&oneline) {
916                self.push(&oneline);
917                return;
918            }
919            self.push("enum {");
920            self.newline();
921            self.indented(|f| {
922                for (i, v) in s.variants.iter().enumerate() {
923                    f.push(&v.name.name);
924                    if i + 1 < s.variants.len() || f.opts.trailing_comma {
925                        f.push(",");
926                    }
927                    f.newline();
928                }
929            });
930            self.push("}");
931            return;
932        }
933        // Pipe form, multi-line.
934        for (i, v) in s.variants.iter().enumerate() {
935            if i > 0 {
936                self.newline();
937            }
938            self.push("| ");
939            self.push(&v.name.name);
940            if !v.payload.is_empty() {
941                self.push("(");
942                let parts: Vec<String> = v
943                    .payload
944                    .iter()
945                    .map(|p| format!("{}: {}", p.name.name, type_ref_to_string(&p.type_ref)))
946                    .collect();
947                self.push(&parts.join(", "));
948                self.push(")");
949            }
950        }
951    }
952
953    fn format_type_ref(&mut self, t: &TypeRef) {
954        self.push(&type_ref_to_string(t));
955    }
956
957    // -- Function declarations --
958
959    fn format_fn_decl(&mut self, f: &FnDecl) {
960        self.emit_leading_comments(&f.trivia.leading);
961        if let Some(doc) = &f.documentation {
962            self.emit_doc(doc);
963        }
964        self.push("fn ");
965        self.push(&f.name.display());
966        // v0.20a: `[A, B]` type parameters.
967        if !f.type_params.is_empty() {
968            let names: Vec<&str> = f
969                .type_params
970                .iter()
971                .map(|tp| tp.name.name.as_str())
972                .collect();
973            self.push(&format!("[{}]", names.join(", ")));
974        }
975        self.format_params(&f.params, f.has_self);
976        self.push(" -> ");
977        self.format_type_ref(&f.return_type);
978        self.push(" ");
979        self.format_block(&f.body);
980        self.emit_trailing_comment(f.trivia.trailing.as_deref());
981        if f.trivia.trailing.is_none() {
982            self.newline();
983        }
984    }
985
986    fn format_params(&mut self, params: &[Param], has_self: bool) {
987        let mut rendered: Vec<String> = Vec::new();
988        if has_self {
989            rendered.push("self".to_string());
990        }
991        // `params` never includes `self` — it is tracked separately via the
992        // `has_self` flag (see parser.rs parse_fn_decl).
993        for p in params {
994            rendered.push(format!(
995                "{}: {}",
996                p.name.name,
997                type_ref_to_string(&p.type_ref)
998            ));
999        }
1000        let oneline = format!("({})", rendered.join(", "));
1001        if self.line_fits(&oneline) || rendered.len() <= 1 {
1002            self.push(&oneline);
1003            return;
1004        }
1005        // Multi-line params.
1006        self.push("(");
1007        self.newline();
1008        self.indented(|f| {
1009            for (i, r) in rendered.iter().enumerate() {
1010                f.push(r);
1011                // Parameter lists — unlike records, enum/sum variants, agent
1012                // state fields and exports — do NOT accept a trailing comma in
1013                // the grammar, so never emit one here regardless of the
1014                // `trailing_comma` option, or the wrapped output fails to
1015                // re-parse.
1016                if i + 1 < rendered.len() {
1017                    f.push(",");
1018                }
1019                f.newline();
1020            }
1021        });
1022        self.push(")");
1023    }
1024
1025    // -- Capability / provider / service / agent (v0.5) --
1026
1027    fn format_capability(&mut self, c: &CapabilityDecl) {
1028        self.emit_leading_comments(&c.trivia.leading);
1029        if let Some(doc) = &c.documentation {
1030            self.emit_doc(doc);
1031        }
1032        self.push(&format!("capability {} {{", c.name.name));
1033        self.newline();
1034        self.indented(|f| {
1035            for op in &c.ops {
1036                f.emit_leading_comments(&op.trivia.leading);
1037                if let Some(doc) = &op.documentation {
1038                    f.emit_doc(doc);
1039                }
1040                f.push("fn ");
1041                f.push(&op.name.name);
1042                f.format_params(&op.params, false);
1043                f.push(" -> ");
1044                f.format_type_ref(&op.return_type);
1045                f.emit_trailing_comment(op.trivia.trailing.as_deref());
1046                if op.trivia.trailing.is_none() {
1047                    f.newline();
1048                }
1049            }
1050        });
1051        self.push("}");
1052        self.emit_trailing_comment(c.trivia.trailing.as_deref());
1053        if c.trivia.trailing.is_none() {
1054            self.newline();
1055        }
1056    }
1057
1058    fn format_provider(&mut self, p: &ProviderDecl) {
1059        self.emit_leading_comments(&p.trivia.leading);
1060        if let Some(doc) = &p.documentation {
1061            self.emit_doc(doc);
1062        }
1063        self.push(&format!(
1064            "provides {} = {}",
1065            p.capability.name, p.provider_name.name
1066        ));
1067        if !p.given.is_empty() {
1068            self.push(" given ");
1069            let names: Vec<String> = p.given.iter().map(cap_ref_src).collect();
1070            self.push(&names.join(", "));
1071        }
1072        // v0.17: an external provider (inside an adapter) has no body.
1073        if p.external {
1074            self.emit_trailing_comment(p.trivia.trailing.as_deref());
1075            if p.trivia.trailing.is_none() {
1076                self.newline();
1077            }
1078            return;
1079        }
1080        self.push(" {");
1081        self.newline();
1082        self.indented(|f| {
1083            for (i, op) in p.ops.iter().enumerate() {
1084                if i > 0 {
1085                    f.newline();
1086                }
1087                f.emit_leading_comments(&op.trivia.leading);
1088                f.push("fn ");
1089                f.push(&op.name.name);
1090                f.format_params(&op.params, false);
1091                f.push(" -> ");
1092                f.format_type_ref(&op.return_type);
1093                f.push(" ");
1094                f.format_block(&op.body);
1095                f.emit_trailing_comment(op.trivia.trailing.as_deref());
1096                if op.trivia.trailing.is_none() {
1097                    f.newline();
1098                }
1099            }
1100        });
1101        self.push("}");
1102        self.emit_trailing_comment(p.trivia.trailing.as_deref());
1103        if p.trivia.trailing.is_none() {
1104            self.newline();
1105        }
1106    }
1107
1108    fn format_service(&mut self, s: &ServiceDecl) {
1109        self.emit_leading_comments(&s.trivia.leading);
1110        if let Some(doc) = &s.documentation {
1111            self.emit_doc(doc);
1112        }
1113        let from = match &s.protocol {
1114            ServiceProtocol::Call => String::new(),
1115            ServiceProtocol::Http => " from http".to_string(),
1116            ServiceProtocol::Cron => " from cron".to_string(),
1117            ServiceProtocol::Queue { name } => {
1118                format!(" from queue(\"{}\")", escape_string(name))
1119            }
1120            ServiceProtocol::WebSocket { in_type, out_type } => {
1121                format!(
1122                    " from WebSocket(in: {}, out: {})",
1123                    type_ref_to_string(in_type),
1124                    type_ref_to_string(out_type)
1125                )
1126            }
1127        };
1128        self.push(&format!("service {}{} {{", s.name.name, from));
1129        self.newline();
1130        self.indented(|f| {
1131            for (i, h) in s.handlers.iter().enumerate() {
1132                if i > 0 {
1133                    f.newline();
1134                }
1135                f.format_handler(h);
1136            }
1137        });
1138        self.push("}");
1139        self.emit_trailing_comment(s.trivia.trailing.as_deref());
1140        if s.trivia.trailing.is_none() {
1141            self.newline();
1142        }
1143    }
1144
1145    fn format_agent(&mut self, a: &AgentDecl) {
1146        self.emit_leading_comments(&a.trivia.leading);
1147        if let Some(doc) = &a.documentation {
1148            self.emit_doc(doc);
1149        }
1150        self.push(&format!("agent {} {{", a.name.name));
1151        self.newline();
1152        self.indented(|f| {
1153            // key
1154            f.push(&format!(
1155                "key {}: {}",
1156                a.key_name.name,
1157                type_ref_to_string(&a.key_type)
1158            ));
1159            f.newline();
1160            f.newline();
1161            // storage (v0.81, storage track): the agent's `store` fields.
1162            for sf in &a.store_fields {
1163                f.format_store_field(sf);
1164                f.newline();
1165            }
1166            // v0.80: invariants form a phase between the storage fields and the
1167            // handlers.
1168            for inv in &a.invariants {
1169                f.newline();
1170                f.format_invariant(inv);
1171            }
1172            // handlers
1173            for h in &a.handlers {
1174                f.newline();
1175                f.format_handler(h);
1176            }
1177        });
1178        self.push("}");
1179        self.emit_trailing_comment(a.trivia.trailing.as_deref());
1180        if a.trivia.trailing.is_none() {
1181            self.newline();
1182        }
1183    }
1184
1185    /// Format a `store` field (v0.81): `store <name>: <Kind> [= <init>]`, with
1186    /// its leading comments / doc and trailing comment. The enclosing loop adds
1187    /// the line break.
1188    fn format_store_field(&mut self, sf: &StoreField) {
1189        self.emit_leading_comments(&sf.trivia.leading);
1190        if let Some(doc) = &sf.documentation {
1191            self.emit_doc(doc);
1192        }
1193        self.push(&format!(
1194            "store {}: {}",
1195            sf.name.name,
1196            store_kind_to_string(&sf.kind)
1197        ));
1198        // v0.85 (ADR 0111): annotations follow the kind, one space-separated each.
1199        for ann in &sf.annotations {
1200            self.push(&format!(" {}", annotation_to_string(ann)));
1201        }
1202        if let Some(init) = &sf.init {
1203            self.push(&format!(" = {}", expr_with_prec(init, 0)));
1204        }
1205        self.emit_trailing_comment(sf.trivia.trailing.as_deref());
1206    }
1207
1208    /// Format an agent invariant (v0.80): the name on one line, the predicate
1209    /// indented beneath, matching the §14 worked examples.
1210    fn format_invariant(&mut self, inv: &Invariant) {
1211        self.emit_leading_comments(&inv.trivia.leading);
1212        if let Some(doc) = &inv.documentation {
1213            self.emit_doc(doc);
1214        }
1215        self.push(&format!("invariant {}:", inv.name.name));
1216        self.newline();
1217        self.indented(|f| {
1218            f.push(&expr_to_string(&inv.predicate));
1219        });
1220        self.emit_trailing_comment(inv.trivia.trailing.as_deref());
1221        if inv.trivia.trailing.is_none() {
1222            self.newline();
1223        }
1224    }
1225
1226    fn format_actor(&mut self, a: &ActorDecl) {
1227        self.emit_leading_comments(&a.trivia.leading);
1228        if let Some(doc) = &a.documentation {
1229            self.emit_doc(doc);
1230        }
1231        if let Some(r) = &a.refinement {
1232            // Reserved refinement form: `actor Name = Base where <predicate>`.
1233            self.push(&format!(
1234                "actor {} = {} where {}",
1235                a.name.name,
1236                r.base.name,
1237                expr_to_string(&r.predicate)
1238            ));
1239        } else {
1240            // Normal form: `actor Name { auth = Scheme(, identity = Type)? }`.
1241            let auth = a.auth.as_ref().map(|i| i.name.as_str()).unwrap_or("None");
1242            self.push(&format!("actor {} {{ auth = {auth}", a.name.name));
1243            if !a.auth_config.is_empty() {
1244                let args: Vec<String> = a
1245                    .auth_config
1246                    .iter()
1247                    .map(|arg| match &arg.value {
1248                        bynk_syntax::ast::SchemeArgValue::Str(s) => {
1249                            format!("{} = \"{}\"", arg.key.name, escape_string(s))
1250                        }
1251                        bynk_syntax::ast::SchemeArgValue::Int(n) => {
1252                            format!("{} = {n}", arg.key.name)
1253                        }
1254                    })
1255                    .collect();
1256                self.push(&format!("({})", args.join(", ")));
1257            }
1258            if let Some(id) = &a.identity {
1259                self.push(&format!(", identity = {}", type_ref_to_string(id)));
1260            }
1261            self.push(" }");
1262        }
1263        self.emit_trailing_comment(a.trivia.trailing.as_deref());
1264        if a.trivia.trailing.is_none() {
1265            self.newline();
1266        }
1267    }
1268
1269    fn format_handler(&mut self, h: &Handler) {
1270        self.emit_leading_comments(&h.trivia.leading);
1271        if let Some(doc) = &h.documentation {
1272            self.emit_doc(doc);
1273        }
1274        // The handler kind prefix: `on call`, `on http METHOD "path"`, or
1275        // `on cron("expr")`. Agent `on call` handlers carry a method name.
1276        match &h.kind {
1277            HandlerKind::Call => {
1278                self.push("on call");
1279                if let Some(m) = &h.method_name {
1280                    self.push(&format!(" {}", m.name));
1281                }
1282            }
1283            HandlerKind::Http { method, path } => {
1284                // Trailing space: the path string is followed by the param list,
1285                // which reads better separated (`… "/path" (params)`).
1286                self.push(&format!(
1287                    "on {}(\"{}\") ",
1288                    method.as_str(),
1289                    escape_string(path)
1290                ));
1291            }
1292            HandlerKind::Cron { expr } => {
1293                self.push(&format!("on schedule(\"{}\") ", escape_string(expr)));
1294            }
1295            HandlerKind::Message => {
1296                self.push("on message");
1297            }
1298            HandlerKind::Open => {
1299                self.push("on open");
1300            }
1301            HandlerKind::Close => {
1302                self.push("on close");
1303            }
1304        }
1305        // v0.45: the `by <binder>: <Actor>` clause sits between the config and
1306        // the parameters. The Http/Cron config prefixes already emit a trailing
1307        // space; Call/Message do not, so add one before the clause.
1308        if let Some(by) = &h.by_clause {
1309            if matches!(
1310                h.kind,
1311                HandlerKind::Call | HandlerKind::Message | HandlerKind::Open | HandlerKind::Close
1312            ) {
1313                self.push(" ");
1314            }
1315            let actors = by
1316                .actors
1317                .iter()
1318                .map(|a| a.name.as_str())
1319                .collect::<Vec<_>>()
1320                .join(" | ");
1321            match &by.binder {
1322                Some(b) => self.push(&format!("by {}: {actors} ", b.name)),
1323                None => self.push(&format!("by {actors} ")),
1324            }
1325        }
1326        self.format_params(&h.params, false);
1327        self.push(" -> ");
1328        self.format_type_ref(&h.return_type);
1329        if !h.given.is_empty() {
1330            self.push(" given ");
1331            let names: Vec<String> = h.given.iter().map(cap_ref_src).collect();
1332            self.push(&names.join(", "));
1333        }
1334        self.push(" ");
1335        self.format_block(&h.body);
1336        self.emit_trailing_comment(h.trivia.trailing.as_deref());
1337        if h.trivia.trailing.is_none() {
1338            self.newline();
1339        }
1340    }
1341
1342    // -- Blocks, statements, expressions --
1343
1344    fn format_block(&mut self, b: &Block) {
1345        // A block with no statements, no trivia, and a simple tail
1346        // expression can be emitted inline if it fits; otherwise multi-line.
1347        let tail_oneline = expr_to_string(&b.tail);
1348        let any_stmt_trivia = b.statements.iter().any(|s| !statement_trivia(s).is_empty());
1349        if b.statements.is_empty()
1350            && b.tail_leading_comments.is_empty()
1351            && !any_stmt_trivia
1352            && self.line_fits(&format!("{{ {tail_oneline} }}"))
1353            && !tail_oneline.contains('\n')
1354        {
1355            self.push("{ ");
1356            self.format_expr(&b.tail);
1357            self.push(" }");
1358            return;
1359        }
1360        self.push("{");
1361        self.newline();
1362        self.indented(|f| {
1363            for stmt in &b.statements {
1364                let trivia = statement_trivia(stmt);
1365                f.emit_leading_comments(&trivia.leading);
1366                f.format_statement(stmt);
1367                f.emit_trailing_comment(trivia.trailing.as_deref());
1368                if trivia.trailing.is_none() {
1369                    f.newline();
1370                }
1371            }
1372            f.emit_leading_comments(&b.tail_leading_comments);
1373            // v0.7: a block whose last statement is `assert` carries an implicit
1374            // `()` tail that the parser synthesises. Don't print it — Bynk has
1375            // no statement terminators, so a printed `()` on the next line would
1376            // re-attach to the assert's expression on re-parse (`x == y` `()` →
1377            // `x == y()`), breaking idempotency. The parser re-derives the
1378            // implicit unit tail, so omitting it is loss-free.
1379            let implicit_unit_after_assert = matches!(b.tail.kind, ExprKind::UnitLit)
1380                && matches!(b.statements.last(), Some(Statement::Expect(_)))
1381                && b.tail_leading_comments.is_empty();
1382            if !implicit_unit_after_assert {
1383                f.format_expr(&b.tail);
1384                f.newline();
1385            }
1386        });
1387        self.push("}");
1388    }
1389
1390    fn format_statement(&mut self, s: &Statement) {
1391        match s {
1392            Statement::Let(l) => {
1393                self.push("let ");
1394                self.push(&l.name.name);
1395                if let Some(t) = &l.type_annot {
1396                    self.push(": ");
1397                    self.format_type_ref(t);
1398                }
1399                self.push(" = ");
1400                self.format_expr(&l.value);
1401            }
1402            Statement::EffectLet(l) => {
1403                self.push("let ");
1404                self.push(&l.name.name);
1405                if let Some(t) = &l.type_annot {
1406                    self.push(": ");
1407                    self.format_type_ref(t);
1408                }
1409                self.push(" <- ");
1410                self.format_expr(&l.value);
1411            }
1412            Statement::Expect(a) => {
1413                self.push("expect ");
1414                self.format_expr(&a.value);
1415            }
1416            Statement::Send(s) => {
1417                self.push("~> ");
1418                self.format_expr(&s.value);
1419            }
1420            Statement::Assign(a) => {
1421                self.push(&a.target.name);
1422                self.push(" := ");
1423                self.format_expr(&a.value);
1424            }
1425        }
1426    }
1427
1428    fn format_expr(&mut self, e: &Expr) {
1429        // `match` renders multi-line, so it must go through the indent-aware
1430        // emitter rather than `expr_to_string` — the latter builds a flat
1431        // string with hardcoded single-tab arms that ignores the current
1432        // nesting depth (the closing brace and every arm would land at column
1433        // one regardless of how deeply the `match` is nested). Everything else
1434        // is single-line and renders fine as a string.
1435        match &e.kind {
1436            ExprKind::Match { discriminant, arms } => self.format_match(discriminant, arms),
1437            _ => self.push(&expr_to_string(e)),
1438        }
1439    }
1440
1441    /// Emit a `match` expression at the current indent level. Arms sit one
1442    /// level deeper than the `match`/`}`; block-bodied arms recurse through
1443    /// `format_block` so their statements indent correctly in turn.
1444    fn format_match(&mut self, discriminant: &Expr, arms: &[MatchArm]) {
1445        self.push("match ");
1446        self.format_expr(discriminant);
1447        self.push(" {");
1448        self.newline();
1449        self.indented(|f| {
1450            for arm in arms {
1451                f.push(&pattern_to_string(&arm.pattern));
1452                f.push(" => ");
1453                match &arm.body {
1454                    MatchBody::Expr(e) => f.format_expr(e),
1455                    MatchBody::Block(b) => f.format_block(b),
1456                }
1457                f.push(",");
1458                f.newline();
1459            }
1460        });
1461        self.push("}");
1462    }
1463}
1464
1465/// Borrow the trivia attached to a statement variant.
1466/// Render a `given`-clause capability reference back to source: a bare name
1467/// for a local capability, or `prefix.Name` for a cross-context one (v0.15).
1468fn cap_ref_src(c: &CapRef) -> String {
1469    match &c.context {
1470        Some(prefix) => format!("{}.{}", prefix.joined(), c.name.name),
1471        None => c.name.name.clone(),
1472    }
1473}
1474
1475/// Render a storage kind: `Cell[Int]`, `Map[K, V]`, or a bare head (v0.81).
1476fn store_kind_to_string(k: &StoreKind) -> String {
1477    if k.args.is_empty() {
1478        k.head.name.clone()
1479    } else {
1480        format!(
1481            "{}[{}]",
1482            k.head.name,
1483            k.args
1484                .iter()
1485                .map(type_ref_to_string)
1486                .collect::<Vec<_>>()
1487                .join(", ")
1488        )
1489    }
1490}
1491
1492/// Render a storage annotation (v0.85; ADR 0111): `@name`, or `@name(arg, …)`
1493/// where each argument is an optional `label: ` then the value expression.
1494fn annotation_to_string(ann: &Annotation) -> String {
1495    if ann.args.is_empty() {
1496        return format!("@{}", ann.name.name);
1497    }
1498    let args = ann
1499        .args
1500        .iter()
1501        .map(|a| match &a.label {
1502            Some(l) => format!("{}: {}", l.name, expr_with_prec(&a.value, 0)),
1503            None => expr_with_prec(&a.value, 0),
1504        })
1505        .collect::<Vec<_>>()
1506        .join(", ");
1507    format!("@{}({})", ann.name.name, args)
1508}
1509
1510fn statement_trivia(s: &Statement) -> &Trivia {
1511    match s {
1512        Statement::Let(l) | Statement::EffectLet(l) => &l.trivia,
1513        Statement::Expect(a) => &a.trivia,
1514        Statement::Send(s) => &s.trivia,
1515        Statement::Assign(a) => &a.trivia,
1516    }
1517}
1518
1519// -- String-rendering helpers (used by inline single-line emission) --
1520
1521fn type_ref_to_string(t: &TypeRef) -> String {
1522    match t {
1523        TypeRef::Base(b, _) => b.name().to_string(),
1524        TypeRef::Named(id) => id.name.clone(),
1525        TypeRef::Result(a, b, _) => format!(
1526            "Result[{}, {}]",
1527            type_ref_to_string(a),
1528            type_ref_to_string(b)
1529        ),
1530        TypeRef::Option(t, _) => format!("Option[{}]", type_ref_to_string(t)),
1531        TypeRef::Effect(t, _) => format!("Effect[{}]", type_ref_to_string(t)),
1532        TypeRef::HttpResult(t, _) => format!("HttpResult[{}]", type_ref_to_string(t)),
1533        TypeRef::QueueResult(_) => "QueueResult".to_string(),
1534        TypeRef::List(t, _) => format!("List[{}]", type_ref_to_string(t)),
1535        TypeRef::Query(t, _) => format!("Query[{}]", type_ref_to_string(t)),
1536        TypeRef::Stream(t, _) => format!("Stream[{}]", type_ref_to_string(t)),
1537        TypeRef::Connection(t, _) => format!("Connection[{}]", type_ref_to_string(t)),
1538        TypeRef::Map(k, v, _) => {
1539            format!("Map[{}, {}]", type_ref_to_string(k), type_ref_to_string(v))
1540        }
1541        TypeRef::ValidationError(_) => "ValidationError".to_string(),
1542        TypeRef::JsonError(_) => "JsonError".to_string(),
1543        TypeRef::Unit(_) => "()".to_string(),
1544        TypeRef::Fn(params, ret, _) => {
1545            let lhs = match params.len() {
1546                0 => "()".to_string(),
1547                1 if !matches!(params[0], TypeRef::Fn(..)) => type_ref_to_string(&params[0]),
1548                _ => format!(
1549                    "({})",
1550                    params
1551                        .iter()
1552                        .map(type_ref_to_string)
1553                        .collect::<Vec<_>>()
1554                        .join(", ")
1555                ),
1556            };
1557            format!("{lhs} -> {}", type_ref_to_string(ret))
1558        }
1559    }
1560}
1561
1562fn refinement_to_string(r: &Refinement) -> String {
1563    let mut s = String::new();
1564    for (i, p) in r.predicates.iter().enumerate() {
1565        if i > 0 {
1566            s.push_str(" and ");
1567        }
1568        s.push_str(&pred_to_string(p));
1569    }
1570    s
1571}
1572
1573fn pred_to_string(p: &RefinementPred) -> String {
1574    match &p.kind {
1575        PredKind::Matches(re) => format!("Matches(\"{}\")", escape_string(re)),
1576        PredKind::InRange(a, b) => format!("InRange({}, {})", a.value, b.value),
1577        PredKind::InRangeF(a, b) => format!("InRange({}, {})", a.lexeme, b.lexeme),
1578        PredKind::MinLength(n) => format!("MinLength({n})"),
1579        PredKind::MaxLength(n) => format!("MaxLength({n})"),
1580        PredKind::Length(n) => format!("Length({n})"),
1581        PredKind::NonNegative => "NonNegative".to_string(),
1582        PredKind::Positive => "Positive".to_string(),
1583        PredKind::NonEmpty => "NonEmpty".to_string(),
1584    }
1585}
1586
1587fn escape_string(s: &str) -> String {
1588    let mut out = String::with_capacity(s.len());
1589    for ch in s.chars() {
1590        match ch {
1591            '\\' => out.push_str("\\\\"),
1592            '"' => out.push_str("\\\""),
1593            '\n' => out.push_str("\\n"),
1594            '\t' => out.push_str("\\t"),
1595            c => out.push(c),
1596        }
1597    }
1598    out
1599}
1600
1601fn expr_to_string(e: &Expr) -> String {
1602    expr_with_prec(e, 0)
1603}
1604
1605// Operator precedences (smaller = binds looser):
1606//   1: || 2: && 3: == != 4: < <= > >= 5: + - 6: * / 7: unary ! - 8: postfix . () ?
1607fn binop_prec(op: BinOp) -> u8 {
1608    match op {
1609        // v0.80: `implies` is the lowest-precedence binary operator (below `||`).
1610        BinOp::Implies => 0,
1611        BinOp::Or => 1,
1612        BinOp::And => 2,
1613        BinOp::Eq | BinOp::NotEq => 3,
1614        BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq => 4,
1615        BinOp::Add | BinOp::Sub => 5,
1616        BinOp::Mul | BinOp::Div => 6,
1617    }
1618}
1619
1620fn expr_with_prec(e: &Expr, parent_prec: u8) -> String {
1621    match &e.kind {
1622        ExprKind::IntLit(n) => n.to_string(),
1623        // v0.21: the stored lexeme verbatim — formatting must not normalise.
1624        ExprKind::FloatLit { lexeme, .. } => lexeme.clone(),
1625        // v0.86 (ADR 0112): a duration literal `<value>.<unit>`.
1626        ExprKind::DurationLit { value, unit, .. } => format!("{value}.{}", unit.name()),
1627        ExprKind::StrLit(s) => format!("\"{}\"", escape_string(s)),
1628        // v0.43: re-emit the interpolated string — chunks re-escaped, each
1629        // hole as `\(expr)`. Re-escaping a chunk's literal `\` to `\\` keeps a
1630        // source `\\(` (an escaped `\(`) round-tripping as text, not a hole.
1631        ExprKind::InterpStr(parts) => {
1632            let mut out = String::from("\"");
1633            for part in parts {
1634                match part {
1635                    InterpPart::Chunk(text) => out.push_str(&escape_string(text)),
1636                    InterpPart::Hole(hole) => {
1637                        out.push_str(&format!("\\({})", expr_with_prec(hole, 0)));
1638                    }
1639                }
1640            }
1641            out.push('"');
1642            out
1643        }
1644        ExprKind::BoolLit(b) => b.to_string(),
1645        ExprKind::UnitLit => "()".to_string(),
1646        ExprKind::Ident(id) => id.name.clone(),
1647        ExprKind::ListLit(elems) => format!(
1648            "[{}]",
1649            elems
1650                .iter()
1651                .map(expr_to_string)
1652                .collect::<Vec<_>>()
1653                .join(", ")
1654        ),
1655        ExprKind::Call {
1656            name,
1657            type_args,
1658            args,
1659        } => {
1660            let targs = if type_args.is_empty() {
1661                String::new()
1662            } else {
1663                format!(
1664                    "[{}]",
1665                    type_args
1666                        .iter()
1667                        .map(type_ref_to_string)
1668                        .collect::<Vec<_>>()
1669                        .join(", ")
1670                )
1671            };
1672            let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
1673            format!("{}{}({})", name.name, targs, parts.join(", "))
1674        }
1675        ExprKind::BinOp(op, l, r) => {
1676            let prec = binop_prec(*op);
1677            let inner = format!(
1678                "{} {} {}",
1679                expr_with_prec(l, prec),
1680                op.name(),
1681                expr_with_prec(r, prec + 1)
1682            );
1683            if prec < parent_prec {
1684                format!("({inner})")
1685            } else {
1686                inner
1687            }
1688        }
1689        ExprKind::UnaryOp(op, inner) => {
1690            // Unary binds tightly (prec 7).
1691            let s = format!("{}{}", op.name(), expr_with_prec(inner, 7));
1692            if parent_prec > 7 { format!("({s})") } else { s }
1693        }
1694        ExprKind::Paren(inner) => format!("({})", expr_with_prec(inner, 0)),
1695        // v0.20a: a lambda prints as `(params) => body`.
1696        ExprKind::Lambda(lambda) => {
1697            let params: Vec<String> = lambda
1698                .params
1699                .iter()
1700                .map(|p| match &p.type_ref {
1701                    Some(tr) => format!("{}: {}", p.name.name, type_ref_to_string(tr)),
1702                    None => p.name.name.clone(),
1703                })
1704                .collect();
1705            let body = match &lambda.body.kind {
1706                ExprKind::Block(b) => format_block_oneline(b),
1707                _ => expr_with_prec(&lambda.body, 0),
1708            };
1709            format!("({}) => {}", params.join(", "), body)
1710        }
1711        ExprKind::Block(b) => format_block_oneline(b),
1712        ExprKind::If {
1713            cond,
1714            then_block,
1715            else_block,
1716        } => {
1717            format!(
1718                "if {} {} else {}",
1719                expr_with_prec(cond, 0),
1720                format_block_oneline(then_block),
1721                format_block_oneline(else_block),
1722            )
1723        }
1724        ExprKind::Ok(v) => format!("Ok({})", expr_with_prec(v, 0)),
1725        ExprKind::Err(v) => format!("Err({})", expr_with_prec(v, 0)),
1726        ExprKind::Some(v) => format!("Some({})", expr_with_prec(v, 0)),
1727        ExprKind::None => "None".to_string(),
1728        ExprKind::Question(v) => format!("{}?", expr_with_prec(v, 8)),
1729        ExprKind::ConstructorCall {
1730            type_name,
1731            method,
1732            args,
1733        } => {
1734            let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
1735            format!("{}.{}({})", type_name.name, method.name, parts.join(", "))
1736        }
1737        ExprKind::RecordConstruction { type_name, fields } => {
1738            let parts: Vec<String> = fields
1739                .iter()
1740                .map(|f| match &f.value {
1741                    Some(v) => format!("{}: {}", f.name.name, expr_with_prec(v, 0)),
1742                    None => f.name.name.clone(),
1743                })
1744                .collect();
1745            if parts.is_empty() {
1746                format!("{} {{}}", type_name.name)
1747            } else {
1748                format!("{} {{ {} }}", type_name.name, parts.join(", "))
1749            }
1750        }
1751        ExprKind::FieldAccess { receiver, field } => {
1752            format!("{}.{}", expr_with_prec(receiver, 8), field.name)
1753        }
1754        ExprKind::MethodCall {
1755            receiver,
1756            method,
1757            type_args,
1758            args,
1759        } => {
1760            let targs = if type_args.is_empty() {
1761                String::new()
1762            } else {
1763                format!(
1764                    "[{}]",
1765                    type_args
1766                        .iter()
1767                        .map(type_ref_to_string)
1768                        .collect::<Vec<_>>()
1769                        .join(", ")
1770                )
1771            };
1772            let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
1773            format!(
1774                "{}.{}{targs}({})",
1775                expr_with_prec(receiver, 8),
1776                method.name,
1777                parts.join(", ")
1778            )
1779        }
1780        ExprKind::Match { discriminant, arms } => {
1781            let mut out = String::new();
1782            out.push_str("match ");
1783            out.push_str(&expr_with_prec(discriminant, 0));
1784            out.push_str(" {\n");
1785            for arm in arms {
1786                out.push('\t');
1787                out.push_str(&pattern_to_string(&arm.pattern));
1788                out.push_str(" => ");
1789                match &arm.body {
1790                    MatchBody::Expr(e) => out.push_str(&expr_with_prec(e, 0)),
1791                    MatchBody::Block(b) => out.push_str(&format_block_oneline(b)),
1792                }
1793                out.push_str(",\n");
1794            }
1795            out.push('}');
1796            out
1797        }
1798        ExprKind::Is { value, pattern } => {
1799            format!(
1800                "{} is {}",
1801                expr_with_prec(value, 4),
1802                pattern_to_string(pattern)
1803            )
1804        }
1805        ExprKind::RecordSpread {
1806            type_name,
1807            base,
1808            overrides,
1809        } => {
1810            let mut parts = vec![format!("...{}", expr_with_prec(base, 0))];
1811            for f in overrides {
1812                if let Some(v) = &f.value {
1813                    parts.push(format!("{}: {}", f.name.name, expr_with_prec(v, 0)));
1814                } else {
1815                    parts.push(f.name.name.clone());
1816                }
1817            }
1818            let body = parts.join(", ");
1819            match type_name {
1820                Some(tn) => format!("{} {{ {} }}", tn.name, body),
1821                None => format!("{{ {} }}", body),
1822            }
1823        }
1824        ExprKind::EffectPure(v) => format!("Effect.pure({})", expr_with_prec(v, 0)),
1825        ExprKind::Expect(v) => format!("expect {}", expr_with_prec(v, 0)),
1826        ExprKind::Val { type_ref, args } => {
1827            let t = type_ref_to_string(type_ref);
1828            if args.is_empty() {
1829                format!("Val[{t}]")
1830            } else {
1831                let a = args
1832                    .iter()
1833                    .map(|x| expr_with_prec(x, 0))
1834                    .collect::<Vec<_>>()
1835                    .join(", ");
1836                format!("Val[{t}]({a})")
1837            }
1838        }
1839    }
1840}
1841
1842fn pattern_to_string(p: &Pattern) -> String {
1843    match p {
1844        Pattern::Wildcard(_) => "_".to_string(),
1845        Pattern::Variant {
1846            type_name,
1847            variant,
1848            bindings,
1849            ..
1850        } => {
1851            let name_part = match type_name {
1852                Some(t) => format!("{}.{}", t.name, variant.name),
1853                None => variant.name.clone(),
1854            };
1855            if bindings.is_empty() {
1856                name_part
1857            } else {
1858                let parts: Vec<String> = bindings
1859                    .iter()
1860                    .map(|b| match &b.kind {
1861                        PatternBindingKind::Positional { name } => name.name.clone(),
1862                        PatternBindingKind::Named { field, name } => {
1863                            format!("{}: {}", field.name, name.name)
1864                        }
1865                    })
1866                    .collect();
1867                format!("{}({})", name_part, parts.join(", "))
1868            }
1869        }
1870    }
1871}
1872
1873fn format_block_oneline(b: &Block) -> String {
1874    if b.statements.is_empty() {
1875        format!("{{ {} }}", expr_with_prec(&b.tail, 0))
1876    } else {
1877        // Multi-line block — render with newlines and tab indentation.
1878        let mut out = String::from("{\n");
1879        for stmt in &b.statements {
1880            out.push('\t');
1881            out.push_str(&stmt_to_string(stmt));
1882            out.push('\n');
1883        }
1884        // Omit the implicit `()` tail after a trailing `assert` (see
1885        // `format_block`) — printing it breaks round-trip idempotency.
1886        let implicit_unit_after_assert = matches!(b.tail.kind, ExprKind::UnitLit)
1887            && matches!(b.statements.last(), Some(Statement::Expect(_)));
1888        if !implicit_unit_after_assert {
1889            out.push('\t');
1890            out.push_str(&expr_with_prec(&b.tail, 0));
1891            out.push('\n');
1892        }
1893        out.push('}');
1894        out
1895    }
1896}
1897
1898fn stmt_to_string(s: &Statement) -> String {
1899    match s {
1900        Statement::Let(l) => {
1901            let mut out = format!("let {}", l.name.name);
1902            if let Some(t) = &l.type_annot {
1903                out.push_str(&format!(": {}", type_ref_to_string(t)));
1904            }
1905            out.push_str(&format!(" = {}", expr_with_prec(&l.value, 0)));
1906            out
1907        }
1908        Statement::EffectLet(l) => {
1909            let mut out = format!("let {}", l.name.name);
1910            if let Some(t) = &l.type_annot {
1911                out.push_str(&format!(": {}", type_ref_to_string(t)));
1912            }
1913            out.push_str(&format!(" <- {}", expr_with_prec(&l.value, 0)));
1914            out
1915        }
1916        Statement::Expect(a) => format!("expect {}", expr_with_prec(&a.value, 0)),
1917        Statement::Send(s) => format!("~> {}", expr_with_prec(&s.value, 0)),
1918        Statement::Assign(a) => format!("{} := {}", a.target.name, expr_with_prec(&a.value, 0)),
1919    }
1920}
1921
1922#[cfg(test)]
1923mod tests {
1924    use super::*;
1925
1926    fn fmt(src: &str) -> String {
1927        format_source(src, &FormatOptions::default()).expect("format failed")
1928    }
1929
1930    #[test]
1931    fn formats_minimal_commons() {
1932        let src = "commons fitness.units {}";
1933        let out = fmt(src);
1934        assert!(out.starts_with("commons fitness.units"));
1935        // Idempotency.
1936        let out2 = fmt(&out);
1937        assert_eq!(out, out2);
1938    }
1939
1940    #[test]
1941    fn formats_refined_type() {
1942        let src = "commons x { type Metres = Int where NonNegative }";
1943        let out = fmt(src);
1944        assert!(out.contains("type Metres = Int where NonNegative"));
1945        let out2 = fmt(&out);
1946        assert_eq!(out, out2);
1947    }
1948
1949    #[test]
1950    fn formats_function_decl() {
1951        let src = "commons x { fn add(a: Int, b: Int) -> Int { a + b } }";
1952        let out = fmt(src);
1953        assert!(out.contains("fn add(a: Int, b: Int) -> Int"));
1954        let out2 = fmt(&out);
1955        assert_eq!(out, out2);
1956    }
1957
1958    #[test]
1959    fn formats_record() {
1960        let src = "commons x { type Pt = { x: Int, y: Int } }";
1961        let out = fmt(src);
1962        let out2 = fmt(&out);
1963        assert_eq!(out, out2, "formatter not idempotent: {out}");
1964    }
1965
1966    #[test]
1967    fn formats_doc_block() {
1968        let src = "commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}";
1969        let out = fmt(src);
1970        assert!(out.contains("A descriptive doc."));
1971        let out2 = fmt(&out);
1972        assert_eq!(out, out2);
1973    }
1974
1975    // -- v1.1 comment preservation --
1976
1977    #[test]
1978    fn preserves_leading_line_comment_on_decl() {
1979        let src = "commons x {\n-- explain T\ntype T = Int where NonNegative\n}";
1980        let out = fmt(src);
1981        assert!(out.contains("-- explain T"), "comment dropped: {out}");
1982        // Idempotent.
1983        assert_eq!(out, fmt(&out));
1984    }
1985
1986    #[test]
1987    fn preserves_trailing_line_comment_on_decl() {
1988        let src = "commons x {\ntype T = Int where NonNegative  -- short\n}";
1989        let out = fmt(src);
1990        assert!(out.contains("-- short"));
1991        // The trailing comment must remain on the same line as the decl.
1992        assert!(
1993            out.lines()
1994                .any(|l| l.contains("type T") && l.contains("-- short")),
1995            "trailing comment not on same line: {out}"
1996        );
1997        assert_eq!(out, fmt(&out));
1998    }
1999
2000    #[test]
2001    fn preserves_grouped_leading_comments() {
2002        let src = "commons x {\n-- one\n-- two\ntype T = Int where Positive\n}";
2003        let out = fmt(src);
2004        assert!(out.contains("-- one"));
2005        assert!(out.contains("-- two"));
2006        // Adjacent — no blank line between the comments.
2007        let i1 = out.find("-- one").unwrap();
2008        let i2 = out.find("-- two").unwrap();
2009        let between = &out[i1..i2];
2010        assert_eq!(
2011            between.matches('\n').count(),
2012            1,
2013            "blank line inserted: {out}"
2014        );
2015        assert_eq!(out, fmt(&out));
2016    }
2017
2018    #[test]
2019    fn preserves_comment_before_block_tail() {
2020        let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
2021        let out = fmt(src);
2022        assert!(out.contains("-- result"), "tail comment dropped: {out}");
2023        assert_eq!(out, fmt(&out));
2024    }
2025
2026    #[test]
2027    fn preserves_comment_with_doc_block_above_decl() {
2028        let src = "commons x {\n-- TODO: rename\n---\nThe canonical T.\n---\ntype T = Int where Positive\n}";
2029        let out = fmt(src);
2030        assert!(out.contains("-- TODO: rename"));
2031        assert!(out.contains("The canonical T."));
2032        // Spec layout: comment, then doc block, then declaration.
2033        let ic = out.find("-- TODO: rename").unwrap();
2034        let id = out.find("The canonical T.").unwrap();
2035        let it = out.find("type T").unwrap();
2036        assert!(ic < id && id < it, "ordering wrong: {out}");
2037        assert_eq!(out, fmt(&out));
2038    }
2039
2040    #[test]
2041    fn preserves_trailing_file_comment() {
2042        let src = "commons x.y\n\ntype T = Int where Positive\n-- TODO\n";
2043        let out = fmt(src);
2044        assert!(out.contains("-- TODO"));
2045        assert_eq!(out, fmt(&out));
2046    }
2047
2048    #[test]
2049    fn unchanged_files_without_comments_format_identically() {
2050        let src = "commons x { type T = Int where NonNegative }";
2051        let out = fmt(src);
2052        // Sanity: the formatter still produces the canonical output for
2053        // existing fixtures (no spurious comment rendering).
2054        assert!(!out.contains("--"), "unexpected comment in output: {out}");
2055    }
2056
2057    // -- v0.81 storage track: `store` fields and the `:=` write --
2058
2059    #[test]
2060    fn formats_store_field_and_cell_write() {
2061        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}";
2062        let out = fmt(src);
2063        assert!(
2064            out.contains("store count: Cell[Int] = 0"),
2065            "store field not formatted: {out}"
2066        );
2067        assert!(
2068            out.contains("count := count + 1"),
2069            "cell write not formatted: {out}"
2070        );
2071        assert_eq!(out, fmt(&out), "formatter not idempotent: {out}");
2072    }
2073
2074    #[test]
2075    fn formats_store_only_agent_without_state_block() {
2076        let src = "context shop {\nagent Counter {\nkey id: String\nstore count: Cell[Int] = 0\non call get() -> Effect[Int] {\ncount\n}\n}\n}";
2077        let out = fmt(src);
2078        // A `store`-only agent emits no empty `state { }` block.
2079        assert!(!out.contains("state {"), "spurious state block: {out}");
2080        assert!(out.contains("store count: Cell[Int] = 0"), "{out}");
2081        assert_eq!(out, fmt(&out), "not idempotent: {out}");
2082    }
2083}