Skip to main content

nickel_lang_parser/ast/
pretty.rs

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