nickel_lang_core/bytecode/
pretty.rs

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