Skip to main content

nickel_lang_core/
pretty.rs

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    /// Never allow rendering as a multiline string
26    ForceMonoline,
27    /// Render as a multiline string if the string contains a newline
28    Multiline,
29}
30
31/// Helper to find the min number of `%` sign needed to interpolate a string containing this chunk.
32fn min_interpolate_sign(text: &str) -> usize {
33    let reg = Regex::new(r#"([%]+\{)|("[%]+)"#).unwrap();
34    reg.find_iter(text)
35        .map(|m| {
36            // We iterate over all sequences `%+{` and `"%+`, which could clash with the
37            // interpolation syntax, and return the maximum number of `%` insead each sequence.
38            //
39            // For the case of a closing delimiter `"%`, we could actually be slightly smarter as we
40            // don't necessarily need more `%`, but just a different number of `%`. For example, if
41            // the string contains only one `"%%`, then single `%` delimiters like `m%"` and `"%`
42            // would be fine. But picking the maximum results in a simpler algorithm for now, which
43            // we can update later if necessary.
44            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
56/// Escape a string to make it suitable for placing between quotes in Nickel
57fn 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
67/// Return the string representation of an identifier, and add enclosing double quotes if the
68/// label isn't a valid identifier according to the parser, for example if it contains a
69/// special character like a space.
70pub 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
80/// Return a string representation of an identifier, adding enclosing double quotes if
81/// the label isn't valid for an enum tag. This is like `ident_quoted` except that keywords
82/// aren't wrapped in quotes (because `'if` is a valid enum tag, for example).
83pub 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
106/// We need the field's value to inspect it for an application of
107/// `$rec_default` or `$rec_force`. The recursive priority metadata
108/// annotations `rec default` and `rec force` are converted at parsing
109/// time into applications of the internal symbols `$rec_default` and
110/// `$rec_force` respectively. These symbols are not valid identifiers in
111/// Nickel's surface syntax, so we need to handle them specially.
112///
113/// If we find a recursive priority annotation we return the field's value
114/// with the internal application removed.
115fn 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
130/// Does a sequence of `StrChunk`s contain a literal newline?
131fn 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
138/// Does a sequence of `StrChunk`s contain a carriage return? Lone carriage
139/// returns are forbidden in Nickel's surface syntax.
140fn 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
147/// Determines if a type to be printed in type position needs additional parentheses.
148///
149/// Terms in type position don't need to be atoms: for example, we should pretty print `foo |
150/// Contract arg1 arg2` without parentheses instead of `foo | (Contract arg1 arg2)`.
151///
152/// However, some terms (i.e. contracts) in type position still need parentheses in some cases, for
153/// example when said term is a function function. This function precisely determines if the given
154/// type is such a term.
155fn 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
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: &[(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                            // For now we don't need to escape the included id, as it must be a
303                            // valid variable name, and thus can't contain non-identifier
304                            // characters such as spaces.
305                            .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    /// Escape the special characters in a string, including the newline character, so that it can
376    /// be enclosed by double quotes a be a valid Nickel string.
377    fn escaped_string<'a>(&'a self, s: &str) -> DocBuilder<'a, Self> {
378        self.text(escape(s))
379    }
380
381    /// Print string chunks, either in the single line or multiline style.
382    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                    }, // be sure we have at least 1 `%` sign when an interpolation is present
402                )
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                                // We do this manually instead of using
437                                // `str::lines` because we need to be careful
438                                // about whether a trailing newline appears at
439                                // the end of the last line.
440                                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    /// Print a function, which can have several parameters (represented as nested functions), and
481    /// where each layer might be a normal function, a pattern matching function or a custom
482    /// contract. [function] automatically unwrap any of those nested layers to print the function
483    /// with as many parameters as possible on the left of the `=>` separator.
484    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            // Nickel will not parse a multiline string literal in this position
559            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    /// Almost identical to calling `typ.pretty(self)`, but adds parentheses when the type is
621    /// actually a contract that has a top-level form that needs parentheses (let-binding,
622    /// if-then-else, etc.).
623    ///
624    /// Although terms can appear in type position as contracts, the parenthesis rules are slightly
625    /// more restrictive than for a generic term: for example, `{foo | let x = Contract in x}` is
626    /// not valid Nickel. It must be parenthesised as `{foo | (let x = Contract in x)}`.
627    ///
628    /// This method must be used whenever a type is rendered either as component of another type or
629    /// in the position of an annotation. Rendering stand-alone types (for example as part of error
630    /// messages) can avoid those parentheses and directly call to `typ.pretty(allocator)` instead.
631    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    /// Call `self.parens()` but only if `parens` is `true`.
638    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            // NOTE: the Array attributes are ignored here. They contain only
758            // information that has no surface syntax.
759            {
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            // Format this as the primop application `%contract/custom% <ctr>`.
795            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            // This type is in term position, so we don't need to add parentheses.
802            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        // TODO: move some of this to NickelAllocatorExt so we can impose size limits
1030        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/// Generate an implementation of `fmt::Display` for types that implement `Pretty`.
1167#[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
1178/// Provide a method to pretty-print a long term, type, etc. (anything that implements `ToString`,
1179/// really) capped to a maximum length.
1180pub trait PrettyPrintCap: ToString {
1181    /// Pretty print an object capped to a given max length (in characters). Useful to limit the
1182    /// size of terms reported e.g. in typechecking errors. If the output of pretty printing is
1183    /// greater than the bound, the string is truncated to `max_width` and the last character after
1184    /// truncate is replaced by the ellipsis unicode character U+2026.
1185    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    /// Parse a type represented as a string.
1221    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    /// Parse a term represented as a string.
1230    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    /// Parse a string representation `long` of a type, and assert that
1239    /// formatting it gives back `long`, if the line length is set to `80`, or
1240    /// alternatively results in `short`, if the line length is set to `0`
1241    #[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    /// Parse a string representation `long` of a Nickel term, and assert that
1258    /// formatting it gives back `long`, if the line length is set to `80`, or
1259    /// alternatively results in `short`, if the line length is set to `0`
1260    #[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        // The string `"\n1."` contains a newline, so it will be pretty-printed using Nickel's
1575        // multiline string syntax. The result looks like:
1576        // ```
1577        // m%"
1578        //
1579        //   1.
1580        // "%
1581        // ```
1582        // The newline after `m%"` and the newline before `"%` are removed by the parser, as is the
1583        // indentation. Unfortunately, we can't use `indoc!` in this test because `pretty.rs`
1584        // insists on putting two spaces after every newline (but the last one), even if the line
1585        // is otherwise empty.
1586        // But `indoc!` would rightfully strip those empty spaces.
1587        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    /// Take a string representation of a type, parse it, and assert that formatting it gives the
1630    /// same string as the original argument.
1631    ///
1632    /// Note that there are infinitely many string representations of the same type since, for
1633    /// example, spaces are ignored: for the outcome of this function to be meaningful, the
1634    /// original type must be written in the same way as types are formatted.
1635    #[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}