1use std::cell::Cell;
2use std::fmt;
3
4use crate::{
5 cache::InputFormat,
6 eval::value::{Container, NickelValue, ValueContentRef},
7 identifier::{Ident, LocIdent},
8 parser::lexer::KEYWORDS,
9 term::{
10 self,
11 record::{Field, FieldMetadata, Include, RecordData},
12 *,
13 },
14 typ::*,
15};
16
17use malachite::base::num::conversion::traits::ToSci;
18use once_cell::sync::Lazy;
19use pretty::docs;
20pub use pretty::{DocAllocator, DocBuilder, Pretty};
21use regex::Regex;
22
23#[derive(Clone, Copy, Eq, PartialEq)]
24pub enum StringRenderStyle {
25 ForceMonoline,
27 Multiline,
29}
30
31fn min_interpolate_sign(text: &str) -> usize {
33 let reg = Regex::new(r#"([%]+\{)|("[%]+)"#).unwrap();
34 reg.find_iter(text)
35 .map(|m| {
36 m.end() - m.start()
45 })
46 .max()
47 .unwrap_or(1)
48}
49
50fn sorted_map<K: Ord, V>(m: &'_ IndexMap<K, V>) -> Vec<(&'_ K, &'_ V)> {
51 let mut ret: Vec<(&K, &V)> = m.iter().collect();
52 ret.sort_by_key(|(k, _)| *k);
53 ret
54}
55
56fn escape(s: &str) -> String {
58 s.replace('\\', "\\\\")
59 .replace("%{", "\\%{")
60 .replace('\"', "\\\"")
61 .replace('\n', "\\n")
62 .replace('\r', "\\r")
63}
64
65static QUOTING_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("^_*[a-zA-Z][_a-zA-Z0-9-]*$").unwrap());
66
67pub fn ident_quoted(ident: impl Into<Ident>) -> String {
71 let ident = ident.into();
72 let label = ident.label();
73 if QUOTING_REGEX.is_match(label) && !KEYWORDS.contains(&label) {
74 String::from(label)
75 } else {
76 format!("\"{}\"", escape(label))
77 }
78}
79
80pub fn enum_tag_quoted(ident: impl Into<Ident>) -> String {
84 let ident = ident.into();
85 let label = ident.label();
86 if QUOTING_REGEX.is_match(label) {
87 String::from(label)
88 } else {
89 format!("\"{}\"", escape(label))
90 }
91}
92
93#[derive(Clone, Copy, Eq, PartialEq, Debug)]
94enum RecursivePriority {
95 Default,
96 Force,
97 None,
98}
99
100impl RecursivePriority {
101 fn is_present(&self) -> bool {
102 !matches!(self, RecursivePriority::None)
103 }
104}
105
106fn split_recursive_priority(value: &NickelValue) -> (RecursivePriority, NickelValue) {
116 if let Some(Term::App(AppData { head, arg })) = value.as_term() {
117 match head.as_term() {
118 Some(Term::Var(id)) if id.label() == "$rec_default" => {
119 return (RecursivePriority::Default, arg.clone());
120 }
121 Some(Term::Var(id)) if id.label() == "$rec_force" => {
122 return (RecursivePriority::Force, arg.clone());
123 }
124 _ => (),
125 }
126 };
127 (RecursivePriority::None, value.clone())
128}
129
130fn contains_newline<T>(chunks: &[StrChunk<T>]) -> bool {
132 chunks.iter().any(|chunk| match chunk {
133 StrChunk::Literal(str) => str.contains('\n'),
134 StrChunk::Expr(_, _) => false,
135 })
136}
137
138fn contains_carriage_return<T>(chunks: &[StrChunk<T>]) -> bool {
141 chunks.iter().any(|chunk| match chunk {
142 StrChunk::Literal(str) => str.contains('\r'),
143 StrChunk::Expr(_, _) => false,
144 })
145}
146
147fn needs_parens_in_type_pos(typ: &Type) -> bool {
156 if let TypeF::Contract(term) = &typ.typ {
157 match term.content_ref() {
158 ValueContentRef::Term(
159 Term::Fun(..) | Term::Let(..) | Term::Import { .. } | Term::ResolvedImport(..),
160 )
161 | ValueContentRef::CustomContract(_) => true,
162 ValueContentRef::Term(Term::Op1(data)) => matches!(data.op, UnaryOp::IfThenElse),
163 _ => false,
164 }
165 } else {
166 false
167 }
168}
169
170pub fn fmt_pretty<T>(value: &T, f: &mut fmt::Formatter) -> fmt::Result
171where
172 T: for<'a> Pretty<'a, Allocator, ()> + Clone,
173{
174 let allocator = Allocator::default();
175 let doc: DocBuilder<_, ()> = value.clone().pretty(&allocator);
176 doc.render_fmt(80, f)
177}
178
179#[derive(Clone, Copy, Debug, Default)]
180struct SizeBound {
181 depth: usize,
182 size: usize,
183}
184
185pub struct Allocator {
197 inner: pretty::BoxAllocator,
198 bound: Option<Cell<SizeBound>>,
199}
200
201impl Default for Allocator {
203 fn default() -> Self {
204 Self {
205 inner: pretty::BoxAllocator,
206 bound: None,
207 }
208 }
209}
210
211impl Allocator {
212 pub fn bounded(max_depth: usize, max_size: usize) -> Self {
214 Self {
215 inner: pretty::BoxAllocator,
216 bound: Some(Cell::new(SizeBound {
217 depth: max_depth,
218 size: max_size,
219 })),
220 }
221 }
222
223 fn shrunken<'a, F: FnOnce(&'a Allocator) -> DocBuilder<'a, Self>>(
225 &'a self,
226 child_size: usize,
227 f: F,
228 ) -> DocBuilder<'a, Self> {
229 if let Some(bound) = self.bound.as_ref() {
230 let old = bound.get();
231 bound.set(SizeBound {
232 depth: old.depth.saturating_sub(1),
233 size: child_size,
234 });
235
236 let ret = f(self);
237
238 bound.set(old);
239
240 ret
241 } else {
242 f(self)
243 }
244 }
245
246 fn depth_constraint(&self) -> usize {
247 self.bound.as_ref().map_or(usize::MAX, |b| b.get().depth)
248 }
249
250 fn size_constraint(&self) -> usize {
251 self.bound.as_ref().map_or(usize::MAX, |b| b.get().size)
252 }
253}
254
255impl<'a> DocAllocator<'a> for Allocator {
256 type Doc = pretty::BoxDoc<'a>;
257
258 fn alloc(&'a self, doc: pretty::Doc<'a, Self::Doc>) -> Self::Doc {
259 self.inner.alloc(doc)
260 }
261
262 fn alloc_column_fn(
263 &'a self,
264 f: impl Fn(usize) -> Self::Doc + 'a,
265 ) -> <Self::Doc as pretty::DocPtr<'a, ()>>::ColumnFn {
266 self.inner.alloc_column_fn(f)
267 }
268
269 fn alloc_width_fn(
270 &'a self,
271 f: impl Fn(isize) -> Self::Doc + 'a,
272 ) -> <Self::Doc as pretty::DocPtr<'a, ()>>::WidthFn {
273 self.inner.alloc_width_fn(f)
274 }
275}
276
277impl Allocator {
278 fn record<'a>(
279 &'a self,
280 record_data: &RecordData,
281 includes: &[Include],
282 dyn_fields: &[(NickelValue, Field)],
283 ) -> DocBuilder<'a, Self> {
284 let size_per_child =
285 self.size_constraint() / (record_data.fields.len() + dyn_fields.len()).max(1);
286 if record_data.fields.is_empty()
287 && dyn_fields.is_empty()
288 && includes.is_empty()
289 && !record_data.attrs.open
290 {
291 self.text("{}")
292 } else if size_per_child == 0 || self.depth_constraint() == 0 {
293 "{…}".pretty(self)
294 } else {
295 self.shrunken(size_per_child, |alloc| {
296 docs![
297 alloc,
298 alloc.line(),
299 alloc.intersperse(
300 includes
301 .iter()
302 .map(|incl| {
306 docs![
307 alloc,
308 "include",
309 alloc.space(),
310 incl.ident.to_string(),
311 self.field_metadata(&incl.metadata, true)
312 ]
313 }),
314 docs![alloc, ",", alloc.line()]
315 ),
316 if !includes.is_empty() {
317 docs![alloc, ",", alloc.line()]
318 } else {
319 alloc.nil()
320 },
321 alloc.fields(&record_data.fields),
322 if !dyn_fields.is_empty() {
323 docs![alloc, alloc.line(), alloc.dyn_fields(dyn_fields)]
324 } else {
325 alloc.nil()
326 },
327 if record_data.attrs.open {
328 docs![alloc, alloc.line(), ".."]
329 } else {
330 alloc.nil()
331 }
332 ]
333 .nest(2)
334 .append(self.line())
335 .braces()
336 .group()
337 })
338 }
339 }
340
341 fn record_type<'a>(&'a self, rows: &RecordRows) -> DocBuilder<'a, Self> {
342 let child_count = rows.iter().count().max(1);
343 let size_per_child = self.size_constraint() / child_count.max(1);
344 if size_per_child == 0 || self.depth_constraint() == 0 {
345 "{…}".pretty(self)
346 } else {
347 self.shrunken(size_per_child, |alloc| {
348 let tail = match rows.iter().last() {
349 Some(RecordRowsIteratorItem::TailDyn) => docs![alloc, ";", alloc.line(), "Dyn"],
350 Some(RecordRowsIteratorItem::TailVar(id)) => {
351 docs![alloc, ";", alloc.line(), id.to_string()]
352 }
353 _ => alloc.nil(),
354 };
355
356 let rows = rows.iter().filter_map(|r| match r {
357 RecordRowsIteratorItem::Row(r) => Some(r),
358 _ => None,
359 });
360
361 docs![
362 alloc,
363 alloc.line(),
364 alloc.intersperse(rows, docs![alloc, ",", alloc.line()]),
365 tail
366 ]
367 .nest(2)
368 .append(alloc.line())
369 .braces()
370 .group()
371 })
372 }
373 }
374
375 fn escaped_string<'a>(&'a self, s: &str) -> DocBuilder<'a, Self> {
378 self.text(escape(s))
379 }
380
381 fn chunks<'a>(
383 &'a self,
384 chunks: &[StrChunk<NickelValue>],
385 string_style: StringRenderStyle,
386 ) -> DocBuilder<'a, Self> {
387 let multiline = string_style == StringRenderStyle::Multiline
388 && contains_newline(chunks)
389 && !contains_carriage_return(chunks);
390
391 let nb_perc = if multiline {
392 chunks
393 .iter()
394 .map(
395 |c| {
396 if let StrChunk::Literal(s) = c {
397 min_interpolate_sign(s)
398 } else {
399 1
400 }
401 }, )
403 .max()
404 .unwrap_or(1)
405 } else {
406 1
407 };
408
409 let interp: String = "%".repeat(nb_perc);
410
411 let line_maybe = if multiline {
412 self.hardline()
413 } else {
414 self.nil()
415 };
416
417 let start_delimiter = if multiline {
418 format!("m{interp}")
419 } else {
420 String::new()
421 };
422
423 let end_delimiter = if multiline {
424 interp.clone()
425 } else {
426 String::new()
427 };
428
429 line_maybe
430 .clone()
431 .append(self.concat(chunks.iter().rev().map(|c| {
432 match c {
433 StrChunk::Literal(s) => {
434 if multiline {
435 self.concat(
436 s.split_inclusive('\n').map(|line| {
441 if let Some(s) = line.strip_suffix('\n') {
442 self.text(s.to_owned()).append(self.hardline())
443 } else {
444 self.text(line.to_owned())
445 }
446 }),
447 )
448 } else {
449 self.escaped_string(s)
450 }
451 }
452 StrChunk::Expr(e, _i) => docs![self, interp.clone(), "{", e, "}"],
453 }
454 })))
455 .nest(if multiline { 2 } else { 0 })
456 .append(line_maybe)
457 .double_quotes()
458 .enclose(start_delimiter, end_delimiter)
459 }
460
461 fn binding<'a>(&'a self, lhs: impl Pretty<'a, Self>, val: NickelValue) -> DocBuilder<'a, Self> {
462 docs![
463 self,
464 lhs,
465 if let Some(Term::Annotated(data)) = val.as_term() {
466 data.annot.pretty(self)
467 } else {
468 self.nil()
469 },
470 self.line(),
471 "= ",
472 if let Some(Term::Annotated(data)) = val.as_term() {
473 data.inner.pretty(self)
474 } else {
475 val.pretty(self)
476 },
477 ]
478 }
479
480 fn function<'a>(
485 &'a self,
486 first_param: impl Pretty<'a, Self>,
487 mut body: &NickelValue,
488 ) -> DocBuilder<'a, Self> {
489 let mut builder = docs![self, "fun", self.line(), first_param];
490
491 while let Some(Term::Fun(data)) = body.as_term() {
492 builder = docs![self, builder, self.line(), self.as_string(data.arg)];
493 body = &data.body;
494 }
495
496 docs![
497 self,
498 builder,
499 self.line(),
500 "=>",
501 self.line(),
502 body.pretty(self)
503 ]
504 .nest(2)
505 .group()
506 }
507
508 fn field_metadata<'a>(
509 &'a self,
510 metadata: &FieldMetadata,
511 with_doc: bool,
512 ) -> DocBuilder<'a, Self> {
513 docs![
514 self,
515 &metadata.annotation,
516 if with_doc {
517 metadata
518 .doc
519 .clone()
520 .map(|doc| {
521 docs![
522 self,
523 self.line(),
524 "| doc ",
525 self.chunks(
526 &[StrChunk::Literal(doc.to_string())],
527 StringRenderStyle::Multiline
528 ),
529 ]
530 })
531 .unwrap_or_else(|| self.nil())
532 } else {
533 self.nil()
534 },
535 if metadata.opt {
536 docs![self, self.line(), "| optional"]
537 } else {
538 self.nil()
539 },
540 match &metadata.priority {
541 MergePriority::Bottom => docs![self, self.line(), "| default"],
542 MergePriority::Neutral => self.nil(),
543 MergePriority::Numeral(p) =>
544 docs![self, self.line(), "| priority ", p.to_sci().to_string()],
545 MergePriority::Top => docs![self, self.line(), "| force"],
546 }
547 ]
548 }
549
550 fn field<'a>(&'a self, id: &LocIdent, field: &Field) -> DocBuilder<'a, Self> {
551 self.text(ident_quoted(id))
552 .append(self.field_body(field))
553 .group()
554 }
555
556 fn dyn_field<'a>(&'a self, id_expr: &NickelValue, field: &Field) -> DocBuilder<'a, Self> {
557 match id_expr.as_term() {
558 Some(Term::StrChunks(chunks)) => {
560 self.chunks(chunks.as_slice(), StringRenderStyle::ForceMonoline)
561 }
562 Some(Term::ParseError(_)) => docs![self, "%<parse error>"],
563 _ => panic!("Dynamic record fields must be StrChunks currently"),
564 }
565 .append(self.field_body(field))
566 .group()
567 }
568
569 fn field_body<'a>(&'a self, field: &Field) -> DocBuilder<'a, Self> {
570 docs![
571 self,
572 if let Some(metadata) = field.metadata.as_ref() {
573 self.field_metadata(metadata, true)
574 } else {
575 self.nil()
576 },
577 if let Some((priority, value)) = field.value.as_ref().map(split_recursive_priority) {
578 let has_metadata = !field.metadata.is_empty() || priority.is_present();
579
580 docs![
581 self,
582 priority,
583 if has_metadata {
584 docs![self, self.line(), "= "]
585 } else {
586 docs![self, " =", self.line()]
587 },
588 value.pretty(self).nest(2)
589 ]
590 } else {
591 self.nil()
592 },
593 ","
594 ]
595 .nest(2)
596 }
597
598 fn fields<'a>(&'a self, fields: &IndexMap<LocIdent, Field>) -> DocBuilder<'a, Self> {
599 self.intersperse(
600 sorted_map(fields)
601 .iter()
602 .map(|(id, field)| self.field(id, field)),
603 self.line(),
604 )
605 }
606
607 fn dyn_fields<'a>(&'a self, fields: &[(NickelValue, Field)]) -> DocBuilder<'a, Self> {
608 self.intersperse(
609 fields
610 .iter()
611 .map(|(id_term, field)| self.dyn_field(id_term, field)),
612 self.line(),
613 )
614 }
615
616 fn atom<'a>(&'a self, val: &NickelValue) -> DocBuilder<'a, Self> {
617 val.pretty(self).parens_if(!val.fmt_is_atom())
618 }
619
620 fn type_part<'a>(&'a self, typ: &Type) -> DocBuilder<'a, Self> {
632 typ.pretty(self).parens_if(needs_parens_in_type_pos(typ))
633 }
634}
635
636trait NickelDocBuilderExt {
637 fn parens_if(self, parens: bool) -> Self;
639}
640
641impl NickelDocBuilderExt for DocBuilder<'_, Allocator> {
642 fn parens_if(self, parens: bool) -> Self {
643 if parens { self.parens() } else { self }
644 }
645}
646
647impl<'a> Pretty<'a, Allocator> for LocIdent {
648 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
649 allocator.text(self.into_label())
650 }
651}
652
653impl<'a> Pretty<'a, Allocator> for RecursivePriority {
654 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
655 match self {
656 RecursivePriority::Default => allocator.text("| rec default"),
657 RecursivePriority::Force => allocator.text("| rec force"),
658 RecursivePriority::None => allocator.nil(),
659 }
660 }
661}
662
663impl<'a> Pretty<'a, Allocator> for &TypeAnnotation {
664 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
665 docs![
666 allocator,
667 if let Some(typ) = &self.typ {
668 docs![
669 allocator,
670 allocator.line(),
671 ": ",
672 allocator.type_part(&typ.typ)
673 ]
674 } else {
675 allocator.nil()
676 },
677 if !self.contracts.is_empty() {
678 allocator.line()
679 } else {
680 allocator.nil()
681 },
682 allocator.intersperse(
683 self.contracts
684 .iter()
685 .map(|c| { docs![allocator, "| ", allocator.type_part(&c.typ)] }),
686 allocator.line(),
687 )
688 ]
689 }
690}
691
692impl<'a> Pretty<'a, Allocator> for &UnaryOp {
693 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
694 use UnaryOp::*;
695 match self {
696 BoolNot => allocator.text("!"),
697 BoolAnd | BoolOr | RecordAccess(_) => {
698 unreachable!(
699 "These are handled specially since they are actually encodings \
700 of binary operators (`BoolAnd` and `BoolOr`) or need special \
701 formatting (`StaticAccess`). This currently happens in the `App` \
702 branch of `Term::pretty`"
703 )
704 }
705 EnumEmbed(id) => docs![
706 allocator,
707 "%enum/embed%",
708 docs![allocator, allocator.line(), id.to_string()].nest(2)
709 ],
710 op => allocator.text(format!("%{op}%")).append(allocator.space()),
711 }
712 }
713}
714
715impl<'a> Pretty<'a, Allocator> for &BinaryOp {
716 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
717 use BinaryOp::*;
718 match self {
719 Plus => allocator.text("+"),
720 Sub => allocator.text("-"),
721
722 Mult => allocator.text("*"),
723 Div => allocator.text("/"),
724 Modulo => allocator.text("%"),
725
726 Eq => allocator.text("=="),
727 LessThan => allocator.text("<"),
728 GreaterThan => allocator.text(">"),
729 GreaterOrEq => allocator.text(">="),
730 LessOrEq => allocator.text("<="),
731
732 Merge(_) => allocator.text("&"),
733
734 StringConcat => allocator.text("++"),
735 ArrayConcat => allocator.text("@"),
736
737 RecordGet => allocator.text("."),
738
739 op => allocator.as_string(format!("%{op}%")),
740 }
741 }
742}
743
744impl<'a> Pretty<'a, Allocator> for &NAryOp {
745 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
746 allocator.as_string(format!("%{self}%"))
747 }
748}
749
750impl<'a> Pretty<'a, Allocator> for &NickelValue {
751 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
752 match self.content_ref() {
753 ValueContentRef::Null => allocator.text("null"),
754 ValueContentRef::Bool(b) => allocator.as_string(b),
755 ValueContentRef::Number(n) => allocator.as_string(n.to_sci()),
756 ValueContentRef::Array(container) =>
757 {
760 docs![
761 allocator,
762 allocator.line(),
763 allocator.intersperse(
764 container.iter().map(|val| val.pretty(allocator)),
765 allocator.text(",").append(allocator.line()),
766 ),
767 ]
768 .nest(2)
769 .append(allocator.line())
770 .brackets()
771 .group()
772 }
773 ValueContentRef::Record(Container::Empty) => allocator.text("{}"),
774 ValueContentRef::Record(Container::Alloc(record)) => allocator.record(record, &[], &[]),
775 ValueContentRef::String(s) => allocator.escaped_string(s).double_quotes(),
776 ValueContentRef::Thunk(thunk) => allocator.text(format!("%<closure@{thunk:p}>")),
777 ValueContentRef::Term(term) => term.pretty(allocator),
778 ValueContentRef::Label(_label) => allocator.text("%<label>").append(allocator.line()),
779 ValueContentRef::EnumVariant(enum_variant) => {
780 let tag = allocator
781 .text("'")
782 .append(allocator.text(enum_tag_quoted(&enum_variant.tag)));
783
784 if let Some(arg) = &enum_variant.arg {
785 docs![allocator, tag, allocator.line(), allocator.atom(arg)]
786 .nest(2)
787 .group()
788 } else {
789 tag
790 }
791 }
792 ValueContentRef::ForeignId(_) => allocator.text("%<foreign>"),
793 ValueContentRef::SealingKey(key) => allocator.text(format!("%<sealing key: {key}>")),
794 ValueContentRef::CustomContract(custom_ctr) => docs![
796 allocator,
797 "%contract/custom%",
798 docs![allocator, allocator.line(), allocator.atom(custom_ctr),].nest(2),
799 ]
800 .group(),
801 ValueContentRef::Type(typ) => typ.typ.pretty(allocator),
803 }
804 }
805}
806
807impl<'a> Pretty<'a, Allocator> for &Term {
808 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
809 match self {
810 Term::StrChunks(chunks) => allocator.chunks(chunks, StringRenderStyle::Multiline),
811 Term::Fun(data) => allocator.function(allocator.as_string(data.arg), &data.body),
812 Term::Let(data) => docs![
813 allocator,
814 "let",
815 allocator.space(),
816 if data.attrs.rec {
817 docs![allocator, "rec", allocator.space()]
818 } else {
819 allocator.nil()
820 },
821 allocator.intersperse(
822 data.bindings
823 .iter()
824 .map(|(k, v)| allocator.binding(*k, v.clone())),
825 docs![allocator, ",", allocator.line()]
826 ),
827 allocator.line(),
828 "in",
829 ]
830 .nest(2)
831 .group()
832 .append(allocator.line())
833 .append(data.body.pretty(allocator).nest(2))
834 .group(),
835 Term::App(AppData { head, arg }) => match head.as_term() {
836 Some(Term::App(AppData { head: lazy_op, arg: arg_op }))
837 if matches!(lazy_op.as_term(), Some(Term::Op1(data_op)) if data_op.op == UnaryOp::IfThenElse) =>
838 {
839 let Some(Term::Op1(Op1Data { op: UnaryOp::IfThenElse, arg: cond })) = lazy_op.as_term() else {
840 unreachable!();
841 };
842
843 docs![
844 allocator,
845 "if ",
846 cond,
847 " then",
848 docs![allocator, allocator.line(), arg_op].nest(2),
849 allocator.line(),
850 "else",
851 docs![allocator, allocator.line(), arg].nest(2)
852 ]
853 }
854 Some(Term::Op1(data)) if matches!(data.op, UnaryOp::BoolAnd | UnaryOp::BoolOr) => {
855 docs![
856 allocator,
857 allocator.atom(&data.arg),
858 allocator.line(),
859 match data.op {
860 UnaryOp::BoolAnd => "&& ",
861 UnaryOp::BoolOr => "|| ",
862 _ => unreachable!(),
863 },
864 allocator.atom(arg)
865 ]
866 }
867 Some(Term::App(..)) => docs![
868 allocator,
869 head,
870 docs![allocator, allocator.line(), allocator.atom(arg)].nest(2)
871 ],
872 _ => docs![
873 allocator,
874 allocator.atom(head),
875 docs![allocator, allocator.line(), allocator.atom(arg)].nest(2)
876 ],
877 }
878 .group(),
879 Term::Var(id) => allocator.as_string(id),
880 Term::RecRecord(data) => {
881 allocator.record(&data.record, &data.includes, &data.dyn_fields)
882 }
883 Term::Op1(data) => {
884 match &data.op {
885 UnaryOp::RecordAccess(id) => docs![allocator, allocator.atom(&data.arg), ".", ident_quoted(id)],
886 UnaryOp::BoolNot => docs![allocator, "!", allocator.atom(&data.arg)],
887 UnaryOp::BoolAnd => docs![allocator, "(&&)", allocator.atom(&data.arg)],
888 UnaryOp::BoolOr => docs![allocator, "(||)", allocator.atom(&data.arg)],
889 UnaryOp::IfThenElse => unreachable!(),
890 _ if data.op.pos() == OpPos::Prefix => docs![
891 allocator,
892 &data.op,
893 docs![allocator, allocator.line(), allocator.atom(&data.arg)].nest(2)
894 ]
895 .group(),
896 _ => {
897 panic!("pretty print is not implemented for {:?}", &data.op)
898 }
899 }
900 }
901 Term::Op2(data) if data.op == BinaryOp::RecordGet => {
902 docs![allocator, &data.arg2, ".", &data.arg1]
903 }
904 Term::Op2(data) => docs![
905 allocator,
906 if let (&BinaryOp::Sub, Some(&Number::ZERO)) = (&data.op, data.arg1.as_number()) {
907 docs![allocator, allocator.text("-"), allocator.atom(&data.arg2)]
908 } else if data.op.pos() == OpPos::Prefix {
909 data.op.pretty(allocator).append(
910 docs![
911 allocator,
912 allocator.line(),
913 allocator.atom(&data.arg1),
914 allocator.line(),
915 allocator.atom(&data.arg2)
916 ]
917 .nest(2),
918 )
919 } else {
920 docs![
921 allocator,
922 allocator.atom(&data.arg1),
923 allocator.line(),
924 &data.op,
925 " ",
926 allocator.atom(&data.arg2)
927 ]
928 },
929 ]
930 .group(),
931 Term::OpN(data) => docs![
932 allocator,
933 &data.op,
934 docs![
935 allocator,
936 allocator.line(),
937 allocator
938 .intersperse(data.args.iter().map(|val| allocator.atom(val)), allocator.line())
939 ]
940 .nest(2)
941 ]
942 .group(),
943 Term::Sealed(_) => allocator.text("%<sealed>"),
944 Term::Annotated(data) => allocator.atom(&data.inner).append(data.annot.pretty(allocator)),
945 Term::Import(term::Import::Path { path, format }) => {
946 docs![
947 allocator,
948 "import",
949 allocator.space(),
950 allocator
951 .escaped_string(path.to_string_lossy().as_ref())
952 .double_quotes(),
953 if Some(*format) != InputFormat::from_path(path) {
954 docs![
955 allocator,
956 allocator.space(),
957 "as",
958 allocator.space(),
959 "'",
960 format.to_str()
961 ]
962 } else {
963 allocator.nil()
964 },
965 ]
966 }
967 Term::Import(term::Import::Package { id }) => {
968 allocator.text("import ").append(id.to_string())
969 }
970 Term::ResolvedImport(id) => allocator.text(format!("import <file_id: {id:?}>")),
971 Term::ParseError(_) => allocator.text("%<parse error>"),
972 Term::RuntimeError(_) => allocator.text("%<runtime error>"),
973 Term::Closurize(val) => val.pretty(allocator),
974 }
975 }
976}
977
978impl<'a> Pretty<'a, Allocator> for &EnumRows {
979 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
980 match &self.0 {
981 EnumRowsF::Empty => allocator.nil(),
982 EnumRowsF::TailVar(id) => docs![allocator, ";", allocator.line(), id.to_string()],
983 EnumRowsF::Extend { row, tail } => {
984 let mut result = row.pretty(allocator);
985
986 if let EnumRowsF::Extend { .. } = tail.0 {
987 result = result.append(allocator.text(",").append(allocator.line()));
988 }
989
990 result.append(tail.as_ref())
991 }
992 }
993 }
994}
995
996impl<'a> Pretty<'a, Allocator> for &EnumRowF<Box<Type>> {
997 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
998 let mut result = allocator
999 .text("'")
1000 .append(allocator.text(enum_tag_quoted(&self.id)));
1001
1002 if let Some(typ) = self.typ.as_ref() {
1003 let ty_parenthesized = if typ.fmt_is_atom() {
1004 typ.pretty(allocator)
1005 } else {
1006 allocator
1007 .text("(")
1008 .append(allocator.line_())
1009 .append(typ.pretty(allocator))
1010 .append(allocator.line_())
1011 .append(")")
1012 };
1013
1014 result = result.append(allocator.text(" ")).append(ty_parenthesized);
1015 }
1016
1017 result
1018 }
1019}
1020
1021impl<'a> Pretty<'a, Allocator> for &EnumRow {
1022 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1023 self.0.pretty(allocator)
1024 }
1025}
1026
1027impl<'a> Pretty<'a, Allocator> for &RecordRows {
1028 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1029 match &self.0 {
1031 RecordRowsF::Empty => allocator.nil(),
1032 RecordRowsF::TailDyn => docs![allocator, ";", allocator.line(), "Dyn"],
1033 RecordRowsF::TailVar(id) => docs![allocator, ";", allocator.line(), id.to_string()],
1034 RecordRowsF::Extend { row, tail } => docs![
1035 allocator,
1036 row,
1037 if let RecordRowsF::Extend { .. } = tail.0 {
1038 docs![allocator, ",", allocator.line()]
1039 } else {
1040 allocator.nil()
1041 },
1042 tail.as_ref()
1043 ],
1044 }
1045 }
1046}
1047
1048impl<'a, Ty> Pretty<'a, Allocator> for &RecordRowF<Ty>
1049where
1050 Ty: std::ops::Deref<Target = Type>,
1051{
1052 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1053 docs![
1054 allocator,
1055 ident_quoted(&self.id),
1056 " : ",
1057 allocator.type_part(self.typ.deref()),
1058 ]
1059 }
1060}
1061
1062impl<'a> Pretty<'a, Allocator> for RecordRowF<&Type> {
1063 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1064 (&self).pretty(allocator)
1065 }
1066}
1067
1068impl<'a> Pretty<'a, Allocator> for &RecordRow {
1069 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1070 self.0.pretty(allocator)
1071 }
1072}
1073
1074impl<'a> Pretty<'a, Allocator> for &Type {
1075 fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1076 use TypeF::*;
1077 match &self.typ {
1078 Dyn => allocator.text("Dyn"),
1079 Number => allocator.text("Number"),
1080 Bool => allocator.text("Bool"),
1081 String => allocator.text("String"),
1082 Array(ty) => if ty.fmt_is_atom() {
1083 docs![allocator, "Array", allocator.line(), ty.as_ref()].nest(2)
1084 } else {
1085 docs![
1086 allocator,
1087 "Array (",
1088 docs![allocator, allocator.line_(), ty.as_ref()].nest(2),
1089 allocator.line_(),
1090 ")"
1091 ]
1092 }
1093 .group(),
1094 ForeignId => allocator.text("ForeignId"),
1095 Symbol => allocator.text("Symbol"),
1096 Contract(t) => t.pretty(allocator),
1097 Var(var) => allocator.as_string(var),
1098 Forall { var, body, .. } => {
1099 let mut curr = body.as_ref();
1100 let mut foralls = vec![var];
1101 while let Type {
1102 typ: Forall { var, body, .. },
1103 ..
1104 } = curr
1105 {
1106 foralls.push(var);
1107 curr = body;
1108 }
1109 docs![
1110 allocator,
1111 "forall",
1112 allocator.line(),
1113 allocator.intersperse(
1114 foralls.iter().map(|i| allocator.as_string(i)),
1115 allocator.line(),
1116 ),
1117 ".",
1118 allocator.line(),
1119 allocator.type_part(curr)
1120 ]
1121 .nest(2)
1122 .group()
1123 }
1124 Enum(erows) => docs![allocator, allocator.line(), erows]
1125 .nest(2)
1126 .append(allocator.line())
1127 .enclose("[|", "|]")
1128 .group(),
1129 Record(rrows) => allocator.record_type(rrows),
1130 Dict {
1131 type_fields: ty,
1132 flavour: attrs,
1133 } => docs![
1134 allocator,
1135 allocator.line(),
1136 "_ ",
1137 match attrs {
1138 DictTypeFlavour::Type => ":",
1139 DictTypeFlavour::Contract => "|",
1140 },
1141 " ",
1142 allocator.type_part(ty.as_ref()),
1143 ]
1144 .nest(2)
1145 .append(allocator.line())
1146 .braces()
1147 .group(),
1148 Arrow(dom, codom) => docs![
1149 allocator,
1150 allocator
1151 .type_part(dom)
1152 .parens_if(matches!(dom.typ, Arrow(..) | Forall { .. }))
1153 .nest(2),
1154 allocator.line(),
1155 "-> ",
1156 allocator
1157 .type_part(codom)
1158 .parens_if(matches!(codom.typ, Forall { .. }))
1159 ]
1160 .group(),
1161 Wildcard(_) => allocator.text("_"),
1162 }
1163 }
1164}
1165
1166#[macro_export]
1168macro_rules! impl_display_from_pretty {
1169 ($ty:ty) => {
1170 impl std::fmt::Display for $ty {
1171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1172 $crate::pretty::fmt_pretty(&self, f)
1173 }
1174 }
1175 };
1176}
1177
1178pub trait PrettyPrintCap: ToString {
1181 fn pretty_print_cap(&self, max_width: usize) -> String {
1186 let output = self.to_string();
1187
1188 if output.len() <= max_width {
1189 output
1190 } else {
1191 let (end, _) = output.char_indices().nth(max_width).unwrap();
1192 let mut truncated = String::from(&output[..end]);
1193
1194 if max_width >= 2 {
1195 truncated.pop();
1196 truncated.push('\u{2026}');
1197 }
1198
1199 truncated
1200 }
1201 }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use crate::{
1207 files::Files,
1208 parser::{
1209 ErrorTolerantParserCompat,
1210 grammar::{FixedTypeParser, TermParser},
1211 lexer::Lexer,
1212 },
1213 position::PosTable,
1214 };
1215 use pretty::Doc;
1216
1217 use super::*;
1218 use indoc::indoc;
1219
1220 fn parse_type(s: &str) -> Type {
1222 let id = Files::empty().add("<test>", s);
1223
1224 FixedTypeParser::new()
1225 .parse_strict_compat(&mut PosTable::new(), id, Lexer::new(s))
1226 .unwrap()
1227 }
1228
1229 fn parse_term(s: &str) -> NickelValue {
1231 let id = Files::empty().add("<test>", s);
1232
1233 TermParser::new()
1234 .parse_strict_compat(&mut PosTable::new(), id, Lexer::new(s))
1235 .unwrap()
1236 }
1237
1238 #[track_caller]
1242 fn assert_long_short_type(long: &str, short: &str) {
1243 let ty = parse_type(long);
1244 let alloc = Allocator::default();
1245 let doc: DocBuilder<'_, _, ()> = ty.pretty(&alloc);
1246
1247 let mut long_lines = String::new();
1248 doc.render_fmt(usize::MAX, &mut long_lines).unwrap();
1249
1250 let mut short_lines = String::new();
1251 doc.render_fmt(0, &mut short_lines).unwrap();
1252
1253 assert_eq!(long_lines, long);
1254 assert_eq!(short_lines, short);
1255 }
1256
1257 #[track_caller]
1261 fn assert_long_short_term(long: &str, short: &str) {
1262 let term = parse_term(long);
1263 let alloc = Allocator::default();
1264 let doc: DocBuilder<'_, _, ()> = term.pretty(&alloc);
1265
1266 let mut long_lines = String::new();
1267 doc.render_fmt(160, &mut long_lines).unwrap();
1268
1269 let mut short_lines = String::new();
1270 doc.render_fmt(0, &mut short_lines).unwrap();
1271
1272 assert_eq!(long_lines, long);
1273 assert_eq!(short_lines, short);
1274 }
1275
1276 #[test]
1277 fn pretty_array_type() {
1278 assert_long_short_type("Array String", "Array\n String");
1279 assert_long_short_type(
1280 "Array (Number -> Array Dyn)",
1281 indoc! {"
1282 Array (
1283 Number
1284 -> Array
1285 Dyn
1286 )"
1287 },
1288 );
1289 }
1290
1291 #[test]
1292 fn pretty_arrow_type() {
1293 assert_long_short_type("Number -> Number", "Number\n-> Number");
1294 assert_long_short_type(
1295 "(Number -> Number -> Dyn) -> Number",
1296 indoc! {"
1297 (Number
1298 -> Number
1299 -> Dyn)
1300 -> Number"
1301 },
1302 );
1303 }
1304
1305 #[test]
1306 fn pretty_dict_type() {
1307 assert_long_short_type(
1308 "{ _ : Number }",
1309 indoc! {"
1310 {
1311 _ : Number
1312 }"
1313 },
1314 );
1315 assert_long_short_type(
1316 "{ _ : { x : Number, y : String } }",
1317 indoc! {"
1318 {
1319 _ : {
1320 x : Number,
1321 y : String
1322 }
1323 }"
1324 },
1325 );
1326 }
1327
1328 #[test]
1329 fn pretty_record_type() {
1330 assert_long_short_type(
1331 "{ x : Number, y : String; Dyn }",
1332 indoc! {"
1333 {
1334 x : Number,
1335 y : String;
1336 Dyn
1337 }"
1338 },
1339 );
1340 }
1341
1342 #[test]
1343 fn pretty_enum_type() {
1344 assert_long_short_type(
1345 "forall r. [| 'tag1, 'tag2, 'tag3; r |]",
1346 indoc! {"
1347 forall
1348 r.
1349 [|
1350 'tag1,
1351 'tag2,
1352 'tag3;
1353 r
1354 |]"
1355 },
1356 )
1357 }
1358
1359 #[test]
1360 fn pretty_forall_type() {
1361 assert_long_short_type(
1362 "forall a r. a -> { foo : a; r }",
1363 indoc! {"
1364 forall
1365 a
1366 r.
1367 a
1368 -> {
1369 foo : a;
1370 r
1371 }"
1372 },
1373 );
1374 }
1375
1376 #[test]
1377 fn pretty_opn() {
1378 assert_long_short_term(
1379 "%string/replace% string pattern replace",
1380 indoc! {"
1381 %string/replace%
1382 string
1383 pattern
1384 replace"
1385 },
1386 );
1387 }
1388
1389 #[test]
1390 fn pretty_binop() {
1391 assert_long_short_term(
1392 "a + b",
1393 indoc! {"
1394 a
1395 + b"
1396 },
1397 );
1398 assert_long_short_term(
1399 "%string/split% string sep",
1400 indoc! {"
1401 %string/split%
1402 string
1403 sep"
1404 },
1405 );
1406 assert_long_short_term("-5", "-5");
1407 assert_long_short_term(
1408 "a - (-b)",
1409 indoc! {"
1410 a
1411 - (-b)"
1412 },
1413 );
1414 }
1415
1416 #[test]
1417 fn pretty_unop() {
1418 assert_long_short_term("!xyz", "!xyz");
1419 assert_long_short_term(
1420 "a && b",
1421 indoc! {"
1422 a
1423 && b"
1424 },
1425 );
1426 assert_long_short_term(
1427 "(a && b) && c",
1428 indoc! {"
1429 (a
1430 && b)
1431 && c"
1432 },
1433 );
1434 assert_long_short_term(
1435 "a || b",
1436 indoc! {"
1437 a
1438 || b"
1439 },
1440 );
1441 assert_long_short_term(
1442 "if true then false else not",
1443 indoc! {"
1444 if true then
1445 false
1446 else
1447 not"
1448 },
1449 );
1450 assert_long_short_term(
1451 "%enum/embed% foo bar",
1452 indoc! {"
1453 %enum/embed%
1454 foo
1455 bar"
1456 },
1457 );
1458 }
1459
1460 #[test]
1461 fn pretty_arrays() {
1462 assert_long_short_term(
1463 "[ 1, 2, 3, 4 ]",
1464 indoc! {"
1465 [
1466 1,
1467 2,
1468 3,
1469 4
1470 ]"
1471 },
1472 );
1473 }
1474
1475 #[test]
1476 fn pretty_record() {
1477 assert_long_short_term("{}", "{}");
1478 assert_long_short_term(
1479 "{ a = b, c = d, }",
1480 indoc! {"
1481 {
1482 a =
1483 b,
1484 c =
1485 d,
1486 }"
1487 },
1488 );
1489 assert_long_short_term(
1490 r#"{ a | String | force = b, c | Number | doc "" = d, }"#,
1491 indoc! {r#"
1492 {
1493 a
1494 | String
1495 | force
1496 = b,
1497 c
1498 | Number
1499 | doc ""
1500 = d,
1501 }"#
1502 },
1503 );
1504 assert_long_short_term(
1505 "{ a = b, .. }",
1506 indoc! {"
1507 {
1508 a =
1509 b,
1510 ..
1511 }"
1512 },
1513 );
1514 assert_long_short_term(
1515 r#"{ a = b, "%{a}" = c, .. }"#,
1516 indoc! {r#"
1517 {
1518 a =
1519 b,
1520 "%{a}" =
1521 c,
1522 ..
1523 }"#
1524 },
1525 );
1526 assert_long_short_term(
1527 r#"{ "=" = a, }"#,
1528 indoc! {r#"
1529 {
1530 "=" =
1531 a,
1532 }"#
1533 },
1534 );
1535 }
1536
1537 #[test]
1538 fn pretty_let() {
1539 assert_long_short_term(
1540 "let rec foo | String = c in {}",
1541 indoc! {"
1542 let rec foo
1543 | String
1544 = c
1545 in
1546 {}"
1547 },
1548 );
1549 assert_long_short_term(
1550 "let foo = c bar in {}",
1551 indoc! {"
1552 let foo
1553 = c
1554 bar
1555 in
1556 {}"
1557 },
1558 );
1559 assert_long_short_term(
1560 "let foo | String = c bar in {}",
1561 indoc! {"
1562 let foo
1563 | String
1564 = c
1565 bar
1566 in
1567 {}"
1568 },
1569 );
1570 }
1571
1572 #[test]
1573 fn pretty_multiline_strings() {
1574 let t: NickelValue = Term::StrChunks(vec![StrChunk::Literal("\n1.".to_owned())]).into();
1588 assert_eq!(format!("{t}"), "m%\"\n \n 1.\n\"%");
1589
1590 let t: NickelValue = Term::StrChunks(vec![StrChunk::Literal(
1591 "a multiline string\n\n\n\n".to_owned(),
1592 )])
1593 .into();
1594 assert_eq!(
1595 format!("{t}"),
1596 "m%\"\n a multiline string\n \n \n \n\n\"%"
1597 );
1598 }
1599
1600 #[test]
1601 fn pretty_fun() {
1602 assert_long_short_term(
1603 "fun x y z => x y z",
1604 indoc! {"
1605 fun
1606 x
1607 y
1608 z
1609 =>
1610 x
1611 y
1612 z"
1613 },
1614 );
1615 }
1616
1617 #[test]
1618 fn pretty_app() {
1619 assert_long_short_term(
1620 "x y z",
1621 indoc! {"
1622 x
1623 y
1624 z"
1625 },
1626 );
1627 }
1628
1629 #[track_caller]
1636 fn assert_format_eq(s: &str) {
1637 let ty = parse_type(s);
1638 assert_eq!(s, &format!("{ty}"));
1639 }
1640
1641 #[test]
1642 fn types_pretty_printing() {
1643 assert_format_eq("Number");
1644 assert_format_eq("Number -> Number");
1645 assert_format_eq("(Number -> Number) -> (Number -> Number) -> Number -> Number");
1646 assert_format_eq("((Number -> Number) -> Number) -> Number");
1647 assert_format_eq("Number -> (forall a. a -> String) -> String");
1648
1649 assert_format_eq("{ _ : String }");
1650 assert_format_eq("{ _ : (String -> String) -> String }");
1651 assert_format_eq("{ _ | String }");
1652 assert_format_eq("{ _ | (String -> String) -> String }");
1653
1654 assert_format_eq("{ x : (Bool -> Bool) -> Bool, y : Bool }");
1655 assert_format_eq("forall r. { x : Bool, y : Bool, z : Bool; r }");
1656 assert_format_eq("{ x : Bool, y : Bool, z : Bool }");
1657
1658 assert_format_eq("[| 'a, 'b, 'c, 'd |]");
1659 assert_format_eq("forall r. [| 'tag1, 'tag2, 'tag3; r |]");
1660
1661 assert_format_eq("Array Number");
1662 assert_format_eq("Array (Array Number)");
1663 assert_format_eq("Number -> Array (Array String) -> Number");
1664 assert_format_eq("Array (Number -> Number)");
1665 assert_format_eq("Array (Array (Array Dyn) -> Number)");
1666
1667 assert_format_eq("_");
1668 assert_format_eq("_ -> _");
1669 assert_format_eq("{ x : _, y : Bool }");
1670 assert_format_eq("{ _ : _ }");
1671 }
1672
1673 fn format_short_term(input: &str, depth: usize, size: usize) -> String {
1674 let term = parse_term(input);
1675 let allocator = Allocator::bounded(depth, size);
1676 let doc: DocBuilder<_, ()> = term.pretty(&allocator);
1677 Doc::pretty(&doc, 1000).to_string()
1678 }
1679
1680 #[test]
1681 fn bounded_pretty_printing() {
1682 assert_eq!("{ hello = 1, }", &format_short_term("{hello = 1}", 1, 1));
1683 assert_eq!("{…}", &format_short_term("{hello = 1, bye = 2}", 1, 1));
1684 assert_eq!(
1685 "{ hello = 1, inner = { bye = 2, }, }",
1686 &format_short_term("{hello = 1, inner = { bye = 2 }}", 2, 2)
1687 );
1688 assert_eq!(
1689 "{ hello = 1, inner = {…}, }",
1690 &format_short_term("{hello = 1, inner = { bye = 2 }}", 1, 100)
1691 );
1692 assert_eq!(
1693 "{ hello = 1, inner = {…}, }",
1694 &format_short_term("{hello = 1, inner = { bye = 2, other = 3 }}", 100, 2)
1695 );
1696 }
1697}