1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum IndentStyle {
63 #[default]
64 Tab,
65 Spaces(u8),
66}
67
68#[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#[derive(Debug, Clone)]
92pub struct FormatError {
93 pub errors: Vec<CompileError>,
94}
95
96pub fn format_source(source: &str, opts: &FormatOptions) -> Result<String, FormatError> {
101 let tokens = tokenize(source).map_err(|e| FormatError { errors: vec![e] })?;
102 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 if !fully_drained && let Some(error) = comment_loss(source, &tokens, &output) {
121 return Err(FormatError {
122 errors: vec![error],
123 });
124 }
125 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
141fn 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
158fn 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
169fn 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
187fn roundtrip_divergence(
202 tokens: &[Token],
203 source: &str,
204 output: &str,
205 opts: &FormatOptions,
206) -> Option<CompileError> {
207 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 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
227fn 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
252fn 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 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
313struct Formatter<'a> {
316 opts: &'a FormatOptions,
317 out: String,
318 indent_level: u32,
319 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 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 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 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 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 self.out.push_str(&sub.out[produced_from..]);
452 self.at_line_start = sub.at_line_start;
453 true
454 }
455
456 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 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 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 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 fn emit_trailing_comment(&mut self, body: Option<&str>) {
515 if let Some(body) = body {
516 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 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 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 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 if !omit_unit_tail(b) {
770 f.format_expr(&b.tail);
771 f.newline();
772 }
773 });
774 self.push("}");
775 }
776
777 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 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 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 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 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 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 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 fn current_column(&self) -> usize {
1034 if self.at_line_start {
1035 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 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 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 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 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 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 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 let any_payload = s.variants.iter().any(|v| !v.payload.is_empty());
1260 if !any_payload {
1261 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 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 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 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 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 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 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 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 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 if rendered.is_empty() || self.fits(&oneline, reserve) {
1406 self.push(&oneline);
1407 return;
1408 }
1409 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 self.push("(");
1419 self.newline();
1420 self.indented(|f| {
1421 for (i, r) in rendered.iter().enumerate() {
1422 f.push(r);
1423 if i + 1 < rendered.len() {
1429 f.push(",");
1430 }
1431 f.newline();
1432 }
1433 });
1434 self.push(")");
1435 }
1436
1437 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 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 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 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 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 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 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 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 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 for sf in &a.store_fields {
1692 f.format_store_field(sf);
1693 f.newline();
1694 }
1695 for inv in &a.invariants {
1698 f.newline();
1699 f.format_invariant(inv);
1700 }
1701 for tr in &a.transitions {
1704 f.newline();
1705 f.format_transition(tr);
1706 }
1707 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 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 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 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 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 self.push(&format!(
1788 "actor {} = {} where {}",
1789 a.name.name,
1790 r.base.name,
1791 expr_to_string(&r.predicate)
1792 ));
1793 } else {
1794 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 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 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 for ann in &h.annotations {
1870 self.push(&annotation_to_string(ann));
1871 self.newline();
1872 }
1873 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 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 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 fn format_block(&mut self, b: &Block) {
1936 self.format_block_with_reserve(b, 0);
1937 }
1938
1939 fn format_block_with_reserve(&mut self, b: &Block, reserve: usize) {
1943 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 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 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 fn format_expr_at(&mut self, e: &Expr, parent_prec: u8, reserve: usize) {
2054 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 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 fn format_expr_broken(&mut self, e: &Expr, reserve: usize) {
2084 match &e.kind {
2085 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 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 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 ExprKind::MethodCall { .. } | ExprKind::FieldAccess { .. } => {
2131 self.format_chain(e, reserve);
2132 }
2133 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 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 ExprKind::BinOp(op, lhs, rhs) => {
2170 let prec = binop_prec(*op);
2171 let tail = format!(" {} {}", op.name(), expr_with_prec(rhs, prec + 1));
2172 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 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 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 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 _ => self.push(&expr_with_prec(e, 0)),
2239 }
2240 }
2241
2242 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 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 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 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 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 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 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 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 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 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 if let Some(guard) = &arm.guard {
2458 f.push(" if ");
2459 f.format_expr_at(guard, 0, " => ".len());
2460 }
2461 f.push(" => ");
2462 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
2475enum ChainLink<'e> {
2477 Field(&'e str),
2478 Method {
2479 method: &'e str,
2480 type_args: &'e [TypeRef],
2481 args: &'e [Expr],
2482 },
2483}
2484
2485fn 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
2517fn 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
2532fn is_logical(op: BinOp) -> bool {
2536 matches!(op, BinOp::And | BinOp::Or | BinOp::Implies)
2537}
2538
2539fn 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
2552fn 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
2569fn 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
2580fn 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
2590fn 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
2606fn 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
2636fn schema_dispatch_src(d: &SchemaDispatch) -> String {
2639 match &d.pattern {
2640 SchemaVersionPattern::Literal(n) => format!("via schema({n})"),
2641 }
2642}
2643
2644fn 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
2653fn 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
2670pub 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
2692fn 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
2735fn 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 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(¶ms[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
2847fn binop_prec(op: BinOp) -> u8 {
2850 match op {
2851 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 ExprKind::IntLit { lexeme, .. } => lexeme.clone(),
2867 ExprKind::FloatLit { lexeme, .. } => lexeme.clone(),
2869 ExprKind::DurationLit { value, unit, .. } => format!("{value}.{}", unit.name()),
2871 ExprKind::StrLit(s) => format!("\"{}\"", escape_string(s)),
2872 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 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 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 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 Pattern::Binding(id) => id.name.clone(),
3130 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 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 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 Pattern::Or(alts, _) => alts
3173 .iter()
3174 .map(pattern_to_string)
3175 .collect::<Vec<_>>()
3176 .join(" | "),
3177 }
3178}
3179
3180fn 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 if b.implicit_tail {
3207 "{}".to_string()
3208 } else {
3209 format!("{{ {} }}", expr_with_prec(&b.tail, 0))
3210 }
3211 } else {
3212 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 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 #[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 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 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 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 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 #[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 #[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 #[test]
3428 fn match_arm_block_tail_unit_after_assign_does_not_reattach_as_a_call() {
3429 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 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 #[test]
3465 fn code_only_canonical_ignores_comments() {
3466 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 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 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 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 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 assert!(!out.contains("--"), "unexpected comment in output: {out}");
3554 }
3555
3556 #[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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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}