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