nickel_lang_core/
pretty.rs

1use std::cell::Cell;
2use std::fmt;
3
4use crate::cache::InputFormat;
5use crate::identifier::{Ident, LocIdent};
6use crate::parser::lexer::KEYWORDS;
7use crate::term::{
8    pattern::*,
9    record::{Field, FieldMetadata, Include, RecordData},
10    *,
11};
12use crate::{term, typ::*};
13
14use malachite::base::num::{basic::traits::Zero, conversion::traits::ToSci};
15use once_cell::sync::Lazy;
16use pretty::docs;
17pub use pretty::{DocAllocator, DocBuilder, Pretty};
18use regex::Regex;
19
20#[derive(Clone, Copy, Eq, PartialEq)]
21pub enum StringRenderStyle {
22    /// Never allow rendering as a multiline string
23    ForceMonoline,
24    /// Render as a multiline string if the string contains a newline
25    Multiline,
26}
27
28/// Helper to find the min number of `%` sign needed to interpolate a string containing this chunk.
29fn min_interpolate_sign(text: &str) -> usize {
30    let reg = Regex::new(r#"([%]+\{)|("[%]+)"#).unwrap();
31    reg.find_iter(text)
32        .map(|m| {
33            // We iterate over all sequences `%+{` and `"%+`, which could clash with the
34            // interpolation syntax, and return the maximum number of `%` insead each sequence.
35            //
36            // For the case of a closing delimiter `"%`, we could actually be slightly smarter as we
37            // don't necessarily need more `%`, but just a different number of `%`. For example, if
38            // the string contains only one `"%%`, then single `%` delimiters like `m%"` and `"%`
39            // would be fine. But picking the maximum results in a simpler algorithm for now, which
40            // we can update later if necessary.
41            m.end() - m.start()
42        })
43        .max()
44        .unwrap_or(1)
45}
46
47fn sorted_map<K: Ord, V>(m: &'_ IndexMap<K, V>) -> Vec<(&'_ K, &'_ V)> {
48    let mut ret: Vec<(&K, &V)> = m.iter().collect();
49    ret.sort_by_key(|(k, _)| *k);
50    ret
51}
52
53/// Escape a string to make it suitable for placing between quotes in Nickel
54fn escape(s: &str) -> String {
55    s.replace('\\', "\\\\")
56        .replace("%{", "\\%{")
57        .replace('\"', "\\\"")
58        .replace('\n', "\\n")
59        .replace('\r', "\\r")
60}
61
62static QUOTING_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("^_*[a-zA-Z][_a-zA-Z0-9-]*$").unwrap());
63
64/// Return the string representation of an identifier, and add enclosing double quotes if the
65/// label isn't a valid identifier according to the parser, for example if it contains a
66/// special character like a space.
67pub fn ident_quoted(ident: impl Into<Ident>) -> String {
68    let ident = ident.into();
69    let label = ident.label();
70    if QUOTING_REGEX.is_match(label) && !KEYWORDS.contains(&label) {
71        String::from(label)
72    } else {
73        format!("\"{}\"", escape(label))
74    }
75}
76
77/// Return a string representation of an identifier, adding enclosing double quotes if
78/// the label isn't valid for an enum tag. This is like `ident_quoted` except that keywords
79/// aren't wrapped in quotes (because `'if` is a valid enum tag, for example).
80pub fn enum_tag_quoted(ident: impl Into<Ident>) -> String {
81    let ident = ident.into();
82    let label = ident.label();
83    if QUOTING_REGEX.is_match(label) {
84        String::from(label)
85    } else {
86        format!("\"{}\"", escape(label))
87    }
88}
89
90#[derive(Clone, Copy, Eq, PartialEq, Debug)]
91enum RecursivePriority {
92    Default,
93    Force,
94    None,
95}
96
97impl RecursivePriority {
98    fn is_present(&self) -> bool {
99        !matches!(self, RecursivePriority::None)
100    }
101}
102
103/// We need the field's value to inspect it for an application of
104/// `$rec_default` or `$rec_force`. The recursive priority metadata
105/// annotations `rec default` and `rec force` are converted at parsing
106/// time into applications of the internal symbols `$rec_default` and
107/// `$rec_force` respectively. These symbols are not valid identifiers in
108/// Nickel's surface syntax, so we need to handle them specially.
109///
110/// If we find a recursive priority annotation we return the field's value
111/// with the internal application removed.
112fn split_recursive_priority(value: &RichTerm) -> (RecursivePriority, RichTerm) {
113    if let Term::App(f, x) = value.as_ref() {
114        match f.as_ref() {
115            Term::Var(id) if id.label() == "$rec_default" => {
116                return (RecursivePriority::Default, x.clone());
117            }
118            Term::Var(id) if id.label() == "$rec_force" => {
119                return (RecursivePriority::Force, x.clone());
120            }
121            _ => (),
122        }
123    };
124    (RecursivePriority::None, value.clone())
125}
126
127/// Does a sequence of `StrChunk`s contain a literal newline?
128fn contains_newline<T>(chunks: &[StrChunk<T>]) -> bool {
129    chunks.iter().any(|chunk| match chunk {
130        StrChunk::Literal(str) => str.contains('\n'),
131        StrChunk::Expr(_, _) => false,
132    })
133}
134
135/// Does a sequence of `StrChunk`s contain a carriage return? Lone carriage
136/// returns are forbidden in Nickel's surface syntax.
137fn contains_carriage_return<T>(chunks: &[StrChunk<T>]) -> bool {
138    chunks.iter().any(|chunk| match chunk {
139        StrChunk::Literal(str) => str.contains('\r'),
140        StrChunk::Expr(_, _) => false,
141    })
142}
143
144/// Determines if a type to be printed in type position needs additional parentheses.
145///
146/// Terms in type position don't need to be atoms: for example, we should pretty print `foo |
147/// Contract arg1 arg2` without parentheses instead of `foo | (Contract arg1 arg2)`.
148///
149/// However, some terms (i.e. contracts) in type position still need parentheses in some cases, for
150/// example when said term is a function function. This function precisely determines if the given
151/// type is such a term.
152fn needs_parens_in_type_pos(typ: &Type) -> bool {
153    if let TypeF::Contract(term) = &typ.typ {
154        matches!(
155            term.as_ref(),
156            Term::Fun(..)
157                | Term::FunPattern(..)
158                | Term::CustomContract(_)
159                | Term::Let(..)
160                | Term::LetPattern(..)
161                | Term::Op1(UnaryOp::IfThenElse, _)
162                | Term::Import { .. }
163                | Term::ResolvedImport(..)
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
185/// A pretty-printing allocator that supports rough bounds on the
186/// size of the output.
187///
188/// When a pretty-printed object is too large, it will be abbreviated.
189/// For example, a record will be abbreviated as "{…}".
190///
191/// The bounds are "rough" in that the depth bound only (currently; this might
192/// be extended in the future) constrains the number of nested records: you can
193/// still have deeply nested terms of other kinds. The size bound only constrains
194/// the number of children of nested records. As such, neither constraint gives
195/// precise control over the size of the output.
196pub struct Allocator {
197    inner: pretty::BoxAllocator,
198    bound: Option<Cell<SizeBound>>,
199}
200
201/// The default `BoundedAllocator` imposes no constraints.
202impl Default for Allocator {
203    fn default() -> Self {
204        Self {
205            inner: pretty::BoxAllocator,
206            bound: None,
207        }
208    }
209}
210
211impl Allocator {
212    /// Creates a `BoundedAllocator` with constraints.
213    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    /// Runs a callback with a "smaller" allocator.
224    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: &[(RichTerm, 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() && dyn_fields.is_empty() && !record_data.attrs.open {
287            self.text("{}")
288        } else if size_per_child == 0 || self.depth_constraint() == 0 {
289            "{…}".pretty(self)
290        } else {
291            self.shrunken(size_per_child, |alloc| {
292                docs![
293                    alloc,
294                    alloc.line(),
295                    alloc.intersperse(
296                        includes
297                            .iter()
298                            // For now we don't need to escape the included id, as it must be a
299                            // valid variable name, and thus can't contain non-identifier
300                            // characters such as spaces.
301                            .map(|incl| {
302                                docs![
303                                    alloc,
304                                    "include",
305                                    alloc.space(),
306                                    incl.ident.to_string(),
307                                    self.field_metadata(&incl.metadata, true)
308                                ]
309                            }),
310                        docs![alloc, ",", alloc.line()]
311                    ),
312                    if !includes.is_empty() {
313                        docs![alloc, ",", alloc.line()]
314                    } else {
315                        alloc.nil()
316                    },
317                    alloc.fields(&record_data.fields),
318                    if !dyn_fields.is_empty() {
319                        docs![alloc, alloc.line(), alloc.dyn_fields(dyn_fields)]
320                    } else {
321                        alloc.nil()
322                    },
323                    if record_data.attrs.open {
324                        docs![alloc, alloc.line(), ".."]
325                    } else {
326                        alloc.nil()
327                    }
328                ]
329                .nest(2)
330                .append(self.line())
331                .braces()
332                .group()
333            })
334        }
335    }
336
337    fn record_type<'a>(&'a self, rows: &RecordRows) -> DocBuilder<'a, Self> {
338        let child_count = rows.iter().count().max(1);
339        let size_per_child = self.size_constraint() / child_count.max(1);
340        if size_per_child == 0 || self.depth_constraint() == 0 {
341            "{…}".pretty(self)
342        } else {
343            self.shrunken(size_per_child, |alloc| {
344                let tail = match rows.iter().last() {
345                    Some(RecordRowsIteratorItem::TailDyn) => docs![alloc, ";", alloc.line(), "Dyn"],
346                    Some(RecordRowsIteratorItem::TailVar(id)) => {
347                        docs![alloc, ";", alloc.line(), id.to_string()]
348                    }
349                    _ => alloc.nil(),
350                };
351
352                let rows = rows.iter().filter_map(|r| match r {
353                    RecordRowsIteratorItem::Row(r) => Some(r),
354                    _ => None,
355                });
356
357                docs![
358                    alloc,
359                    alloc.line(),
360                    alloc.intersperse(rows, docs![alloc, ",", alloc.line()]),
361                    tail
362                ]
363                .nest(2)
364                .append(alloc.line())
365                .braces()
366                .group()
367            })
368        }
369    }
370
371    /// Escape the special characters in a string, including the newline character, so that it can
372    /// be enclosed by double quotes a be a valid Nickel string.
373    fn escaped_string<'a>(&'a self, s: &str) -> DocBuilder<'a, Self> {
374        self.text(escape(s))
375    }
376
377    /// Print string chunks, either in the single line or multiline style.
378    fn chunks<'a>(
379        &'a self,
380        chunks: &[StrChunk<RichTerm>],
381        string_style: StringRenderStyle,
382    ) -> DocBuilder<'a, Self> {
383        let multiline = string_style == StringRenderStyle::Multiline
384            && contains_newline(chunks)
385            && !contains_carriage_return(chunks);
386
387        let nb_perc = if multiline {
388            chunks
389                .iter()
390                .map(
391                    |c| {
392                        if let StrChunk::Literal(s) = c {
393                            min_interpolate_sign(s)
394                        } else {
395                            1
396                        }
397                    }, // be sure we have at least 1 `%` sign when an interpolation is present
398                )
399                .max()
400                .unwrap_or(1)
401        } else {
402            1
403        };
404
405        let interp: String = "%".repeat(nb_perc);
406
407        let line_maybe = if multiline {
408            self.hardline()
409        } else {
410            self.nil()
411        };
412
413        let start_delimiter = if multiline {
414            format!("m{interp}")
415        } else {
416            String::new()
417        };
418
419        let end_delimiter = if multiline {
420            interp.clone()
421        } else {
422            String::new()
423        };
424
425        line_maybe
426            .clone()
427            .append(self.concat(chunks.iter().rev().map(|c| {
428                match c {
429                    StrChunk::Literal(s) => {
430                        if multiline {
431                            self.concat(
432                                // We do this manually instead of using
433                                // `str::lines` because we need to be careful
434                                // about whether a trailing newline appears at
435                                // the end of the last line.
436                                s.split_inclusive('\n').map(|line| {
437                                    if let Some(s) = line.strip_suffix('\n') {
438                                        self.text(s.to_owned()).append(self.hardline())
439                                    } else {
440                                        self.text(line.to_owned())
441                                    }
442                                }),
443                            )
444                        } else {
445                            self.escaped_string(s)
446                        }
447                    }
448                    StrChunk::Expr(e, _i) => docs![self, interp.clone(), "{", e, "}"],
449                }
450            })))
451            .nest(if multiline { 2 } else { 0 })
452            .append(line_maybe)
453            .double_quotes()
454            .enclose(start_delimiter, end_delimiter)
455    }
456
457    fn binding<'a>(&'a self, lhs: impl Pretty<'a, Self>, rt: RichTerm) -> DocBuilder<'a, Self> {
458        docs![
459            self,
460            lhs,
461            if let Term::Annotated(annot, _) = rt.as_ref() {
462                annot.pretty(self)
463            } else {
464                self.nil()
465            },
466            self.line(),
467            "= ",
468            if let Term::Annotated(_, inner) = rt.as_ref() {
469                inner.pretty(self)
470            } else {
471                rt.pretty(self)
472            },
473        ]
474    }
475
476    /// Print a function, which can have several parameters (represented as nested functions), and
477    /// where each layer might be a normal function, a pattern matching function or a custom
478    /// contract. [function] automatically unwrap any of those nested layers to print the function
479    /// with as many parameters as possible on the left of the `=>` separator.
480    fn function<'a>(
481        &'a self,
482        first_param: impl Pretty<'a, Self>,
483        mut body: &RichTerm,
484    ) -> DocBuilder<'a, Self> {
485        let mut builder = docs![self, "fun", self.line(), first_param];
486
487        loop {
488            match body.as_ref() {
489                Term::Fun(id, rt) => {
490                    builder = docs![self, builder, self.line(), self.as_string(id)];
491                    body = rt;
492                }
493                Term::FunPattern(pat, rt) => {
494                    builder = docs![self, builder, self.line(), self.pat_with_parens(pat)];
495                    body = rt;
496                }
497                _ => break,
498            }
499        }
500
501        docs![
502            self,
503            builder,
504            self.line(),
505            "=>",
506            self.line(),
507            body.pretty(self)
508        ]
509        .nest(2)
510        .group()
511    }
512
513    fn field_metadata<'a>(
514        &'a self,
515        metadata: &FieldMetadata,
516        with_doc: bool,
517    ) -> DocBuilder<'a, Self> {
518        docs![
519            self,
520            &metadata.annotation,
521            if with_doc {
522                metadata
523                    .doc
524                    .clone()
525                    .map(|doc| {
526                        docs![
527                            self,
528                            self.line(),
529                            "| doc ",
530                            self.chunks(&[StrChunk::Literal(doc)], StringRenderStyle::Multiline),
531                        ]
532                    })
533                    .unwrap_or_else(|| self.nil())
534            } else {
535                self.nil()
536            },
537            if metadata.opt {
538                docs![self, self.line(), "| optional"]
539            } else {
540                self.nil()
541            },
542            match &metadata.priority {
543                MergePriority::Bottom => docs![self, self.line(), "| default"],
544                MergePriority::Neutral => self.nil(),
545                MergePriority::Numeral(p) =>
546                    docs![self, self.line(), "| priority ", p.to_sci().to_string()],
547                MergePriority::Top => docs![self, self.line(), "| force"],
548            }
549        ]
550    }
551
552    fn field<'a>(&'a self, id: &LocIdent, field: &Field) -> DocBuilder<'a, Self> {
553        self.text(ident_quoted(id))
554            .append(self.field_body(field))
555            .group()
556    }
557
558    fn dyn_field<'a>(&'a self, id_expr: &RichTerm, field: &Field) -> DocBuilder<'a, Self> {
559        match id_expr.as_ref() {
560            // Nickel will not parse a multiline string literal in this position
561            Term::StrChunks(chunks) => self.chunks(chunks, StringRenderStyle::ForceMonoline),
562            Term::ParseError(_) => docs![self, "%<parse error>"],
563            _ => unimplemented!("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            self.field_metadata(&field.metadata, true),
573            if let Some((priority, value)) = field.value.as_ref().map(split_recursive_priority) {
574                let has_metadata =
575                    field.metadata != FieldMetadata::default() || priority.is_present();
576
577                docs![
578                    self,
579                    priority,
580                    if has_metadata {
581                        docs![self, self.line(), "= "]
582                    } else {
583                        docs![self, " =", self.line()]
584                    },
585                    value.pretty(self).nest(2)
586                ]
587            } else {
588                self.nil()
589            },
590            ","
591        ]
592        .nest(2)
593    }
594
595    fn fields<'a>(&'a self, fields: &IndexMap<LocIdent, Field>) -> DocBuilder<'a, Self> {
596        self.intersperse(
597            sorted_map(fields)
598                .iter()
599                .map(|(id, field)| self.field(id, field)),
600            self.line(),
601        )
602    }
603
604    fn dyn_fields<'a>(&'a self, fields: &[(RichTerm, Field)]) -> DocBuilder<'a, Self> {
605        self.intersperse(
606            fields
607                .iter()
608                .map(|(id_term, field)| self.dyn_field(id_term, field)),
609            self.line(),
610        )
611    }
612
613    fn atom<'a>(&'a self, rt: &RichTerm) -> DocBuilder<'a, Self> {
614        rt.pretty(self).parens_if(!rt.as_ref().is_atom())
615    }
616
617    /// Almost identical to calling `typ.pretty(self)`, but adds parentheses when the type is
618    /// actually a contract that has a top-level form that needs parentheses (let-binding,
619    /// if-then-else, etc.).
620    ///
621    /// Although terms can appear in type position as contracts, the parenthesis rules are slightly
622    /// more restrictive than for a generic term: for example, `{foo | let x = Contract in x}` is
623    /// not valid Nickel. It must be parenthesised as `{foo | (let x = Contract in x)}`.
624    ///
625    /// This method must be used whenever a type is rendered either as component of another type or
626    /// in the position of an annotation. Rendering stand-alone types (for example as part of error
627    /// messages) can avoid those parentheses and directly call to `typ.pretty(allocator)` instead.
628    fn type_part<'a>(&'a self, typ: &Type) -> DocBuilder<'a, Self> {
629        typ.pretty(self).parens_if(needs_parens_in_type_pos(typ))
630    }
631
632    /// Pretty printing of a restricted patterns that requires enum variant patterns and or
633    /// patterns to be parenthesized (typically function pattern arguments). The only difference
634    /// with a general pattern is that for a function, a top-level enum variant pattern with an
635    /// enum tag as an argument such as `'Foo 'Bar` must be parenthesized, because `fun 'Foo 'Bar
636    /// => ...` is parsed as a function of two arguments, which are bare enum tags `'Foo` and
637    /// `'Bar`. We must print `fun ('Foo 'Bar) => ..` instead.
638    fn pat_with_parens<'a>(&'a self, pattern: &Pattern) -> DocBuilder<'a, Self> {
639        pattern.pretty(self).parens_if(matches!(
640            pattern.data,
641            PatternData::Enum(EnumPattern {
642                pattern: Some(_),
643                ..
644            }) | PatternData::Or(_)
645        ))
646    }
647}
648
649trait NickelDocBuilderExt {
650    /// Call `self.parens()` but only if `parens` is `true`.
651    fn parens_if(self, parens: bool) -> Self;
652}
653
654impl NickelDocBuilderExt for DocBuilder<'_, Allocator> {
655    fn parens_if(self, parens: bool) -> Self {
656        if parens {
657            self.parens()
658        } else {
659            self
660        }
661    }
662}
663
664impl<'a> Pretty<'a, Allocator> for LocIdent {
665    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
666        allocator.text(self.into_label())
667    }
668}
669
670impl<'a> Pretty<'a, Allocator> for RecursivePriority {
671    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
672        match self {
673            RecursivePriority::Default => allocator.text("| rec default"),
674            RecursivePriority::Force => allocator.text("| rec force"),
675            RecursivePriority::None => allocator.nil(),
676        }
677    }
678}
679
680impl<'a> Pretty<'a, Allocator> for &TypeAnnotation {
681    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
682        docs![
683            allocator,
684            if let Some(typ) = &self.typ {
685                docs![
686                    allocator,
687                    allocator.line(),
688                    ": ",
689                    allocator.type_part(&typ.typ)
690                ]
691            } else {
692                allocator.nil()
693            },
694            if !self.contracts.is_empty() {
695                allocator.line()
696            } else {
697                allocator.nil()
698            },
699            allocator.intersperse(
700                self.contracts
701                    .iter()
702                    .map(|c| { docs![allocator, "| ", allocator.type_part(&c.typ)] }),
703                allocator.line(),
704            )
705        ]
706    }
707}
708
709impl<'a> Pretty<'a, Allocator> for &UnaryOp {
710    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
711        use UnaryOp::*;
712        match self {
713            BoolNot => allocator.text("!"),
714            BoolAnd | BoolOr | RecordAccess(_) => {
715                unreachable!(
716                    "These are handled specially since they are actually encodings \
717                    of binary operators (`BoolAnd` and `BoolOr`) or need special \
718                    formatting (`StaticAccess`). This currently happens in the `App` \
719                    branch of `Term::pretty`"
720                )
721            }
722            EnumEmbed(id) => docs![
723                allocator,
724                "%enum/embed%",
725                docs![allocator, allocator.line(), id.to_string()].nest(2)
726            ],
727            op => allocator.text(format!("%{op}%")).append(allocator.space()),
728        }
729    }
730}
731
732impl<'a> Pretty<'a, Allocator> for &BinaryOp {
733    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
734        use BinaryOp::*;
735        match self {
736            Plus => allocator.text("+"),
737            Sub => allocator.text("-"),
738
739            Mult => allocator.text("*"),
740            Div => allocator.text("/"),
741            Modulo => allocator.text("%"),
742
743            Eq => allocator.text("=="),
744            LessThan => allocator.text("<"),
745            GreaterThan => allocator.text(">"),
746            GreaterOrEq => allocator.text(">="),
747            LessOrEq => allocator.text("<="),
748
749            Merge(_) => allocator.text("&"),
750
751            StringConcat => allocator.text("++"),
752            ArrayConcat => allocator.text("@"),
753
754            RecordGet => allocator.text("."),
755
756            op => allocator.as_string(format!("%{op}%")),
757        }
758    }
759}
760
761impl<'a> Pretty<'a, Allocator> for &NAryOp {
762    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
763        allocator.as_string(format!("%{self}%"))
764    }
765}
766
767impl<'a> Pretty<'a, Allocator> for &Pattern {
768    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
769        let alias_prefix = if let Some(alias) = self.alias {
770            docs![
771                allocator,
772                alias.to_string(),
773                allocator.space(),
774                "@",
775                allocator.space()
776            ]
777        } else {
778            allocator.nil()
779        };
780
781        docs![allocator, alias_prefix, &self.data]
782    }
783}
784
785impl<'a> Pretty<'a, Allocator> for &PatternData {
786    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
787        match self {
788            PatternData::Wildcard => allocator.text("_"),
789            PatternData::Any(id) => allocator.as_string(id),
790            PatternData::Record(rp) => rp.pretty(allocator),
791            PatternData::Array(ap) => ap.pretty(allocator),
792            PatternData::Enum(evp) => evp.pretty(allocator),
793            PatternData::Constant(cp) => cp.pretty(allocator),
794            PatternData::Or(op) => op.pretty(allocator),
795        }
796    }
797}
798
799impl<'a> Pretty<'a, Allocator> for &ConstantPattern {
800    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
801        self.data.pretty(allocator)
802    }
803}
804
805impl<'a> Pretty<'a, Allocator> for &ConstantPatternData {
806    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
807        match self {
808            ConstantPatternData::Bool(b) => allocator.as_string(b),
809            ConstantPatternData::Number(n) => allocator.as_string(format!("{}", n.to_sci())),
810            ConstantPatternData::String(s) => allocator.escaped_string(s).double_quotes(),
811            ConstantPatternData::Null => allocator.text("null"),
812        }
813    }
814}
815
816impl<'a> Pretty<'a, Allocator> for &EnumPattern {
817    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
818        docs![
819            allocator,
820            "'",
821            enum_tag_quoted(&self.tag),
822            if let Some(ref arg_pat) = self.pattern {
823                docs![
824                    allocator,
825                    allocator.line(),
826                    allocator.pat_with_parens(arg_pat)
827                ]
828            } else {
829                allocator.nil()
830            }
831        ]
832    }
833}
834
835impl<'a> Pretty<'a, Allocator> for &RecordPattern {
836    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
837        let RecordPattern {
838            patterns: matches,
839            tail,
840            ..
841        } = self;
842        docs![
843            allocator,
844            allocator.line(),
845            allocator.intersperse(
846                matches.iter().map(|field_pat| {
847                    docs![
848                        allocator,
849                        field_pat.matched_id.to_string(),
850                        allocator.field_metadata(
851                            &FieldMetadata {
852                                annotation: field_pat.annotation.clone(),
853                                ..Default::default()
854                            },
855                            false
856                        ),
857                        if let Some(default) = field_pat.default.as_ref() {
858                            docs![allocator, allocator.line(), "? ", allocator.atom(default),]
859                        } else {
860                            allocator.nil()
861                        },
862                        match &field_pat.pattern.data {
863                            PatternData::Any(id) if *id == field_pat.matched_id => allocator.nil(),
864                            _ => docs![allocator, allocator.line(), "= ", &field_pat.pattern],
865                        },
866                        ","
867                    ]
868                    .nest(2)
869                }),
870                allocator.line()
871            ),
872            match tail {
873                TailPattern::Empty => allocator.nil(),
874                TailPattern::Open => docs![allocator, allocator.line(), ".."],
875                TailPattern::Capture(id) =>
876                    docs![allocator, allocator.line(), "..", id.ident().to_string()],
877            },
878        ]
879        .nest(2)
880        .append(allocator.line())
881        .braces()
882        .group()
883    }
884}
885
886impl<'a> Pretty<'a, Allocator> for &ArrayPattern {
887    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
888        docs![
889            allocator,
890            allocator.intersperse(
891                self.patterns.iter(),
892                docs![allocator, ",", allocator.line()],
893            ),
894            if !self.patterns.is_empty() && self.is_open() {
895                docs![allocator, ",", allocator.line()]
896            } else {
897                allocator.nil()
898            },
899            match self.tail {
900                TailPattern::Empty => allocator.nil(),
901                TailPattern::Open => allocator.text(".."),
902                TailPattern::Capture(id) => docs![allocator, "..", id.ident().to_string()],
903            },
904        ]
905        .nest(2)
906        .brackets()
907        .group()
908    }
909}
910
911impl<'a> Pretty<'a, Allocator> for &OrPattern {
912    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
913        docs![
914            allocator,
915            allocator.intersperse(
916                self.patterns
917                    .iter()
918                    .map(|pat| allocator.pat_with_parens(pat)),
919                docs![allocator, allocator.line(), "or", allocator.space()],
920            ),
921        ]
922        .group()
923    }
924}
925
926impl<'a> Pretty<'a, Allocator> for &RichTerm {
927    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
928        self.as_ref().pretty(allocator)
929    }
930}
931
932impl<'a> Pretty<'a, Allocator> for &Term {
933    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
934        use Term::*;
935
936        match self {
937            Null => allocator.text("null"),
938            Bool(v) => allocator.as_string(v),
939            Num(n) => allocator.as_string(format!("{}", n.to_sci())),
940            Str(v) => allocator.escaped_string(v).double_quotes(),
941            StrChunks(chunks) => allocator.chunks(chunks, StringRenderStyle::Multiline),
942            Fun(id, body) => allocator.function(allocator.as_string(id), body),
943            // Format this as the primop application `%contract/custom% <ctr>`.
944            CustomContract(ctr) => docs![
945                allocator,
946                "%contract/custom%",
947                docs![allocator, allocator.line(), allocator.atom(ctr),].nest(2),
948            ]
949            .group(),
950            FunPattern(pat, body) => allocator.function(allocator.pat_with_parens(pat), body),
951            Lbl(_lbl) => allocator.text("%<label>").append(allocator.line()),
952            Let(bindings, body, attrs) => docs![
953                allocator,
954                "let",
955                allocator.space(),
956                if attrs.rec {
957                    docs![allocator, "rec", allocator.space()]
958                } else {
959                    allocator.nil()
960                },
961                allocator.intersperse(
962                    bindings
963                        .iter()
964                        .map(|(k, v)| allocator.binding(*k, v.clone())),
965                    docs![allocator, ",", allocator.line()]
966                ),
967                allocator.line(),
968                "in",
969            ]
970            .nest(2)
971            .group()
972            .append(allocator.line())
973            .append(body.pretty(allocator).nest(2))
974            .group(),
975            LetPattern(bindings, body, attrs) => docs![
976                allocator,
977                "let",
978                allocator.space(),
979                if attrs.rec {
980                    docs![allocator, "rec", allocator.space()]
981                } else {
982                    allocator.nil()
983                },
984                allocator.intersperse(
985                    bindings
986                        .iter()
987                        .map(|(k, v)| allocator.binding(k, v.clone())),
988                    docs![allocator, ",", allocator.line()]
989                ),
990                allocator.line(),
991                "in",
992            ]
993            .nest(2)
994            .group()
995            .append(allocator.line())
996            .append(body.pretty(allocator).nest(2))
997            .group(),
998            App(rt1, rt2) => match rt1.as_ref() {
999                App(iop, t) if matches!(iop.as_ref(), Op1(UnaryOp::IfThenElse, _)) => {
1000                    match iop.as_ref() {
1001                        Op1(UnaryOp::IfThenElse, i) => docs![
1002                            allocator,
1003                            "if ",
1004                            i,
1005                            " then",
1006                            docs![allocator, allocator.line(), t].nest(2),
1007                            allocator.line(),
1008                            "else",
1009                            docs![allocator, allocator.line(), rt2].nest(2)
1010                        ],
1011                        _ => unreachable!(),
1012                    }
1013                }
1014                Op1(op @ (UnaryOp::BoolAnd | UnaryOp::BoolOr), rt1) => docs![
1015                    allocator,
1016                    allocator.atom(rt1),
1017                    allocator.line(),
1018                    match op {
1019                        UnaryOp::BoolAnd => "&& ",
1020                        UnaryOp::BoolOr => "|| ",
1021                        _ => unreachable!(),
1022                    },
1023                    allocator.atom(rt2)
1024                ],
1025                App(..) => docs![
1026                    allocator,
1027                    rt1,
1028                    docs![allocator, allocator.line(), allocator.atom(rt2)].nest(2)
1029                ],
1030                _ => docs![
1031                    allocator,
1032                    allocator.atom(rt1),
1033                    docs![allocator, allocator.line(), allocator.atom(rt2)].nest(2)
1034                ],
1035            }
1036            .group(),
1037            Var(id) => allocator.as_string(id),
1038            Enum(id) => allocator
1039                .text("'")
1040                .append(allocator.text(enum_tag_quoted(id))),
1041            EnumVariant { tag, arg, attrs: _ } => allocator
1042                .text("'")
1043                .append(allocator.text(enum_tag_quoted(tag)))
1044                .append(
1045                    docs![allocator, allocator.line(), allocator.atom(arg)]
1046                        .nest(2)
1047                        .group(),
1048                ),
1049            Record(record_data) => allocator.record(record_data, &[], &[]),
1050            RecRecord(record_data, includes, dyn_fields, _) => {
1051                allocator.record(record_data, includes, dyn_fields)
1052            }
1053            Match(data) => docs![
1054                allocator,
1055                "match ",
1056                docs![
1057                    allocator,
1058                    allocator.line(),
1059                    allocator.intersperse(
1060                        data.branches.iter().map(|branch| docs![
1061                            allocator,
1062                            branch.pretty(allocator),
1063                            ","
1064                        ]),
1065                        allocator.line(),
1066                    ),
1067                ]
1068                .nest(2)
1069                .append(allocator.line())
1070                .braces()
1071            ]
1072            .group(),
1073            Array(fields, _) =>
1074            // NOTE: the Array attributes are ignored here. They contain only
1075            // information that has no surface syntax.
1076            {
1077                docs![
1078                    allocator,
1079                    allocator.line(),
1080                    allocator.intersperse(
1081                        fields.iter().map(|rt| rt.pretty(allocator)),
1082                        allocator.text(",").append(allocator.line()),
1083                    ),
1084                ]
1085                .nest(2)
1086                .append(allocator.line())
1087                .brackets()
1088                .group()
1089            }
1090
1091            Op1(UnaryOp::RecordAccess(id), rt) => {
1092                docs![allocator, allocator.atom(rt), ".", ident_quoted(id)]
1093            }
1094            Op1(UnaryOp::BoolNot, rt) => docs![allocator, "!", allocator.atom(rt)],
1095            Op1(UnaryOp::BoolAnd, rt) => docs![allocator, "(&&)", allocator.atom(rt)],
1096            Op1(UnaryOp::BoolOr, rt) => docs![allocator, "(||)", allocator.atom(rt)],
1097            Op1(UnaryOp::IfThenElse, _) => unreachable!(),
1098            Op1(op, rt) => match op.pos() {
1099                OpPos::Prefix => docs![
1100                    allocator,
1101                    op,
1102                    docs![allocator, allocator.line(), allocator.atom(rt)].nest(2)
1103                ]
1104                .group(),
1105                OpPos::Special | OpPos::Postfix | OpPos::Infix => {
1106                    panic!("pretty print is not implemented for {op:?}")
1107                }
1108            },
1109            Op2(BinaryOp::RecordGet, rtl, rtr) => {
1110                docs![allocator, rtr, ".", rtl]
1111            }
1112            Op2(op, rtl, rtr) => docs![
1113                allocator,
1114                if (&BinaryOp::Sub, &Num(Number::ZERO)) == (op, rtl.as_ref()) {
1115                    docs![allocator, allocator.text("-"), allocator.atom(rtr)]
1116                } else if op.pos() == OpPos::Prefix {
1117                    op.pretty(allocator).append(
1118                        docs![
1119                            allocator,
1120                            allocator.line(),
1121                            allocator.atom(rtl),
1122                            allocator.line(),
1123                            allocator.atom(rtr)
1124                        ]
1125                        .nest(2),
1126                    )
1127                } else {
1128                    docs![
1129                        allocator,
1130                        allocator.atom(rtl),
1131                        allocator.line(),
1132                        op,
1133                        " ",
1134                        allocator.atom(rtr)
1135                    ]
1136                },
1137            ]
1138            .group(),
1139            OpN(op, rts) => docs![
1140                allocator,
1141                op,
1142                docs![
1143                    allocator,
1144                    allocator.line(),
1145                    allocator
1146                        .intersperse(rts.iter().map(|rt| allocator.atom(rt)), allocator.line())
1147                ]
1148                .nest(2)
1149            ]
1150            .group(),
1151            ForeignId(_) => allocator.text("%<foreign>"),
1152            SealingKey(sym) => allocator.text(format!("%<sealing key: {sym}>")),
1153            Sealed(_i, _rt, _lbl) => allocator.text("%<sealed>"),
1154            Annotated(annot, rt) => allocator.atom(rt).append(annot.pretty(allocator)),
1155            Import(term::Import::Path { path, format }) => {
1156                docs![
1157                    allocator,
1158                    "import",
1159                    allocator.space(),
1160                    allocator
1161                        .escaped_string(path.to_string_lossy().as_ref())
1162                        .double_quotes(),
1163                    if Some(*format) != InputFormat::from_path(path) {
1164                        docs![
1165                            allocator,
1166                            allocator.space(),
1167                            "as",
1168                            allocator.space(),
1169                            "'",
1170                            format.to_str()
1171                        ]
1172                    } else {
1173                        allocator.nil()
1174                    },
1175                ]
1176            }
1177            Import(term::Import::Package { id }) => {
1178                allocator.text("import ").append(id.to_string())
1179            }
1180            ResolvedImport(id) => allocator.text(format!("import <file_id: {id:?}>")),
1181            // This type is in term position, so we don't need to add parentheses.
1182            Type { typ, contract: _ } => typ.pretty(allocator),
1183            ParseError(_) => allocator.text("%<parse error>"),
1184            RuntimeError(_) => allocator.text("%<runtime error>"),
1185            Closure(idx) => allocator.text(format!("%<closure@{idx:p}>")),
1186        }
1187    }
1188}
1189
1190impl<'a> Pretty<'a, Allocator> for &EnumRows {
1191    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1192        match &self.0 {
1193            EnumRowsF::Empty => allocator.nil(),
1194            EnumRowsF::TailVar(id) => docs![allocator, ";", allocator.line(), id.to_string()],
1195            EnumRowsF::Extend { row, tail } => {
1196                let mut result = row.pretty(allocator);
1197
1198                if let EnumRowsF::Extend { .. } = tail.0 {
1199                    result = result.append(allocator.text(",").append(allocator.line()));
1200                }
1201
1202                result.append(tail.as_ref())
1203            }
1204        }
1205    }
1206}
1207
1208impl<'a> Pretty<'a, Allocator> for &EnumRow {
1209    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1210        let mut result = allocator
1211            .text("'")
1212            .append(allocator.text(enum_tag_quoted(&self.id)));
1213
1214        if let Some(typ) = self.typ.as_ref() {
1215            let ty_parenthesized = if typ.fmt_is_atom() {
1216                typ.pretty(allocator)
1217            } else {
1218                allocator
1219                    .text("(")
1220                    .append(allocator.line_())
1221                    .append(typ.pretty(allocator))
1222                    .append(allocator.line_())
1223                    .append(")")
1224            };
1225
1226            result = result.append(allocator.text(" ")).append(ty_parenthesized);
1227        }
1228
1229        result
1230    }
1231}
1232
1233impl<'a> Pretty<'a, Allocator> for &RecordRows {
1234    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1235        // TODO: move some of this to NickelAllocatorExt so we can impose size limits
1236        match &self.0 {
1237            RecordRowsF::Empty => allocator.nil(),
1238            RecordRowsF::TailDyn => docs![allocator, ";", allocator.line(), "Dyn"],
1239            RecordRowsF::TailVar(id) => docs![allocator, ";", allocator.line(), id.to_string()],
1240            RecordRowsF::Extend { row, tail } => docs![
1241                allocator,
1242                row,
1243                if let RecordRowsF::Extend { .. } = tail.0 {
1244                    docs![allocator, ",", allocator.line()]
1245                } else {
1246                    allocator.nil()
1247                },
1248                tail.as_ref()
1249            ],
1250        }
1251    }
1252}
1253
1254impl<'a, Ty> Pretty<'a, Allocator> for &RecordRowF<Ty>
1255where
1256    Ty: std::ops::Deref<Target = Type>,
1257{
1258    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1259        docs![
1260            allocator,
1261            ident_quoted(&self.id),
1262            " : ",
1263            allocator.type_part(self.typ.deref()),
1264        ]
1265    }
1266}
1267
1268impl<'a> Pretty<'a, Allocator> for RecordRowF<&Type> {
1269    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1270        (&self).pretty(allocator)
1271    }
1272}
1273
1274impl<'a> Pretty<'a, Allocator> for &Type {
1275    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1276        use TypeF::*;
1277        match &self.typ {
1278            Dyn => allocator.text("Dyn"),
1279            Number => allocator.text("Number"),
1280            Bool => allocator.text("Bool"),
1281            String => allocator.text("String"),
1282            Array(ty) => if ty.fmt_is_atom() {
1283                docs![allocator, "Array", allocator.line(), ty.as_ref()].nest(2)
1284            } else {
1285                docs![
1286                    allocator,
1287                    "Array (",
1288                    docs![allocator, allocator.line_(), ty.as_ref()].nest(2),
1289                    allocator.line_(),
1290                    ")"
1291                ]
1292            }
1293            .group(),
1294            ForeignId => allocator.text("ForeignId"),
1295            Symbol => allocator.text("Symbol"),
1296            Contract(t) => t.pretty(allocator),
1297            Var(var) => allocator.as_string(var),
1298            Forall { var, ref body, .. } => {
1299                let mut curr = body.as_ref();
1300                let mut foralls = vec![var];
1301                while let Type {
1302                    typ: Forall { var, ref body, .. },
1303                    ..
1304                } = curr
1305                {
1306                    foralls.push(var);
1307                    curr = body;
1308                }
1309                docs![
1310                    allocator,
1311                    "forall",
1312                    allocator.line(),
1313                    allocator.intersperse(
1314                        foralls.iter().map(|i| allocator.as_string(i)),
1315                        allocator.line(),
1316                    ),
1317                    ".",
1318                    allocator.line(),
1319                    allocator.type_part(curr)
1320                ]
1321                .nest(2)
1322                .group()
1323            }
1324            Enum(erows) => docs![allocator, allocator.line(), erows]
1325                .nest(2)
1326                .append(allocator.line())
1327                .enclose("[|", "|]")
1328                .group(),
1329            Record(rrows) => allocator.record_type(rrows),
1330            Dict {
1331                type_fields: ty,
1332                flavour: attrs,
1333            } => docs![
1334                allocator,
1335                allocator.line(),
1336                "_ ",
1337                match attrs {
1338                    DictTypeFlavour::Type => ":",
1339                    DictTypeFlavour::Contract => "|",
1340                },
1341                " ",
1342                allocator.type_part(ty.as_ref()),
1343            ]
1344            .nest(2)
1345            .append(allocator.line())
1346            .braces()
1347            .group(),
1348            Arrow(dom, codom) => docs![
1349                allocator,
1350                allocator
1351                    .type_part(dom)
1352                    .parens_if(matches!(dom.typ, Arrow(..) | Forall { .. }))
1353                    .nest(2),
1354                allocator.line(),
1355                "-> ",
1356                allocator
1357                    .type_part(codom)
1358                    .parens_if(matches!(codom.typ, Forall { .. }))
1359            ]
1360            .group(),
1361            Wildcard(_) => allocator.text("_"),
1362        }
1363    }
1364}
1365
1366impl<'a> Pretty<'a, Allocator> for &MatchBranch {
1367    fn pretty(self, allocator: &'a Allocator) -> DocBuilder<'a, Allocator> {
1368        let guard = if let Some(guard) = &self.guard {
1369            docs![allocator, allocator.line(), "if", allocator.space(), guard]
1370        } else {
1371            allocator.nil()
1372        };
1373
1374        docs![
1375            allocator,
1376            &self.pattern,
1377            guard,
1378            allocator.space(),
1379            "=>",
1380            docs![allocator, allocator.line(), self.body.pretty(allocator),].nest(2),
1381        ]
1382    }
1383}
1384
1385/// Generate an implementation of `fmt::Display` for types that implement `Pretty`.
1386#[macro_export]
1387macro_rules! impl_display_from_pretty {
1388    ($ty:ty) => {
1389        impl std::fmt::Display for $ty {
1390            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1391                $crate::pretty::fmt_pretty(&self, f)
1392            }
1393        }
1394    };
1395}
1396
1397/// Provide a method to pretty-print a long term, type, etc. (anything that implements `ToString`,
1398/// really) capped to a maximum length.
1399pub trait PrettyPrintCap: ToString {
1400    /// Pretty print an object capped to a given max length (in characters). Useful to limit the
1401    /// size of terms reported e.g. in typechecking errors. If the output of pretty printing is
1402    /// greater than the bound, the string is truncated to `max_width` and the last character after
1403    /// truncate is replaced by the ellipsis unicode character U+2026.
1404    fn pretty_print_cap(&self, max_width: usize) -> String {
1405        let output = self.to_string();
1406
1407        if output.len() <= max_width {
1408            output
1409        } else {
1410            let (end, _) = output.char_indices().nth(max_width).unwrap();
1411            let mut truncated = String::from(&output[..end]);
1412
1413            if max_width >= 2 {
1414                truncated.pop();
1415                truncated.push('\u{2026}');
1416            }
1417
1418            truncated
1419        }
1420    }
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425    use crate::files::Files;
1426    use crate::parser::lexer::Lexer;
1427    use crate::parser::{
1428        grammar::{FixedTypeParser, TermParser},
1429        ErrorTolerantParserCompat,
1430    };
1431    use pretty::Doc;
1432
1433    use super::*;
1434    use indoc::indoc;
1435
1436    /// Parse a type represented as a string.
1437    fn parse_type(s: &str) -> Type {
1438        let id = Files::new().add("<test>", s);
1439
1440        FixedTypeParser::new()
1441            .parse_strict_compat(id, Lexer::new(s))
1442            .unwrap()
1443    }
1444
1445    /// Parse a term represented as a string.
1446    fn parse_term(s: &str) -> RichTerm {
1447        let id = Files::new().add("<test>", s);
1448
1449        TermParser::new()
1450            .parse_strict_compat(id, Lexer::new(s))
1451            .unwrap()
1452    }
1453
1454    /// Parse a string representation `long` of a type, and assert that
1455    /// formatting it gives back `long`, if the line length is set to `80`, or
1456    /// alternatively results in `short`, if the line length is set to `0`
1457    #[track_caller]
1458    fn assert_long_short_type(long: &str, short: &str) {
1459        let ty = parse_type(long);
1460        let alloc = Allocator::default();
1461        let doc: DocBuilder<'_, _, ()> = ty.pretty(&alloc);
1462
1463        let mut long_lines = String::new();
1464        doc.render_fmt(usize::MAX, &mut long_lines).unwrap();
1465
1466        let mut short_lines = String::new();
1467        doc.render_fmt(0, &mut short_lines).unwrap();
1468
1469        assert_eq!(long_lines, long);
1470        assert_eq!(short_lines, short);
1471    }
1472
1473    /// Parse a string representation `long` of a Nickel term, and assert that
1474    /// formatting it gives back `long`, if the line length is set to `80`, or
1475    /// alternatively results in `short`, if the line length is set to `0`
1476    #[track_caller]
1477    fn assert_long_short_term(long: &str, short: &str) {
1478        let term = parse_term(long);
1479        let alloc = Allocator::default();
1480        let doc: DocBuilder<'_, _, ()> = term.pretty(&alloc);
1481
1482        let mut long_lines = String::new();
1483        doc.render_fmt(160, &mut long_lines).unwrap();
1484
1485        let mut short_lines = String::new();
1486        doc.render_fmt(0, &mut short_lines).unwrap();
1487
1488        assert_eq!(long_lines, long);
1489        assert_eq!(short_lines, short);
1490    }
1491
1492    #[test]
1493    fn pretty_array_type() {
1494        assert_long_short_type("Array String", "Array\n  String");
1495        assert_long_short_type(
1496            "Array (Number -> Array Dyn)",
1497            indoc! {"
1498                Array (
1499                  Number
1500                  -> Array
1501                    Dyn
1502                )"
1503            },
1504        );
1505    }
1506
1507    #[test]
1508    fn pretty_arrow_type() {
1509        assert_long_short_type("Number -> Number", "Number\n-> Number");
1510        assert_long_short_type(
1511            "(Number -> Number -> Dyn) -> Number",
1512            indoc! {"
1513                (Number
1514                  -> Number
1515                  -> Dyn)
1516                -> Number"
1517            },
1518        );
1519    }
1520
1521    #[test]
1522    fn pretty_dict_type() {
1523        assert_long_short_type(
1524            "{ _ : Number }",
1525            indoc! {"
1526                {
1527                  _ : Number
1528                }"
1529            },
1530        );
1531        assert_long_short_type(
1532            "{ _ : { x : Number, y : String } }",
1533            indoc! {"
1534                {
1535                  _ : {
1536                    x : Number,
1537                    y : String
1538                  }
1539                }"
1540            },
1541        );
1542    }
1543
1544    #[test]
1545    fn pretty_record_type() {
1546        assert_long_short_type(
1547            "{ x : Number, y : String; Dyn }",
1548            indoc! {"
1549                {
1550                  x : Number,
1551                  y : String;
1552                  Dyn
1553                }"
1554            },
1555        );
1556    }
1557
1558    #[test]
1559    fn pretty_enum_type() {
1560        assert_long_short_type(
1561            "forall r. [| 'tag1, 'tag2, 'tag3; r |]",
1562            indoc! {"
1563                forall
1564                  r.
1565                  [|
1566                    'tag1,
1567                    'tag2,
1568                    'tag3;
1569                    r
1570                  |]"
1571            },
1572        )
1573    }
1574
1575    #[test]
1576    fn pretty_forall_type() {
1577        assert_long_short_type(
1578            "forall a r. a -> { foo : a; r }",
1579            indoc! {"
1580                forall
1581                  a
1582                  r.
1583                  a
1584                  -> {
1585                    foo : a;
1586                    r
1587                  }"
1588            },
1589        );
1590    }
1591
1592    #[test]
1593    fn pretty_opn() {
1594        assert_long_short_term(
1595            "%string/replace% string pattern replace",
1596            indoc! {"
1597                %string/replace%
1598                  string
1599                  pattern
1600                  replace"
1601            },
1602        );
1603    }
1604
1605    #[test]
1606    fn pretty_binop() {
1607        assert_long_short_term(
1608            "a + b",
1609            indoc! {"
1610                a
1611                + b"
1612            },
1613        );
1614        assert_long_short_term(
1615            "%string/split% string sep",
1616            indoc! {"
1617                %string/split%
1618                  string
1619                  sep"
1620            },
1621        );
1622        assert_long_short_term("-5", "-5");
1623        assert_long_short_term(
1624            "a - (-b)",
1625            indoc! {"
1626                a
1627                - (-b)"
1628            },
1629        );
1630    }
1631
1632    #[test]
1633    fn pretty_unop() {
1634        assert_long_short_term("!xyz", "!xyz");
1635        assert_long_short_term(
1636            "a && b",
1637            indoc! {"
1638                a
1639                && b"
1640            },
1641        );
1642        assert_long_short_term(
1643            "(a && b) && c",
1644            indoc! {"
1645                (a
1646                && b)
1647                && c"
1648            },
1649        );
1650        assert_long_short_term(
1651            "a || b",
1652            indoc! {"
1653                a
1654                || b"
1655            },
1656        );
1657        assert_long_short_term(
1658            "if true then false else not",
1659            indoc! {"
1660                if true then
1661                  false
1662                else
1663                  not"
1664            },
1665        );
1666        assert_long_short_term(
1667            "%enum/embed% foo bar",
1668            indoc! {"
1669                %enum/embed%
1670                  foo
1671                  bar"
1672            },
1673        );
1674    }
1675
1676    #[test]
1677    fn pretty_arrays() {
1678        assert_long_short_term(
1679            "[ 1, 2, 3, 4 ]",
1680            indoc! {"
1681                [
1682                  1,
1683                  2,
1684                  3,
1685                  4
1686                ]"
1687            },
1688        );
1689    }
1690
1691    #[test]
1692    fn pretty_match() {
1693        assert_long_short_term(
1694            "match { 'A => a, 'B => b, 'C => c, }",
1695            indoc! {"
1696                match {
1697                  'A =>
1698                    a,
1699                  'B =>
1700                    b,
1701                  'C =>
1702                    c,
1703                }"
1704            },
1705        );
1706    }
1707
1708    #[test]
1709    fn pretty_record() {
1710        assert_long_short_term("{}", "{}");
1711        assert_long_short_term(
1712            "{ a = b, c = d, }",
1713            indoc! {"
1714                {
1715                  a =
1716                    b,
1717                  c =
1718                    d,
1719                }"
1720            },
1721        );
1722        assert_long_short_term(
1723            r#"{ a | String | force = b, c | Number | doc "" = d, }"#,
1724            indoc! {r#"
1725                {
1726                  a
1727                    | String
1728                    | force
1729                    = b,
1730                  c
1731                    | Number
1732                    | doc ""
1733                    = d,
1734                }"#
1735            },
1736        );
1737        assert_long_short_term(
1738            "{ a = b, .. }",
1739            indoc! {"
1740                {
1741                  a =
1742                    b,
1743                  ..
1744                }"
1745            },
1746        );
1747        assert_long_short_term(
1748            r#"{ a = b, "%{a}" = c, .. }"#,
1749            indoc! {r#"
1750                {
1751                  a =
1752                    b,
1753                  "%{a}" =
1754                    c,
1755                  ..
1756                }"#
1757            },
1758        );
1759        assert_long_short_term(
1760            r#"{ "=" = a, }"#,
1761            indoc! {r#"
1762                {
1763                  "=" =
1764                    a,
1765                }"#
1766            },
1767        );
1768    }
1769
1770    #[test]
1771    fn pretty_let() {
1772        assert_long_short_term(
1773            "let rec foo | String = c in {}",
1774            indoc! {"
1775                let rec foo
1776                  | String
1777                  = c
1778                  in
1779                {}"
1780            },
1781        );
1782        assert_long_short_term(
1783            "let foo = c bar in {}",
1784            indoc! {"
1785                let foo
1786                  = c
1787                    bar
1788                  in
1789                {}"
1790            },
1791        );
1792        assert_long_short_term(
1793            "let foo | String = c bar in {}",
1794            indoc! {"
1795                let foo
1796                  | String
1797                  = c
1798                    bar
1799                  in
1800                {}"
1801            },
1802        );
1803    }
1804
1805    #[test]
1806    fn pretty_multiline_strings() {
1807        // The string `"\n1."` contains a newline, so it will be pretty-printed using Nickel's
1808        // multiline string syntax. The result looks like:
1809        // ```
1810        // m%"
1811        //
1812        //   1.
1813        // "%
1814        // ```
1815        // The newline after `m%"` and the newline before `"%` are removed by the parser, as is the
1816        // indentation. Unfortunately, we can't use `indoc!` in this test because `pretty.rs`
1817        // insists on putting two spaces after every newline (but the last one), even if the line
1818        // is otherwise empty.
1819        // But `indoc!` would rightfully strip those empty spaces.
1820        let t: RichTerm = Term::StrChunks(vec![StrChunk::Literal("\n1.".to_owned())]).into();
1821        assert_eq!(format!("{t}"), "m%\"\n  \n  1.\n\"%");
1822
1823        let t: RichTerm = Term::StrChunks(vec![StrChunk::Literal(
1824            "a multiline string\n\n\n\n".to_owned(),
1825        )])
1826        .into();
1827        assert_eq!(
1828            format!("{t}"),
1829            "m%\"\n  a multiline string\n  \n  \n  \n\n\"%"
1830        );
1831    }
1832
1833    #[test]
1834    fn pretty_let_pattern() {
1835        assert_long_short_term(
1836            "let foo @ { a | Bool ? true = a', b ? false, } = c in {}",
1837            indoc! {"
1838                let foo @ {
1839                    a
1840                      | Bool
1841                      ? true
1842                      = a',
1843                    b
1844                      ? false,
1845                  }
1846                  = c
1847                  in
1848                {}"
1849            },
1850        );
1851        assert_long_short_term(
1852            "let foo @ { a = a', b = e @ { foo, .. }, } = c in {}",
1853            indoc! {"
1854                let foo @ {
1855                    a
1856                      = a',
1857                    b
1858                      = e @ {
1859                        foo,
1860                        ..
1861                      },
1862                  }
1863                  = c
1864                  in
1865                {}"
1866            },
1867        );
1868        assert_long_short_term(
1869            "let foo @ { a = a', b, } | String = c in {}",
1870            indoc! {"
1871                let foo @ {
1872                    a
1873                      = a',
1874                    b,
1875                  }
1876                  | String
1877                  = c
1878                  in
1879                {}"
1880            },
1881        );
1882    }
1883
1884    #[test]
1885    fn pretty_fun() {
1886        assert_long_short_term(
1887            "fun x y z => x y z",
1888            indoc! {"
1889                fun
1890                  x
1891                  y
1892                  z
1893                  =>
1894                  x
1895                    y
1896                    z"
1897            },
1898        );
1899        assert_long_short_term(
1900            "fun x @ { foo, bar ? true, } y @ { baz, } => x y z",
1901            indoc! {"
1902                fun
1903                  x @ {
1904                    foo,
1905                    bar
1906                      ? true,
1907                  }
1908                  y @ {
1909                    baz,
1910                  }
1911                  =>
1912                  x
1913                    y
1914                    z"
1915            },
1916        );
1917    }
1918
1919    #[test]
1920    fn pretty_app() {
1921        assert_long_short_term(
1922            "x y z",
1923            indoc! {"
1924                x
1925                  y
1926                  z"
1927            },
1928        );
1929    }
1930
1931    /// Take a string representation of a type, parse it, and assert that formatting it gives the
1932    /// same string as the original argument.
1933    ///
1934    /// Note that there are infinitely many string representations of the same type since, for
1935    /// example, spaces are ignored: for the outcome of this function to be meaningful, the
1936    /// original type must be written in the same way as types are formatted.
1937    #[track_caller]
1938    fn assert_format_eq(s: &str) {
1939        let ty = parse_type(s);
1940        assert_eq!(s, &format!("{ty}"));
1941    }
1942
1943    #[test]
1944    fn types_pretty_printing() {
1945        assert_format_eq("Number");
1946        assert_format_eq("Number -> Number");
1947        assert_format_eq("(Number -> Number) -> (Number -> Number) -> Number -> Number");
1948        assert_format_eq("((Number -> Number) -> Number) -> Number");
1949        assert_format_eq("Number -> (forall a. a -> String) -> String");
1950
1951        assert_format_eq("{ _ : String }");
1952        assert_format_eq("{ _ : (String -> String) -> String }");
1953        assert_format_eq("{ _ | String }");
1954        assert_format_eq("{ _ | (String -> String) -> String }");
1955
1956        assert_format_eq("{ x : (Bool -> Bool) -> Bool, y : Bool }");
1957        assert_format_eq("forall r. { x : Bool, y : Bool, z : Bool; r }");
1958        assert_format_eq("{ x : Bool, y : Bool, z : Bool }");
1959
1960        assert_format_eq("[| 'a, 'b, 'c, 'd |]");
1961        assert_format_eq("forall r. [| 'tag1, 'tag2, 'tag3; r |]");
1962
1963        assert_format_eq("Array Number");
1964        assert_format_eq("Array (Array Number)");
1965        assert_format_eq("Number -> Array (Array String) -> Number");
1966        assert_format_eq("Array (Number -> Number)");
1967        assert_format_eq("Array (Array (Array Dyn) -> Number)");
1968
1969        assert_format_eq("_");
1970        assert_format_eq("_ -> _");
1971        assert_format_eq("{ x : _, y : Bool }");
1972        assert_format_eq("{ _ : _ }");
1973    }
1974
1975    fn format_short_term(input: &str, depth: usize, size: usize) -> String {
1976        let term = parse_term(input);
1977        let allocator = Allocator::bounded(depth, size);
1978        let doc: DocBuilder<_, ()> = term.pretty(&allocator);
1979        Doc::pretty(&doc, 1000).to_string()
1980    }
1981
1982    #[test]
1983    fn bounded_pretty_printing() {
1984        assert_eq!("{ hello = 1, }", &format_short_term("{hello = 1}", 1, 1));
1985        assert_eq!("{…}", &format_short_term("{hello = 1, bye = 2}", 1, 1));
1986        assert_eq!(
1987            "{ hello = 1, inner = { bye = 2, }, }",
1988            &format_short_term("{hello = 1, inner = { bye = 2 }}", 2, 2)
1989        );
1990        assert_eq!(
1991            "{ hello = 1, inner = {…}, }",
1992            &format_short_term("{hello = 1, inner = { bye = 2 }}", 1, 100)
1993        );
1994        assert_eq!(
1995            "{ hello = 1, inner = {…}, }",
1996            &format_short_term("{hello = 1, inner = { bye = 2, other = 3 }}", 100, 2)
1997        );
1998    }
1999}