1use std::cell::Cell;
3use std::fmt;
4use std::sync::LazyLock;
5
6use crate::{
7 ast::{
8 Annotation, Ast, Import, InputFormat, LetBinding, MatchBranch, MergePriority, Node, Number,
9 StringChunk,
10 pattern::{
11 ArrayPattern, ConstantPattern, ConstantPatternData, EnumPattern, OrPattern, Pattern,
12 PatternData, RecordPattern, TailPattern,
13 },
14 primop::{OpPos, PrimOp},
15 record::{FieldDef, FieldMetadata, FieldPathElem, Record},
16 typ::{EnumRow, EnumRows, RecordRows, Type, iter::RecordRowsItem},
17 },
18 identifier::{Ident, LocIdent},
19 lexer::KEYWORDS,
20 typ::{DictTypeFlavour, EnumRowsF, RecordRowF, RecordRowsF, TypeF},
21};
22
23use malachite::base::num::{basic::traits::Zero, conversion::traits::ToSci};
24use pretty::docs;
25pub use pretty::{DocAllocator, DocBuilder, Pretty};
26use regex::Regex;
27
28#[derive(Clone, Copy, Eq, PartialEq)]
29pub enum StringRenderStyle {
30 ForceMonoline,
32 Multiline,
34}
35
36pub trait IsAtom {
37 fn is_atom(&self) -> bool;
40}
41
42impl IsAtom for Node<'_> {
43 fn is_atom(&self) -> bool {
44 match self {
45 Node::Null
46 | Node::Bool(..)
47 | Node::String(..)
48 | Node::StringChunks(..)
49 | Node::EnumVariant { tag: _, arg: None }
50 | Node::Record(_)
51 | Node::Array(_)
52 | Node::Var(_)
53 | Node::PrimOpApp { op: PrimOp::RecordStatAccess(_), args: _ }
54 | Node::PrimOpApp { op: PrimOp::RecordGet, args: _ }
55 | Node::PrimOpApp { op: PrimOp::BoolAnd, args: _ }
64 | Node::PrimOpApp { op: PrimOp::BoolOr, args: _ } => true,
65 Node::Number(n) => **n >= 0,
67 Node::Type(typ) => typ.is_atom(),
68 Node::Let {..}
69 | Node::IfThenElse {..}
70 | Node::EnumVariant {..}
71 | Node::Match { .. }
72 | Node::Fun{ .. }
73 | Node::App{ .. }
74 | Node::PrimOpApp {.. }
75 | Node::Annotated{ .. }
76 | Node::Import(_)
77 | Node::ParseError(_) => false,
78 }
79 }
80}
81
82impl IsAtom for Type<'_> {
83 fn is_atom(&self) -> bool {
84 match &self.typ {
85 TypeF::Dyn
86 | TypeF::Number
87 | TypeF::Bool
88 | TypeF::String
89 | TypeF::Var(_)
90 | TypeF::Record(_)
91 | TypeF::Enum(_) => true,
92 TypeF::Contract(ast) => ast.node.is_atom(),
93 _ => false,
94 }
95 }
96}
97
98fn min_interpolate_sign(text: &str) -> usize {
100 let reg = Regex::new(r#"([%]+\{)|("[%]+)"#).unwrap();
101 reg.find_iter(text)
102 .map(|m| {
103 m.end() - m.start()
112 })
113 .max()
114 .unwrap_or(1)
115}
116
117fn escape(s: &str) -> String {
119 s.replace('\\', "\\\\")
120 .replace("%{", "\\%{")
121 .replace('\"', "\\\"")
122 .replace('\n', "\\n")
123 .replace('\r', "\\r")
124}
125
126static QUOTING_REGEX: LazyLock<Regex> =
127 LazyLock::new(|| Regex::new("^_*[a-zA-Z][_a-zA-Z0-9-]*$").unwrap());
128
129pub fn ident_quoted(ident: impl Into<Ident>) -> String {
133 let ident = ident.into();
134 let label = ident.label();
135 if QUOTING_REGEX.is_match(label) && !KEYWORDS.contains(&label) {
136 String::from(label)
137 } else {
138 format!("\"{}\"", escape(label))
139 }
140}
141
142pub fn enum_tag_quoted(ident: impl Into<Ident>) -> String {
146 let ident = ident.into();
147 let label = ident.label();
148 if QUOTING_REGEX.is_match(label) {
149 String::from(label)
150 } else {
151 format!("\"{}\"", escape(label))
152 }
153}
154
155fn contains_newline<T>(chunks: &[StringChunk<T>]) -> bool {
157 chunks.iter().any(|chunk| match chunk {
158 StringChunk::Literal(str) => str.contains('\n'),
159 StringChunk::Expr(_, _) => false,
160 })
161}
162
163fn contains_carriage_return<T>(chunks: &[StringChunk<T>]) -> bool {
166 chunks.iter().any(|chunk| match chunk {
167 StringChunk::Literal(str) => str.contains('\r'),
168 StringChunk::Expr(_, _) => false,
169 })
170}
171
172fn needs_parens_in_type_pos(typ: &Type) -> bool {
181 if let TypeF::Contract(ast) = &typ.typ {
182 matches!(
183 &ast.node,
184 Node::Fun { .. } | Node::Let { .. } | Node::IfThenElse { .. } | Node::Import { .. }
185 )
186 } else {
187 false
188 }
189}
190
191pub fn fmt_pretty<T>(value: &T, f: &mut fmt::Formatter) -> fmt::Result
192where
193 T: for<'a> Pretty<'a, Allocator, ()> + Clone,
194{
195 let allocator = Allocator::default();
196 let doc: DocBuilder<_, ()> = value.clone().pretty(&allocator);
197 doc.render_fmt(80, f)
198}
199
200#[derive(Clone, Copy, Debug, Default)]
201struct SizeBound {
202 depth: usize,
203 size: usize,
204}
205
206pub struct Allocator {
218 inner: pretty::BoxAllocator,
219 bound: Option<Cell<SizeBound>>,
220}
221
222impl Default for Allocator {
224 fn default() -> Self {
225 Self {
226 inner: pretty::BoxAllocator,
227 bound: None,
228 }
229 }
230}
231
232impl Allocator {
233 pub fn bounded(max_depth: usize, max_size: usize) -> Self {
235 Self {
236 inner: pretty::BoxAllocator,
237 bound: Some(Cell::new(SizeBound {
238 depth: max_depth,
239 size: max_size,
240 })),
241 }
242 }
243
244 fn shrunken<'a, F: FnOnce(&'a Allocator) -> DocBuilder<'a, Self>>(
246 &'a self,
247 child_size: usize,
248 f: F,
249 ) -> DocBuilder<'a, Self> {
250 if let Some(bound) = self.bound.as_ref() {
251 let old = bound.get();
252 bound.set(SizeBound {
253 depth: old.depth.saturating_sub(1),
254 size: child_size,
255 });
256
257 let ret = f(self);
258
259 bound.set(old);
260
261 ret
262 } else {
263 f(self)
264 }
265 }
266
267 fn depth_constraint(&self) -> usize {
268 self.bound.as_ref().map_or(usize::MAX, |b| b.get().depth)
269 }
270
271 fn size_constraint(&self) -> usize {
272 self.bound.as_ref().map_or(usize::MAX, |b| b.get().size)
273 }
274}
275
276impl<'a> DocAllocator<'a> for Allocator {
277 type Doc = pretty::BoxDoc<'a>;
278
279 fn alloc(&'a self, doc: pretty::Doc<'a, Self::Doc>) -> Self::Doc {
280 self.inner.alloc(doc)
281 }
282
283 fn alloc_column_fn(
284 &'a self,
285 f: impl Fn(usize) -> Self::Doc + 'a,
286 ) -> <Self::Doc as pretty::DocPtr<'a, ()>>::ColumnFn {
287 self.inner.alloc_column_fn(f)
288 }
289
290 fn alloc_width_fn(
291 &'a self,
292 f: impl Fn(isize) -> Self::Doc + 'a,
293 ) -> <Self::Doc as pretty::DocPtr<'a, ()>>::WidthFn {
294 self.inner.alloc_width_fn(f)
295 }
296}
297
298impl Allocator {
299 fn record<'a>(&'a self, record: &Record) -> DocBuilder<'a, Self> {
300 let size_per_child = self.size_constraint() / record.field_defs.len().max(1);
301 if record.field_defs.is_empty() && !record.open {
302 self.text("{}")
303 } else if record.field_defs.is_empty() {
304 "{..}".pretty(self)
305 } else if size_per_child == 0 || self.depth_constraint() == 0 {
306 "{…}".pretty(self)
307 } else {
308 self.shrunken(size_per_child, |alloc| {
309 docs![
310 alloc,
311 alloc.line(),
312 alloc.intersperse(
313 record
314 .includes
315 .iter()
316 .map(|include| {
320 docs![
321 alloc,
322 "include",
323 alloc.space(),
324 include.ident.to_string(),
325 self.field_metadata(&include.metadata, true)
326 ]
327 }),
328 docs![alloc, ",", alloc.line()]
329 ),
330 if !record.includes.is_empty() {
331 docs![alloc, ",", alloc.line()]
332 } else {
333 alloc.nil()
334 },
335 alloc.intersperse(record.field_defs.iter(), docs![alloc, ",", alloc.line()]),
336 if record.open {
337 docs![alloc, ",", alloc.line(), ".."]
338 } else {
339 alloc.nil()
340 }
341 ]
342 .nest(2)
343 .append(self.line())
344 .braces()
345 .group()
346 })
347 }
348 }
349
350 fn record_type<'a>(&'a self, rows: &RecordRows) -> DocBuilder<'a, Self> {
351 let child_count = rows.iter().count().max(1);
352 let size_per_child = self.size_constraint() / child_count.max(1);
353 if size_per_child == 0 || self.depth_constraint() == 0 {
354 "{…}".pretty(self)
355 } else {
356 self.shrunken(size_per_child, |alloc| {
357 let tail = match rows.iter().last() {
358 Some(RecordRowsItem::TailDyn) => docs![alloc, ";", alloc.line(), "Dyn"],
359 Some(RecordRowsItem::TailVar(id)) => {
360 docs![alloc, ";", alloc.line(), id.to_string()]
361 }
362 _ => alloc.nil(),
363 };
364
365 let rows = rows.iter().filter_map(|r| match r {
366 RecordRowsItem::Row(r) => Some(r),
367 _ => None,
368 });
369
370 docs![
371 alloc,
372 alloc.line(),
373 alloc.intersperse(rows, docs![alloc, ",", alloc.line()]),
374 tail
375 ]
376 .nest(2)
377 .append(alloc.line())
378 .braces()
379 .group()
380 })
381 }
382 }
383
384 fn escaped_string<'a>(&'a self, s: &str) -> DocBuilder<'a, Self> {
387 self.text(escape(s))
388 }
389
390 fn chunks<'a>(
392 &'a self,
393 chunks: &[StringChunk<Ast>],
394 string_style: StringRenderStyle,
395 ) -> DocBuilder<'a, Self> {
396 let multiline = string_style == StringRenderStyle::Multiline
397 && contains_newline(chunks)
398 && !contains_carriage_return(chunks);
399
400 let nb_perc = if multiline {
401 chunks
402 .iter()
403 .map(
404 |c| {
405 if let StringChunk::Literal(s) = c {
406 min_interpolate_sign(s)
407 } else {
408 1
409 }
410 }, )
412 .max()
413 .unwrap_or(1)
414 } else {
415 1
416 };
417
418 let interp: String = "%".repeat(nb_perc);
419
420 let line_maybe = if multiline {
421 self.hardline()
422 } else {
423 self.nil()
424 };
425
426 let start_delimiter = if multiline {
427 format!("m{interp}")
428 } else {
429 String::new()
430 };
431
432 let end_delimiter = if multiline {
433 interp.clone()
434 } else {
435 String::new()
436 };
437
438 line_maybe
439 .clone()
440 .append(self.concat(chunks.iter().map(|c| {
441 match c {
442 StringChunk::Literal(s) => {
443 if multiline {
444 self.concat(
445 s.split_inclusive('\n').map(|line| {
450 if let Some(s) = line.strip_suffix('\n') {
451 self.text(s.to_owned()).append(self.hardline())
452 } else {
453 self.text(line.to_owned())
454 }
455 }),
456 )
457 } else {
458 self.escaped_string(s)
459 }
460 }
461 StringChunk::Expr(e, _i) => docs![self, interp.clone(), "{", e, "}"],
462 }
463 })))
464 .nest(if multiline { 2 } else { 0 })
465 .append(line_maybe)
466 .double_quotes()
467 .enclose(start_delimiter, end_delimiter)
468 }
469
470 fn field_metadata<'a>(
471 &'a self,
472 metadata: &FieldMetadata,
473 with_doc: bool,
474 ) -> DocBuilder<'a, Self> {
475 docs![
476 self,
477 &metadata.annotation,
478 if with_doc {
479 metadata
480 .doc
481 .map(|doc| {
482 docs![
483 self,
484 self.line(),
485 "| doc ",
486 self.chunks(
487 &[StringChunk::Literal(doc.to_owned())],
488 StringRenderStyle::Multiline
489 ),
490 ]
491 })
492 .unwrap_or_else(|| self.nil())
493 } else {
494 self.nil()
495 },
496 if metadata.opt {
497 docs![self, self.line(), "| optional"]
498 } else {
499 self.nil()
500 },
501 match &metadata.priority {
502 MergePriority::Bottom => docs![self, self.line(), "| default"],
503 MergePriority::Neutral => self.nil(),
504 MergePriority::Numeral(p) =>
505 docs![self, self.line(), "| priority ", p.to_sci().to_string()],
506 MergePriority::Top => docs![self, self.line(), "| force"],
507 }
508 ]
509 }
510
511 fn atom<'a>(&'a self, ast: &Ast) -> DocBuilder<'a, Self> {
512 ast.pretty(self).parens_if(!ast.node.is_atom())
513 }
514
515 fn type_part<'a>(&'a self, typ: &Type) -> DocBuilder<'a, Self> {
527 typ.pretty(self).parens_if(needs_parens_in_type_pos(typ))
528 }
529
530 fn pat_with_parens<'a>(&'a self, pattern: &Pattern) -> DocBuilder<'a, Self> {
537 pattern.pretty(self).parens_if(matches!(
538 pattern.data,
539 PatternData::Enum(EnumPattern {
540 pattern: Some(_),
541 ..
542 }) | PatternData::Or(_)
543 ))
544 }
545
546 fn application<'a, 'b, I, T, U>(&'a self, head: T, args: I) -> DocBuilder<'a, Self>
548 where
549 I: Iterator<Item = U>,
550 T: for<'c> Pretty<'c, Self, ()> + Clone,
551 U: for<'c> Pretty<'c, Self, ()> + Clone,
552 {
553 docs![
554 self,
555 head,
556 self.concat(args.map(|arg| docs![self, self.line(), arg]))
557 .nest(2)
558 ]
559 .group()
560 }
561}
562
563trait NickelDocBuilderExt {
564 fn parens_if(self, parens: bool) -> Self;
566}
567
568impl NickelDocBuilderExt for DocBuilder<'_, Allocator> {
569 fn parens_if(self, parens: bool) -> Self {
570 if parens { self.parens() } else { self }
571 }
572}
573
574#[derive(Copy, Clone)]
576struct Atom<'ast> {
577 inner: &'ast Ast<'ast>,
578}
579
580impl<'ast> Atom<'ast> {
581 fn new(ast: &'ast Ast<'ast>) -> Self {
582 Self { inner: ast }
583 }
584}
585
586impl<'a> Pretty<'a, Allocator> for Atom<'_> {
587 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator, ()> {
588 allocator.atom(self.inner)
589 }
590}
591
592impl<'a> Pretty<'a, Allocator> for LocIdent {
593 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
594 allocator.text(self.into_label())
595 }
596}
597
598impl<'a> Pretty<'a, Allocator> for &Annotation<'_> {
599 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
600 docs![
601 allocator,
602 if let Some(typ) = &self.typ {
603 docs![allocator, allocator.line(), ": ", allocator.type_part(typ)]
604 } else {
605 allocator.nil()
606 },
607 if !self.contracts.is_empty() {
608 allocator.line()
609 } else {
610 allocator.nil()
611 },
612 allocator.intersperse(
613 self.contracts
614 .iter()
615 .map(|t| { docs![allocator, "| ", allocator.type_part(t)] }),
616 allocator.line(),
617 )
618 ]
619 }
620}
621
622impl<'a> Pretty<'a, Allocator> for &PrimOp {
623 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
624 match self {
625 PrimOp::BoolNot => allocator.text("!"),
626 PrimOp::BoolAnd | PrimOp::BoolOr | PrimOp::RecordStatAccess(_) => {
627 unreachable!(
628 "These are handled specially since they are actually encodings \
629 of binary operators (`BoolAnd` and `BoolOr`) or need special \
630 formatting (`StaticAccess`). This currently happens in the `App` \
631 branch of `Term::pretty`"
632 )
633 }
634 PrimOp::EnumEmbed(id) => docs![
635 allocator,
636 "%enum/embed%",
637 docs![allocator, allocator.line(), id.to_string()].nest(2)
638 ],
639 PrimOp::Plus => allocator.text("+"),
640 PrimOp::Sub => allocator.text("-"),
641
642 PrimOp::Mult => allocator.text("*"),
643 PrimOp::Div => allocator.text("/"),
644 PrimOp::Modulo => allocator.text("%"),
645
646 PrimOp::Eq => allocator.text("=="),
647 PrimOp::LessThan => allocator.text("<"),
648 PrimOp::GreaterThan => allocator.text(">"),
649 PrimOp::GreaterOrEq => allocator.text(">="),
650 PrimOp::LessOrEq => allocator.text("<="),
651
652 PrimOp::Merge(_) => allocator.text("&"),
653
654 PrimOp::StringConcat => allocator.text("++"),
655 PrimOp::ArrayConcat => allocator.text("@"),
656
657 PrimOp::RecordGet => allocator.text("."),
658
659 op => allocator.text(format!("%{op}%")),
660 }
661 }
662}
663
664impl<'a> Pretty<'a, Allocator> for &Pattern<'_> {
665 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
666 let alias_prefix = if let Some(alias) = self.alias {
667 docs![
668 allocator,
669 alias.to_string(),
670 allocator.space(),
671 "@",
672 allocator.space()
673 ]
674 } else {
675 allocator.nil()
676 };
677
678 docs![allocator, alias_prefix, &self.data]
679 }
680}
681
682impl<'a> Pretty<'a, Allocator> for &PatternData<'_> {
683 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
684 match self {
685 PatternData::Wildcard => allocator.text("_"),
686 PatternData::Any(id) => allocator.as_string(id),
687 PatternData::Record(rp) => rp.pretty(allocator),
688 PatternData::Array(ap) => ap.pretty(allocator),
689 PatternData::Enum(evp) => evp.pretty(allocator),
690 PatternData::Constant(cp) => cp.pretty(allocator),
691 PatternData::Or(op) => op.pretty(allocator),
692 }
693 }
694}
695
696impl<'a> Pretty<'a, Allocator> for &ConstantPattern<'_> {
697 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
698 self.data.pretty(allocator)
699 }
700}
701
702impl<'a> Pretty<'a, Allocator> for &ConstantPatternData<'_> {
703 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
704 match self {
705 ConstantPatternData::Bool(b) => allocator.as_string(b),
706 ConstantPatternData::Number(n) => allocator.as_string(format!("{}", n.to_sci())),
707 ConstantPatternData::String(s) => allocator.escaped_string(s).double_quotes(),
708 ConstantPatternData::Null => allocator.text("null"),
709 }
710 }
711}
712
713impl<'a> Pretty<'a, Allocator> for &EnumPattern<'_> {
714 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
715 docs![
716 allocator,
717 "'",
718 enum_tag_quoted(&self.tag),
719 if let Some(ref arg_pat) = self.pattern {
720 docs![
721 allocator,
722 allocator.line(),
723 allocator.pat_with_parens(arg_pat)
724 ]
725 } else {
726 allocator.nil()
727 }
728 ]
729 }
730}
731
732impl<'a> Pretty<'a, Allocator> for &RecordPattern<'_> {
733 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
734 let RecordPattern {
735 patterns: matches,
736 tail,
737 ..
738 } = self;
739 docs![
740 allocator,
741 allocator.line(),
742 allocator.intersperse(
743 matches.iter().map(|field_pat| {
744 docs![
745 allocator,
746 field_pat.matched_id.to_string(),
747 allocator.field_metadata(
748 &FieldMetadata {
749 annotation: field_pat.annotation.clone(),
750 ..Default::default()
751 },
752 false
753 ),
754 if let Some(default) = field_pat.default.as_ref() {
755 docs![allocator, allocator.line(), "? ", allocator.atom(default),]
756 } else {
757 allocator.nil()
758 },
759 match &field_pat.pattern.data {
760 PatternData::Any(id) if *id == field_pat.matched_id => allocator.nil(),
761 _ => docs![allocator, allocator.line(), "= ", &field_pat.pattern],
762 },
763 ","
764 ]
765 .nest(2)
766 }),
767 allocator.line()
768 ),
769 match tail {
770 TailPattern::Empty => allocator.nil(),
771 TailPattern::Open => docs![allocator, allocator.line(), ".."],
772 TailPattern::Capture(id) =>
773 docs![allocator, allocator.line(), "..", id.ident().to_string()],
774 },
775 ]
776 .nest(2)
777 .append(allocator.line())
778 .braces()
779 .group()
780 }
781}
782
783impl<'a> Pretty<'a, Allocator> for &ArrayPattern<'_> {
784 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
785 docs![
786 allocator,
787 allocator.intersperse(
788 self.patterns.iter(),
789 docs![allocator, ",", allocator.line()],
790 ),
791 if !self.patterns.is_empty() && self.is_open() {
792 docs![allocator, ",", allocator.line()]
793 } else {
794 allocator.nil()
795 },
796 match self.tail {
797 TailPattern::Empty => allocator.nil(),
798 TailPattern::Open => allocator.text(".."),
799 TailPattern::Capture(id) => docs![allocator, "..", id.ident().to_string()],
800 },
801 ]
802 .nest(2)
803 .brackets()
804 .group()
805 }
806}
807
808impl<'a> Pretty<'a, Allocator> for &OrPattern<'_> {
809 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
810 docs![
811 allocator,
812 allocator.intersperse(
813 self.patterns
814 .iter()
815 .map(|pat| allocator.pat_with_parens(pat)),
816 docs![allocator, allocator.line(), "or", allocator.space()],
817 ),
818 ]
819 .group()
820 }
821}
822
823impl<'a> Pretty<'a, Allocator> for &Ast<'_> {
824 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
825 self.node.pretty(allocator)
826 }
827}
828
829impl<'a> Pretty<'a, Allocator> for &Node<'_> {
830 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
831 match self {
832 Node::Null => allocator.text("null"),
833 Node::Bool(v) => allocator.as_string(v),
834 Node::Number(n) => allocator.as_string(format!("{}", n.to_sci())),
835 Node::String(v) => allocator.escaped_string(v).double_quotes(),
836 Node::StringChunks(chunks) => allocator.chunks(chunks, StringRenderStyle::Multiline),
837 Node::IfThenElse {
838 cond,
839 then_branch,
840 else_branch,
841 } => docs![
842 allocator,
843 "if ",
844 *cond,
845 " then",
846 docs![allocator, allocator.line(), *then_branch].nest(2),
847 allocator.line(),
848 "else",
849 docs![allocator, allocator.line(), *else_branch].nest(2)
850 ]
851 .group(),
852 Node::Fun { args, body } => docs![
853 allocator,
854 "fun",
855 docs![
856 allocator,
857 allocator.concat(args.iter().map(|pat| docs![
858 allocator,
859 allocator.line(),
860 allocator.pat_with_parens(pat)
861 ])),
862 allocator.line(),
863 "=>"
864 ]
865 .nest(2),
866 docs![allocator, allocator.line(), body.pretty(allocator)].nest(2),
867 ]
868 .group(),
869 Node::Let {
870 bindings,
871 body,
872 rec,
873 } => docs![
874 allocator,
875 "let",
876 allocator.space(),
877 if *rec {
878 docs![allocator, "rec", allocator.space()]
879 } else {
880 allocator.nil()
881 },
882 allocator.intersperse(bindings.iter(), docs![allocator, ",", allocator.line()]),
883 allocator.line(),
884 "in",
885 ]
886 .nest(2)
887 .group()
888 .append(allocator.line())
889 .append(body.pretty(allocator).nest(2))
890 .group(),
891 Node::App { head, args } => match &head.node {
892 Node::PrimOpApp {
893 op: op @ (PrimOp::BoolAnd | PrimOp::BoolOr),
894 args: [fst],
895 } => {
896 let [snd] = args else {
898 panic!("pretty-printer: ill-formed `&&` or `||` with more than 2 arguments")
899 };
900
901 docs![
902 allocator,
903 allocator.atom(fst),
904 allocator.line(),
905 match op {
906 PrimOp::BoolAnd => "&& ",
907 PrimOp::BoolOr => "|| ",
908 _ => unreachable!(),
909 },
910 allocator.atom(snd)
911 ]
912 }
913 _ => allocator.application(Atom::new(head), args.iter().map(Atom::new)),
914 }
915 .group(),
916 Node::Var(id) => allocator.as_string(id),
917 Node::EnumVariant { tag, arg } => docs![
918 allocator,
919 "'",
920 allocator.text(enum_tag_quoted(tag)),
921 if let Some(arg) = arg {
922 docs![allocator, allocator.line(), allocator.atom(arg)].nest(2)
923 } else {
924 allocator.nil()
925 }
926 ]
927 .group(),
928 Node::Record(record_data) => allocator.record(record_data),
929 Node::Match(data) => docs![
930 allocator,
931 "match ",
932 docs![
933 allocator,
934 allocator.line(),
935 allocator.concat(data.branches.iter().map(|b| docs![
936 allocator,
937 b,
938 ",",
939 allocator.line()
940 ]))
941 ]
942 .nest(2)
943 .braces()
944 ]
945 .group(),
946 Node::Array(elts) => docs![
947 allocator,
948 allocator.line(),
949 allocator.intersperse(elts.iter(), allocator.text(",").append(allocator.line()),),
950 ]
951 .nest(2)
952 .append(allocator.line())
953 .brackets()
954 .group(),
955 Node::PrimOpApp {
956 op: PrimOp::RecordStatAccess(id),
957 args: [arg],
958 } => {
959 docs![allocator, allocator.atom(arg), ".", ident_quoted(id)]
960 }
961 Node::PrimOpApp {
962 op: PrimOp::BoolNot,
963 args: [arg],
964 } => docs![allocator, "!", allocator.atom(arg)],
965 Node::PrimOpApp {
966 op: PrimOp::BoolAnd,
967 args: [arg],
968 } => docs![allocator, "(&&)", allocator.line(), allocator.atom(arg)].group(),
969 Node::PrimOpApp {
970 op: PrimOp::BoolOr,
971 args: [arg],
972 } => docs![allocator, "(||)", allocator.line(), allocator.atom(arg)].group(),
973 Node::PrimOpApp {
974 op: PrimOp::RecordGet,
975 args: [field, record],
976 } => docs![allocator, record, ".", field],
977 Node::PrimOpApp {
978 op: PrimOp::Sub,
979 args: [left, right],
980 } if matches!(left.node, Node::Number(&Number::ZERO)) => {
981 docs![allocator, allocator.text("-"), allocator.atom(right)]
982 }
983 Node::PrimOpApp { op, args } => match op.positioning() {
984 OpPos::Postfix => docs![
985 allocator,
986 allocator
987 .intersperse(args.iter().map(|arg| allocator.atom(arg)), allocator.line()),
988 allocator.line(),
989 *op,
990 ]
991 .group(),
992 OpPos::Infix if args.len() == 2 => docs![
993 allocator,
994 allocator.atom(&args[0]),
995 allocator.line(),
996 *op,
997 allocator.space(),
998 allocator.atom(&args[1]),
999 ]
1000 .group(),
1001 OpPos::Prefix | OpPos::Infix => {
1004 allocator.application(*op, args.iter().map(Atom::new))
1005 }
1006 },
1007 Node::Annotated { annot, inner } => {
1008 allocator.atom(inner).append(annot.pretty(allocator))
1009 }
1010 Node::Import(Import::Path { path, format }) => {
1011 docs![
1012 allocator,
1013 "import",
1014 allocator.space(),
1015 allocator
1016 .escaped_string(path.to_string_lossy().as_ref())
1017 .double_quotes(),
1018 if Some(*format) != InputFormat::from_path(path) {
1019 docs![
1020 allocator,
1021 allocator.space(),
1022 "as",
1023 allocator.space(),
1024 "'",
1025 format.to_str()
1026 ]
1027 } else {
1028 allocator.nil()
1029 },
1030 ]
1031 }
1032 Node::Import(Import::Package { id }) => {
1033 allocator.text("import ").append(id.to_string())
1034 }
1035 Node::Type(typ) => typ.pretty(allocator),
1037 Node::ParseError(_) => allocator.text("%<parse error>"),
1038 }
1039 }
1040}
1041
1042impl<'a> Pretty<'a, Allocator> for &FieldDef<'_> {
1043 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1044 docs![
1045 allocator,
1046 allocator.intersperse(self.path.iter(), allocator.text(".")),
1047 docs![
1048 allocator,
1049 allocator.field_metadata(&self.metadata, true),
1050 if let Some(value) = &self.value {
1051 docs![
1052 allocator,
1053 if self.metadata.is_empty() {
1054 docs![allocator, allocator.space(), "=", allocator.line()]
1055 } else {
1056 docs![allocator, allocator.line(), "=", allocator.space()]
1057 },
1058 value.pretty(allocator).nest(2)
1059 ]
1060 .group()
1061 } else {
1062 allocator.nil()
1063 },
1064 ]
1065 .nest(2),
1066 ]
1067 .group()
1068 }
1069}
1070
1071impl<'a> Pretty<'a, Allocator> for &FieldPathElem<'_> {
1072 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1073 match self {
1074 FieldPathElem::Ident(id) => allocator.text(ident_quoted(id)),
1075 FieldPathElem::Expr(ast) => match &ast.node {
1076 Node::StringChunks(chunks) => {
1077 allocator.chunks(chunks, StringRenderStyle::ForceMonoline)
1078 }
1079 Node::ParseError(_) => allocator.text("%<parse error>"),
1080 _ => {
1081 panic!(
1082 "pretty printer: unexpected content of field path element (was not chunks or parse error)"
1083 );
1084 }
1085 },
1086 }
1087 }
1088}
1089
1090impl<'a> Pretty<'a, Allocator> for &LetBinding<'_> {
1091 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1092 docs![
1093 allocator,
1094 &self.pattern,
1095 allocator.field_metadata(&self.metadata.clone().into(), true),
1096 allocator.line(),
1097 "=",
1098 allocator.space(),
1099 &self.value,
1100 ]
1101 }
1102}
1103
1104impl<'a> Pretty<'a, Allocator> for &EnumRows<'_> {
1105 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1106 match &self.0 {
1107 EnumRowsF::Empty => allocator.nil(),
1108 EnumRowsF::TailVar(id) => docs![allocator, ";", allocator.line(), id.to_string()],
1109 EnumRowsF::Extend { row, tail } => {
1110 let mut result = row.pretty(allocator);
1111
1112 if let EnumRowsF::Extend { .. } = tail.0 {
1113 result = result.append(allocator.text(",").append(allocator.line()));
1114 }
1115
1116 result.append(*tail)
1117 }
1118 }
1119 }
1120}
1121
1122impl<'a> Pretty<'a, Allocator> for &EnumRow<'_> {
1123 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1124 let mut result = allocator
1125 .text("'")
1126 .append(allocator.text(enum_tag_quoted(&self.id)));
1127
1128 if let Some(typ) = self.typ.as_ref() {
1129 let ty_parenthesized = if typ.is_atom() {
1130 typ.pretty(allocator)
1131 } else {
1132 allocator
1133 .text("(")
1134 .append(allocator.line_())
1135 .append(typ.pretty(allocator))
1136 .append(allocator.line_())
1137 .append(")")
1138 };
1139
1140 result = result.append(allocator.text(" ")).append(ty_parenthesized);
1141 }
1142
1143 result
1144 }
1145}
1146
1147impl<'a> Pretty<'a, Allocator> for &RecordRows<'_> {
1148 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1149 match &self.0 {
1151 RecordRowsF::Empty => allocator.nil(),
1152 RecordRowsF::TailDyn => docs![allocator, ";", allocator.line(), "Dyn"],
1153 RecordRowsF::TailVar(id) => docs![allocator, ";", allocator.line(), id.to_string()],
1154 RecordRowsF::Extend { row, tail } => docs![
1155 allocator,
1156 row,
1157 if let RecordRowsF::Extend { .. } = tail.0 {
1158 docs![allocator, ",", allocator.line()]
1159 } else {
1160 allocator.nil()
1161 },
1162 *tail
1163 ],
1164 }
1165 }
1166}
1167
1168impl<'a, 'ast, Ty> Pretty<'a, Allocator> for &RecordRowF<Ty>
1169where
1170 Ty: std::ops::Deref<Target = Type<'ast>>,
1171{
1172 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1173 docs![
1174 allocator,
1175 ident_quoted(&self.id),
1176 " : ",
1177 allocator.type_part(self.typ.deref()),
1178 ]
1179 }
1180}
1181
1182impl<'a> Pretty<'a, Allocator> for RecordRowF<&Type<'_>> {
1183 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1184 (&self).pretty(allocator)
1185 }
1186}
1187
1188impl<'a> Pretty<'a, Allocator> for &Type<'_> {
1189 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1190 use TypeF::*;
1191 match &self.typ {
1192 Dyn => allocator.text("Dyn"),
1193 Number => allocator.text("Number"),
1194 Bool => allocator.text("Bool"),
1195 String => allocator.text("String"),
1196 Array(ty) => if ty.is_atom() {
1197 docs![allocator, "Array", allocator.line(), *ty].nest(2)
1198 } else {
1199 docs![
1200 allocator,
1201 "Array (",
1202 docs![allocator, allocator.line_(), *ty].nest(2),
1203 allocator.line_(),
1204 ")"
1205 ]
1206 }
1207 .group(),
1208 ForeignId => allocator.text("ForeignId"),
1209 Symbol => allocator.text("Symbol"),
1210 Contract(t) => t.pretty(allocator),
1211 Var(var) => allocator.as_string(var),
1212 Forall { var, body, .. } => {
1213 let mut curr = *body;
1214 let mut foralls = vec![var];
1215 while let Type {
1216 typ: Forall { var, body, .. },
1217 ..
1218 } = curr
1219 {
1220 foralls.push(var);
1221 curr = *body;
1222 }
1223 docs![
1224 allocator,
1225 "forall",
1226 allocator.line(),
1227 allocator.intersperse(
1228 foralls.iter().map(|i| allocator.as_string(i)),
1229 allocator.line(),
1230 ),
1231 ".",
1232 allocator.line(),
1233 allocator.type_part(curr)
1234 ]
1235 .nest(2)
1236 .group()
1237 }
1238 Enum(erows) => docs![allocator, allocator.line(), erows]
1239 .nest(2)
1240 .append(allocator.line())
1241 .enclose("[|", "|]")
1242 .group(),
1243 Record(rrows) => allocator.record_type(rrows),
1244 Dict {
1245 type_fields: ty,
1246 flavour: attrs,
1247 } => docs![
1248 allocator,
1249 allocator.line(),
1250 "_ ",
1251 match attrs {
1252 DictTypeFlavour::Type => ":",
1253 DictTypeFlavour::Contract => "|",
1254 },
1255 " ",
1256 allocator.type_part(ty),
1257 ]
1258 .nest(2)
1259 .append(allocator.line())
1260 .braces()
1261 .group(),
1262 Arrow(dom, codom) => docs![
1263 allocator,
1264 allocator
1265 .type_part(dom)
1266 .parens_if(matches!(dom.typ, Arrow(..) | Forall { .. }))
1267 .nest(2),
1268 allocator.line(),
1269 "-> ",
1270 allocator
1271 .type_part(codom)
1272 .parens_if(matches!(codom.typ, Forall { .. }))
1273 ]
1274 .group(),
1275 Wildcard(_) => allocator.text("_"),
1276 }
1277 }
1278}
1279
1280impl<'a> Pretty<'a, Allocator> for &MatchBranch<'_> {
1281 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1282 let guard = if let Some(guard) = &self.guard {
1283 docs![allocator, allocator.line(), "if", allocator.space(), guard]
1284 } else {
1285 allocator.nil()
1286 };
1287
1288 docs![
1289 allocator,
1290 &self.pattern,
1291 guard,
1292 allocator.space(),
1293 "=>",
1294 docs![allocator, allocator.line(), self.body.pretty(allocator),].nest(2),
1295 ]
1296 }
1297}
1298
1299#[macro_export]
1301macro_rules! impl_display_from_bytecode_pretty {
1302 ($ty:ty) => {
1303 impl std::fmt::Display for $ty {
1304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1305 $crate::ast::pretty::fmt_pretty(&self, f)
1306 }
1307 }
1308 };
1309}
1310
1311pub trait PrettyPrintCap: ToString {
1314 fn pretty_print_cap(&self, max_width: usize) -> String {
1319 let output = self.to_string();
1320
1321 if output.len() <= max_width {
1322 output
1323 } else {
1324 let (end, _) = output.char_indices().nth(max_width).unwrap();
1325 let mut truncated = String::from(&output[..end]);
1326
1327 if max_width >= 2 {
1328 truncated.pop();
1329 truncated.push('\u{2026}');
1330 }
1331
1332 truncated
1333 }
1334 }
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use crate::{
1340 ErrorTolerantParser,
1341 ast::AstAlloc,
1342 files::Files,
1343 grammar::{FixedTypeParser, TermParser},
1344 lexer::Lexer,
1345 };
1346 use pretty::Doc;
1347
1348 use super::*;
1349 use indoc::indoc;
1350
1351 fn parse_type<'ast>(ast_alloc: &'ast AstAlloc, s: &str) -> Type<'ast> {
1353 let id = Files::empty().add("<test>", s);
1354
1355 FixedTypeParser::new()
1356 .parse_strict(ast_alloc, id, Lexer::new(s))
1357 .unwrap()
1358 }
1359
1360 fn parse_term<'ast>(ast_alloc: &'ast AstAlloc, s: &str) -> Ast<'ast> {
1362 let id = Files::empty().add("<test>", s);
1363
1364 TermParser::new()
1365 .parse_strict(ast_alloc, id, Lexer::new(s))
1366 .unwrap()
1367 }
1368
1369 #[track_caller]
1373 fn assert_long_short_type(long: &str, short: &str) {
1374 let ast_alloc = AstAlloc::new();
1375 let ty = parse_type(&ast_alloc, long);
1376 let alloc = Allocator::default();
1377 let doc: DocBuilder<'_, _, ()> = ty.pretty(&alloc);
1378
1379 let mut long_lines = String::new();
1380 doc.render_fmt(usize::MAX, &mut long_lines).unwrap();
1381
1382 let mut short_lines = String::new();
1383 doc.render_fmt(0, &mut short_lines).unwrap();
1384
1385 assert_eq!(long_lines, long);
1386 assert_eq!(short_lines, short);
1387 }
1388
1389 #[track_caller]
1393 fn assert_long_short_term(long: &str, short: &str) {
1394 let ast_alloc = AstAlloc::new();
1395 let term = parse_term(&ast_alloc, long);
1396 let alloc = Allocator::default();
1397 let doc: DocBuilder<'_, _, ()> = term.pretty(&alloc);
1398
1399 let mut long_lines = String::new();
1400 doc.render_fmt(160, &mut long_lines).unwrap();
1401
1402 let mut short_lines = String::new();
1403 doc.render_fmt(0, &mut short_lines).unwrap();
1404
1405 assert_eq!(long_lines, long);
1406 assert_eq!(short_lines, short);
1407 }
1408
1409 #[test]
1410 fn pretty_array_type() {
1411 assert_long_short_type("Array String", "Array\n String");
1412 assert_long_short_type(
1413 "Array (Number -> Array Dyn)",
1414 indoc! {"
1415 Array (
1416 Number
1417 -> Array
1418 Dyn
1419 )"
1420 },
1421 );
1422 }
1423
1424 #[test]
1425 fn pretty_arrow_type() {
1426 assert_long_short_type("Number -> Number", "Number\n-> Number");
1427 assert_long_short_type(
1428 "(Number -> Number -> Dyn) -> Number",
1429 indoc! {"
1430 (Number
1431 -> Number
1432 -> Dyn)
1433 -> Number"
1434 },
1435 );
1436 }
1437
1438 #[test]
1439 fn pretty_dict_type() {
1440 assert_long_short_type(
1441 "{ _ : Number }",
1442 indoc! {"
1443 {
1444 _ : Number
1445 }"
1446 },
1447 );
1448 assert_long_short_type(
1449 "{ _ : { x : Number, y : String } }",
1450 indoc! {"
1451 {
1452 _ : {
1453 x : Number,
1454 y : String
1455 }
1456 }"
1457 },
1458 );
1459 }
1460
1461 #[test]
1462 fn pretty_record_type() {
1463 assert_long_short_type(
1464 "{ x : Number, y : String; Dyn }",
1465 indoc! {"
1466 {
1467 x : Number,
1468 y : String;
1469 Dyn
1470 }"
1471 },
1472 );
1473 }
1474
1475 #[test]
1476 fn pretty_enum_type() {
1477 assert_long_short_type(
1478 "forall r. [| 'tag1, 'tag2, 'tag3; r |]",
1479 indoc! {"
1480 forall
1481 r.
1482 [|
1483 'tag1,
1484 'tag2,
1485 'tag3;
1486 r
1487 |]"
1488 },
1489 )
1490 }
1491
1492 #[test]
1493 fn pretty_forall_type() {
1494 assert_long_short_type(
1495 "forall a r. a -> { foo : a; r }",
1496 indoc! {"
1497 forall
1498 a
1499 r.
1500 a
1501 -> {
1502 foo : a;
1503 r
1504 }"
1505 },
1506 );
1507 }
1508
1509 #[test]
1510 fn pretty_opn() {
1511 assert_long_short_term(
1512 "%string/replace% string pattern replace",
1513 indoc! {"
1514 %string/replace%
1515 string
1516 pattern
1517 replace"
1518 },
1519 );
1520 }
1521
1522 #[test]
1523 fn pretty_binop() {
1524 assert_long_short_term(
1525 "a + b",
1526 indoc! {"
1527 a
1528 + b"
1529 },
1530 );
1531 assert_long_short_term(
1532 "%string/split% string sep",
1533 indoc! {"
1534 %string/split%
1535 string
1536 sep"
1537 },
1538 );
1539 assert_long_short_term("-5", "-5");
1540 assert_long_short_term(
1541 "a - (-b)",
1542 indoc! {"
1543 a
1544 - (-b)"
1545 },
1546 );
1547 }
1548
1549 #[test]
1550 fn pretty_unop() {
1551 assert_long_short_term("!xyz", "!xyz");
1552 assert_long_short_term(
1553 "a && b",
1554 indoc! {"
1555 a
1556 && b"
1557 },
1558 );
1559 assert_long_short_term(
1560 "(a && b) && c",
1561 indoc! {"
1562 (a
1563 && b)
1564 && c"
1565 },
1566 );
1567 assert_long_short_term(
1568 "a || b",
1569 indoc! {"
1570 a
1571 || b"
1572 },
1573 );
1574 assert_long_short_term(
1575 "if true then false else not",
1576 indoc! {"
1577 if true then
1578 false
1579 else
1580 not"
1581 },
1582 );
1583 assert_long_short_term(
1584 "%enum/embed% foo bar",
1585 indoc! {"
1586 %enum/embed%
1587 foo
1588 bar"
1589 },
1590 );
1591 }
1592
1593 #[test]
1594 fn pretty_arrays() {
1595 assert_long_short_term(
1596 "[ 1, 2, 3, 4 ]",
1597 indoc! {"
1598 [
1599 1,
1600 2,
1601 3,
1602 4
1603 ]"
1604 },
1605 );
1606 }
1607
1608 #[test]
1609 fn pretty_match() {
1610 assert_long_short_term(
1611 "match { 'A => a, 'B => b, 'C => c, }",
1612 indoc! {"
1613 match {
1614 'A =>
1615 a,
1616 'B =>
1617 b,
1618 'C =>
1619 c,
1620 }"
1621 },
1622 );
1623 }
1624
1625 #[test]
1626 fn pretty_record() {
1627 assert_long_short_term("{}", "{}");
1628 assert_long_short_term(
1629 "{ a = b, c = d }",
1630 indoc! {"
1631 {
1632 a =
1633 b,
1634 c =
1635 d
1636 }"
1637 },
1638 );
1639 assert_long_short_term(
1640 r#"{ a | String | force = b, c | Number | doc "" = d }"#,
1641 indoc! {r#"
1642 {
1643 a
1644 | String
1645 | force
1646 = b,
1647 c
1648 | Number
1649 | doc ""
1650 = d
1651 }"#
1652 },
1653 );
1654 assert_long_short_term(
1655 "{ a = b, .. }",
1656 indoc! {"
1657 {
1658 a =
1659 b,
1660 ..
1661 }"
1662 },
1663 );
1664 assert_long_short_term(
1665 r#"{ a = b, "%{a}" = c, .. }"#,
1666 indoc! {r#"
1667 {
1668 a =
1669 b,
1670 "%{a}" =
1671 c,
1672 ..
1673 }"#
1674 },
1675 );
1676 assert_long_short_term(
1677 r#"{ "=" = a }"#,
1678 indoc! {r#"
1679 {
1680 "=" =
1681 a
1682 }"#
1683 },
1684 );
1685 }
1686
1687 #[test]
1688 fn pretty_let() {
1689 assert_long_short_term(
1690 "let rec foo | String = c in {}",
1691 indoc! {"
1692 let rec foo
1693 | String
1694 = c
1695 in
1696 {}"
1697 },
1698 );
1699 assert_long_short_term(
1700 "let foo = c bar in {}",
1701 indoc! {"
1702 let foo
1703 = c
1704 bar
1705 in
1706 {}"
1707 },
1708 );
1709 assert_long_short_term(
1710 "let foo | String = c bar in {}",
1711 indoc! {"
1712 let foo
1713 | String
1714 = c
1715 bar
1716 in
1717 {}"
1718 },
1719 );
1720 }
1721
1722 #[test]
1723 fn pretty_multiline_strings() {
1724 let ast_alloc = AstAlloc::new();
1725 let ast: Ast<'_> = ast_alloc
1739 .string_chunks(vec![StringChunk::Literal("\n1.".to_owned())])
1740 .into();
1741 assert_eq!(format!("{ast}"), "m%\"\n \n 1.\n\"%");
1742
1743 let ast: Ast<'_> = ast_alloc
1744 .string_chunks(vec![StringChunk::Literal(
1745 "a multiline string\n\n\n\n".to_owned(),
1746 )])
1747 .into();
1748 assert_eq!(
1749 format!("{ast}"),
1750 "m%\"\n a multiline string\n \n \n \n\n\"%"
1751 );
1752 }
1753
1754 #[test]
1755 fn pretty_let_pattern() {
1756 assert_long_short_term(
1757 "let foo @ { a | Bool ? true = a', b ? false, } = c in {}",
1758 indoc! {"
1759 let foo @ {
1760 a
1761 | Bool
1762 ? true
1763 = a',
1764 b
1765 ? false,
1766 }
1767 = c
1768 in
1769 {}"
1770 },
1771 );
1772 assert_long_short_term(
1773 "let foo @ { a = a', b = e @ { foo, .. }, } = c in {}",
1774 indoc! {"
1775 let foo @ {
1776 a
1777 = a',
1778 b
1779 = e @ {
1780 foo,
1781 ..
1782 },
1783 }
1784 = c
1785 in
1786 {}"
1787 },
1788 );
1789 assert_long_short_term(
1790 "let foo @ { a = a', b, } | String = c in {}",
1791 indoc! {"
1792 let foo @ {
1793 a
1794 = a',
1795 b,
1796 }
1797 | String
1798 = c
1799 in
1800 {}"
1801 },
1802 );
1803 }
1804
1805 #[test]
1806 fn pretty_fun() {
1807 assert_long_short_term(
1808 "fun x y z => x y z",
1809 indoc! {"
1810 fun
1811 x
1812 y
1813 z
1814 =>
1815 x
1816 y
1817 z"
1818 },
1819 );
1820 assert_long_short_term(
1821 "fun x @ { foo, bar ? true, } y @ { baz, } => x y z",
1822 indoc! {"
1823 fun
1824 x @ {
1825 foo,
1826 bar
1827 ? true,
1828 }
1829 y @ {
1830 baz,
1831 }
1832 =>
1833 x
1834 y
1835 z"
1836 },
1837 );
1838 }
1839
1840 #[test]
1841 fn pretty_app() {
1842 assert_long_short_term(
1843 "x y z",
1844 indoc! {"
1845 x
1846 y
1847 z"
1848 },
1849 );
1850 }
1851
1852 #[track_caller]
1859 fn assert_format_eq(s: &str) {
1860 let ast_alloc = AstAlloc::new();
1861 let ty = parse_type(&ast_alloc, s);
1862 assert_eq!(s, &format!("{ty}"));
1863 }
1864
1865 #[test]
1866 fn types_pretty_printing() {
1867 assert_format_eq("Number");
1868 assert_format_eq("Number -> Number");
1869 assert_format_eq("(Number -> Number) -> (Number -> Number) -> Number -> Number");
1870 assert_format_eq("((Number -> Number) -> Number) -> Number");
1871 assert_format_eq("Number -> (forall a. a -> String) -> String");
1872
1873 assert_format_eq("{ _ : String }");
1874 assert_format_eq("{ _ : (String -> String) -> String }");
1875 assert_format_eq("{ _ | String }");
1876 assert_format_eq("{ _ | (String -> String) -> String }");
1877
1878 assert_format_eq("{ x : (Bool -> Bool) -> Bool, y : Bool }");
1879 assert_format_eq("forall r. { x : Bool, y : Bool, z : Bool; r }");
1880 assert_format_eq("{ x : Bool, y : Bool, z : Bool }");
1881
1882 assert_format_eq("[| 'a, 'b, 'c, 'd |]");
1883 assert_format_eq("forall r. [| 'tag1, 'tag2, 'tag3; r |]");
1884
1885 assert_format_eq("Array Number");
1886 assert_format_eq("Array (Array Number)");
1887 assert_format_eq("Number -> Array (Array String) -> Number");
1888 assert_format_eq("Array (Number -> Number)");
1889 assert_format_eq("Array (Array (Array Dyn) -> Number)");
1890
1891 assert_format_eq("_");
1892 assert_format_eq("_ -> _");
1893 assert_format_eq("{ x : _, y : Bool }");
1894 assert_format_eq("{ _ : _ }");
1895 }
1896
1897 fn format_short_term(input: &str, depth: usize, size: usize) -> String {
1898 let ast_alloc = AstAlloc::new();
1899 let term = parse_term(&ast_alloc, input);
1900 let allocator = Allocator::bounded(depth, size);
1901 let doc: DocBuilder<_, ()> = term.pretty(&allocator);
1902 Doc::pretty(&doc, 1000).to_string()
1903 }
1904
1905 #[test]
1906 fn bounded_pretty_printing() {
1907 assert_eq!("{ hello = 1 }", &format_short_term("{hello = 1}", 1, 1));
1908 assert_eq!("{…}", &format_short_term("{hello = 1, bye = 2}", 1, 1));
1909 assert_eq!(
1910 "{ hello = 1, inner = { bye = 2 } }",
1911 &format_short_term("{hello = 1, inner = { bye = 2 }}", 2, 2)
1912 );
1913 assert_eq!(
1914 "{ hello = 1, inner = {…} }",
1915 &format_short_term("{hello = 1, inner = { bye = 2 }}", 1, 100)
1916 );
1917 assert_eq!(
1918 "{ hello = 1, inner = {…} }",
1919 &format_short_term("{hello = 1, inner = { bye = 2, other = 3 }}", 100, 2)
1920 );
1921 }
1922}