1use bynk_syntax::ast::*;
54use bynk_syntax::error::CompileError;
55use bynk_syntax::lexer::{
56 Token, TokenKind, comment_is_legacy, doc_fence_len, doc_line_is_legacy, one_line_doc, tokenize,
57 tokenize_legacy,
58};
59use bynk_syntax::parser::parse_units_migrating;
60use bynk_syntax::span::Span;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum IndentStyle {
66 #[default]
67 Tab,
68 Spaces(u8),
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct FormatOptions {
77 pub indent: IndentStyle,
78 pub max_line_width: u32,
79 pub trailing_comma: bool,
80}
81
82impl Default for FormatOptions {
83 fn default() -> Self {
84 Self {
85 indent: IndentStyle::Tab,
86 max_line_width: 100,
87 trailing_comma: true,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
95pub struct FormatError {
96 pub errors: Vec<CompileError>,
97}
98
99pub fn format_source(source: &str, opts: &FormatOptions) -> Result<String, FormatError> {
104 let source = &*normalize_line_endings(source);
109 let tokens = migration_tokens(source).map_err(|e| FormatError { errors: vec![e] })?;
110 let (units, _warnings, _) =
116 parse_units_migrating(&tokens, source).map_err(|errors| FormatError { errors })?;
117 let output = render_units(&units, opts);
118 if let Some(error) = comment_loss(source, &tokens, &output) {
130 return Err(FormatError {
131 errors: vec![error],
132 });
133 }
134 if let Some(error) = doc_block_loss(source, &tokens, &output) {
141 return Err(FormatError {
142 errors: vec![error],
143 });
144 }
145 if let Some(error) = roundtrip_divergence(&tokens, source, &output, opts) {
154 return Err(FormatError {
155 errors: vec![error],
156 });
157 }
158 Ok(output)
159}
160
161fn migration_tokens(source: &str) -> Result<Vec<Token>, CompileError> {
174 let is_legacy = |t: &Token| match t.kind {
175 TokenKind::Comment => comment_is_legacy(source, t.span),
176 TokenKind::DocLine => doc_line_is_legacy(source, t.span),
177 _ => false,
178 };
179 let current = tokenize(source);
180 if current.as_ref().is_ok_and(|t| !t.iter().any(is_legacy)) {
181 return current;
182 }
183 let old = match tokenize_legacy(source) {
184 Ok(old) if old.iter().any(is_legacy) => old,
185 _ => return current,
186 };
187 let Ok(new) = ¤t else {
188 return Ok(old);
189 };
190 let key = |t: &Token| (t.span.start, t.span.end);
191 let shared: std::collections::HashSet<_> =
192 new.iter().filter(|t| is_legacy(t)).map(key).collect();
193 let rest = |tokens: &[Token]| -> Vec<Token> {
194 tokens
195 .iter()
196 .filter(|t| !(is_legacy(t) && shared.contains(&key(t))))
197 .copied()
198 .collect()
199 };
200 let (old_rest, new_rest) = (rest(&old), rest(new));
201 let differ = old_rest
202 .iter()
203 .zip(&new_rest)
204 .find(|(a, b)| a.kind != b.kind || key(a) != key(b))
205 .map(|(a, b)| {
206 if a.span.start <= b.span.start {
207 a.span
208 } else {
209 b.span
210 }
211 })
212 .or_else(|| {
213 (old_rest.len() != new_rest.len()).then(|| {
214 let n = old_rest.len().min(new_rest.len());
215 old_rest
216 .get(n)
217 .or(new_rest.get(n))
218 .expect("one is longer")
219 .span
220 })
221 });
222 match differ {
223 None => Ok(old),
224 Some(span) => Err(mixed_comment_syntax(source, span)),
225 }
226}
227
228fn mixed_comment_syntax(source: &str, span: Span) -> CompileError {
231 let line_start = source[..span.start].rfind('\n').map_or(0, |i| i + 1);
232 let line_end = source[span.start..]
233 .find('\n')
234 .map_or(source.len(), |i| span.start + i);
235 let line = Span::new_in(span.file, line_start, line_end);
236 CompileError::new(
237 "bynk.fmt.mixed_comment_syntax",
238 line,
239 "this file uses the retired `--` comment or `--|` doc spelling, and this line \
240 reads differently under the old and new comment rules — the file was left unchanged",
241 )
242 .with_label(
243 line,
244 "under the old rules a `--` here is a comment, and `--- text` is a comment, not a doc",
245 )
246 .with_note(
247 "change the retired `--` comments and `--|` docs to `//` and `--- ` (VS Code's Fix All \
248 does this), or rewrite this line, then format again",
249 )
250}
251
252pub fn normalize_line_endings(source: &str) -> std::borrow::Cow<'_, str> {
264 if !source.contains("\r\n") {
265 return std::borrow::Cow::Borrowed(source);
266 }
267 let mut out = String::with_capacity(source.len());
268 let mut pending_crs = 0usize;
269 for c in source.chars() {
270 match c {
271 '\r' => pending_crs += 1,
272 '\n' => {
273 pending_crs = 0;
274 out.push('\n');
275 }
276 _ => {
277 out.extend(std::iter::repeat_n('\r', pending_crs));
278 pending_crs = 0;
279 out.push(c);
280 }
281 }
282 }
283 out.extend(std::iter::repeat_n('\r', pending_crs));
284 std::borrow::Cow::Owned(out)
285}
286
287fn render_units(units: &[SourceUnit], opts: &FormatOptions) -> String {
293 let parts: Vec<String> = units
294 .iter()
295 .map(|unit| {
296 let mut f = Formatter::new(opts);
297 f.format_unit(unit);
298 f.finish()
299 })
300 .collect();
301 parts.join("\n")
302}
303
304fn code_only_canonical(source: &str, opts: &FormatOptions) -> Result<String, Vec<CompileError>> {
311 let tokens = tokenize(source).map_err(|e| vec![e])?;
312 code_only_canonical_from_tokens(&tokens, source, opts)
313}
314
315fn code_only_canonical_from_tokens(
320 tokens: &[Token],
321 source: &str,
322 opts: &FormatOptions,
323) -> Result<String, Vec<CompileError>> {
324 let code: Vec<Token> = tokens
325 .iter()
326 .filter(|t| t.kind != TokenKind::Comment)
327 .cloned()
328 .collect();
329 let (units, _, _) = parse_units_migrating(&code, source)?;
330 Ok(render_units(&units, opts))
331}
332
333fn roundtrip_divergence(
348 tokens: &[Token],
349 source: &str,
350 output: &str,
351 opts: &FormatOptions,
352) -> Option<CompileError> {
353 let canon_out = match code_only_canonical(output, opts) {
356 Ok(canon) => canon,
357 Err(_) => {
358 return Some(roundtrip_error(
359 "the formatter produced output that no longer parses",
360 ));
361 }
362 };
363 let canon_in = code_only_canonical_from_tokens(tokens, source, opts).ok()?;
369 (canon_in != canon_out)
370 .then(|| roundtrip_error("the formatter's output does not round-trip to the same code"))
371}
372
373fn roundtrip_error(what: &str) -> CompileError {
384 CompileError {
385 category: "bynk.fmt.roundtrip",
386 span: Span::default(),
387 message: format!("{what} — the file was left unchanged"),
388 labels: Vec::new(),
389 notes: vec![
390 "this is a formatter bug, not a problem with your source; please report it \
391 with the file that triggered it"
392 .to_string(),
393 ],
394 suggestions: Vec::new(),
395 }
396}
397
398fn comment_loss(source: &str, tokens: &[Token], output: &str) -> Option<CompileError> {
405 use bynk_syntax::lexer::comment_body;
406 let in_comments: Vec<Span> = tokens
407 .iter()
408 .filter(|t| t.kind == TokenKind::Comment)
409 .map(|t| t.span)
410 .collect();
411 if in_comments.is_empty() {
412 return None;
413 }
414 let mut out_bodies: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
417 if let Ok(out_tokens) = tokenize(output) {
418 for t in &out_tokens {
419 if t.kind == TokenKind::Comment {
420 *out_bodies
421 .entry(comment_body(output, t.span).trim().to_string())
422 .or_insert(0) += 1;
423 }
424 }
425 }
426 let mut lost = 0usize;
427 let mut first_lost: Option<Span> = None;
428 for span in &in_comments {
429 let body = comment_body(source, *span).trim().to_string();
430 match out_bodies.get_mut(&body) {
431 Some(n) if *n > 0 => *n -= 1,
432 _ => {
433 lost += 1;
434 first_lost.get_or_insert(*span);
435 }
436 }
437 }
438 let span = first_lost?;
439 Some(CompileError {
440 category: "bynk.fmt.comment_loss",
441 span,
442 message: format!(
443 "formatting would lose {lost} comment{} — the file was left unchanged",
444 if lost == 1 { "" } else { "s" }
445 ),
446 labels: vec![(
447 span,
448 "this comment sits where the formatter cannot yet re-attach it".to_string(),
449 )],
450 notes: vec![
451 "comments inside expression subtrees are not yet preserved; move the comment onto \
452 its own line before the enclosing statement to format this file"
453 .to_string(),
454 ],
455 suggestions: Vec::new(),
456 })
457}
458
459fn source_docs(source: &str, tokens: &[Token]) -> Vec<(Span, DocForm, String)> {
464 use bynk_syntax::lexer::{doc_block_content, doc_line_text};
465 let normalise = |content: &str| {
466 content
467 .lines()
468 .map(str::trim)
469 .collect::<Vec<_>>()
470 .join("\n")
471 };
472 tokens
473 .iter()
474 .filter_map(|t| match t.kind {
475 TokenKind::DocLine => Some((
476 t.span,
477 DocForm::Lines,
478 normalise(doc_line_text(source, t.span)),
479 )),
480 TokenKind::DocBlock => Some((
481 t.span,
482 DocForm::Block,
483 normalise(&doc_block_content(source, t.span)),
484 )),
485 _ => None,
486 })
487 .collect()
488}
489
490fn doc_block_loss(source: &str, tokens: &[Token], output: &str) -> Option<CompileError> {
504 let in_docs = source_docs(source, tokens);
505 if in_docs.is_empty() {
506 return None;
507 }
508 let out_tokens = tokenize(output).ok()?;
509 let mut out_docs: std::collections::HashMap<(DocForm, String), usize> =
510 std::collections::HashMap::new();
511 for (_, form, content) in source_docs(output, &out_tokens) {
512 *out_docs.entry((form, content)).or_insert(0) += 1;
513 }
514 let mut lost = 0usize;
515 let mut first_lost: Option<Span> = None;
516 for (span, form, content) in in_docs {
517 match out_docs.get_mut(&(form, content)) {
518 Some(n) if *n > 0 => *n -= 1,
519 _ => {
520 lost += 1;
521 first_lost.get_or_insert(span);
522 }
523 }
524 }
525 let span = first_lost?;
526 Some(CompileError {
527 category: "bynk.fmt.comment_loss",
528 span,
529 message: format!(
530 "formatting would lose {lost} doc{} — the file was left unchanged",
531 if lost == 1 { "" } else { "s" }
532 ),
533 labels: vec![(
534 span,
535 "this doc has no counterpart in the formatted output".to_string(),
536 )],
537 notes: vec![
538 "a doc (a `---` block or `--|` lines) attaches to the declaration or member \
539 directly below it; one separated from it by a blank line, or with nothing after \
540 it, attaches to nothing. Remove the blank line to attach it, or make it a `--` \
541 comment if it documents nothing"
542 .to_string(),
543 "if the block is already directly above a declaration, this is a formatter bug; \
544 please report it with the file that triggered it"
545 .to_string(),
546 ],
547 suggestions: Vec::new(),
548 })
549}
550
551struct Formatter<'a> {
554 opts: &'a FormatOptions,
555 out: String,
556 indent_level: u32,
557 at_line_start: bool,
560}
561
562impl<'a> Formatter<'a> {
563 fn new(opts: &'a FormatOptions) -> Self {
564 Self {
565 opts,
566 out: String::new(),
567 indent_level: 0,
568 at_line_start: true,
569 }
570 }
571
572 fn finish(mut self) -> String {
573 while self.out.ends_with("\n\n") {
575 self.out.pop();
576 }
577 if !self.out.ends_with('\n') {
578 self.out.push('\n');
579 }
580 self.out
581 }
582
583 fn indent_unit(&self) -> String {
584 match self.opts.indent {
585 IndentStyle::Tab => "\t".to_string(),
586 IndentStyle::Spaces(n) => " ".repeat(n as usize),
587 }
588 }
589
590 fn emit_indent(&mut self) {
591 let unit = self.indent_unit();
592 for _ in 0..self.indent_level {
593 self.out.push_str(&unit);
594 }
595 }
596
597 fn push(&mut self, s: &str) {
598 if self.at_line_start && !s.starts_with('\n') {
599 self.emit_indent();
600 self.at_line_start = false;
601 }
602 if s.contains('\n') {
603 self.push_reindented(s);
604 } else {
605 self.out.push_str(s);
606 }
607 }
608
609 fn push_reindented(&mut self, s: &str) {
619 let prefix = self.indent_unit().repeat(self.indent_level as usize);
620 for (i, line) in s.split('\n').enumerate() {
621 if i > 0 {
622 self.out.push('\n');
623 if !line.is_empty() {
624 self.out.push_str(&prefix);
625 }
626 }
627 self.out.push_str(line);
628 }
629 }
630
631 fn newline(&mut self) {
632 self.out.push('\n');
633 self.at_line_start = true;
634 }
635
636 fn blank_line(&mut self) {
639 if !self.out.ends_with('\n') {
640 self.out.push('\n');
641 }
642 if !self.out.ends_with("\n\n") {
643 self.out.push('\n');
644 }
645 self.at_line_start = true;
646 }
647
648 fn indented<F: FnOnce(&mut Self)>(&mut self, f: F) {
649 self.indent_level += 1;
650 f(self);
651 self.indent_level -= 1;
652 }
653
654 fn try_layout<F: FnOnce(&mut Self)>(&mut self, reserve: usize, body: F) -> bool {
664 self.try_layout_if(reserve, body, || true)
665 }
666
667 fn try_layout_if<F: FnOnce(&mut Self), A: FnOnce() -> bool>(
671 &mut self,
672 reserve: usize,
673 body: F,
674 accept: A,
675 ) -> bool {
676 let line_start = self.out.rfind('\n').map_or(0, |i| i + 1);
677 let mut sub = Formatter {
678 opts: self.opts,
679 out: self.out[line_start..].to_string(),
680 indent_level: self.indent_level,
681 at_line_start: self.at_line_start,
682 };
683 let produced_from = sub.out.len();
684 body(&mut sub);
685 if !sub.every_line_within_budget(reserve) || !accept() {
686 return false;
687 }
688 self.out.push_str(&sub.out[produced_from..]);
691 self.at_line_start = sub.at_line_start;
692 true
693 }
694
695 fn every_line_within_budget(&self, reserve: usize) -> bool {
698 let tab = self.indent_width();
699 let mut lines = self.out.split('\n').peekable();
700 let mut last = "";
701 while let Some(line) = lines.next() {
702 if lines.peek().is_none() {
703 last = line;
704 break;
705 }
706 if display_width(line, tab) > self.opts.max_line_width as usize {
707 return false;
708 }
709 }
710 let last_width = if self.at_line_start {
711 self.indent_level as usize * tab
713 } else {
714 display_width(last, tab)
715 };
716 last_width + reserve <= self.opts.max_line_width as usize
717 }
718
719 fn emit_doc(&mut self, doc: &Doc) {
734 if doc.form == DocForm::Lines {
735 self.push(&one_line_doc(doc));
736 self.newline();
737 return;
738 }
739 let fence = "-".repeat(doc_fence_len(doc));
740 self.push(&fence);
741 self.newline();
742 for line in doc.lines() {
743 if line.is_empty() {
744 self.newline();
745 } else {
746 self.push(line);
747 self.newline();
748 }
749 }
750 self.push(&fence);
751 self.newline();
752 }
753
754 fn emit_leading_comments(&mut self, comments: &[Comment]) {
763 self.emit_comments(comments, true);
764 }
765
766 fn emit_trailing_comments(&mut self, comments: &[Comment]) {
771 self.emit_comments(comments, false);
772 }
773
774 fn emit_comments(&mut self, comments: &[Comment], blank_after_last: bool) {
775 for (i, comment) in comments.iter().enumerate() {
776 let blank_after = i + 1 < comments.len() || blank_after_last;
777 match comment {
778 Comment::Line(body) => {
779 self.push("//");
780 self.push(body);
781 self.newline();
782 }
783 Comment::OrphanDoc(doc) => {
784 self.emit_doc(doc);
785 if blank_after {
786 self.newline();
787 }
788 }
789 Comment::Blank if blank_after => self.blank_line(),
790 Comment::Blank => {}
791 }
792 }
793 }
794
795 fn emit_trailing_comment(&mut self, body: Option<&str>) {
798 if let Some(body) = body {
799 while self.out.ends_with('\n') {
802 self.out.pop();
803 }
804 self.out.push_str(" //");
805 self.out.push_str(body);
806 self.newline();
807 }
808 }
809
810 fn emit_trailing_comment_or_newline(&mut self, trailing: Option<&str>) {
815 if trailing.is_some() {
816 self.emit_trailing_comment(trailing);
817 } else {
818 self.newline();
819 }
820 }
821
822 fn format_unit(&mut self, unit: &SourceUnit) {
825 match unit {
826 SourceUnit::Commons(c) => self.format_commons(c),
827 SourceUnit::Context(c) => self.format_context(c),
828 SourceUnit::Suite(t) => self.format_test(t),
829 SourceUnit::Adapter(a) => self.format_adapter(a),
830 }
831 }
832
833 fn format_adapter(&mut self, a: &AdapterDecl) {
834 self.emit_leading_comments(&a.trivia.leading);
835 if let Some(doc) = &a.documentation {
836 self.emit_doc(doc);
837 }
838 let header = format!("adapter {}", a.name.joined());
839 match a.form {
840 CommonsForm::Brace => {
841 self.push(&header);
842 self.push(" {");
843 self.newline();
844 self.indented(|f| {
845 f.format_adapter_body(a);
846 });
847 self.push("}");
848 self.newline();
849 }
850 CommonsForm::Fragment => {
851 self.push(&header);
852 self.newline();
853 self.newline();
854 self.format_adapter_body(a);
855 }
856 }
857 }
858
859 fn format_adapter_body(&mut self, a: &AdapterDecl) {
860 let mut any_header = false;
861 if let Some(b) = &a.binding {
862 self.emit_leading_comments(&b.trivia.leading);
863 self.push(&format!("binding {:?}", b.module));
864 if !b.includes.is_empty() {
865 let entries: Vec<String> =
866 b.includes.iter().map(|i| format!("{:?}", i.path)).collect();
867 self.push(&format!(" includes [{}]", entries.join(", ")));
868 }
869 if !b.requires.is_empty() {
870 let entries: Vec<String> = b
871 .requires
872 .iter()
873 .map(|r| format!("{:?}: {:?}", r.package, r.range))
874 .collect();
875 self.push(&format!(" requires {{ {} }}", entries.join(", ")));
876 }
877 self.emit_trailing_comment_or_newline(b.trivia.trailing.as_deref());
878 any_header = true;
879 }
880 for u in &a.uses {
881 self.emit_leading_comments(&u.trivia.leading);
882 self.push(&format!("uses {}", u.target.joined()));
883 self.emit_trailing_comment_or_newline(u.trivia.trailing.as_deref());
884 any_header = true;
885 }
886 for c in &a.consumes {
887 self.format_consumes(c);
888 any_header = true;
889 }
890 for e in &a.exports {
891 self.emit_leading_comments(&e.trivia.leading);
892 self.format_exports(e);
893 if e.trivia.trailing.is_some() {
894 self.emit_trailing_comment(e.trivia.trailing.as_deref());
895 }
896 any_header = true;
897 }
898 if any_header && !a.items.is_empty() {
899 self.newline();
900 }
901 let mut first = true;
902 for item in &a.items {
903 if !first {
904 self.newline();
905 }
906 self.format_item(item);
907 first = false;
908 }
909 if !a.trailing_comments.is_empty() {
910 if !a.items.is_empty() || any_header {
911 self.newline();
912 }
913 self.emit_trailing_comments(&a.trailing_comments);
914 }
915 }
916
917 fn format_test(&mut self, t: &SuiteDecl) {
918 self.emit_leading_comments(&t.trivia.leading);
919 if let Some(doc) = &t.documentation {
920 self.emit_doc(doc);
921 }
922 let mut header = format!("suite {}", t.target.joined());
923 if let Some(tier) = t.tier {
924 header.push_str(&format!(" as {}", tier.as_str()));
925 }
926 match t.form {
927 CommonsForm::Brace => {
928 self.push(&header);
929 self.push(" {");
930 self.newline();
931 self.indented(|f| {
932 f.format_test_body(
933 &t.uses,
934 &t.envs,
935 &t.stubs,
936 &t.cases,
937 &t.properties,
938 &t.trailing_comments,
939 );
940 });
941 self.push("}");
942 self.newline();
943 }
944 CommonsForm::Fragment => {
945 self.push(&header);
946 self.newline();
947 self.newline();
948 self.format_test_body(
949 &t.uses,
950 &t.envs,
951 &t.stubs,
952 &t.cases,
953 &t.properties,
954 &t.trailing_comments,
955 );
956 }
957 }
958 }
959
960 fn format_test_body(
961 &mut self,
962 uses: &[UsesDecl],
963 envs: &[EnvClause],
964 stubs: &[StubClause],
965 cases: &[Case],
966 properties: &[PropertyDecl],
967 trailing_comments: &[Comment],
968 ) {
969 let mut first = true;
970 for u in uses {
971 if !first {
972 self.newline();
973 }
974 self.emit_leading_comments(&u.trivia.leading);
975 self.push(&format!("uses {}", u.target.joined()));
976 self.emit_trailing_comment_or_newline(u.trivia.trailing.as_deref());
977 first = false;
978 }
979 for e in envs {
980 if !first {
981 self.newline();
982 }
983 self.format_env_clause(e);
984 first = false;
985 }
986 for pv in stubs {
987 if !first {
988 self.newline();
989 }
990 self.format_stub_clause(pv);
991 first = false;
992 }
993 let mut members: Vec<(usize, Result<&Case, &PropertyDecl>)> = cases
996 .iter()
997 .map(|c| (c.name_span.start, Ok(c)))
998 .chain(properties.iter().map(|p| (p.name_span.start, Err(p))))
999 .collect();
1000 members.sort_by_key(|(start, _)| *start);
1001 for (_, member) in members {
1002 if !first {
1003 self.newline();
1004 }
1005 match member {
1006 Ok(c) => self.format_case(c),
1007 Err(p) => self.format_property(p),
1008 }
1009 first = false;
1010 }
1011 self.emit_trailing_comments(trailing_comments);
1012 }
1013
1014 fn format_case(&mut self, c: &Case) {
1015 self.emit_leading_comments(&c.trivia.leading);
1016 if let Some(doc) = &c.documentation {
1017 self.emit_doc(doc);
1018 }
1019 let mut ch = format!("case \"{}\"", escape_string(&c.name));
1020 if let Some(tier) = c.tier {
1021 ch.push_str(&format!(" as {}", tier.as_str()));
1022 }
1023 ch.push(' ');
1024 self.push(&ch);
1025 self.format_case_block(&c.body, &c.envs, &c.stubs);
1026 self.newline();
1027 }
1028
1029 fn format_property(&mut self, p: &PropertyDecl) {
1030 self.emit_leading_comments(&p.trivia.leading);
1031 if let Some(doc) = &p.documentation {
1032 self.emit_doc(doc);
1033 }
1034 self.push(&format!("property \"{}\" {{", escape_string(&p.name)));
1035 self.newline();
1036 self.indented(|f| f.format_for_all(&p.forall));
1037 self.push("}");
1038 self.newline();
1039 }
1040
1041 fn format_stub_clause(&mut self, pv: &StubClause) {
1045 self.emit_leading_comments(&pv.trivia.leading);
1046 if let Some(doc) = &pv.documentation {
1047 self.emit_doc(doc);
1048 }
1049 self.push(&stub_clause_to_string(pv));
1050 self.emit_trailing_comment_or_newline(pv.trivia.trailing.as_deref());
1051 }
1052
1053 fn format_env_clause(&mut self, e: &EnvClause) {
1056 self.emit_leading_comments(&e.trivia.leading);
1057 if let Some(doc) = &e.documentation {
1058 self.emit_doc(doc);
1059 }
1060 self.push("env {");
1061 self.newline();
1062 self.indented(|f| {
1063 for entry in &e.entries {
1064 f.push(&format!(
1065 "{} = {}",
1066 entry.name.name,
1067 expr_to_string(&entry.value)
1068 ));
1069 f.newline();
1070 }
1071 });
1072 self.push("}");
1073 self.emit_trailing_comment_or_newline(e.trivia.trailing.as_deref());
1074 }
1075
1076 fn format_case_block(&mut self, b: &Block, envs: &[EnvClause], stubs: &[StubClause]) {
1080 if stubs.is_empty() && envs.is_empty() {
1081 self.format_block(b);
1082 return;
1083 }
1084 self.push("{");
1085 self.newline();
1086 self.indented(|f| {
1087 for e in envs {
1088 f.format_env_clause(e);
1089 }
1090 for pv in stubs {
1091 f.format_stub_clause(pv);
1092 }
1093 f.format_block_body(b);
1096 });
1097 self.push("}");
1098 }
1099
1100 fn format_for_all(&mut self, fa: &ForAll) {
1103 let bindings = fa
1104 .bindings
1105 .iter()
1106 .map(|b| format!("{}: {}", b.name.name, type_ref_to_string(&b.type_ref)))
1107 .collect::<Vec<_>>()
1108 .join(", ");
1109 let mut header = format!("for all {bindings}");
1110 if let Some(w) = &fa.where_pred {
1111 header.push_str(&format!(" where {}", expr_to_string(w)));
1112 }
1113 self.push(&format!("{header} "));
1114 self.format_block(&fa.body);
1115 self.newline();
1116 }
1117
1118 fn format_commons(&mut self, c: &Commons) {
1119 self.emit_leading_comments(&c.trivia.leading);
1120 if let Some(doc) = &c.documentation {
1121 self.emit_doc(doc);
1122 }
1123 let header = format!("commons {}", c.name.joined());
1124 match c.form {
1125 CommonsForm::Brace => {
1126 self.push(&header);
1127 self.push(" {");
1128 self.newline();
1129 self.indented(|f| {
1130 f.format_commons_body(&c.uses, &c.items, &c.trailing_comments);
1131 });
1132 self.push("}");
1133 self.newline();
1134 }
1135 CommonsForm::Fragment => {
1136 self.push(&header);
1137 self.newline();
1138 self.newline();
1139 self.format_commons_body(&c.uses, &c.items, &c.trailing_comments);
1140 }
1141 }
1142 }
1143
1144 fn format_commons_body(
1145 &mut self,
1146 uses: &[UsesDecl],
1147 items: &[CommonsItem],
1148 trailing_comments: &[Comment],
1149 ) {
1150 let mut any_uses = false;
1151 for u in uses {
1152 self.emit_leading_comments(&u.trivia.leading);
1153 self.push(&format!("uses {}", u.target.joined()));
1154 self.emit_trailing_comment_or_newline(u.trivia.trailing.as_deref());
1155 any_uses = true;
1156 }
1157 if any_uses && !items.is_empty() {
1158 self.newline();
1159 }
1160 let mut first = true;
1161 for item in items {
1162 if !first {
1163 self.newline();
1164 }
1165 self.format_item(item);
1166 first = false;
1167 }
1168 if !trailing_comments.is_empty() {
1169 if !items.is_empty() || any_uses {
1172 self.newline();
1173 }
1174 self.emit_trailing_comments(trailing_comments);
1175 }
1176 }
1177
1178 fn format_context(&mut self, c: &Context) {
1179 self.emit_leading_comments(&c.trivia.leading);
1180 if let Some(doc) = &c.documentation {
1181 self.emit_doc(doc);
1182 }
1183 let header = format!("context {}", c.name.joined());
1184 match c.form {
1185 CommonsForm::Brace => {
1186 self.push(&header);
1187 self.push(" {");
1188 self.newline();
1189 self.indented(|f| {
1190 f.format_context_body(
1191 &c.uses,
1192 &c.consumes,
1193 &c.exports,
1194 &c.items,
1195 &c.removed_agents,
1196 &c.trailing_comments,
1197 );
1198 });
1199 self.push("}");
1200 self.newline();
1201 }
1202 CommonsForm::Fragment => {
1203 self.push(&header);
1204 self.newline();
1205 self.newline();
1206 self.format_context_body(
1207 &c.uses,
1208 &c.consumes,
1209 &c.exports,
1210 &c.items,
1211 &c.removed_agents,
1212 &c.trailing_comments,
1213 );
1214 }
1215 }
1216 }
1217
1218 fn format_consumes(&mut self, c: &ConsumesDecl) {
1222 self.emit_leading_comments(&c.trivia.leading);
1223 match (&c.alias, &c.selected) {
1224 (Some(alias), _) => {
1225 self.push(&format!("consumes {} as {}", c.target.joined(), alias.name))
1226 }
1227 (None, Some(selected)) if selected.is_empty() => {
1228 self.push(&format!("consumes {} {{ }}", c.target.joined()));
1229 }
1230 (None, Some(selected)) => {
1231 let names: Vec<&str> = selected.iter().map(|i| i.name.as_str()).collect();
1232 self.push(&format!(
1233 "consumes {} {{ {} }}",
1234 c.target.joined(),
1235 names.join(", ")
1236 ));
1237 }
1238 (None, None) => self.push(&format!("consumes {}", c.target.joined())),
1239 }
1240 self.emit_trailing_comment_or_newline(c.trivia.trailing.as_deref());
1241 }
1242
1243 fn format_context_body(
1244 &mut self,
1245 uses: &[UsesDecl],
1246 consumes: &[ConsumesDecl],
1247 exports: &[ExportsDecl],
1248 items: &[CommonsItem],
1249 removed_agents: &[RemovedAgentDecl],
1250 trailing_comments: &[Comment],
1251 ) {
1252 let mut any_header = false;
1253 for u in uses {
1254 self.emit_leading_comments(&u.trivia.leading);
1255 self.push(&format!("uses {}", u.target.joined()));
1256 self.emit_trailing_comment_or_newline(u.trivia.trailing.as_deref());
1257 any_header = true;
1258 }
1259 for c in consumes {
1260 self.format_consumes(c);
1261 any_header = true;
1262 }
1263 for e in exports {
1264 self.emit_leading_comments(&e.trivia.leading);
1265 self.format_exports(e);
1266 if e.trivia.trailing.is_some() {
1270 self.emit_trailing_comment(e.trivia.trailing.as_deref());
1271 }
1272 any_header = true;
1273 }
1274 if any_header && (!items.is_empty() || !removed_agents.is_empty()) {
1275 self.newline();
1276 }
1277 let mut first = true;
1278 for item in items {
1279 if !first {
1280 self.newline();
1281 }
1282 self.format_item(item);
1283 first = false;
1284 }
1285 for r in removed_agents {
1288 if !first {
1289 self.newline();
1290 }
1291 self.emit_leading_comments(&r.trivia.leading);
1292 if let Some(doc) = &r.documentation {
1293 self.emit_doc(doc);
1294 }
1295 self.push(&format!("removed agent {}", r.name.name));
1296 self.emit_trailing_comment_or_newline(r.trivia.trailing.as_deref());
1297 first = false;
1298 }
1299 if !trailing_comments.is_empty() {
1300 if !items.is_empty() || !removed_agents.is_empty() || any_header {
1301 self.newline();
1302 }
1303 self.emit_trailing_comments(trailing_comments);
1304 }
1305 }
1306
1307 fn format_exports(&mut self, e: &ExportsDecl) {
1308 let vis = match e.kind {
1309 ExportKind::Type(Visibility::Opaque) => "opaque",
1310 ExportKind::Type(Visibility::Transparent) => "transparent",
1311 ExportKind::Capability => "capability",
1312 };
1313 let has_comments = !e.trailing_comments.is_empty()
1315 || e.names
1316 .iter()
1317 .any(|n| !n.trivia.leading.is_empty() || n.trivia.trailing.is_some());
1318 if e.names.is_empty() && !has_comments {
1319 self.push(&format!("exports {} {{}}", vis));
1320 self.newline();
1321 return;
1322 }
1323 if !has_comments {
1325 let oneline = format!(
1326 "exports {} {{ {} }}",
1327 vis,
1328 e.names
1329 .iter()
1330 .map(|n| n.name.name.as_str())
1331 .collect::<Vec<_>>()
1332 .join(", ")
1333 );
1334 if self.fits(&oneline, 0) {
1335 self.push(&oneline);
1336 self.newline();
1337 return;
1338 }
1339 }
1340 self.push(&format!("exports {} {{", vis));
1342 self.newline();
1343 self.indented(|f| {
1344 for (i, n) in e.names.iter().enumerate() {
1345 f.emit_leading_comments(&n.trivia.leading);
1346 f.push(&n.name.name);
1347 if i + 1 < e.names.len() || f.opts.trailing_comma {
1348 f.push(",");
1349 }
1350 f.emit_trailing_comment_or_newline(n.trivia.trailing.as_deref());
1351 }
1352 f.emit_trailing_comments(&e.trailing_comments);
1353 });
1354 self.push("}");
1355 self.newline();
1356 }
1357
1358 fn indent_width(&self) -> usize {
1362 match self.opts.indent {
1363 IndentStyle::Tab => 4,
1364 IndentStyle::Spaces(n) => n as usize,
1365 }
1366 }
1367
1368 fn current_column(&self) -> usize {
1374 if self.at_line_start {
1375 return self.indent_level as usize * self.indent_width();
1377 }
1378 let line = match self.out.rfind('\n') {
1379 Some(i) => &self.out[i + 1..],
1380 None => self.out.as_str(),
1381 };
1382 display_width(line, self.indent_width())
1383 }
1384
1385 fn fits(&self, candidate: &str, reserve: usize) -> bool {
1391 if candidate.contains('\n') {
1392 return false;
1393 }
1394 let column =
1395 self.current_column() + display_width(candidate, self.indent_width()) + reserve;
1396 column <= self.opts.max_line_width as usize
1397 }
1398
1399 fn format_item(&mut self, item: &CommonsItem) {
1400 match item {
1401 CommonsItem::Type(t) => self.format_type_decl(t),
1402 CommonsItem::Fn(f) => self.format_fn_decl(f),
1403 CommonsItem::Capability(c) => self.format_capability(c),
1404 CommonsItem::Provider(p) => self.format_provider(p),
1405 CommonsItem::Service(s) => self.format_service(s),
1406 CommonsItem::Agent(a) => self.format_agent(a),
1407 CommonsItem::Actor(a) => self.format_actor(a),
1408 CommonsItem::Messages(m) => self.format_messages(m),
1409 CommonsItem::Event(e) => self.format_event_decl(e),
1410 CommonsItem::Asset(a) => self.format_asset_decl(a),
1411 CommonsItem::Embed(e) => self.format_embed_decl(e),
1412 }
1413 }
1414
1415 fn format_asset_decl(&mut self, a: &AssetDecl) {
1417 self.emit_leading_comments(&a.trivia.leading);
1418 if let Some(doc) = &a.documentation {
1419 self.emit_doc(doc);
1420 }
1421 self.push(&format!(
1422 "asset {} = \"{}\"",
1423 a.name.name,
1424 escape_string(&a.path)
1425 ));
1426 if let Some((at, _)) = &a.at {
1427 self.push(&format!(" at \"{}\"", escape_string(at)));
1428 }
1429 self.emit_trailing_comment_or_newline(a.trivia.trailing.as_deref());
1430 }
1431
1432 fn format_embed_decl(&mut self, e: &EmbedDecl) {
1434 self.emit_leading_comments(&e.trivia.leading);
1435 if let Some(doc) = &e.documentation {
1436 self.emit_doc(doc);
1437 }
1438 self.push(&format!(
1439 "embed {} = \"{}\"",
1440 e.name.name,
1441 escape_string(&e.path)
1442 ));
1443 self.emit_trailing_comment_or_newline(e.trivia.trailing.as_deref());
1444 }
1445
1446 fn format_event_decl(&mut self, e: &EventDecl) {
1447 self.emit_leading_comments(&e.trivia.leading);
1448 if let Some(doc) = &e.documentation {
1449 self.emit_doc(doc);
1450 }
1451 self.push(&format!("event {}", e.name.name));
1452 for ann in &e.annotations {
1455 self.push(" ");
1456 self.push(&annotation_to_string(ann));
1457 }
1458 self.push(" = ");
1459 self.format_record_body(&e.body);
1460 self.emit_trailing_comment_or_newline(e.trivia.trailing.as_deref());
1461 }
1462
1463 fn format_messages(&mut self, m: &MessagesDecl) {
1464 self.emit_leading_comments(&m.trivia.leading);
1465 if let Some(doc) = &m.documentation {
1466 self.emit_doc(doc);
1467 }
1468 self.push(&format!("messages \"{}\"", escape_string(&m.tag)));
1469 for ann in &m.annotations {
1470 self.push(" ");
1471 self.push(&annotation_to_string(ann));
1472 }
1473 self.push(" {");
1474 self.newline();
1475 self.indented(|f| {
1476 for entry in &m.entries {
1477 if let Some(doc) = &entry.documentation {
1478 f.emit_doc(doc);
1479 }
1480 f.push(&format!(
1481 "\"{}\" => \"{}\"",
1482 escape_string(&entry.code),
1483 escape_string(&entry.template)
1484 ));
1485 f.newline();
1486 }
1487 });
1488 self.push("}");
1489 self.emit_trailing_comment_or_newline(m.trivia.trailing.as_deref());
1490 }
1491
1492 fn format_type_decl(&mut self, t: &TypeDecl) {
1495 self.emit_leading_comments(&t.trivia.leading);
1496 if let Some(doc) = &t.documentation {
1497 self.emit_doc(doc);
1498 }
1499 let params = if t.type_params.is_empty() {
1501 String::new()
1502 } else {
1503 let names: Vec<&str> = t
1504 .type_params
1505 .iter()
1506 .map(|tp| tp.name.name.as_str())
1507 .collect();
1508 format!("[{}]", names.join(", "))
1509 };
1510 self.push(&format!("type {}{} = ", t.name.name, params));
1511 self.format_type_body(&t.body);
1512 self.emit_trailing_comment_or_newline(t.trivia.trailing.as_deref());
1513 }
1514
1515 fn format_type_body(&mut self, body: &TypeBody) {
1516 match body {
1517 TypeBody::Refined {
1518 base, refinement, ..
1519 } => {
1520 self.push(base.name());
1521 if let Some(r) = refinement {
1522 self.push(" where ");
1523 self.format_refinement(r);
1524 }
1525 }
1526 TypeBody::Opaque {
1527 base, refinement, ..
1528 } => {
1529 self.push("opaque ");
1530 self.push(base.name());
1531 if let Some(r) = refinement {
1532 self.push(" where ");
1533 self.format_refinement(r);
1534 }
1535 }
1536 TypeBody::Record(r) => self.format_record_body(r),
1537 TypeBody::Sum(s) => self.format_sum_body(s),
1538 }
1539 }
1540
1541 fn format_refinement(&mut self, r: &Refinement) {
1542 for (i, p) in r.predicates.iter().enumerate() {
1543 if i > 0 {
1544 self.push(" && ");
1545 }
1546 self.format_pred(p);
1547 }
1548 }
1549
1550 fn format_pred(&mut self, p: &RefinementPred) {
1551 match &p.kind {
1552 PredKind::Matches(re) => self.push(&format!("Matches(\"{}\")", escape_string(re))),
1553 PredKind::InRange(a, b) => self.push(&format!("InRange({}, {})", a.value, b.value)),
1554 PredKind::InRangeF(a, b) => self.push(&format!("InRange({}, {})", a.lexeme, b.lexeme)),
1555 PredKind::MinLength(n) => self.push(&format!("MinLength({n})")),
1556 PredKind::MaxLength(n) => self.push(&format!("MaxLength({n})")),
1557 PredKind::Length(n) => self.push(&format!("Length({n})")),
1558 PredKind::NonNegative => self.push("NonNegative"),
1559 PredKind::Positive => self.push("Positive"),
1560 PredKind::NonEmpty => self.push("NonEmpty"),
1561 }
1562 }
1563
1564 fn format_record_body(&mut self, r: &RecordBody) {
1565 let has_comments = !r.trailing_comments.is_empty()
1567 || r.fields.iter().any(|f| {
1568 !f.trivia.leading.is_empty()
1569 || f.trivia.trailing.is_some()
1570 || f.documentation.is_some()
1571 });
1572 if r.fields.is_empty() && !has_comments {
1573 self.push("{}");
1574 return;
1575 }
1576 self.push("{");
1578 self.newline();
1579 self.indented(|f| {
1580 for (i, field) in r.fields.iter().enumerate() {
1581 f.emit_leading_comments(&field.trivia.leading);
1582 if let Some(doc) = &field.documentation {
1583 f.emit_doc(doc);
1584 }
1585 f.format_record_field(field);
1586 if i + 1 < r.fields.len() || f.opts.trailing_comma {
1587 f.push(",");
1588 }
1589 f.emit_trailing_comment_or_newline(field.trivia.trailing.as_deref());
1590 }
1591 f.emit_trailing_comments(&r.trailing_comments);
1592 });
1593 self.push("}");
1594 }
1595
1596 fn format_record_field(&mut self, field: &RecordField) {
1597 self.push(&format!("{}: ", field.name.name));
1598 self.format_type_ref(&field.type_ref);
1599 if let Some(r) = &field.refinement {
1600 self.push(" where ");
1601 self.format_refinement(r);
1602 }
1603 if let Some(init) = &field.init {
1604 self.push(" = ");
1605 self.format_expr(init);
1606 }
1607 }
1608
1609 fn format_sum_body(&mut self, s: &SumBody) {
1610 let any_payload = s.variants.iter().any(|v| !v.payload.is_empty());
1614 if !any_payload {
1615 let has_comments = !s.trailing_comments.is_empty()
1618 || s.variants.iter().any(|v| {
1619 !v.trivia.leading.is_empty()
1620 || v.trivia.trailing.is_some()
1621 || v.documentation.is_some()
1622 });
1623 if !has_comments {
1624 let names: Vec<&str> = s.variants.iter().map(|v| v.name.name.as_str()).collect();
1625 let oneline = format!("enum {{ {} }}", names.join(", "));
1626 if self.fits(&oneline, 0) {
1627 self.push(&oneline);
1628 return;
1629 }
1630 }
1631 self.push("enum {");
1632 self.newline();
1633 self.indented(|f| {
1634 for (i, v) in s.variants.iter().enumerate() {
1635 f.emit_leading_comments(&v.trivia.leading);
1636 if let Some(doc) = &v.documentation {
1637 f.emit_doc(doc);
1638 }
1639 f.push(&v.name.name);
1640 if i + 1 < s.variants.len() || f.opts.trailing_comma {
1641 f.push(",");
1642 }
1643 f.emit_trailing_comment_or_newline(v.trivia.trailing.as_deref());
1644 }
1645 f.emit_trailing_comments(&s.trailing_comments);
1646 });
1647 self.push("}");
1648 return;
1649 }
1650 while self.out.ends_with(' ') {
1654 self.out.pop();
1655 }
1656 self.indented(|f| {
1657 for (i, v) in s.variants.iter().enumerate() {
1658 if i == 0 || s.variants[i - 1].trivia.trailing.is_none() {
1659 f.newline();
1660 }
1661 f.emit_leading_comments(&v.trivia.leading);
1662 if let Some(doc) = &v.documentation {
1663 f.emit_doc(doc);
1664 }
1665 f.push("| ");
1666 f.push(&v.name.name);
1667 if v.payload.iter().any(|p| p.documentation.is_some()) {
1668 f.push("(");
1671 f.newline();
1672 let last = v.payload.len() - 1;
1673 f.indented(|f| {
1674 for (j, p) in v.payload.iter().enumerate() {
1675 if let Some(doc) = &p.documentation {
1676 f.emit_doc(doc);
1677 }
1678 f.push(&format!(
1679 "{}: {}",
1680 p.name.name,
1681 type_ref_to_string(&p.type_ref)
1682 ));
1683 if j < last || f.opts.trailing_comma {
1684 f.push(",");
1685 }
1686 f.newline();
1687 }
1688 });
1689 f.push(")");
1690 } else if !v.payload.is_empty() {
1691 f.push("(");
1692 let parts: Vec<String> = v
1693 .payload
1694 .iter()
1695 .map(|p| format!("{}: {}", p.name.name, type_ref_to_string(&p.type_ref)))
1696 .collect();
1697 f.push(&parts.join(", "));
1698 f.push(")");
1699 }
1700 f.emit_trailing_comment(v.trivia.trailing.as_deref());
1701 }
1702 if !s.embeds.is_empty() {
1707 if s.variants
1708 .last()
1709 .is_none_or(|v| v.trivia.trailing.is_none())
1710 {
1711 f.newline();
1712 }
1713 f.emit_trailing_comments(&s.trailing_comments);
1714 let parts: Vec<String> = s
1715 .embeds
1716 .iter()
1717 .map(|e| {
1718 format!(
1719 "{} as {}",
1720 type_ref_to_string(&e.source_type),
1721 e.variant.name
1722 )
1723 })
1724 .collect();
1725 f.push(&format!("embeds {}", parts.join(", ")));
1726 }
1727 });
1728 }
1729
1730 fn format_type_ref(&mut self, t: &TypeRef) {
1731 self.push(&type_ref_to_string(t));
1732 }
1733
1734 fn format_fn_decl(&mut self, f: &FnDecl) {
1737 self.emit_leading_comments(&f.trivia.leading);
1738 if let Some(doc) = &f.documentation {
1739 self.emit_doc(doc);
1740 }
1741 self.push("fn ");
1742 self.push(&f.name.display());
1743 if !f.type_params.is_empty() {
1745 let names: Vec<&str> = f
1746 .type_params
1747 .iter()
1748 .map(|tp| tp.name.name.as_str())
1749 .collect();
1750 self.push(&format!("[{}]", names.join(", ")));
1751 }
1752 let tail = format!(" -> {}", type_ref_to_string(&f.return_type));
1756 let reserve = if f.requires.is_empty() && f.ensures.is_empty() {
1757 tail.chars().count() + " {".len()
1758 } else {
1759 tail.chars().count()
1760 };
1761 self.format_params(&f.params, f.has_self, reserve);
1762 self.push(" -> ");
1763 self.format_type_ref(&f.return_type);
1764 if f.requires.is_empty() && f.ensures.is_empty() {
1767 self.push(" ");
1768 } else {
1769 self.newline();
1770 self.indented(|f2| {
1771 for c in &f.requires {
1772 f2.push(&format!(
1773 "requires {}: {}",
1774 c.name.name,
1775 expr_to_string(&c.predicate)
1776 ));
1777 f2.newline();
1778 }
1779 for c in &f.ensures {
1780 f2.push(&format!(
1781 "ensures {}: {}",
1782 c.name.name,
1783 expr_to_string(&c.predicate)
1784 ));
1785 f2.newline();
1786 }
1787 });
1788 }
1789 self.format_block_multiline(&f.body);
1790 self.emit_trailing_comment_or_newline(f.trivia.trailing.as_deref());
1791 }
1792
1793 fn format_params(&mut self, params: &[Param], has_self: bool, reserve: usize) {
1798 let mut rendered: Vec<String> = Vec::new();
1799 if has_self {
1800 rendered.push("self".to_string());
1801 }
1802 for p in params {
1805 rendered.push(format!(
1806 "{}: {}",
1807 p.name.name,
1808 type_ref_to_string(&p.type_ref)
1809 ));
1810 }
1811 let oneline = format!("({})", rendered.join(", "));
1812 if rendered.is_empty() || self.fits(&oneline, reserve) {
1814 self.push(&oneline);
1815 return;
1816 }
1817 let closing_line = self.indent_level as usize * self.indent_width() + 1 + reserve;
1821 if closing_line > self.opts.max_line_width as usize {
1822 self.push(&oneline);
1823 return;
1824 }
1825 self.push("(");
1827 self.newline();
1828 self.indented(|f| {
1829 for (i, r) in rendered.iter().enumerate() {
1830 f.push(r);
1831 if i + 1 < rendered.len() {
1837 f.push(",");
1838 }
1839 f.newline();
1840 }
1841 });
1842 self.push(")");
1843 }
1844
1845 fn format_capability(&mut self, c: &CapabilityDecl) {
1848 self.emit_leading_comments(&c.trivia.leading);
1849 if let Some(doc) = &c.documentation {
1850 self.emit_doc(doc);
1851 }
1852 if let Some(d) = &c.derived {
1855 self.push(&format!("capability {} = {}", c.name.name, d.base.name));
1856 for ann in &d.annotations {
1857 self.push(&format!(" {}", annotation_to_string(ann)));
1858 }
1859 self.emit_trailing_comment_or_newline(c.trivia.trailing.as_deref());
1860 return;
1861 }
1862 self.push(&format!("capability {} {{", c.name.name));
1863 self.newline();
1864 self.indented(|f| {
1865 for op in &c.ops {
1866 f.emit_leading_comments(&op.trivia.leading);
1867 if let Some(doc) = &op.documentation {
1868 f.emit_doc(doc);
1869 }
1870 f.push("fn ");
1871 f.push(&op.name.name);
1872 if !op.type_params.is_empty() {
1874 let names: Vec<&str> = op
1875 .type_params
1876 .iter()
1877 .map(|tp| tp.name.name.as_str())
1878 .collect();
1879 f.push(&format!("[{}]", names.join(", ")));
1880 }
1881 let reserve = 4 + type_ref_to_string(&op.return_type).chars().count();
1882 f.format_params(&op.params, false, reserve);
1883 f.push(" -> ");
1884 f.format_type_ref(&op.return_type);
1885 f.emit_trailing_comment_or_newline(op.trivia.trailing.as_deref());
1886 }
1887 if !c.trailing_comments.is_empty() {
1889 if !c.ops.is_empty() {
1890 f.newline();
1891 }
1892 f.emit_trailing_comments(&c.trailing_comments);
1893 }
1894 });
1895 self.push("}");
1896 self.emit_trailing_comment_or_newline(c.trivia.trailing.as_deref());
1897 }
1898
1899 fn format_provider(&mut self, p: &ProviderDecl) {
1900 self.emit_leading_comments(&p.trivia.leading);
1901 if let Some(doc) = &p.documentation {
1902 self.emit_doc(doc);
1903 }
1904 self.push(&format!(
1905 "provides {} = {}",
1906 p.capability.name, p.provider_name.name
1907 ));
1908 if !p.given.is_empty() {
1909 self.push(" given ");
1910 let names: Vec<String> = p.given.iter().map(cap_ref_src).collect();
1911 self.push(&names.join(", "));
1912 }
1913 if p.external {
1915 self.emit_trailing_comment_or_newline(p.trivia.trailing.as_deref());
1916 return;
1917 }
1918 self.push(" {");
1919 self.newline();
1920 self.indented(|f| {
1921 for (i, op) in p.ops.iter().enumerate() {
1922 if i > 0 {
1923 f.newline();
1924 }
1925 f.emit_leading_comments(&op.trivia.leading);
1926 f.push("fn ");
1927 f.push(&op.name.name);
1928 let reserve = 4 + type_ref_to_string(&op.return_type).chars().count() + 2;
1929 f.format_params(&op.params, false, reserve);
1930 f.push(" -> ");
1931 f.format_type_ref(&op.return_type);
1932 f.push(" ");
1933 f.format_block_multiline(&op.body);
1934 f.emit_trailing_comment_or_newline(op.trivia.trailing.as_deref());
1935 }
1936 });
1937 self.push("}");
1938 self.emit_trailing_comment_or_newline(p.trivia.trailing.as_deref());
1939 }
1940
1941 fn format_service(&mut self, s: &ServiceDecl) {
1942 self.emit_leading_comments(&s.trivia.leading);
1943 if let Some(doc) = &s.documentation {
1944 self.emit_doc(doc);
1945 }
1946 let from = match &s.protocol {
1947 ServiceProtocol::Call => String::new(),
1948 ServiceProtocol::Http => " from http".to_string(),
1949 ServiceProtocol::Cron => " from cron".to_string(),
1950 ServiceProtocol::Queue { name } => {
1951 format!(" from queue(\"{}\")", escape_string(name))
1952 }
1953 ServiceProtocol::WebSocket { in_type, out_type } => {
1954 format!(
1955 " from websocket(in: {}, out: {})",
1956 type_ref_to_string(in_type),
1957 type_ref_to_string(out_type)
1958 )
1959 }
1960 ServiceProtocol::Events {
1961 event_type,
1962 pattern,
1963 schema_dispatch,
1964 } => {
1965 let header = match pattern {
1966 Some(p) => format!(
1967 " from Events({} {})",
1968 type_ref_to_string(event_type),
1969 event_pattern_src(p)
1970 ),
1971 None => format!(" from Events({})", type_ref_to_string(event_type)),
1972 };
1973 match schema_dispatch {
1974 Some(d) => format!("{header} {}", schema_dispatch_src(d)),
1975 None => header,
1976 }
1977 }
1978 };
1979 let mut header = format!("service {}{}", s.name.name, from);
1983 if let Some(by) = &s.default_by {
1984 header.push_str(&format!(" {}", by_clause_src(by)));
1985 }
1986 if !s.default_given.is_empty() {
1987 let names: Vec<String> = s.default_given.iter().map(cap_ref_src).collect();
1988 header.push_str(&format!(" given {}", names.join(", ")));
1989 }
1990 self.push(&format!("{header} {{"));
1991 self.newline();
1992 self.indented(|f| {
1993 if let Some(cors) = &s.cors {
1998 f.format_cors_policy(cors);
1999 if s.security.is_some() || s.limits.is_some() || !s.handlers.is_empty() {
2000 f.newline();
2001 }
2002 }
2003 if let Some(security) = &s.security {
2004 f.format_security_policy(security);
2005 if s.limits.is_some() || !s.handlers.is_empty() {
2006 f.newline();
2007 }
2008 }
2009 if let Some(limits) = &s.limits {
2010 f.format_limits_policy(limits);
2011 if !s.handlers.is_empty() {
2012 f.newline();
2013 }
2014 }
2015 for (i, h) in s.handlers.iter().enumerate() {
2016 if i > 0 {
2017 f.newline();
2018 }
2019 f.format_handler(h);
2020 }
2021 if !s.trailing_comments.is_empty() {
2023 if !s.handlers.is_empty()
2024 || s.cors.is_some()
2025 || s.security.is_some()
2026 || s.limits.is_some()
2027 {
2028 f.newline();
2029 }
2030 f.emit_trailing_comments(&s.trailing_comments);
2031 }
2032 });
2033 self.push("}");
2034 self.emit_trailing_comment_or_newline(s.trivia.trailing.as_deref());
2035 }
2036
2037 fn format_cors_policy(&mut self, cors: &CorsPolicy) {
2040 let fields: Vec<_> = cors
2041 .fields
2042 .iter()
2043 .map(|f| (&f.name, &f.value, &f.trivia))
2044 .collect();
2045 self.format_policy("cors", &cors.trivia, &fields, &cors.trailing_comments);
2046 }
2047
2048 fn format_security_policy(&mut self, security: &SecurityPolicy) {
2050 let fields: Vec<_> = security
2051 .fields
2052 .iter()
2053 .map(|f| (&f.name, &f.value, &f.trivia))
2054 .collect();
2055 self.format_policy(
2056 "security",
2057 &security.trivia,
2058 &fields,
2059 &security.trailing_comments,
2060 );
2061 }
2062
2063 fn format_limits_policy(&mut self, limits: &LimitsPolicy) {
2067 let fields: Vec<_> = limits
2068 .fields
2069 .iter()
2070 .map(|f| (&f.name, &f.value, &f.trivia))
2071 .collect();
2072 self.format_policy("limits", &limits.trivia, &fields, &limits.trailing_comments);
2073 }
2074
2075 fn format_policy(
2079 &mut self,
2080 keyword: &str,
2081 trivia: &Trivia,
2082 fields: &[(&Ident, &Expr, &Trivia)],
2083 trailing_comments: &[Comment],
2084 ) {
2085 self.emit_leading_comments(&trivia.leading);
2086 self.push(&format!("{keyword} {{"));
2087 self.newline();
2088 self.indented(|f| {
2089 for (name, value, field_trivia) in fields {
2090 f.emit_leading_comments(&field_trivia.leading);
2091 f.push(&format!("{}: ", name.name));
2092 f.format_expr_at(value, 0, 1);
2093 f.push(",");
2094 f.emit_trailing_comment_or_newline(field_trivia.trailing.as_deref());
2095 }
2096 f.emit_trailing_comments(trailing_comments);
2097 });
2098 self.push("}");
2099 self.emit_trailing_comment_or_newline(trivia.trailing.as_deref());
2100 }
2101
2102 fn format_agent(&mut self, a: &AgentDecl) {
2103 self.emit_leading_comments(&a.trivia.leading);
2104 if let Some(doc) = &a.documentation {
2105 self.emit_doc(doc);
2106 }
2107 match &a.replaces {
2108 Some(old) => self.push(&format!("agent {} replaces {} {{", a.name.name, old.name)),
2109 None => self.push(&format!("agent {} {{", a.name.name)),
2110 }
2111 self.newline();
2112 self.indented(|f| {
2113 f.emit_leading_comments(&a.key_trivia.leading);
2115 f.push(&format!(
2116 "key {}: {}",
2117 a.key_name.name,
2118 type_ref_to_string(&a.key_type)
2119 ));
2120 f.emit_trailing_comment_or_newline(a.key_trivia.trailing.as_deref());
2121 f.newline();
2122 for sf in &a.store_fields {
2124 f.format_store_field(sf);
2125 }
2126 for inv in &a.invariants {
2129 f.newline();
2130 f.format_invariant(inv);
2131 }
2132 for tr in &a.transitions {
2135 f.newline();
2136 f.format_transition(tr);
2137 }
2138 for m in &a.migrations {
2140 f.newline();
2141 f.format_migrate(m);
2142 }
2143 for h in &a.handlers {
2145 f.newline();
2146 f.format_handler(h);
2147 }
2148 if !a.trailing_comments.is_empty() {
2150 f.newline();
2151 f.emit_trailing_comments(&a.trailing_comments);
2152 }
2153 });
2154 self.push("}");
2155 self.emit_trailing_comment_or_newline(a.trivia.trailing.as_deref());
2156 }
2157
2158 fn format_store_field(&mut self, sf: &StoreField) {
2162 self.emit_leading_comments(&sf.trivia.leading);
2163 if let Some(doc) = &sf.documentation {
2164 self.emit_doc(doc);
2165 }
2166 self.push(&format!(
2167 "store {}: {}",
2168 sf.name.name,
2169 store_kind_to_string(&sf.kind)
2170 ));
2171 for ann in &sf.annotations {
2173 self.push(&format!(" {}", annotation_to_string(ann)));
2174 }
2175 if let Some(init) = &sf.init {
2176 self.push(" = ");
2177 self.format_expr(init);
2178 }
2179 self.emit_trailing_comment_or_newline(sf.trivia.trailing.as_deref());
2180 }
2181
2182 fn format_invariant(&mut self, inv: &Invariant) {
2185 self.emit_leading_comments(&inv.trivia.leading);
2186 if let Some(doc) = &inv.documentation {
2187 self.emit_doc(doc);
2188 }
2189 self.push(&format!("invariant {}:", inv.name.name));
2190 self.newline();
2191 self.indented(|f| {
2192 f.format_expr(&inv.predicate);
2193 });
2194 self.emit_trailing_comment_or_newline(inv.trivia.trailing.as_deref());
2195 }
2196
2197 fn format_transition(&mut self, tr: &Transition) {
2200 self.emit_leading_comments(&tr.trivia.leading);
2201 if let Some(doc) = &tr.documentation {
2202 self.emit_doc(doc);
2203 }
2204 self.push(&format!("transition {}:", tr.name.name));
2205 self.newline();
2206 self.indented(|f| {
2207 f.format_expr(&tr.predicate);
2208 });
2209 self.emit_trailing_comment_or_newline(tr.trivia.trailing.as_deref());
2210 }
2211
2212 fn format_migrate(&mut self, m: &MigrateDecl) {
2215 self.emit_leading_comments(&m.trivia.leading);
2216 if let Some(doc) = &m.documentation {
2217 self.emit_doc(doc);
2218 }
2219 self.push(&format!("migrate from {}", m.from));
2220 if let Some(old) = &m.old {
2221 self.push(&format!(
2222 " ({}: {})",
2223 old.name.name,
2224 type_ref_to_string(&old.type_ref)
2225 ));
2226 }
2227 self.push(" {");
2228 self.newline();
2229 self.indented(|f| {
2230 for item in &m.items {
2231 let trivia = match item {
2232 MigrateItem::Let(l) => &l.trivia,
2233 MigrateItem::Assign(a) => &a.trivia,
2234 MigrateItem::Drop(d) => &d.trivia,
2235 };
2236 f.emit_leading_comments(&trivia.leading);
2237 match item {
2238 MigrateItem::Let(l) => {
2239 f.push("let ");
2240 f.push(&l.name.name);
2241 if let Some(t) = &l.type_annot {
2242 f.push(": ");
2243 f.format_type_ref(t);
2244 }
2245 f.push(" = ");
2246 f.format_expr(&l.value);
2247 }
2248 MigrateItem::Assign(a) => {
2249 f.push(&a.target.name);
2250 f.push(" := ");
2251 f.format_expr(&a.value);
2252 }
2253 MigrateItem::Drop(d) => f.push(&format!("drop {}", d.field.name)),
2254 }
2255 f.emit_trailing_comment_or_newline(trivia.trailing.as_deref());
2256 }
2257 if !m.trailing_comments.is_empty() {
2258 f.emit_trailing_comments(&m.trailing_comments);
2259 }
2260 });
2261 self.push("}");
2262 self.emit_trailing_comment_or_newline(m.trivia.trailing.as_deref());
2263 }
2264
2265 fn format_actor(&mut self, a: &ActorDecl) {
2266 self.emit_leading_comments(&a.trivia.leading);
2267 if let Some(doc) = &a.documentation {
2268 self.emit_doc(doc);
2269 }
2270 if let Some(r) = &a.refinement {
2271 self.push(&format!(
2273 "actor {} = {} where {}",
2274 a.name.name,
2275 r.base.name,
2276 expr_to_string(&r.predicate)
2277 ));
2278 } else {
2279 let auth = a.auth.as_ref().map(|i| i.name.as_str()).unwrap_or("None");
2281 let args: Vec<String> = a
2282 .auth_config
2283 .iter()
2284 .map(|arg| match &arg.value {
2285 bynk_syntax::ast::SchemeArgValue::Str(s) => {
2286 format!("{} = \"{}\"", arg.key.name, escape_string(s))
2287 }
2288 bynk_syntax::ast::SchemeArgValue::Int(n) => {
2289 format!("{} = {n}", arg.key.name)
2290 }
2291 })
2292 .collect();
2293 let config = if args.is_empty() {
2294 String::new()
2295 } else {
2296 format!("({})", args.join(", "))
2297 };
2298 let identity = a.identity.as_ref().map(type_ref_to_string);
2299 let oneline = format!(
2300 "actor {} {{ auth = {auth}{config}{} }}",
2301 a.name.name,
2302 identity
2303 .as_ref()
2304 .map(|id| format!(", identity = {id}"))
2305 .unwrap_or_default()
2306 );
2307 let break_args = !args.is_empty() && !self.fits(&oneline, 0);
2312 let has_comments = !a.trailing_comments.is_empty()
2314 || [&a.auth_trivia, &a.identity_trivia]
2315 .iter()
2316 .any(|t| !t.leading.is_empty() || t.trailing.is_some());
2317 if !break_args && !has_comments {
2318 self.push(&oneline);
2319 } else {
2320 self.push(&format!("actor {} {{", a.name.name));
2321 self.newline();
2322 self.indented(|f| {
2323 f.emit_leading_comments(&a.auth_trivia.leading);
2324 if break_args {
2325 f.push(&format!("auth = {auth}("));
2326 f.newline();
2327 f.indented(|f2| {
2328 for (i, arg) in args.iter().enumerate() {
2329 f2.push(arg);
2330 if i + 1 < args.len() {
2331 f2.push(",");
2332 }
2333 f2.newline();
2334 }
2335 });
2336 f.push(")");
2337 } else {
2338 f.push(&format!("auth = {auth}{config}"));
2339 }
2340 if let Some(id) = &identity {
2341 f.push(",");
2344 f.emit_trailing_comment_or_newline(a.auth_trivia.trailing.as_deref());
2345 f.emit_leading_comments(&a.identity_trivia.leading);
2346 f.push(&format!("identity = {id}"));
2347 f.emit_trailing_comment_or_newline(a.identity_trivia.trailing.as_deref());
2348 } else {
2349 f.emit_trailing_comment_or_newline(a.auth_trivia.trailing.as_deref());
2350 }
2351 f.emit_trailing_comments(&a.trailing_comments);
2352 });
2353 self.push("}");
2354 }
2355 }
2356 self.emit_trailing_comment_or_newline(a.trivia.trailing.as_deref());
2357 }
2358
2359 fn format_handler(&mut self, h: &Handler) {
2360 self.emit_leading_comments(&h.trivia.leading);
2361 if let Some(doc) = &h.documentation {
2362 self.emit_doc(doc);
2363 }
2364 for ann in &h.annotations {
2368 self.push(&annotation_to_string(ann));
2369 self.newline();
2370 }
2371 match &h.kind {
2374 HandlerKind::Call => {
2375 self.push("on call");
2376 if let Some(m) = &h.method_name {
2377 self.push(&format!(" {}", m.name));
2378 }
2379 }
2380 HandlerKind::Http { method, path } => {
2381 self.push(&format!(
2384 "on {}(\"{}\") ",
2385 method.as_str(),
2386 escape_string(path)
2387 ));
2388 }
2389 HandlerKind::Cron { expr } => {
2390 self.push(&format!("on schedule(\"{}\") ", escape_string(expr)));
2391 }
2392 HandlerKind::Message => {
2393 self.push("on message");
2394 }
2395 HandlerKind::Open => {
2396 self.push("on open");
2397 }
2398 HandlerKind::Close => {
2399 self.push("on close");
2400 }
2401 HandlerKind::Event => {
2402 self.push("on event");
2403 }
2404 HandlerKind::Rejected => {
2405 self.push("on rejected");
2406 }
2407 }
2408 let mut tail = format!(" -> {}", type_ref_to_string(&h.return_type));
2417 if let Some(by) = &h.by_clause {
2418 tail.push_str(&format!(" {}", by_clause_src(by)));
2419 }
2420 if !h.given.is_empty() {
2421 let names: Vec<String> = h.given.iter().map(cap_ref_src).collect();
2422 tail.push_str(&format!(" given {}", names.join(", ")));
2423 }
2424 self.format_params(&h.params, false, tail.chars().count() + " {".len());
2425 self.push(&tail);
2426 self.push(" ");
2427 self.format_block_multiline(&h.body);
2428 self.emit_trailing_comment_or_newline(h.trivia.trailing.as_deref());
2429 }
2430
2431 fn format_block(&mut self, b: &Block) {
2434 self.format_block_with_reserve(b, 0);
2435 }
2436
2437 fn format_block_with_reserve(&mut self, b: &Block, reserve: usize) {
2441 let tail_oneline = expr_to_string(&b.tail);
2444 let any_stmt_trivia = b.statements.iter().any(|s| !statement_trivia(s).is_empty());
2445 if b.statements.is_empty()
2446 && b.tail_leading_comments.is_empty()
2447 && !any_stmt_trivia
2448 && self.fits(&format!("{{ {tail_oneline} }}"), reserve)
2449 {
2450 self.push("{ ");
2451 self.push(&tail_oneline);
2452 self.push(" }");
2453 return;
2454 }
2455 self.format_block_multiline(b);
2456 }
2457
2458 fn format_block_multiline(&mut self, b: &Block) {
2463 if b.statements.is_empty() && b.tail_leading_comments.is_empty() && omit_unit_tail(b) {
2466 self.push("{}");
2467 return;
2468 }
2469 self.push("{");
2470 self.newline();
2471 self.indented(|f| f.format_block_body(b));
2472 self.push("}");
2473 }
2474
2475 fn format_block_body(&mut self, b: &Block) {
2481 for stmt in &b.statements {
2482 let trivia = statement_trivia(stmt);
2483 self.emit_leading_comments(&trivia.leading);
2484 if let Some((l, effectful, le)) = let_else_binding(stmt)
2485 && let ExprKind::Match { arms, .. } = &b.tail.kind
2486 && let Some(MatchBody::Block(rest)) = arms.first().map(|a| &a.body)
2487 {
2488 self.format_let_else(l, effectful, le, arms);
2489 self.emit_trailing_comment_or_newline(trivia.trailing.as_deref());
2490 self.format_block_body(rest);
2491 return;
2492 }
2493 self.format_statement(stmt);
2494 self.emit_trailing_comment_or_newline(trivia.trailing.as_deref());
2495 }
2496 self.emit_leading_comments(&b.tail_leading_comments);
2497 if !omit_unit_tail(b) {
2498 self.format_expr(&b.tail);
2499 self.newline();
2500 }
2501 }
2502
2503 fn format_let_else(&mut self, l: &LetStmt, effectful: bool, le: &LetElse, arms: &[MatchArm]) {
2507 self.push("let ");
2508 self.push(&let_pattern_src(&arms[0].pattern));
2509 self.push(if effectful { " <- " } else { " = " });
2510 let principal = l
2511 .principal
2512 .as_ref()
2513 .map(|p| format!(" {}", call_site_actor_src(p)));
2514 let reserve = principal.as_deref().map_or(0, |p| p.chars().count()) + " else {".len();
2515 self.format_expr_at(&l.value, 0, reserve);
2516 if let Some(principal) = principal {
2517 self.push(&principal);
2518 }
2519 self.push(" else ");
2520 match (le.form, arms.get(1).map(|a| &a.body)) {
2521 (LetElseForm::Block, Some(MatchBody::Block(otherwise))) => self.format_block(otherwise),
2522 _ => {
2523 self.push("match {");
2524 self.newline();
2525 self.indented(|f| f.format_arms(&arms[1..]));
2526 self.push("}");
2527 }
2528 }
2529 }
2530
2531 fn format_statement(&mut self, s: &Statement) {
2532 match s {
2533 Statement::Let(l) => {
2534 self.push("let ");
2535 self.push(&l.name.name);
2536 if let Some(t) = &l.type_annot {
2537 self.push(": ");
2538 self.format_type_ref(t);
2539 }
2540 self.push(" = ");
2541 self.format_expr(&l.value);
2542 }
2543 Statement::EffectLet(l) => {
2544 self.push("let ");
2545 self.push(&l.name.name);
2546 if let Some(t) = &l.type_annot {
2547 self.push(": ");
2548 self.format_type_ref(t);
2549 }
2550 self.push(" <- ");
2551 let principal = l
2553 .principal
2554 .as_ref()
2555 .map(|p| format!(" {}", call_site_actor_src(p)));
2556 let reserve = principal.as_deref().map_or(0, |p| p.chars().count());
2557 self.format_expr_at(&l.value, 0, reserve);
2558 if let Some(principal) = principal {
2559 self.push(&principal);
2560 }
2561 }
2562 Statement::Expect(a) => {
2563 self.push("expect ");
2564 self.format_expr(&a.value);
2565 }
2566 Statement::Send(s) => {
2567 self.push("~> ");
2568 self.format_expr(&s.value);
2569 }
2570 Statement::Do(d) => {
2571 self.push("do ");
2572 self.format_expr(&d.value);
2573 }
2574 Statement::Assign(a) => {
2575 self.push(&a.target.name);
2576 self.push(" := ");
2577 self.format_expr(&a.value);
2578 }
2579 Statement::Advance(a) => {
2580 self.push(&format!("advance {} by ", a.cap.name));
2581 self.format_expr(&a.amount);
2582 }
2583 }
2584 }
2585
2586 fn format_expr(&mut self, e: &Expr) {
2587 self.format_expr_at(e, 0, 0);
2588 }
2589
2590 fn format_expr_at(&mut self, e: &Expr, parent_prec: u8, reserve: usize) {
2603 if let ExprKind::Match { discriminant, arms } = &e.kind {
2609 self.format_match(discriminant, arms);
2610 return;
2611 }
2612 let flat = expr_with_prec(e, parent_prec);
2613 let braced_record = match &e.kind {
2617 ExprKind::RecordConstruction { fields, .. } => fields.len() > 1,
2618 ExprKind::RecordSpread { overrides, .. } => !overrides.is_empty(),
2619 _ => false,
2620 };
2621 if !braced_record && self.fits(&flat, reserve) {
2622 self.push(&flat);
2623 return;
2624 }
2625 if needs_parens(e, parent_prec) {
2628 self.push("(");
2629 self.format_expr_broken(e, reserve + 1);
2630 self.push(")");
2631 } else {
2632 self.format_expr_broken(e, reserve);
2633 }
2634 }
2635
2636 fn format_expr_broken(&mut self, e: &Expr, reserve: usize) {
2641 match &e.kind {
2642 ExprKind::RecordConstruction { type_name, fields } if !fields.is_empty() => {
2644 self.push(&format!("{} {{", type_name.name));
2645 self.format_field_inits(fields.iter(), None);
2646 }
2647 ExprKind::RecordSpread {
2650 type_name,
2651 base,
2652 overrides,
2653 } => {
2654 match type_name {
2655 Some(tn) => self.push(&format!("{} {{", tn.name)),
2656 None => self.push("{"),
2657 }
2658 let spread = format!("...{}", expr_with_prec(base, 0));
2659 self.format_field_inits(overrides.iter(), Some(&spread));
2660 }
2661 ExprKind::Call {
2665 name,
2666 type_args,
2667 args,
2668 } if !args.is_empty() => {
2669 self.push(&format!("{}{}(", name.name, type_args_src(type_args)));
2670 self.format_arg_list(args, reserve);
2671 }
2672 ExprKind::Val { type_ref, args } if !args.is_empty() => {
2673 self.push(&format!("Val[{}](", type_ref_to_string(type_ref)));
2674 self.format_arg_list(args, reserve);
2675 }
2676 ExprKind::MethodCall { .. } | ExprKind::FieldAccess { .. } => {
2680 self.format_chain(e, reserve);
2681 }
2682 ExprKind::ListLit(elems) if !elems.is_empty() => {
2684 self.push("[");
2685 self.newline();
2686 self.indented(|f| {
2687 for (i, elem) in elems.iter().enumerate() {
2688 let last = i + 1 == elems.len();
2689 f.format_expr_at(elem, 0, if last { 0 } else { 1 });
2690 if !last || f.opts.trailing_comma {
2691 f.push(",");
2692 }
2693 f.newline();
2694 }
2695 });
2696 self.push("]");
2697 }
2698 ExprKind::BinOp(op, ..) if is_logical(*op) => {
2703 let prec = binop_prec(*op);
2704 let mut operands = Vec::new();
2705 flatten_binop(e, *op, &mut operands);
2706 self.format_expr_at(operands[0], prec, 0);
2707 self.indented(|f| {
2708 for (i, operand) in operands.iter().enumerate().skip(1) {
2709 f.newline();
2710 f.push(&format!("{} ", op.name()));
2711 let last = i + 1 == operands.len();
2712 f.format_expr_at(operand, prec + 1, if last { reserve } else { 0 });
2713 }
2714 });
2715 }
2716 ExprKind::BinOp(op, lhs, rhs) => {
2719 let prec = binop_prec(*op);
2720 let tail = format!(" {} {}", op.name(), expr_with_prec(rhs, prec + 1));
2721 let lhs_reserve = if tail.contains('\n') {
2724 0
2725 } else {
2726 tail.chars().count() + reserve
2727 };
2728 self.format_expr_at(lhs, prec, lhs_reserve);
2729 self.push(&format!(" {} ", op.name()));
2730 self.format_expr_at(rhs, prec + 1, reserve);
2731 }
2732 ExprKind::Is { value, pattern } => {
2733 let pat = format!(" is {}", pattern_to_string(pattern));
2734 self.format_expr_at(value, 4, pat.chars().count() + reserve);
2735 self.push(&pat);
2736 }
2737 ExprKind::If {
2741 cond,
2742 then_block,
2743 else_block,
2744 } => {
2745 self.push("if ");
2746 self.format_expr_at(cond, 0, 2);
2747 self.push(" ");
2748 self.format_block_multiline(then_block);
2749 if !else_block.is_synth_unit() {
2752 self.push(" else ");
2753 self.format_block_multiline(else_block);
2754 }
2755 }
2756 ExprKind::Block(b) => self.format_block_multiline(b),
2757 ExprKind::Lambda(lambda) => {
2758 let params: Vec<String> = lambda
2759 .params
2760 .iter()
2761 .map(|p| match &p.type_ref {
2762 Some(tr) => format!("{}: {}", p.name.name, type_ref_to_string(tr)),
2763 None => p.name.name.clone(),
2764 })
2765 .collect();
2766 self.push(&format!("({}) => ", params.join(", ")));
2767 self.format_expr_at(&lambda.body, 0, reserve);
2768 }
2769 ExprKind::Ok(v) => self.wrap_call("Ok(", v, reserve),
2772 ExprKind::Err(v) => self.wrap_call("Err(", v, reserve),
2773 ExprKind::Some(v) => self.wrap_call("Some(", v, reserve),
2774 ExprKind::EffectPure(v) => self.wrap_call("Effect.pure(", v, reserve),
2775 ExprKind::Wire(v) => self.wrap_call("Wire(", v, reserve),
2776 ExprKind::Paren(v) => self.wrap_call("(", v, reserve),
2777 ExprKind::Question(v) => {
2778 self.format_expr_at(v, 8, reserve + 1);
2779 self.push("?");
2780 }
2781 ExprKind::Expect(v) => {
2782 self.push("expect ");
2783 self.format_expr_at(v, 0, reserve);
2784 }
2785 _ => self.push(&expr_with_prec(e, 0)),
2788 }
2789 }
2790
2791 fn wrap_call(&mut self, head: &str, inner: &Expr, reserve: usize) {
2793 self.push(head);
2794 self.format_expr_at(inner, 0, reserve + 1);
2795 self.push(")");
2796 }
2797
2798 fn format_field_inits<'f, I>(&mut self, fields: I, spread: Option<&str>)
2803 where
2804 I: ExactSizeIterator<Item = &'f FieldInit>,
2805 {
2806 let total = fields.len() + usize::from(spread.is_some());
2807 self.newline();
2808 self.indented(|f| {
2809 let mut emitted = 0usize;
2810 if let Some(spread) = spread {
2811 f.push(spread);
2812 emitted += 1;
2813 if emitted < total || f.opts.trailing_comma {
2814 f.push(",");
2815 }
2816 f.newline();
2817 }
2818 for field in fields {
2819 f.push(&field.name.name);
2820 if let Some(v) = &field.value {
2821 f.push(": ");
2822 f.format_expr_at(v, 0, 1);
2823 }
2824 emitted += 1;
2825 if emitted < total || f.opts.trailing_comma {
2826 f.push(",");
2827 }
2828 f.newline();
2829 }
2830 });
2831 self.push("}");
2832 }
2833
2834 fn format_arg_list(&mut self, args: &[Expr], reserve: usize) -> bool {
2849 if let Some((last, leading)) = args.split_last()
2850 && (leading.is_empty() || is_block_like(last))
2851 && self.try_layout(reserve, |f| {
2852 for arg in leading {
2853 f.push(&expr_with_prec(arg, 0));
2854 f.push(", ");
2855 }
2856 f.format_expr_at(last, 0, 1);
2857 f.push(")");
2858 })
2859 {
2860 return false;
2861 }
2862 self.newline();
2863 self.indented(|f| {
2864 for (i, arg) in args.iter().enumerate() {
2865 let last = i + 1 == args.len();
2866 f.format_expr_at(arg, 0, if last { 0 } else { 1 });
2867 if !last {
2868 f.push(",");
2869 }
2870 f.newline();
2871 }
2872 });
2873 self.push(")");
2874 true
2875 }
2876
2877 fn format_chain(&mut self, e: &Expr, reserve: usize) {
2884 let (base, links) = flatten_chain(e);
2885 let calls = links
2886 .iter()
2887 .filter(|l| matches!(l, ChainLink::Method { .. }))
2888 .count();
2889 if calls < 2 {
2890 self.format_chain_inline(base, &links, reserve);
2891 return;
2892 }
2893 let exploded = std::cell::Cell::new(false);
2901 if self.try_layout_if(
2902 reserve,
2903 |f| exploded.set(f.format_chain_inline(base, &links, reserve)),
2904 || !exploded.get(),
2905 ) {
2906 return;
2907 }
2908 let first_call = links
2913 .iter()
2914 .position(|l| matches!(l, ChainLink::Method { .. }))
2915 .expect("a chain with two calls has one");
2916 self.format_expr_at(base, 8, 0);
2917 for link in &links[..first_call] {
2918 self.format_chain_link(link, 0);
2919 }
2920 self.indented(|f| {
2921 let mut i = first_call;
2922 while i < links.len() {
2923 let mut end = i;
2924 while end < links.len() && matches!(links[end], ChainLink::Field(_)) {
2925 end += 1;
2926 }
2927 if end < links.len() {
2929 end += 1;
2930 }
2931 f.newline();
2932 for (offset, link) in links[i..end].iter().enumerate() {
2933 let is_last = end == links.len() && i + offset + 1 == links.len();
2934 f.format_chain_link(link, if is_last { reserve } else { 0 });
2935 }
2936 i = end;
2937 }
2938 });
2939 }
2940
2941 fn format_chain_inline(
2947 &mut self,
2948 base: &Expr,
2949 links: &[ChainLink<'_>],
2950 reserve: usize,
2951 ) -> bool {
2952 self.format_expr_at(base, 8, 0);
2953 let mut exploded = false;
2954 for (i, link) in links.iter().enumerate() {
2955 exploded |=
2956 self.format_chain_link(link, if i + 1 == links.len() { reserve } else { 0 });
2957 }
2958 exploded
2959 }
2960
2961 fn format_chain_link(&mut self, link: &ChainLink<'_>, reserve: usize) -> bool {
2965 match link {
2966 ChainLink::Field(name) => {
2967 self.push(&format!(".{name}"));
2968 false
2969 }
2970 ChainLink::Method {
2971 method,
2972 type_args,
2973 args,
2974 } => {
2975 let head = format!(".{}{}", method, type_args_src(type_args));
2976 let flat = format!(
2977 "{head}({})",
2978 args.iter()
2979 .map(|a| expr_with_prec(a, 0))
2980 .collect::<Vec<_>>()
2981 .join(", ")
2982 );
2983 if args.is_empty() || self.fits(&flat, reserve) {
2984 self.push(&flat);
2985 return false;
2986 }
2987 self.push(&head);
2988 self.push("(");
2989 self.format_arg_list(args, reserve)
2990 }
2991 }
2992 }
2993
2994 fn format_match(&mut self, discriminant: &Expr, arms: &[MatchArm]) {
2998 self.push("match ");
2999 self.format_expr_at(discriminant, 0, " {".len());
3000 self.push(" {");
3001 self.newline();
3002 self.indented(|f| f.format_arms(arms));
3003 self.push("}");
3004 }
3005
3006 fn format_arms(&mut self, arms: &[MatchArm]) {
3008 for arm in arms {
3009 self.push(&pattern_to_string(&arm.pattern));
3010 if let Some(guard) = &arm.guard {
3012 self.push(" if ");
3013 self.format_expr_at(guard, 0, " => ".len());
3014 }
3015 self.push(" => ");
3016 match &arm.body {
3018 MatchBody::Expr(e) => self.format_expr_at(e, 0, 1),
3019 MatchBody::Block(b) => self.format_block_with_reserve(b, 1),
3020 }
3021 self.push(",");
3022 self.newline();
3023 }
3024 }
3025}
3026
3027enum ChainLink<'e> {
3029 Field(&'e str),
3030 Method {
3031 method: &'e str,
3032 type_args: &'e [TypeRef],
3033 args: &'e [Expr],
3034 },
3035}
3036
3037fn flatten_chain(e: &Expr) -> (&Expr, Vec<ChainLink<'_>>) {
3041 let mut links = Vec::new();
3042 let mut cur = e;
3043 loop {
3044 match &cur.kind {
3045 ExprKind::FieldAccess { receiver, field } => {
3046 links.push(ChainLink::Field(field.name.as_str()));
3047 cur = receiver;
3048 }
3049 ExprKind::MethodCall {
3050 receiver,
3051 method,
3052 type_args,
3053 args,
3054 } => {
3055 links.push(ChainLink::Method {
3056 method: method.name.as_str(),
3057 type_args,
3058 args,
3059 });
3060 cur = receiver;
3061 }
3062 _ => break,
3063 }
3064 }
3065 links.reverse();
3066 (cur, links)
3067}
3068
3069fn type_args_src(type_args: &[TypeRef]) -> String {
3071 if type_args.is_empty() {
3072 return String::new();
3073 }
3074 format!(
3075 "[{}]",
3076 type_args
3077 .iter()
3078 .map(type_ref_to_string)
3079 .collect::<Vec<_>>()
3080 .join(", ")
3081 )
3082}
3083
3084fn is_logical(op: BinOp) -> bool {
3088 matches!(op, BinOp::And | BinOp::Or | BinOp::Implies)
3089}
3090
3091fn flatten_binop<'e>(e: &'e Expr, op: BinOp, out: &mut Vec<&'e Expr>) {
3094 if let ExprKind::BinOp(inner_op, lhs, rhs) = &e.kind
3095 && *inner_op == op
3096 {
3097 flatten_binop(lhs, op, out);
3098 out.push(rhs);
3099 return;
3100 }
3101 out.push(e);
3102}
3103
3104fn is_block_like(e: &Expr) -> bool {
3110 matches!(
3111 e.kind,
3112 ExprKind::Lambda(_)
3113 | ExprKind::Block(_)
3114 | ExprKind::RecordConstruction { .. }
3115 | ExprKind::RecordSpread { .. }
3116 | ExprKind::Match { .. }
3117 | ExprKind::If { .. }
3118 )
3119}
3120
3121fn needs_parens(e: &Expr, parent_prec: u8) -> bool {
3125 match &e.kind {
3126 ExprKind::BinOp(op, ..) => binop_prec(*op) < parent_prec,
3127 ExprKind::UnaryOp(..) => parent_prec > 7,
3128 _ => false,
3129 }
3130}
3131
3132fn cap_ref_src(c: &CapRef) -> String {
3136 match &c.context {
3137 Some(prefix) => format!("{}.{}", prefix.joined(), c.name.name),
3138 None => c.name.name.clone(),
3139 }
3140}
3141
3142fn by_clause_src(by: &ByClause) -> String {
3146 let actors = by
3147 .actors
3148 .iter()
3149 .map(|a| a.name.as_str())
3150 .collect::<Vec<_>>()
3151 .join(" | ");
3152 match &by.binder {
3153 Some(b) => format!("by {}: {actors}", b.name),
3154 None => format!("by {actors}"),
3155 }
3156}
3157
3158fn event_pattern_src(p: &EventPattern) -> String {
3164 let fields: Vec<String> = p
3165 .fields
3166 .iter()
3167 .map(|f| format!("{}: {}", f.name.name, event_pattern_value_src(&f.value)))
3168 .collect();
3169 format!("{{ {}, .. }}", fields.join(", "))
3170}
3171
3172fn event_pattern_value_src(v: &EventPatternValue) -> String {
3173 match v {
3174 EventPatternValue::Literal { value, .. } => match value {
3175 LiteralValue::Int(n) => n.to_string(),
3176 LiteralValue::Str(s) => format!("\"{}\"", escape_string(s)),
3177 LiteralValue::Bool(b) => b.to_string(),
3178 },
3179 EventPatternValue::Variant {
3180 type_name, variant, ..
3181 } => match type_name {
3182 Some(t) => format!("{}.{}", t.name, variant.name),
3183 None => variant.name.clone(),
3184 },
3185 }
3186}
3187
3188fn schema_dispatch_src(d: &SchemaDispatch) -> String {
3191 match &d.pattern {
3192 SchemaVersionPattern::Literal(n) => format!("via schema({n})"),
3193 }
3194}
3195
3196fn call_site_actor_src(p: &CallSiteActor) -> String {
3199 match (&p.identity, &p.claims) {
3200 (Some(id), Some(claims)) => format!(
3202 "by {}({}, {})",
3203 p.actor.name,
3204 expr_with_prec(id, 0),
3205 expr_with_prec(claims, 0)
3206 ),
3207 (Some(id), None) => format!("by {}({})", p.actor.name, expr_with_prec(id, 0)),
3208 (None, _) => format!("by {}", p.actor.name),
3209 }
3210}
3211
3212fn store_kind_to_string(k: &StoreKind) -> String {
3214 if k.args.is_empty() {
3215 k.head.name.clone()
3216 } else {
3217 format!(
3218 "{}[{}]",
3219 k.head.name,
3220 k.args
3221 .iter()
3222 .map(type_ref_to_string)
3223 .collect::<Vec<_>>()
3224 .join(", ")
3225 )
3226 }
3227}
3228
3229pub fn annotation_to_string(ann: &Annotation) -> String {
3236 if ann.args.is_empty() {
3237 return format!("@{}", ann.name.name);
3238 }
3239 let args = ann
3240 .args
3241 .iter()
3242 .map(|a| match &a.label {
3243 Some(l) => format!("{}: {}", l.name, expr_with_prec(&a.value, 0)),
3244 None => expr_with_prec(&a.value, 0),
3245 })
3246 .collect::<Vec<_>>()
3247 .join(", ");
3248 format!("@{}({})", ann.name.name, args)
3249}
3250
3251fn stub_clause_to_string(pv: &StubClause) -> String {
3254 let args = pv
3255 .args
3256 .iter()
3257 .map(|a| match a {
3258 ArgPattern::Any(_) => "_".to_string(),
3259 ArgPattern::Value(e) => expr_to_string(e),
3260 })
3261 .collect::<Vec<_>>()
3262 .join(", ");
3263 let rhs = match &pv.rhs {
3264 StubRhs::Returns(e) => format!("returns {}", expr_to_string(e)),
3265 StubRhs::Fails(_) => "fails".to_string(),
3266 StubRhs::ReturnsEach(outcomes, _) => {
3267 let items = outcomes
3268 .iter()
3269 .map(|o| match o {
3270 SeqOutcome::Value(e) => expr_to_string(e),
3271 SeqOutcome::Fails(_) => "fails".to_string(),
3272 })
3273 .collect::<Vec<_>>()
3274 .join(", ");
3275 format!("returns each [{items}]")
3276 }
3277 };
3278 format!(
3279 "stub {}.{}({}) {}",
3280 pv.capability.name, pv.method.name, args, rhs
3281 )
3282}
3283
3284fn statement_trivia(s: &Statement) -> &Trivia {
3285 match s {
3286 Statement::Let(l) | Statement::EffectLet(l) => &l.trivia,
3287 Statement::Expect(a) => &a.trivia,
3288 Statement::Send(s) => &s.trivia,
3289 Statement::Do(d) => &d.trivia,
3290 Statement::Assign(a) => &a.trivia,
3291 Statement::Advance(a) => &a.trivia,
3292 }
3293}
3294
3295fn display_width(line: &str, tab: usize) -> usize {
3302 let mut col = 0usize;
3303 for ch in line.chars() {
3304 if ch == '\t' {
3305 col += tab - (col % tab);
3306 } else {
3307 col += 1;
3308 }
3309 }
3310 col
3311}
3312
3313fn type_ref_to_string(t: &TypeRef) -> String {
3314 match t {
3315 TypeRef::Base(b, _) => b.name().to_string(),
3316 TypeRef::Named(id) => id.name.clone(),
3317 TypeRef::Result(a, b, _) => format!(
3318 "Result[{}, {}]",
3319 type_ref_to_string(a),
3320 type_ref_to_string(b)
3321 ),
3322 TypeRef::Option(t, _) => format!("Option[{}]", type_ref_to_string(t)),
3323 TypeRef::Effect(t, _) => format!("Effect[{}]", type_ref_to_string(t)),
3324 TypeRef::HttpResult(t, _) => format!("HttpResult[{}]", type_ref_to_string(t)),
3325 TypeRef::QueueResult(_) => "QueueResult".to_string(),
3326 TypeRef::Asset(_) => "Asset".to_string(),
3327 TypeRef::RequestHeaders(_) => "RequestHeaders".to_string(),
3328 TypeRef::TokenClaims(_) => "TokenClaims".to_string(),
3329 TypeRef::BoundaryRejection(_) => "BoundaryRejection".to_string(),
3330 TypeRef::List(t, _) => format!("List[{}]", type_ref_to_string(t)),
3331 TypeRef::Query(t, _) => format!("Query[{}]", type_ref_to_string(t)),
3332 TypeRef::Stream(t, _) => format!("Stream[{}]", type_ref_to_string(t)),
3333 TypeRef::Connection(t, _) => format!("Connection[{}]", type_ref_to_string(t)),
3334 TypeRef::History(t, _) => format!("History[{}]", type_ref_to_string(t)),
3335 TypeRef::Map(k, v, _) => {
3336 format!("Map[{}, {}]", type_ref_to_string(k), type_ref_to_string(v))
3337 }
3338 TypeRef::ValidationError(_) => "ValidationError".to_string(),
3339 TypeRef::JsonError(_) => "JsonError".to_string(),
3340 TypeRef::Unit(_) => "()".to_string(),
3341 TypeRef::App { name, args, .. } => format!(
3343 "{}[{}]",
3344 name.name,
3345 args.iter()
3346 .map(type_ref_to_string)
3347 .collect::<Vec<_>>()
3348 .join(", ")
3349 ),
3350 TypeRef::Fn(params, ret, _) => {
3351 let lhs = match params.len() {
3352 0 => "()".to_string(),
3353 1 if !matches!(params[0], TypeRef::Fn(..)) => type_ref_to_string(¶ms[0]),
3354 _ => format!(
3355 "({})",
3356 params
3357 .iter()
3358 .map(type_ref_to_string)
3359 .collect::<Vec<_>>()
3360 .join(", ")
3361 ),
3362 };
3363 format!("{lhs} -> {}", type_ref_to_string(ret))
3364 }
3365 }
3366}
3367
3368pub fn refinement_to_string(r: &Refinement) -> String {
3369 let mut s = String::new();
3370 for (i, p) in r.predicates.iter().enumerate() {
3371 if i > 0 {
3372 s.push_str(" && ");
3373 }
3374 s.push_str(&pred_to_string(p));
3375 }
3376 s
3377}
3378
3379fn pred_to_string(p: &RefinementPred) -> String {
3380 match &p.kind {
3381 PredKind::Matches(re) => format!("Matches(\"{}\")", escape_string(re)),
3382 PredKind::InRange(a, b) => format!("InRange({}, {})", a.value, b.value),
3383 PredKind::InRangeF(a, b) => format!("InRange({}, {})", a.lexeme, b.lexeme),
3384 PredKind::MinLength(n) => format!("MinLength({n})"),
3385 PredKind::MaxLength(n) => format!("MaxLength({n})"),
3386 PredKind::Length(n) => format!("Length({n})"),
3387 PredKind::NonNegative => "NonNegative".to_string(),
3388 PredKind::Positive => "Positive".to_string(),
3389 PredKind::NonEmpty => "NonEmpty".to_string(),
3390 }
3391}
3392
3393pub fn escape_string(s: &str) -> String {
3394 let mut out = String::with_capacity(s.len());
3395 for ch in s.chars() {
3396 match ch {
3397 '\\' => out.push_str("\\\\"),
3398 '"' => out.push_str("\\\""),
3399 '\n' => out.push_str("\\n"),
3400 '\t' => out.push_str("\\t"),
3401 c => out.push(c),
3402 }
3403 }
3404 out
3405}
3406
3407pub fn expr_to_string(e: &Expr) -> String {
3408 expr_with_prec(e, 0)
3409}
3410
3411fn binop_prec(op: BinOp) -> u8 {
3414 match op {
3415 BinOp::Implies => 0,
3417 BinOp::Or => 1,
3418 BinOp::And => 2,
3419 BinOp::Eq | BinOp::NotEq => 3,
3420 BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq => 4,
3421 BinOp::Add | BinOp::Sub => 5,
3422 BinOp::Mul | BinOp::Div => 6,
3423 }
3424}
3425
3426fn expr_with_prec(e: &Expr, parent_prec: u8) -> String {
3427 match &e.kind {
3428 ExprKind::IntLit { lexeme, .. } => lexeme.clone(),
3431 ExprKind::FloatLit { lexeme, .. } => lexeme.clone(),
3433 ExprKind::DurationLit { value, unit, .. } => format!("{value}.{}", unit.name()),
3435 ExprKind::StrLit(s) => format!("\"{}\"", escape_string(s)),
3436 ExprKind::InterpStr(parts) => {
3440 let mut out = String::from("\"");
3441 for part in parts {
3442 match part {
3443 InterpPart::Chunk(text) => out.push_str(&escape_string(text)),
3444 InterpPart::Hole(hole) => {
3445 out.push_str(&format!("\\({})", expr_with_prec(hole, 0)));
3446 }
3447 }
3448 }
3449 out.push('"');
3450 out
3451 }
3452 ExprKind::BoolLit(b) => b.to_string(),
3453 ExprKind::UnitLit => "()".to_string(),
3454 ExprKind::Ident(id) => id.name.clone(),
3455 ExprKind::ListLit(elems) => format!(
3456 "[{}]",
3457 elems
3458 .iter()
3459 .map(expr_to_string)
3460 .collect::<Vec<_>>()
3461 .join(", ")
3462 ),
3463 ExprKind::Call {
3464 name,
3465 type_args,
3466 args,
3467 } => {
3468 let targs = if type_args.is_empty() {
3469 String::new()
3470 } else {
3471 format!(
3472 "[{}]",
3473 type_args
3474 .iter()
3475 .map(type_ref_to_string)
3476 .collect::<Vec<_>>()
3477 .join(", ")
3478 )
3479 };
3480 let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
3481 format!("{}{}({})", name.name, targs, parts.join(", "))
3482 }
3483 ExprKind::BinOp(op, l, r) => {
3484 let prec = binop_prec(*op);
3485 let inner = format!(
3486 "{} {} {}",
3487 expr_with_prec(l, prec),
3488 op.name(),
3489 expr_with_prec(r, prec + 1)
3490 );
3491 if prec < parent_prec {
3492 format!("({inner})")
3493 } else {
3494 inner
3495 }
3496 }
3497 ExprKind::UnaryOp(op, inner) => {
3498 let s = format!("{}{}", op.name(), expr_with_prec(inner, 7));
3500 if parent_prec > 7 { format!("({s})") } else { s }
3501 }
3502 ExprKind::Paren(inner) => format!("({})", expr_with_prec(inner, 0)),
3503 ExprKind::Lambda(lambda) => {
3505 let params: Vec<String> = lambda
3506 .params
3507 .iter()
3508 .map(|p| match &p.type_ref {
3509 Some(tr) => format!("{}: {}", p.name.name, type_ref_to_string(tr)),
3510 None => p.name.name.clone(),
3511 })
3512 .collect();
3513 let body = match &lambda.body.kind {
3514 ExprKind::Block(b) => format_block_oneline(b),
3515 _ => expr_with_prec(&lambda.body, 0),
3516 };
3517 format!("({}) => {}", params.join(", "), body)
3518 }
3519 ExprKind::Block(b) => format_block_oneline(b),
3520 ExprKind::If {
3521 cond,
3522 then_block,
3523 else_block,
3524 } => {
3525 if else_block.is_synth_unit() {
3528 format!(
3529 "if {} {}",
3530 expr_with_prec(cond, 0),
3531 format_block_oneline(then_block),
3532 )
3533 } else {
3534 format!(
3535 "if {} {} else {}",
3536 expr_with_prec(cond, 0),
3537 format_block_oneline(then_block),
3538 format_block_oneline(else_block),
3539 )
3540 }
3541 }
3542 ExprKind::Ok(v) => format!("Ok({})", expr_with_prec(v, 0)),
3543 ExprKind::Err(v) => format!("Err({})", expr_with_prec(v, 0)),
3544 ExprKind::Some(v) => format!("Some({})", expr_with_prec(v, 0)),
3545 ExprKind::None => "None".to_string(),
3546 ExprKind::Question(v) => format!("{}?", expr_with_prec(v, 8)),
3547 ExprKind::RecordConstruction { type_name, fields } => {
3548 let parts: Vec<String> = fields
3549 .iter()
3550 .map(|f| match &f.value {
3551 Some(v) => format!("{}: {}", f.name.name, expr_with_prec(v, 0)),
3552 None => f.name.name.clone(),
3553 })
3554 .collect();
3555 if parts.is_empty() {
3556 format!("{} {{}}", type_name.name)
3557 } else {
3558 format!("{} {{ {} }}", type_name.name, parts.join(", "))
3559 }
3560 }
3561 ExprKind::FieldAccess { receiver, field } => {
3562 format!("{}.{}", expr_with_prec(receiver, 8), field.name)
3563 }
3564 ExprKind::MethodCall {
3565 receiver,
3566 method,
3567 type_args,
3568 args,
3569 } => {
3570 let targs = if type_args.is_empty() {
3571 String::new()
3572 } else {
3573 format!(
3574 "[{}]",
3575 type_args
3576 .iter()
3577 .map(type_ref_to_string)
3578 .collect::<Vec<_>>()
3579 .join(", ")
3580 )
3581 };
3582 let parts: Vec<String> = args.iter().map(|a| expr_with_prec(a, 0)).collect();
3583 format!(
3584 "{}.{}{targs}({})",
3585 expr_with_prec(receiver, 8),
3586 method.name,
3587 parts.join(", ")
3588 )
3589 }
3590 ExprKind::Match { discriminant, arms } => {
3591 let mut out = String::new();
3592 out.push_str("match ");
3593 out.push_str(&expr_with_prec(discriminant, 0));
3594 out.push_str(" {\n");
3595 for arm in arms {
3596 out.push('\t');
3597 out.push_str(&pattern_to_string(&arm.pattern));
3598 if let Some(guard) = &arm.guard {
3599 out.push_str(" if ");
3600 out.push_str(&expr_with_prec(guard, 0));
3601 }
3602 out.push_str(" => ");
3603 match &arm.body {
3604 MatchBody::Expr(e) => out.push_str(&expr_with_prec(e, 0)),
3605 MatchBody::Block(b) => out.push_str(&format_block_oneline(b)),
3606 }
3607 out.push_str(",\n");
3608 }
3609 out.push('}');
3610 out
3611 }
3612 ExprKind::Is { value, pattern } => {
3613 format!(
3614 "{} is {}",
3615 expr_with_prec(value, 4),
3616 pattern_to_string(pattern)
3617 )
3618 }
3619 ExprKind::RecordSpread {
3620 type_name,
3621 base,
3622 overrides,
3623 } => {
3624 let mut parts = vec![format!("...{}", expr_with_prec(base, 0))];
3625 for f in overrides {
3626 if let Some(v) = &f.value {
3627 parts.push(format!("{}: {}", f.name.name, expr_with_prec(v, 0)));
3628 } else {
3629 parts.push(f.name.name.clone());
3630 }
3631 }
3632 let body = parts.join(", ");
3633 match type_name {
3634 Some(tn) => format!("{} {{ {} }}", tn.name, body),
3635 None => format!("{{ {} }}", body),
3636 }
3637 }
3638 ExprKind::EffectPure(v) => format!("Effect.pure({})", expr_with_prec(v, 0)),
3639 ExprKind::Expect(v) => format!("expect {}", expr_with_prec(v, 0)),
3640 ExprKind::Val { type_ref, args } => {
3641 let t = type_ref_to_string(type_ref);
3642 if args.is_empty() {
3643 format!("Val[{t}]")
3644 } else {
3645 let a = args
3646 .iter()
3647 .map(|x| expr_with_prec(x, 0))
3648 .collect::<Vec<_>>()
3649 .join(", ");
3650 format!("Val[{t}]({a})")
3651 }
3652 }
3653 ExprKind::Wire(inner) => format!("Wire({})", expr_with_prec(inner, 0)),
3654 ExprKind::Trace { cap, op } => format!("trace({}.{})", cap.name, op.name),
3655 ExprKind::Faults(f) => match &f.principal {
3659 Some(p) => format!(
3660 "{} {} faults",
3661 expr_with_prec(&f.call, 0),
3662 call_site_actor_src(p)
3663 ),
3664 None => format!("{} faults", expr_with_prec(&f.call, 0)),
3665 },
3666 ExprKind::Observation(o) => {
3667 let subject = format!("{}.{}", o.cap.name, o.op.name);
3668 match &o.matcher {
3669 ObservationMatcher::NeverCalled => format!("{subject} never called"),
3670 ObservationMatcher::Before { cap, op } => {
3671 format!("{subject} before {}.{}", cap.name, op.name)
3672 }
3673 ObservationMatcher::Called { count, with_pred } => {
3674 let mut s = format!("{subject} called");
3675 if let Some(c) = count {
3676 if matches!(c.kind, ExprKind::IntLit { value: 1, .. }) {
3677 s.push_str(" once");
3678 } else {
3679 s.push_str(&format!(" {} times", expr_with_prec(c, 0)));
3680 }
3681 }
3682 if let Some(p) = with_pred {
3683 s.push_str(&format!(" with {}", expr_with_prec(p, 0)));
3684 }
3685 s
3686 }
3687 }
3688 }
3689 }
3690}
3691
3692fn pattern_to_string(p: &Pattern) -> String {
3693 match p {
3694 Pattern::Wildcard(_) => "_".to_string(),
3695 Pattern::Binding(id) => id.name.clone(),
3697 Pattern::Literal { value, .. } => match value {
3699 LiteralValue::Int(n) => n.to_string(),
3700 LiteralValue::Str(s) => format!("\"{}\"", escape_string(s)),
3701 LiteralValue::Bool(b) => b.to_string(),
3702 },
3703 Pattern::Refined {
3706 inner, predicate, ..
3707 } => format!(
3708 "{} where {}",
3709 pattern_to_string(inner),
3710 refinement_to_string(predicate)
3711 ),
3712 Pattern::Variant {
3713 type_name,
3714 variant,
3715 bindings,
3716 ..
3717 } => {
3718 let name_part = match type_name {
3719 Some(t) => format!("{}.{}", t.name, variant.name),
3720 None => variant.name.clone(),
3721 };
3722 if bindings.is_empty() {
3723 name_part
3724 } else {
3725 let parts: Vec<String> = bindings
3727 .iter()
3728 .map(|b| match &b.kind {
3729 PatternBindingKind::Positional { pattern } => pattern_to_string(pattern),
3730 PatternBindingKind::Named { field, pattern } => {
3731 format!("{}: {}", field.name, pattern_to_string(pattern))
3732 }
3733 })
3734 .collect();
3735 format!("{}({})", name_part, parts.join(", "))
3736 }
3737 }
3738 Pattern::Or(alts, _) => alts
3740 .iter()
3741 .map(pattern_to_string)
3742 .collect::<Vec<_>>()
3743 .join(" | "),
3744 }
3745}
3746
3747fn omit_unit_tail(b: &Block) -> bool {
3765 matches!(b.tail.kind, ExprKind::UnitLit) && b.tail_leading_comments.is_empty()
3766}
3767
3768fn format_block_oneline(b: &Block) -> String {
3769 if b.statements.is_empty() {
3770 if b.implicit_tail {
3774 "{}".to_string()
3775 } else {
3776 format!("{{ {} }}", expr_with_prec(&b.tail, 0))
3777 }
3778 } else {
3779 let mut out = String::from("{\n");
3781 block_body_lines(b, &mut out);
3782 out.push('}');
3783 out
3784 }
3785}
3786
3787fn block_body_lines(b: &Block, out: &mut String) {
3791 for stmt in &b.statements {
3792 if let Some((l, effectful, le)) = let_else_binding(stmt)
3793 && let ExprKind::Match { arms, .. } = &b.tail.kind
3794 && let Some(MatchBody::Block(rest)) = arms.first().map(|a| &a.body)
3795 {
3796 out.push('\t');
3797 out.push_str(&let_else_to_string(l, effectful, le, arms));
3798 out.push('\n');
3799 block_body_lines(rest, out);
3800 return;
3801 }
3802 out.push('\t');
3803 out.push_str(&stmt_to_string(stmt));
3804 out.push('\n');
3805 }
3806 if !omit_unit_tail(b) {
3807 out.push('\t');
3808 out.push_str(&expr_with_prec(&b.tail, 0));
3809 out.push('\n');
3810 }
3811}
3812
3813fn let_else_binding(s: &Statement) -> Option<(&LetStmt, bool, &LetElse)> {
3816 match s {
3817 Statement::Let(l) => l.let_else.as_ref().map(|le| (l, false, le)),
3818 Statement::EffectLet(l) => l.let_else.as_ref().map(|le| (l, true, le)),
3819 _ => None,
3820 }
3821}
3822
3823fn let_pattern_src(p: &Pattern) -> String {
3829 let src = pattern_to_string(p);
3830 let name_len = src
3831 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
3832 .unwrap_or(src.len());
3833 let opens_with_name = src.starts_with(|c: char| c.is_ascii_alphabetic())
3834 && !matches!(&src[..name_len], "None" | "true" | "false");
3835 let then = src[name_len..].chars().next();
3836 if opens_with_name && !matches!(then, Some('(' | '.')) {
3837 format!("({src})")
3838 } else {
3839 src
3840 }
3841}
3842
3843fn let_else_to_string(l: &LetStmt, effectful: bool, le: &LetElse, arms: &[MatchArm]) -> String {
3846 let mut out = format!(
3847 "let {} {} {}",
3848 let_pattern_src(&arms[0].pattern),
3849 if effectful { "<-" } else { "=" },
3850 expr_with_prec(&l.value, 0)
3851 );
3852 if let Some(p) = &l.principal {
3853 out.push_str(&format!(" {}", call_site_actor_src(p)));
3854 }
3855 match (le.form, arms.get(1).map(|a| &a.body)) {
3856 (LetElseForm::Block, Some(MatchBody::Block(otherwise))) => {
3857 out.push_str(&format!(" else {}", format_block_oneline(otherwise)));
3858 }
3859 _ => {
3860 let rendered: Vec<String> = arms[1..]
3861 .iter()
3862 .map(|a| {
3863 let body = match &a.body {
3864 MatchBody::Expr(e) => expr_with_prec(e, 0),
3865 MatchBody::Block(b) => format_block_oneline(b),
3866 };
3867 let guard = a
3868 .guard
3869 .as_ref()
3870 .map(|g| format!(" if {}", expr_with_prec(g, 0)))
3871 .unwrap_or_default();
3872 format!("{}{guard} => {body}", pattern_to_string(&a.pattern))
3873 })
3874 .collect();
3875 out.push_str(&format!(" else match {{ {} }}", rendered.join(", ")));
3876 }
3877 }
3878 out
3879}
3880
3881fn stmt_to_string(s: &Statement) -> String {
3882 match s {
3883 Statement::Let(l) => {
3884 let mut out = format!("let {}", l.name.name);
3885 if let Some(t) = &l.type_annot {
3886 out.push_str(&format!(": {}", type_ref_to_string(t)));
3887 }
3888 out.push_str(&format!(" = {}", expr_with_prec(&l.value, 0)));
3889 out
3890 }
3891 Statement::EffectLet(l) => {
3892 let mut out = format!("let {}", l.name.name);
3893 if let Some(t) = &l.type_annot {
3894 out.push_str(&format!(": {}", type_ref_to_string(t)));
3895 }
3896 out.push_str(&format!(" <- {}", expr_with_prec(&l.value, 0)));
3897 if let Some(p) = &l.principal {
3898 out.push_str(&format!(" {}", call_site_actor_src(p)));
3899 }
3900 out
3901 }
3902 Statement::Expect(a) => format!("expect {}", expr_with_prec(&a.value, 0)),
3903 Statement::Send(s) => format!("~> {}", expr_with_prec(&s.value, 0)),
3904 Statement::Do(d) => format!("do {}", expr_with_prec(&d.value, 0)),
3905 Statement::Assign(a) => format!("{} := {}", a.target.name, expr_with_prec(&a.value, 0)),
3906 Statement::Advance(a) => {
3907 format!("advance {} by {}", a.cap.name, expr_with_prec(&a.amount, 0))
3908 }
3909 }
3910}
3911
3912#[cfg(test)]
3913mod tests {
3914 use super::*;
3915
3916 #[test]
3919 fn binding_includes_round_trips() {
3920 let src = "adapter x {\n\tbinding \"./x.ts\" includes [\"./a.wasm\", \"b.wasm\"] requires { \"p\": \"^1\" }\n}\n";
3921 let out = format_source(src, &FormatOptions::default()).unwrap();
3922 assert_eq!(out, src);
3923 }
3924
3925 #[test]
3929 fn doc_fence_outgrows_the_marker_lines_it_holds() {
3930 let held = "commons m\n\n-------\nAbove.\n---\nInner.\n-----\nBelow.\n-------\nfn f() -> Int { 1 }\n";
3931 let out = format_source(held, &FormatOptions::default()).unwrap();
3932 assert_eq!(
3933 out,
3934 "commons m\n\n------\nAbove.\n---\nInner.\n-----\nBelow.\n------\nfn f() -> Int {\n\t1\n}\n"
3935 );
3936 assert_eq!(format_source(&out, &FormatOptions::default()).unwrap(), out);
3937 let plain = "commons m\n\n-----\nJust prose.\n-----\nfn f() -> Int { 1 }\n";
3938 assert_eq!(
3939 format_source(plain, &FormatOptions::default()).unwrap(),
3940 "commons m\n\n---\nJust prose.\n---\nfn f() -> Int {\n\t1\n}\n"
3941 );
3942 assert_eq!(doc_fence_len("a\n --- \nb"), 4);
3943 assert_eq!(doc_fence_len("a\n--\n- - -"), 3);
3944 }
3945
3946 #[test]
3950 fn each_doc_form_round_trips_unchanged() {
3951 let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---\nOne line, fenced.\n---\ntype Id = Int\n\ntype Point = {\n\t--- Across.\n\tx: Int,\n}\n\n--- One line.\ntype Run = {\n\t--- The repository.\n\trepo: String,\n\t---\n\tThe version.\n\t---\n\tversion: String,\n\t// a note\n\tsha: String, // trailing\n}\n\ntype Outcome = enum {\n\t--- Passed.\n\tPass,\n\t---\n\tFailed.\n\t---\n\tFail,\n}\n\ntype Shape =\n\t--- Round.\n\t| Circle(\n\t\t--- The radius.\n\t\tr: Int,\n\t\td: Int,\n\t)\n\t--- A dot.\n\t| Dot\n\nmessages \"en\" @reference {\n\t--- Greets.\n\t\"hi\" => \"Hello\"\n}\n";
3952 let out = format_source(src, &FormatOptions::default()).unwrap();
3953 assert_eq!(out, src);
3954 for (written, printed) in [
3958 ("--- bare", "--- bare"),
3959 ("-----\tspaced", "--- spaced"),
3960 ("--- Closed. ---", "--- Closed."),
3961 ("---- Ends in a rule ---", "---- Ends in a rule ---"),
3962 ] {
3963 let src = format!("commons m\n\n{written}\nfn f() -> Int {{ 1 }}\n");
3964 assert_eq!(
3965 format_source(&src, &FormatOptions::default()).unwrap(),
3966 format!("commons m\n\n{printed}\nfn f() -> Int {{\n\t1\n}}\n"),
3967 "{written:?}"
3968 );
3969 }
3970 }
3971
3972 #[test]
3976 fn legacy_spellings_format_to_the_new_ones() {
3977 let src = "-- A header.\ncommons m\n\n--| A run.\ntype Run = {\n\t--| The repo.\n\trepo: String,\n\tn: Int, -- trailing\n}\n\nfn d(a: Int, b: Int) -> Int { a--b }\n";
3978 let out = format_source(src, &FormatOptions::default()).unwrap();
3979 assert_eq!(
3980 out,
3981 "// A header.\ncommons m\n\n--- A run.\ntype Run = {\n\t--- The repo.\n\trepo: String,\n\tn: Int, // trailing\n}\n\nfn d(a: Int, b: Int) -> Int {\n\ta - -b\n}\n"
3982 );
3983 assert_eq!(format_source(&out, &FormatOptions::default()).unwrap(), out);
3984 let ruled = "commons m\n\n--| Ends in a rule ---\nfn f() -> Int { 1 }\n";
3986 assert_eq!(
3987 format_source(ruled, &FormatOptions::default()).unwrap(),
3988 "commons m\n\n---- Ends in a rule ---\nfn f() -> Int {\n\t1\n}\n"
3989 );
3990 }
3991
3992 #[test]
3997 fn a_half_migrated_file_is_refused_where_the_reads_differ() {
3998 let opts = FormatOptions::default();
3999 for (src, line) in [
4000 (
4001 "commons m\n\n-- note\n--- Adds two ints.\nfn add(a: Int, b: Int) -> Int { a + b }\n",
4002 "--- Adds two ints.",
4003 ),
4004 (
4005 "commons m\n\n-- note\nfn f(a: Int, b: Int) -> Int { a --b }\n",
4006 "fn f(a: Int, b: Int) -> Int { a --b }",
4007 ),
4008 ] {
4009 let err = format_source(src, &opts).unwrap_err();
4010 assert_eq!(
4011 err.errors[0].category, "bynk.fmt.mixed_comment_syntax",
4012 "{src}"
4013 );
4014 assert_eq!(&src[err.errors[0].span.range()], line, "{src}");
4015 }
4016 let divider = "-- A header.\ncommons m\n\n--- Helpers ---\n\nfn f() -> Int { 1 }\n";
4019 let err = format_source(divider, &opts).unwrap_err();
4020 assert_eq!(err.errors[0].category, "bynk.fmt.mixed_comment_syntax");
4021 let agreeing =
4022 "commons m\n\n-- note\n// new\nfn f(a: Int, b: Int) -> Int { a--b } --| old\n";
4023 assert_eq!(
4024 format_source(agreeing, &opts).unwrap(),
4025 "commons m\n\n// note\n// new\nfn f(a: Int, b: Int) -> Int {\n\ta - -b\n} // old\n"
4026 );
4027 }
4028
4029 #[test]
4033 fn a_blank_after_a_comment_under_a_doc_line_is_kept() {
4034 for (doc, printed) in [
4035 ("--- Doc.\n", "--- Doc.\n"),
4036 ("---\nDoc.\n---\n", "---\nDoc.\n---\n"),
4037 ] {
4038 let src = format!("commons m\n\n{doc}// note\n\ntype X = Int\n");
4039 assert_eq!(
4040 format_source(&src, &FormatOptions::default()).unwrap(),
4041 format!("commons m\n\n// note\n\n{printed}type X = Int\n"),
4042 "{doc:?}"
4043 );
4044 }
4045 }
4046
4047 #[test]
4050 fn an_orphaned_doc_line_run_keeps_its_form() {
4051 let src = "commons m\n\n--- Orphaned.\n\nfn f() -> Int { 1 }\n";
4052 assert_eq!(
4053 format_source(src, &FormatOptions::default()).unwrap(),
4054 "commons m\n\n--- Orphaned.\n\nfn f() -> Int {\n\t1\n}\n"
4055 );
4056 }
4057
4058 #[test]
4061 fn the_doc_loss_guard_sees_a_converted_form() {
4062 let src = "commons m\n\n--- Text.\nfn f() -> Int { 1 }\n";
4063 let tokens = tokenize(src).unwrap();
4064 let converted = "commons m\n\n---\nText.\n---\nfn f() -> Int { 1 }\n";
4065 assert!(doc_block_loss(src, &tokens, converted).is_some());
4066 assert!(doc_block_loss(src, &tokens, src).is_none());
4067 }
4068
4069 #[test]
4072 fn crlf_formats_to_the_lf_canonical_form() {
4073 let lf = "context greeting\n\n---\nA greeting for a name.\n---\nfn greet(name: String) -> String {\n\tname\n}\n";
4074 assert_eq!(format_source(lf, &FormatOptions::default()).unwrap(), lf);
4075 let crlf = lf.replace('\n', "\r\n");
4076 let formatted = format_source(&crlf, &FormatOptions::default()).unwrap();
4077 assert_eq!(formatted, lf);
4078 assert!(!formatted.contains('\r'));
4079 }
4080
4081 #[test]
4084 fn braced_declarations_always_break() {
4085 let src = "commons m\n\ntype P = { x: Int, y: Int }\n\nfn id(n: Int) -> Int { n }\n\nfn nop() -> () {}\n";
4086 let out = format_source(src, &FormatOptions::default()).unwrap();
4087 assert_eq!(
4088 out,
4089 "commons m\n\ntype P = {\n\tx: Int,\n\ty: Int,\n}\n\nfn id(n: Int) -> Int {\n\tn\n}\n\nfn nop() -> () {}\n"
4090 );
4091 assert_eq!(format_source(&out, &FormatOptions::default()).unwrap(), out);
4092 }
4093
4094 #[test]
4097 fn record_literals_break_but_match_arms_stay_compact() {
4098 let src = "commons m\n\nfn a(v: Int) -> P { P { x: v, y: v } }\n\nfn b(v: Int) -> P { P { x: v } }\n\nfn c(v: Int) -> Int { match v { 0 => 1, _ => 2 } }\n\nfn d(v: Int) -> Int { if v > 0 { 1 } else { 0 } }\n";
4099 let out = format_source(src, &FormatOptions::default()).unwrap();
4100 assert_eq!(
4101 out,
4102 "commons m\n\nfn a(v: Int) -> P {\n\tP {\n\t\tx: v,\n\t\ty: v,\n\t}\n}\n\nfn b(v: Int) -> P {\n\tP { x: v }\n}\n\nfn c(v: Int) -> Int {\n\tmatch v {\n\t\t0 => 1,\n\t\t_ => 2,\n\t}\n}\n\nfn d(v: Int) -> Int {\n\tif v > 0 { 1 } else { 0 }\n}\n"
4103 );
4104 assert_eq!(format_source(&out, &FormatOptions::default()).unwrap(), out);
4105 }
4106
4107 #[test]
4110 fn suite_header_is_spaced_and_members_keep_source_order() {
4111 let src = "suite shop\nuses money\nproperty \"p1\" {\n for all a: Int { expect a == a }\n}\ncase \"c1\" { expect true }\nproperty \"p2\" {\n for all a: Int { expect a == a }\n}\n";
4112 let out = format_source(src, &FormatOptions::default()).unwrap();
4113 assert_eq!(
4114 out,
4115 "suite shop\n\nuses money\n\nproperty \"p1\" {\n\tfor all a: Int {\n\t\texpect a == a\n\t}\n}\n\ncase \"c1\" {\n\texpect true\n}\n\nproperty \"p2\" {\n\tfor all a: Int {\n\t\texpect a == a\n\t}\n}\n"
4116 );
4117 assert_eq!(format_source(&out, &FormatOptions::default()).unwrap(), out);
4118 }
4119
4120 #[test]
4123 fn spread_literals_break_only_with_overrides() {
4124 let src =
4125 "commons m\n\nfn a(b: P) -> P { P { ...b, x: 1 } }\n\nfn c(b: P) -> P { P { ...b } }\n";
4126 let out = format_source(src, &FormatOptions::default()).unwrap();
4127 assert_eq!(
4128 out,
4129 "commons m\n\nfn a(b: P) -> P {\n\tP {\n\t\t...b,\n\t\tx: 1,\n\t}\n}\n\nfn c(b: P) -> P {\n\tP { ...b }\n}\n"
4130 );
4131 assert_eq!(format_source(&out, &FormatOptions::default()).unwrap(), out);
4132 }
4133
4134 #[test]
4135 fn normalize_line_endings_borrows_when_there_is_nothing_to_do() {
4136 assert!(matches!(
4137 normalize_line_endings("a\nb"),
4138 std::borrow::Cow::Borrowed(_)
4139 ));
4140 assert_eq!(normalize_line_endings("a\r\nb\r\n"), "a\nb\n");
4141 }
4142
4143 #[test]
4146 fn normalize_line_endings_is_a_fixed_point() {
4147 for input in ["a\r\r\nb", "a\r\nb\rc\r\n", "\r\r\r\n", "x\r", "a\r\n\rb"] {
4148 let once = normalize_line_endings(input).into_owned();
4149 assert_eq!(normalize_line_endings(&once), once, "{input:?}");
4150 assert!(!once.contains("\r\n"), "{input:?} -> {once:?}");
4151 }
4152 assert_eq!(normalize_line_endings("a\r\r\nb"), "a\nb");
4153 assert_eq!(normalize_line_endings("a\r\nb\rc"), "a\nb\rc");
4154 }
4155
4156 #[test]
4160 fn doc_block_loss_leaves_an_untokenizable_output_to_the_roundtrip_guard() {
4161 let source = "commons d\n\n---\nattached\n---\nfn f() -> Int { 1 }\n";
4162 let tokens = tokenize(source).unwrap();
4163 assert!(tokenize("commons d\n\"unterminated").is_err());
4164 assert!(
4165 doc_block_loss(source, &tokens, "commons d\n\"unterminated").is_none(),
4166 "an untokenizable output was reported as doc-block loss"
4167 );
4168 }
4169
4170 fn fmt(src: &str) -> String {
4171 format_source(src, &FormatOptions::default()).expect("format failed")
4172 }
4173
4174 #[test]
4175 fn formats_minimal_commons() {
4176 let src = "commons fitness.units {}";
4177 let out = fmt(src);
4178 assert!(out.starts_with("commons fitness.units"));
4179 let out2 = fmt(&out);
4181 assert_eq!(out, out2);
4182 }
4183
4184 #[test]
4185 fn formats_refined_type() {
4186 let src = "commons x { type Metres = Int where NonNegative }";
4187 let out = fmt(src);
4188 assert!(out.contains("type Metres = Int where NonNegative"));
4189 let out2 = fmt(&out);
4190 assert_eq!(out, out2);
4191 }
4192
4193 #[test]
4194 fn formats_function_decl() {
4195 let src = "commons x { fn add(a: Int, b: Int) -> Int { a + b } }";
4196 let out = fmt(src);
4197 assert!(out.contains("fn add(a: Int, b: Int) -> Int"));
4198 let out2 = fmt(&out);
4199 assert_eq!(out, out2);
4200 }
4201
4202 #[test]
4203 fn formats_record() {
4204 let src = "commons x { type Pt = { x: Int, y: Int } }";
4205 let out = fmt(src);
4206 let out2 = fmt(&out);
4207 assert_eq!(out, out2, "formatter not idempotent: {out}");
4208 }
4209
4210 #[test]
4211 fn formats_a_fault_claim_with_a_call_site_principal() {
4212 let src = "suite s {\n case \"c\" {\n expect api.POST(\"/o\", x) by User( \"bob\" ) faults\n expect api.GET(\"/a\") by Visitor faults\n expect box.call() faults\n }\n}\n";
4215 let out = fmt(src);
4216 assert!(
4217 out.contains("expect api.POST(\"/o\", x) by User(\"bob\") faults"),
4218 "{out}"
4219 );
4220 assert!(
4221 out.contains("expect api.GET(\"/a\") by Visitor faults"),
4222 "{out}"
4223 );
4224 assert!(out.contains("expect box.call() faults"), "{out}");
4225 assert_eq!(out, fmt(&out), "formatter not idempotent: {out}");
4226 }
4227
4228 #[test]
4229 fn formats_doc_block() {
4230 let src = "commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}";
4231 let out = fmt(src);
4232 assert!(out.contains("A descriptive doc."));
4233 let out2 = fmt(&out);
4234 assert_eq!(out, out2);
4235 }
4236
4237 #[test]
4240 fn preserves_leading_line_comment_on_decl() {
4241 let src = "commons x {\n// explain T\ntype T = Int where NonNegative\n}";
4242 let out = fmt(src);
4243 assert!(out.contains("// explain T"), "comment dropped: {out}");
4244 assert_eq!(out, fmt(&out));
4246 }
4247
4248 #[test]
4249 fn preserves_trailing_line_comment_on_decl() {
4250 let src = "commons x {\ntype T = Int where NonNegative // short\n}";
4251 let out = fmt(src);
4252 assert!(out.contains("// short"));
4253 assert!(
4255 out.lines()
4256 .any(|l| l.contains("type T") && l.contains("// short")),
4257 "trailing comment not on same line: {out}"
4258 );
4259 assert_eq!(out, fmt(&out));
4260 }
4261
4262 #[test]
4263 fn preserves_grouped_leading_comments() {
4264 let src = "commons x {\n// one\n// two\ntype T = Int where Positive\n}";
4265 let out = fmt(src);
4266 assert!(out.contains("// one"));
4267 assert!(out.contains("// two"));
4268 let i1 = out.find("// one").unwrap();
4270 let i2 = out.find("// two").unwrap();
4271 let between = &out[i1..i2];
4272 assert_eq!(
4273 between.matches('\n').count(),
4274 1,
4275 "blank line inserted: {out}"
4276 );
4277 assert_eq!(out, fmt(&out));
4278 }
4279
4280 #[test]
4281 fn preserves_comment_before_block_tail() {
4282 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
4283 let out = fmt(src);
4284 assert!(out.contains("// result"), "tail comment dropped: {out}");
4285 assert_eq!(out, fmt(&out));
4286 }
4287
4288 #[test]
4289 fn preserves_comment_with_doc_block_above_decl() {
4290 let src = "commons x {\n// TODO: rename\n---\nThe canonical T.\n---\ntype T = Int where Positive\n}";
4291 let out = fmt(src);
4292 assert!(out.contains("// TODO: rename"));
4293 assert!(out.contains("The canonical T."));
4294 let ic = out.find("// TODO: rename").unwrap();
4296 let id = out.find("The canonical T.").unwrap();
4297 let it = out.find("type T").unwrap();
4298 assert!(ic < id && id < it, "ordering wrong: {out}");
4299 assert_eq!(out, fmt(&out));
4300 }
4301
4302 #[test]
4303 fn preserves_trailing_file_comment() {
4304 let src = "commons x.y\n\ntype T = Int where Positive\n// TODO\n";
4305 let out = fmt(src);
4306 assert!(out.contains("// TODO"));
4307 assert_eq!(out, fmt(&out));
4308 }
4309
4310 #[test]
4319 fn expression_interior_comment_is_not_fully_drained_and_still_refused() {
4320 let src = "commons x {\n fn f() -> Int {\n 1 + // note\n 2\n }\n}\n";
4321 let tokens = tokenize(src).unwrap();
4322 let (_, _, fully_drained) = bynk_syntax::parser::parse_units_with_drain_check(&tokens, src)
4323 .expect("a comment inside an expression is still a valid parse");
4324 assert!(
4325 !fully_drained,
4326 "an expression-interior comment must leave the trivia table undrained"
4327 );
4328 let err = format_source(src, &FormatOptions::default())
4329 .expect_err("formatting must still refuse rather than lose the comment");
4330 assert_eq!(err.errors[0].category, "bynk.fmt.comment_loss");
4331 }
4332
4333 #[test]
4338 fn ordinary_comment_is_fully_drained_and_formats_normally() {
4339 let src = "commons x {\n// note\ntype T = Int where Positive\n}\n";
4340 let tokens = tokenize(src).unwrap();
4341 let (_, _, fully_drained) =
4342 bynk_syntax::parser::parse_units_with_drain_check(&tokens, src).expect("should parse");
4343 assert!(
4344 fully_drained,
4345 "a declaration-leading comment must be fully drained"
4346 );
4347 let out = fmt(src);
4348 assert!(out.contains("// note"));
4349 }
4350
4351 #[test]
4355 fn match_arm_block_tail_unit_after_assign_does_not_reattach_as_a_call() {
4356 let src = "commons x { fn f(status: T) -> T {\n match status {\n Draft => {\n status := Paid\n ()\n }\n Paid => (),\n }\n} }";
4362 let out = fmt(src);
4363 assert!(
4364 !out.contains("Paid()"),
4365 "the `()` tail must not re-attach to `Paid` as a call:\n{out}"
4366 );
4367 assert!(
4368 out.contains("status := Paid"),
4369 "the assignment must survive unmangled:\n{out}"
4370 );
4371 assert_eq!(out, fmt(&out), "must be idempotent");
4372 }
4373
4374 #[test]
4375 fn explicit_unit_tail_after_a_statement_is_omitted_like_an_implicit_one() {
4376 let src = "commons x { fn f() -> Effect[()] {\n let a = 1\n ()\n} }";
4380 let out = fmt(src);
4381 let expected = "commons x {\n\tfn f() -> Effect[()] {\n\t\tlet a = 1\n\t}\n}\n";
4382 assert_eq!(
4383 out, expected,
4384 "an explicit unit tail after a statement must be omitted"
4385 );
4386 assert_eq!(out, fmt(&out), "must be idempotent");
4387 }
4388
4389 #[test]
4392 fn code_only_canonical_ignores_comments() {
4393 let opts = FormatOptions::default();
4397 let bare = "commons x { type T = Int where Positive }";
4398 let commented = "commons x {\n// a note\ntype T = Int where Positive // trailing\n}";
4399 assert_eq!(
4400 code_only_canonical(bare, &opts).unwrap(),
4401 code_only_canonical(commented, &opts).unwrap(),
4402 );
4403 }
4404
4405 #[test]
4406 fn roundtrip_guard_accepts_faithful_output() {
4407 let opts = FormatOptions::default();
4409 let src = "commons x { fn add(a: Int, b: Int) -> Int { a + b } }";
4410 let out = format_source(src, &opts).unwrap();
4411 let tokens = tokenize(src).unwrap();
4412 assert!(roundtrip_divergence(&tokens, src, &out, &opts).is_none());
4413 }
4414
4415 #[test]
4416 fn roundtrip_guard_rejects_non_parsing_output() {
4417 let opts = FormatOptions::default();
4425 let src = "commons x { type T = Int where Positive }";
4426 let corrupt = "commons x { type T = Int where Positive } fn f(a: Int) -> Int { a +";
4427 assert!(
4428 corrupt.len() > src.len(),
4429 "output must be the longer buffer"
4430 );
4431 let tokens = tokenize(src).unwrap();
4432 let err = roundtrip_divergence(&tokens, src, corrupt, &opts)
4433 .expect("must reject non-parsing output");
4434 assert_eq!(err.category, "bynk.fmt.roundtrip");
4435 assert!(
4436 err.span.end <= src.len(),
4437 "roundtrip error span {:?} escapes the source it is rendered against (len {})",
4438 err.span,
4439 src.len(),
4440 );
4441 }
4442
4443 #[test]
4444 fn roundtrip_guard_rejects_structural_divergence() {
4445 let opts = FormatOptions::default();
4448 let src = "commons x { type T = Int where Positive }";
4449 let wrong = "commons x { type T = Bool }";
4450 let tokens = tokenize(src).unwrap();
4451 let err = roundtrip_divergence(&tokens, src, wrong, &opts)
4452 .expect("must reject structural divergence");
4453 assert_eq!(err.category, "bynk.fmt.roundtrip");
4454 assert!(err.span.end <= src.len(), "span escapes the source buffer");
4455 }
4456
4457 #[test]
4458 fn roundtrip_error_renders_against_source_without_panicking() {
4459 let err = roundtrip_error("the formatter produced output that no longer parses");
4466 let source = "commons x {}";
4467 let rendered = bynk_render::render_errors(std::slice::from_ref(&err), source, "<test>");
4468 assert!(
4469 rendered.contains("bynk.fmt.roundtrip"),
4470 "diagnostic did not render: {rendered}"
4471 );
4472 }
4473
4474 #[test]
4475 fn unchanged_files_without_comments_format_identically() {
4476 let src = "commons x { type T = Int where NonNegative }";
4477 let out = fmt(src);
4478 assert!(!out.contains("//"), "unexpected comment in output: {out}");
4481 }
4482
4483 #[test]
4486 fn formats_store_field_and_cell_write() {
4487 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}";
4488 let out = fmt(src);
4489 assert!(
4490 out.contains("store count: Cell[Int] = 0"),
4491 "store field not formatted: {out}"
4492 );
4493 assert!(
4494 out.contains("count := count + 1"),
4495 "cell write not formatted: {out}"
4496 );
4497 assert_eq!(out, fmt(&out), "formatter not idempotent: {out}");
4498 }
4499
4500 #[test]
4501 fn formats_store_only_agent_without_state_block() {
4502 let src = "context shop {\nagent Counter {\nkey id: String\nstore count: Cell[Int] = 0\non call get() -> Effect[Int] {\ncount\n}\n}\n}";
4503 let out = fmt(src);
4504 assert!(!out.contains("state {"), "spurious state block: {out}");
4506 assert!(out.contains("store count: Cell[Int] = 0"), "{out}");
4507 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4508 }
4509
4510 fn widths(out: &str) -> Vec<usize> {
4514 out.lines().map(|l| display_width(l, 4)).collect()
4515 }
4516
4517 fn assert_within_budget(out: &str) {
4518 let over: Vec<&str> = out.lines().filter(|l| display_width(l, 4) > 100).collect();
4519 assert!(over.is_empty(), "lines over 100 columns: {over:?}\n{out}");
4520 }
4521
4522 #[test]
4523 fn fit_test_counts_the_prefix_already_on_the_line() {
4524 let src = "commons x {\nfn authorise(amount: Int, ceiling: Int, floor: Int) -> Result[Int, Error] { if amount > ceiling { Err(Declined) } else { Ok(amount) } }\n}";
4527 let out = fmt(src);
4528 assert_within_budget(&out);
4529 assert!(
4530 out.contains("-> Result[Int, Error] {\n"),
4531 "body not broken out: {out}"
4532 );
4533 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4534 }
4535
4536 #[test]
4537 fn wraps_a_long_record_construction_one_field_per_line() {
4538 let src = "commons x {\nfn make() -> R { R { alpha: \"first value here\", beta: \"second value here\", gamma: \"third value here\", delta: \"fourth value\" } }\n}";
4539 let out = fmt(src);
4540 assert_within_budget(&out);
4541 assert!(
4542 out.contains("\t\talpha: \"first value here\",\n"),
4543 "fields not one per line: {out}"
4544 );
4545 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4546 }
4547
4548 #[test]
4549 fn wraps_a_long_argument_list_without_a_trailing_comma() {
4550 let src = "commons x {\nfn go() -> Int { combine(firstOperandValue, secondOperandValue, thirdOperandValue, fourthOperandValue) }\n}";
4554 let out = fmt(src);
4555 assert_within_budget(&out);
4556 assert!(
4557 !out.contains(",\n\t\t)"),
4558 "trailing comma in an argument list: {out}"
4559 );
4560 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4561 }
4562
4563 #[test]
4564 fn wraps_long_parameter_lists_behind_the_return_type() {
4565 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}";
4566 let out = fmt(src);
4567 assert_within_budget(&out);
4568 assert!(
4569 out.contains("\t\t\tidentifier: String,\n"),
4570 "params not wrapped: {out}"
4571 );
4572 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4573 }
4574
4575 #[test]
4576 fn breaks_a_long_chain_at_its_dots() {
4577 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}";
4578 let out = fmt(src);
4579 assert_within_budget(&out);
4580 assert!(
4581 out.contains("\n\t\t\t.collect()"),
4582 "chain not broken at the dots: {out}"
4583 );
4584 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4585 }
4586
4587 #[test]
4588 fn keeps_a_chain_intact_when_only_an_argument_needs_wrapping() {
4589 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}";
4592 let out = fmt(src);
4593 assert_within_budget(&out);
4594 assert!(
4595 out.contains("parts.fold(init, (acc, p) => match acc {"),
4596 "chain broken needlessly: {out}"
4597 );
4598 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4599 }
4600
4601 #[test]
4602 fn breaks_a_long_conjunction_before_each_operator() {
4603 let src = "commons x {\nfn ok(a: Int, b: Int, c: Int, d: Int) -> Bool { aSufficientlyLongPredicateName(a) && anotherRatherLongPredicate(b) && yetAnotherLongishPredicate(c) && theFinalPredicateHere(d) }\n}";
4604 let out = fmt(src);
4605 assert_within_budget(&out);
4606 assert!(
4607 out.lines().any(|l| l.trim_start().starts_with("&& ")),
4608 "conjunction not broken at the operators: {out}"
4609 );
4610 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4611 }
4612
4613 #[test]
4614 fn never_breaks_before_an_arithmetic_operator() {
4615 let src = "commons x {\nfn total(a: Int, b: Int, c: Int, d: Int) -> Int { someLongFunctionName(a) + anotherLongFunction(b) + aThirdLongFunction(c) + lastOne(d) }\n}";
4618 let out = fmt(src);
4619 assert!(
4620 !out.lines().any(|l| l.trim_start().starts_with("+ ")),
4621 "broke before `+`, which does not re-parse: {out}"
4622 );
4623 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4624 }
4625
4626 #[test]
4627 fn wraps_an_over_long_actor_auth_config() {
4628 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}";
4629 let out = fmt(src);
4630 assert_within_budget(&out);
4631 assert!(
4632 out.contains("\t\tissuer = "),
4633 "scheme args not wrapped: {out}"
4634 );
4635 assert!(out.contains("identity = PartnerId"), "identity lost: {out}");
4636 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4637 }
4638
4639 #[test]
4640 fn a_lone_block_like_argument_hugs_its_call() {
4641 let src = "commons x {\nfn go(items: List[Item]) -> Effect[()] { items.forEachInTurnAndOrder((item: Item) => { let _ <- store.put(item.identifier, item) }) }\n}";
4642 let out = fmt(src);
4643 assert_within_budget(&out);
4644 assert!(
4645 out.contains("((item: Item) => {"),
4646 "sole lambda argument did not hug its call: {out}"
4647 );
4648 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4649 }
4650
4651 #[test]
4652 fn an_unbreakable_line_is_left_long_rather_than_mangled() {
4653 let long = "x".repeat(140);
4656 let src = format!("commons x {{\nfn go() -> String {{ \"{long}\" }}\n}}");
4657 let out = fmt(&src);
4658 assert!(
4659 widths(&out).iter().any(|w| *w > 100),
4660 "expected an over-long line: {out}"
4661 );
4662 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4663 }
4664
4665 #[test]
4666 fn wrapping_is_idempotent_and_structure_preserving_across_widths() {
4667 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}";
4671 for width in [40u32, 60, 80, 100, 120] {
4672 let opts = FormatOptions {
4673 max_line_width: width,
4674 ..FormatOptions::default()
4675 };
4676 let out = format_source(src, &opts).unwrap_or_else(|e| {
4677 panic!("width {width}: format refused ({} errors)", e.errors.len())
4678 });
4679 let again = format_source(&out, &opts).unwrap_or_else(|e| {
4680 panic!(
4681 "width {width}: reformat refused ({} errors)",
4682 e.errors.len()
4683 )
4684 });
4685 assert_eq!(out, again, "width {width}: not idempotent:\n{out}");
4686 }
4687 }
4688
4689 #[test]
4692 fn event_schema_annotation_formats_and_is_idempotent() {
4693 let src = "context commerce.order {\nevent PaymentConfirmed @schema(2) = {\norderId: String,\n}\n}";
4694 let out = fmt(src);
4695 assert!(
4696 out.contains("event PaymentConfirmed @schema(2) = {"),
4697 "{out}"
4698 );
4699 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4700 }
4701
4702 #[test]
4705 fn event_with_no_annotation_formats_unchanged() {
4706 let src = "context commerce.order {\nevent PaymentConfirmed = {\norderId: String,\n}\n}";
4707 let out = fmt(src);
4708 assert!(out.contains("event PaymentConfirmed = {"), "{out}");
4709 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4710 }
4711
4712 #[test]
4715 fn via_schema_dispatch_formats_and_is_idempotent() {
4716 let src = "context commerce.order {\nservice OnPayment from Events(PaymentConfirmed) via schema(2) {\non event(e: PaymentConfirmed) -> Effect[()] {\nEffect.pure(())\n}\n}\n}";
4717 let out = fmt(src);
4718 assert!(
4719 out.contains("from Events(PaymentConfirmed) via schema(2)"),
4720 "{out}"
4721 );
4722 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4723 }
4724
4725 #[test]
4729 fn via_schema_dispatch_combines_with_a_payload_pattern() {
4730 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}";
4731 let out = fmt(src);
4732 assert!(
4733 out.contains("from Events(PaymentConfirmed { region: Domestic, .. }) via schema(2)"),
4734 "{out}"
4735 );
4736 assert_eq!(out, fmt(&out), "not idempotent: {out}");
4737 }
4738}