Repository: tweag/nickel
Pull request #683 (2022-04-22 15:10:54)

Feature/pretty print
Pretty print of the AST (implemented as a method of `RichTerm`)

Will help for debuging stufs


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 30751144..db293c6f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,6 +79,12 @@ dependencies = [
  "nodrop",
 ]
 
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
 [[package]]
 name = "ascii-canvas"
 version = "3.0.0"
@@ -315,7 +321,7 @@ dependencies = [
  "shell-words",
  "syntect",
  "twoway",
- "typed-arena",
+ "typed-arena 1.7.0",
  "unicode_categories",
  "xdg",
 ]
@@ -1108,6 +1114,7 @@ dependencies = [
  "md-5",
  "nickel-lang-utilities",
  "pprof",
+ "pretty",
  "pretty_assertions",
  "regex 0.2.11",
  "rustyline",
@@ -1118,6 +1125,7 @@ dependencies = [
  "serde_yaml",
  "sha-1 0.9.8",
  "sha2",
+ "similar",
  "simple-counter",
  "structopt",
  "termimad",
@@ -1206,7 +1214,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.4.12",
  "itoa 0.4.8",
 ]
 
@@ -1465,6 +1473,18 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
+[[package]]
+name = "pretty"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29afd15870fab34b2a8884481a68f052b3d720cfb29666d7ae0ac2a42ea10289"
+dependencies = [
+ "arrayvec 0.5.2",
+ "log",
+ "typed-arena 2.0.1",
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "pretty_assertions"
 version = "0.5.1"
@@ -1856,6 +1876,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "similar"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
+
 [[package]]
 name = "simple-counter"
 version = "0.1.0"
@@ -2140,6 +2166,12 @@ version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
 
+[[package]]
+name = "typed-arena"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae"
+
 [[package]]
 name = "typenum"
 version = "1.15.0"
diff --git a/Cargo.toml b/Cargo.toml
index 88cbfb62..e5906b12 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,6 +55,7 @@ rustyline-derive = { version = "0.4.0", optional = true }
 wasm-bindgen = { version = "=0.2.74", optional = true, features = ["serde-serialize"] }
 js-sys = { version = "0.3", optional = true }
 serde_repr = { version = "0.1", optional = true }
+pretty = "0.11.2"
 
 comrak = { version = "0.12.1", optional = true, features = [] }
 
@@ -64,6 +65,7 @@ assert_matches = "1.4.0"
 criterion = "0.3"
 pprof = { version = "0.4.4", features = ["criterion", "flamegraph"] }
 nickel-lang-utilities = {path = "utilities", version = "0.1.0"}
+similar = "2.1.0"
 
 [workspace]
 members = [
diff --git a/src/bin/nickel.rs b/src/bin/nickel.rs
index 7100e12b..566eac34 100644
--- a/src/bin/nickel.rs
+++ b/src/bin/nickel.rs
@@ -31,6 +31,14 @@ struct Opt {
 /// Available subcommands.
 #[derive(StructOpt, Debug)]
 enum Command {
+    /// Expand the nickel source after requiered transformation.
+    /// Could be used for debugging.
+    /// By default only format the code with comments droped.
+    Expand {
+        /// Performs code transformations before printing
+        #[structopt(short = "t", long)]
+        transform: bool,
+    },
     /// Export the result to a different format
     Export {
         /// Available formats: `raw, json, yaml, toml`. Default format: `json`.
@@ -102,6 +110,10 @@ fn main() {
         }
 
         let result = match opts.command {
+            Some(Command::Expand { transform }) => program.expand(
+                &mut std::io::BufWriter::new(Box::new(std::io::stdout())),
+                transform,
+            ),
             Some(Command::Export { format, output }) => export(&mut program, format, output),
             Some(Command::Query {
                 path,
diff --git a/src/lib.rs b/src/lib.rs
index b1a081fc..e402cf60 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,7 @@ pub mod identifier;
 pub mod label;
 pub mod parser;
 pub mod position;
+pub mod pretty;
 pub mod program;
 pub mod repl;
 pub mod serialize;
diff --git a/src/pretty.rs b/src/pretty.rs
new file mode 100644
index 00000000..8e244712
--- /dev/null
+++ b/src/pretty.rs
@@ -0,0 +1,786 @@
+use crate::destruct::{self, Destruct};
+use crate::term::{BinaryOp, MetaValue, RichTerm, Term, UnaryOp};
+use crate::types::{AbsType, Types};
+pub use pretty::{DocAllocator, DocBuilder, Pretty};
+use regex::Regex;
+use std::collections::HashMap;
+
+/// helper to find the min number of `%` sign needed to interpolate
+/// a string containing this chunk.
+fn min_interpolate_sign(text: &str) -> usize {
+    let reg = Regex::new(r#"([%]+\{)|("[%]+m)"#).unwrap();
+    reg.find_iter(text).fold(0, |nb, m| {
+        let d = m.end() - m.start();
+        // if the match end with `{` the nb of `%` is equal to the size of the match.
+        // This because a `%*{` match len is nb of `%` + 1. It's corect because we are
+        // looking for the minimum number of `%` to perform interpolation on this string
+        // Finaly, if the match ends with `m` we return match len - 1 because of the
+        // extra `"`. In this case, we could improve, because interpolation actualy
+        // need no more than a different number of `%` than the `"%*m` sequence.
+        // But this way is valid anyway and easyer to describe algorithmicaly.
+        // TODO: Improve the `"%*m` case if necessary.
+        let d = if m.as_str().ends_with("{") { d } else { d - 1 };
+        nb.max(d)
+    })
+}
+
+fn sorted_map<'a, K: Ord, V>(m: &'a HashMap<K, V>) -> Vec<(&'a K, &'a V)> {
+    let mut ret: Vec<(&K, &V)> = m.iter().collect();
+    ret.sort_by(|x, y| x.0.cmp(&y.0));
+    ret
+}
+
+impl<'a, A: Clone + 'a> NickelAllocatorExt<'a, A> for pretty::BoxAllocator {}
+
+trait NickelAllocatorExt<'a, A: 'a>: DocAllocator<'a, A> + Sized
+where
+    Self::Doc: Clone,
+    A: Clone,
+{
+    fn quote_if_needed(self: &'a Self, id: &crate::identifier::Ident) -> DocBuilder<'a, Self, A> {
+        let reg = Regex::new("^_?[a-zA-Z][_a-zA-Z0-9-]*$").unwrap();
+        if reg.is_match(&id.to_string()) {
+            self.as_string(id)
+        } else {
+            self.as_string(id).double_quotes()
+        }
+    }
+
+    fn escaped_string(self: &'a Self, s: &str) -> DocBuilder<'a, Self, A> {
+        let s = s
+            .replace("\\", "\\\\")
+            .replace("%{", "\\%{")
+            .replace("\"", "\\\"");
+        self.text(s)
+    }
+
+    fn metadata(self: &'a Self, mv: &MetaValue, with_doc: bool) -> DocBuilder<'a, Self, A> {
+        if let Some(types) = &mv.types {
+            self.text(":")
+                .append(self.space())
+                .append(types.types.clone().pretty(self))
+                .append(self.line())
+        } else {
+            self.nil()
+        }
+        .append(if with_doc {
+            mv.doc
+                .clone()
+                .map(|doc| {
+                    self.text("|")
+                        .append(self.space())
+                        .append(self.text("doc"))
+                        .append(self.space())
+                        .append(
+                            self.hardline()
+                                .append(self.intersperse(
+                                    doc.lines().map(|d| self.escaped_string(&d)),
+                                    self.hardline().clone(),
+                                ))
+                                .double_quotes(),
+                        )
+                        .append(self.line())
+                })
+                .unwrap_or(self.nil())
+        } else {
+            self.nil()
+        })
+        .append(self.intersperse(
+            mv.contracts.iter().map(|c| {
+                self.text("|")
+                    .append(self.space())
+                    .append(c.to_owned().types.pretty(self))
+            }),
+            self.line().clone(),
+        ))
+        .append(if mv.priority == crate::term::MergePriority::Default {
+            self.line().append(self.text("| default"))
+        } else {
+            self.nil()
+        })
+        .nest(2)
+        .group()
+    }
+
+    fn atom(&'a self, rt: &RichTerm) -> DocBuilder<'a, Self, A> {
+        if rt.as_ref().is_atom() {
+            rt.to_owned().pretty(self)
+        } else {
+            rt.to_owned().pretty(self).parens()
+        }
+    }
+}
+
+impl<'a, D, A> Pretty<'a, D, A> for &UnaryOp
+where
+    D: NickelAllocatorExt<'a, A>,
+    D::Doc: Clone,
+    A: Clone + 'a,
+{
+    fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
+        use UnaryOp::*;
+        match self {
+            ArrayGen() => allocator.text("%gen%").append(allocator.space()),
+            ArrayMap() => allocator.text("%map%").append(allocator.space()),
+            ArrayLength() => allocator.text("%length%").append(allocator.space()),
+            ArrayTail() => allocator.text("%tail%").append(allocator.space()),
+            ArrayHead() => allocator.text("%head%").append(allocator.space()),
+            DeepSeq(_) => allocator.text("%deep_seq%").append(allocator.space()),
+            IsNum() => allocator.text("%is_num%").append(allocator.space()),
+            IsFun() => allocator.text("%is_fun%").append(allocator.space()),
+            IsStr() => allocator.text("%is_str%").append(allocator.space()),
+            IsArray() => allocator.text("%is_array%").append(allocator.space()),
+            IsRecord() => allocator.text("%is_record%").append(allocator.space()),
+            BoolNot() => allocator.text("!"),
+            BoolAnd() => allocator.space().append(allocator.text("&&")),
+            BoolOr() => allocator.space().append(allocator.text("||")),
+            StaticAccess(id) => allocator.text(".").append(allocator.quote_if_needed(id)),
+            Embed(id) => allocator
+                .text("%embed%")
+                .append(allocator.space())
+                .append(allocator.as_string(id))
+                .append(allocator.space()),
+            op => allocator
+                .text(format!("%{:?}%", op).to_lowercase())
+                .append(allocator.space()),
+        }
+    }
+}
+
+impl<'a, D, A> Pretty<'a, D, A> for &BinaryOp
+where
+    D: NickelAllocatorExt<'a, A>,
+    D::Doc: Clone,
+    A: Clone + 'a,
+{
+    fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
+        use BinaryOp::*;
+        match self {
+            Plus() => allocator.text("+"),
+            Sub() => allocator.text("-"),
+
+            Mult() => allocator.text("*"),
+            Div() => allocator.text("/"),
+            Modulo() => allocator.text("%"),
+
+            Eq() => allocator.text("=="),
+            LessThan() => allocator.text("<"),
+            GreaterThan() => allocator.text(">"),
+            GreaterOrEq() => allocator.text(">="),
+            LessOrEq() => allocator.text("<="),
+
+            Merge() => allocator.text("&"),
+
+            StrConcat() => allocator.text("++"),
+            ArrayConcat() => allocator.text("@"),
+
+            DynAccess() => allocator.text("."),
+            ArrayElemAt() => allocator.text("%elem_at%"),
+
+            op => allocator.as_string(format!("%{:?}%", op).to_lowercase()),
+        }
+    }
+}
+
+impl<'a, D, A> Pretty<'a, D, A> for &Destruct
+where
+    D: NickelAllocatorExt<'a, A>,
+    D::Doc: Clone,
+    A: Clone + 'a,
+{
+    fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
+        match self {
+            Destruct::Record {
+                matches,
+                // TODO: manage `..}` and `..x}` ending patterns
+                open: _,
+                rest: _,
+                ..
+            } => allocator
+                .intersperse(
+                    matches.iter().map(|m| match m {
+                        destruct::Match::Simple(id, meta) => allocator
+                            .as_string(id)
+                            .append(allocator.space())
+                            .append(allocator.metadata(meta, false)),
+                        _ => unimplemented!(),
+                    }),
+                    allocator.text(",").append(allocator.space()),
+                )
+                .braces(),
+            Destruct::Empty => allocator.nil(),
+            _ => unimplemented!(),
+        }
+    }
+}
+
+impl<'a, D, A> Pretty<'a, D, A> for RichTerm
+where
+    D: NickelAllocatorExt<'a, A>,
+    D::Doc: Clone,
+    A: Clone + 'a,
+{
+    fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
+        use Term::*;
+
+        match self.as_ref() {
+            Null => allocator.text("null"),
+            Bool(v) => allocator.as_string(v),
+            Num(v) => allocator.as_string(v),
+            Str(v) => allocator.escaped_string(v),
+            StrChunks(chunks) => {
+                let multiline = chunks.len() > 1;
+                let nb_perc = chunks
+                    .iter()
+                    .map(
+                        |c| {
+                            if let crate::term::StrChunk::Literal(s) = c {
+                                min_interpolate_sign(s)
+                            } else {
+                                1
+                            }
+                        }, // be sure we have at least 1 `%` sign when an interpolation is present
+                    )
+                    .max()
+                    .unwrap();
+                let interp: String = std::iter::repeat("%").take(nb_perc).collect();
+                allocator
+                    .intersperse(
+                        chunks.iter().rev().map(|c| match c {
+                            crate::term::StrChunk::Literal(s) => {
+                                if multiline {
+                                    allocator.as_string(s)
+                                } else {
+                                    allocator.escaped_string(s)
+                                }
+                            }
+                            crate::term::StrChunk::Expr(e, _i) => allocator
+                                .text(interp.clone())
+                                .append(allocator.text("{"))
+                                .append(e.to_owned().pretty(allocator))
+                                .append(allocator.text("}")),
+                        }),
+                        allocator.nil(),
+                    )
+                    .double_quotes()
+                    .enclose(
+                        if multiline {
+                            format!("m{}", interp)
+                        } else {
+                            "".to_string()
+                        },
+                        if multiline {
+                            format!("{}m", interp)
+                        } else {
+                            "".to_string()
+                        },
+                    )
+            }
+            Fun(id, rt) => {
+                let mut params = vec![id];
+                let mut rt = rt;
+                while let Fun(id, t) = rt.as_ref() {
+                    params.push(id);
+                    rt = t
+                }
+                allocator
+                    .text("fun")
+                    .append(allocator.space())
+                    .append(allocator.intersperse(
+                        params.iter().map(|p| allocator.as_string(p)),
+                        allocator.space(),
+                    ))
+                    .append(allocator.space())
+                    .append(allocator.text("=>"))
+                    .append(allocator.softline())
+                    .append(rt.to_owned().pretty(allocator).nest(2))
+            }
+            // TODO Pattern destructuring to implement.
+            FunPattern(..) => {
+                let mut params = vec![];
+                let mut rt = &self;
+                while let FunPattern(id, _dst, t) = rt.as_ref() {
+                    params.push(
+                        if let Some(id) = id {
+                            allocator.as_string(id)
+                        } else {
+                            allocator.nil()
+                        }
+                        .append(allocator.nil()),
+                    );
+                    rt = t;
+                }
+                allocator
+                    .text("fun")
+                    .append(allocator.space())
+                    .append(allocator.intersperse(params, allocator.space()))
+                    .append(allocator.space())
+                    .append(allocator.text("=>"))
+                    .append(allocator.softline())
+                    .append(rt.to_owned().pretty(allocator).nest(2))
+            }
+            Lbl(_lbl) => allocator.text("# <label>").append(allocator.hardline()),
+            Let(id, rt, body, _ty) => allocator
+                .text("let")
+                .append(allocator.space())
+                .append(allocator.as_string(id))
+                .append(if let MetaValue(ref mv) = rt.as_ref() {
+                    allocator.space().append(allocator.metadata(mv, false))
+                } else {
+                    allocator.nil()
+                })
+                .append(allocator.space())
+                .append(allocator.text("="))
+                .append(allocator.line())
+                .append(
+                    if let MetaValue(crate::term::MetaValue {
+                        value: Some(rt), ..
+                    }) = rt.as_ref()
+                    {
+                        rt
+                    } else {
+                        rt
+                    }
+                    .to_owned()
+                    .pretty(allocator)
+                    .nest(2),
+                )
+                .append(allocator.line())
+                .append(allocator.text("in"))
+                .append(allocator.line_())
+                .group()
+                .append(allocator.line())
+                .append(body.to_owned().pretty(allocator))
+                .group(),
+            LetPattern(opt_id, dst, rt, body) => allocator
+                .text("let")
+                .append(allocator.space())
+                .append(
+                    opt_id
+                        .clone()
+                        .map(|id| {
+                            allocator.as_string(id).append(if dst.is_empty() {
+                                allocator.nil()
+                            } else {
+                                allocator
+                                    .space()
+                                    .append(allocator.text("@"))
+                                    .append(allocator.space())
+                            })
+                        })
+                        .unwrap_or(allocator.nil()),
+                )
+                .append(dst.pretty(allocator))
+                .append(if let MetaValue(ref mv) = rt.as_ref() {
+                    allocator.space().append(allocator.metadata(mv, false))
+                } else {
+                    allocator.nil()
+                })
+                .append(allocator.space())
+                .append(allocator.text("="))
+                .append(allocator.line())
+                .append(
+                    if let MetaValue(crate::term::MetaValue {
+                        value: Some(rt), ..
+                    }) = rt.as_ref()
+                    {
+                        rt
+                    } else {
+                        rt
+                    }
+                    .to_owned()
+                    .pretty(allocator)
+                    .nest(2),
+                )
+                .append(allocator.line())
+                .append(allocator.text("in"))
+                .append(allocator.line_())
+                .group()
+                .append(allocator.line())
+                .append(body.to_owned().pretty(allocator))
+                .group(),
+            App(rt1, rt2) => match rt1.as_ref() {
+                Op1(crate::term::UnaryOp::Ite(), _) => rt1
+                    .to_owned()
+                    .pretty(allocator)
+                    .append(allocator.space())
+                    .append(allocator.text("then"))
+                    .append(allocator.line())
+                    .append(rt2.to_owned().pretty(allocator).nest(2))
+                    .append(allocator.line())
+                    .append(allocator.text("else"))
+                    .group(),
+                App(..) => rt1
+                    .to_owned()
+                    .pretty(allocator)
+                    .append(allocator.line())
+                    .append(allocator.atom(rt2))
+                    .group(),
+                _ => allocator
+                    .atom(rt1)
+                    .append(allocator.line())
+                    .append(allocator.atom(rt2))
+                    .group(),
+            },
+            Var(id) => allocator.as_string(id),
+            Enum(id) => allocator.text("`").append(allocator.quote_if_needed(id)),
+            Record(fields, attr) => allocator
+                .line()
+                .append(allocator.intersperse(
+                    sorted_map(fields).iter().map(|&(id, rt)| {
+                        allocator
+                            .quote_if_needed(id)
+                            .append(allocator.space())
+                            .append(if let MetaValue(mv) = rt.as_ref() {
+                                allocator
+                                    .metadata(&mv, true)
+                                    .append(allocator.space())
+                                    .append(
+                                        mv.value
+                                            .clone()
+                                            .map(|v| {
+                                                allocator
+                                                    .text("=")
+                                                    .append(allocator.space())
+                                                    .append(v.pretty(allocator))
+                                            })
+                                            .unwrap_or(allocator.nil()),
+                                    )
+                            } else {
+                                allocator
+                                    .text("=")
+                                    .append(allocator.line())
+                                    .append(rt.to_owned().pretty(allocator).nest(2))
+                            })
+                            .append(allocator.text(","))
+                    }),
+                    allocator.line(),
+                ))
+                .append(if attr.open {
+                    allocator.line().append(allocator.text(".."))
+                } else {
+                    allocator.nil()
+                })
+                .nest(2)
+                .append(allocator.line())
+                .group()
+                .braces(),
+            RecRecord(
+                fields,
+                dyn_fields, /* field whose name is defined by interpolation */
+                attr,
+                _deps, /* dependency tracking between fields. None before the free var pass */
+            ) => allocator
+                .line()
+                .append(
+                    allocator.intersperse(
+                        sorted_map(fields)
+                            .iter()
+                            .map(|&(id, rt)| {
+                                allocator
+                                    .quote_if_needed(id)
+                                    .append(allocator.space())
+                                    .append(if let MetaValue(mv) = rt.as_ref() {
+                                        allocator
+                                            .metadata(&mv, true)
+                                            .append(allocator.space())
+                                            .append(
+                                                mv.value
+                                                    .clone()
+                                                    .map(|v| {
+                                                        allocator
+                                                            .text("=")
+                                                            .append(allocator.space())
+                                                            .append(v.pretty(allocator))
+                                                    })
+                                                    .unwrap_or(allocator.nil()),
+                                            )
+                                    } else {
+                                        allocator
+                                            .text("=")
+                                            .append(allocator.line())
+                                            .append(rt.to_owned().pretty(allocator).nest(2))
+                                    })
+                                    .append(allocator.text(","))
+                            })
+                            .chain(dyn_fields.iter().map(|(id, rt)| {
+                                id.to_owned()
+                                    .pretty(allocator)
+                                    .append(allocator.space())
+                                    .append(if let MetaValue(mv) = rt.as_ref() {
+                                        allocator
+                                            .metadata(&mv, true)
+                                            .append(allocator.space())
+                                            .append(
+                                                mv.value
+                                                    .clone()
+                                                    .map(|v| {
+                                                        allocator
+                                                            .text("=")
+                                                            .append(v.pretty(allocator))
+                                                    })
+                                                    .unwrap_or(allocator.nil()),
+                                            )
+                                    } else {
+                                        allocator
+                                            .text("=")
+                                            .append(allocator.space())
+                                            .append(allocator.line())
+                                            .append(rt.to_owned().pretty(allocator).nest(2))
+                                    })
+                                    .append(allocator.text(","))
+                            })),
+                        allocator.line(),
+                    ),
+                )
+                .append(if attr.open {
+                    allocator.line().append(allocator.text(".."))
+                } else {
+                    allocator.nil()
+                })
+                .nest(2)
+                .append(allocator.line())
+                .group()
+                .braces(),
+            Switch(tst, cases, def) => allocator
+                .text("switch")
+                .append(allocator.space())
+                .append(
+                    allocator
+                        .intersperse(
+                            sorted_map(cases).iter().map(|&(id, t)| {
+                                allocator
+                                    .text("`")
+                                    .append(allocator.quote_if_needed(id))
+                                    .append(allocator.space())
+                                    .append(allocator.text("=>"))
+                                    .append(allocator.space())
+                                    .append(t.to_owned().pretty(allocator))
+                                    .append(allocator.text(","))
+                            }),
+                            allocator.line(),
+                        )
+                        .append(def.clone().map_or(allocator.nil(), |d| {
+                            allocator
+                                .line()
+                                .append(allocator.text("_"))
+                                .append(allocator.space())
+                                .append(allocator.text("=>"))
+                                .append(allocator.space())
+                                .append(d.to_owned().pretty(allocator))
+                        }))
+                        .nest(2)
+                        .append(allocator.line_())
+                        .braces()
+                        .group(),
+                )
+                .append(allocator.space())
+                .append(allocator.atom(tst)),
+            Array(fields) => allocator
+                .line()
+                .append(allocator.intersperse(
+                    fields.iter().map(|rt| rt.to_owned().pretty(allocator)),
+                    allocator.text(",").append(allocator.line()),
+                ))
+                .nest(2)
+                .append(allocator.line())
+                .group()
+                .brackets(),
+
+            Op1(op, rt) => match op.pos() {
+                crate::term::OpPos::Prefix => op.pretty(allocator).append(allocator.atom(rt)),
+                crate::term::OpPos::Postfix => allocator.atom(rt).append(op.pretty(allocator)),
+                crate::term::OpPos::Infix => unreachable!(),
+                crate::term::OpPos::Special => {
+                    use UnaryOp::*;
+                    match op {
+                        Ite() => allocator
+                            .text("if")
+                            .append(allocator.space())
+                            .append(rt.to_owned().pretty(allocator)),
+                        op => panic!("pretty print is not impleented for {:?}", op),
+                    }
+                }
+            },
+            Op2(op, rtl, rtr) => if op == &BinaryOp::DynAccess() {
+                rtr.to_owned()
+                    .pretty(allocator)
+                    .append(op.pretty(allocator))
+                    .append(rtl.to_owned().pretty(allocator))
+            } else {
+                if (&BinaryOp::Sub(), &Num(0.0)) == (op, rtl.as_ref()) {
+                    allocator.text("-")
+                } else if let crate::term::OpPos::Prefix = op.pos() {
+                    op.pretty(allocator)
+                        .append(allocator.space())
+                        .append(allocator.atom(rtl))
+                } else {
+                    allocator
+                        .atom(rtl)
+                        .append(allocator.space())
+                        .append(op.pretty(allocator))
+                }
+                .append(allocator.line())
+                .append(allocator.atom(rtr))
+                .nest(2)
+            }
+            .group(),
+            OpN(op, rts) => allocator
+                .as_string(op)
+                .append(allocator.line())
+                .append(
+                    allocator
+                        .intersperse(rts.iter().map(|rt| allocator.atom(rt)), allocator.line())
+                        .nest(2),
+                )
+                .group(),
+
+            Sym(sym) => allocator
+                .text(format!("# <symbol: {}>", sym))
+                .append(allocator.hardline()),
+
+            // TODO
+            Wrapped(_i, _rt) => allocator.text("# <wraped>").append(allocator.hardline()),
+
+            MetaValue(mv) => mv.to_owned().pretty(allocator),
+            Import(f) => allocator
+                .text("import")
+                .append(allocator.space())
+                .append(allocator.as_string(f.to_string_lossy()).double_quotes()),
+            ResolvedImport(id) => allocator.text(format!("import <file_id: {:?}>", id)),
+            ParseError => allocator
+                .text("# <PARCE ERROR!>")
+                .append(allocator.hardline()),
+        }
+    }
+}
+
+impl<'a, D, A> Pretty<'a, D, A> for MetaValue
+where
+    D: NickelAllocatorExt<'a, A>,
+    D::Doc: Clone,
+    A: Clone + 'a,
+{
+    fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
+        if let Some(value) = &self.value {
+            allocator.atom(value).append(allocator.space())
+        } else {
+            allocator.nil()
+        }
+        .append(allocator.metadata(&self, false))
+    }
+}
+
+impl<'a, D, A> Pretty<'a, D, A> for Types
+where
+    D: NickelAllocatorExt<'a, A>,
+    D::Doc: Clone,
+    A: Clone + 'a,
+{
+    fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
+        use AbsType::*;
+        match self.0 {
+            Dyn() => allocator.text("Dyn"),
+            Num() => allocator.text("Num"),
+            Bool() => allocator.text("Bool"),
+            Str() => allocator.text("Str"),
+            Array(ty) => allocator
+                .text("Array")
+                .group()
+                .append(allocator.space())
+                .append(if ty.fmt_is_atom() {
+                    ty.pretty(allocator)
+                } else {
+                    ty.pretty(allocator).nest(2).parens()
+                }),
+            Sym() => allocator.text("Sym"),
+            Flat(t) => t.pretty(allocator),
+            Var(var) => allocator.as_string(var),
+            Forall(id, ref ty) => {
+                let mut curr = ty.as_ref();
+                let mut foralls = vec![&id];
+                while let Types(Forall(id, ref ty)) = curr {
+                    foralls.push(id);
+                    curr = ty;
+                }
+                allocator
+                    .text("forall")
+                    .append(allocator.line())
+                    .group()
+                    .append(allocator.intersperse(
+                        foralls.iter().map(|i| allocator.as_string(i)),
+                        allocator.space(), //allocator.softline(),
+                    ))
+                    .append(allocator.text("."))
+                    .append(allocator.line())
+                    .append(curr.to_owned().pretty(allocator))
+            }
+            Enum(row) => row.pretty(allocator).enclose("[|", "|]"),
+            StaticRecord(row) => match &row.0 {
+                AbsType::Var(id) => allocator
+                    .space()
+                    .append(allocator.text(";"))
+                    .append(allocator.space())
+                    .append(allocator.as_string(id))
+                    .braces(),
+                _ => row.pretty(allocator).braces(),
+            },
+            DynRecord(ty) => allocator
+                .line()
+                .append(allocator.text("_"))
+                .append(allocator.space())
+                .append(allocator.text(":"))
+                .append(allocator.space())
+                .append(ty.pretty(allocator))
+                .append(allocator.line())
+                .braces(),
+            RowEmpty() => allocator.nil(),
+            RowExtend(id, ty_opt, tail) => {
+                let mut builder = allocator.quote_if_needed(&id);
+
+                builder = if let Some(ty) = ty_opt {
+                    builder
+                        .append(allocator.text(":"))
+                        .append(allocator.space())
+                        .append(ty.pretty(allocator))
+                } else {
+                    builder
+                };
+
+                match tail.0 {
+                    AbsType::RowEmpty() => builder,
+                    AbsType::Var(_) => builder
+                        .append(allocator.space())
+                        .append(allocator.text(";"))
+                        .append(allocator.space()),
+                    AbsType::Dyn() => {
+                        return builder
+                            .append(allocator.space())
+                            .append(allocator.text(";"))
+                            .append(allocator.space())
+                            .append(allocator.text("Dyn"))
+                    }
+                    _ => builder
+                        .append(allocator.text(","))
+                        .append(allocator.space()),
+                }
+                .append(tail.pretty(allocator))
+            }
+            Arrow(dom, codom) => match dom.0 {
+                Arrow(..) | Forall(..) => dom
+                    .pretty(allocator)
+                    .parens()
+                    .append(allocator.softline())
+                    .append(allocator.text("->"))
+                    .append(allocator.space())
+                    .append(codom.pretty(allocator)),
+                _ => dom
+                    .pretty(allocator)
+                    .append(allocator.softline())
+                    .append(allocator.text("->"))
+                    .append(allocator.space())
+                    .append(codom.pretty(allocator)),
+            },
+        }
+    }
+}
diff --git a/src/program.rs b/src/program.rs
index 8f6bef2f..b97ab6f9 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -136,6 +136,28 @@ impl Program {
     pub fn set_skip_stdlib(&mut self) {
         self.cache.skip_stdlib = true;
     }
+
+    pub fn expand(
+        &mut self,
+        out: &mut std::io::BufWriter<Box<dyn std::io::Write>>,
+        apply_transforms: bool,
+    ) -> Result<(), Error> {
+        use crate::pretty::*;
+        use pretty::BoxAllocator;
+
+        let Program { ref main_id, cache } = self;
+        let allocator = BoxAllocator;
+
+        let rt = cache.parse_nocache(*main_id)?.0;
+        let rt = if apply_transforms {
+            crate::transform::transform(rt).unwrap()
+        } else {
+            rt
+        };
+        let doc: DocBuilder<_, ()> = rt.pretty(&allocator);
+        doc.render(80, out).unwrap();
+        Ok(())
+    }
 }
 
 /// Query the metadata of a path of a term in the cache.
diff --git a/src/term.rs b/src/term.rs
index 3708aa1b..def58e64 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -340,7 +340,7 @@ impl<E> StrChunk<E> {
 }
 
 impl Term {
-    #[cfg(test)]
+    //#[cfg(test)]
     /// Recursively apply a function to all `Term`s contained in a `RichTerm`.
     pub fn apply_to_rich_terms<F>(&mut self, func: F)
     where
@@ -609,6 +609,38 @@ impl Term {
             | Term::ParseError => false,
         }
     }
+
+    /// determine if a term is atomic
+    pub fn is_atom(&self) -> bool {
+        match self {
+            Term::Null
+            | Term::Bool(..)
+            | Term::Num(..)
+            | Term::Str(..)
+            | Term::StrChunks(..)
+            | Term::Lbl(..)
+            | Term::Enum(..)
+            | Term::Record(..)
+            | Term::RecRecord(..)
+            | Term::Array(..)
+            | Term::Var(..)
+            | Term::Op1(..)
+            | Term::Sym(..) => true,
+            Term::Let(..)
+            | Term::Switch(..)
+            | Term::LetPattern(..)
+            | Term::Fun(..)
+            | Term::FunPattern(..)
+            | Term::App(..)
+            | Term::Op2(..)
+            | Term::OpN(..)
+            | Term::Wrapped(..)
+            | Term::MetaValue(..)
+            | Term::Import(..)
+            | Term::ResolvedImport(..)
+            | Term::ParseError => false,
+        }
+    }
 }
 
 #[derive(Debug, Clone, PartialEq)]
@@ -803,6 +835,26 @@ pub enum UnaryOp {
     EnumFromStr(),
 }
 
+/// position of a unary operator
+pub enum OpPos {
+    Infix,
+    Postfix,
+    Prefix,
+    /// A special operator like `if ... then ... else ...`
+    Special,
+}
+
+impl UnaryOp {
+    pub fn pos(&self) -> OpPos {
+        use UnaryOp::*;
+        match self {
+            BoolAnd() | BoolOr() | StaticAccess(_) => OpPos::Postfix,
+            Ite() => OpPos::Special,
+            _ => OpPos::Prefix,
+        }
+    }
+}
+
 /// Primitive binary operators
 #[derive(Clone, Debug, PartialEq)]
 pub enum BinaryOp {
@@ -893,6 +945,16 @@ impl BinaryOp {
             _ => true,
         }
     }
+
+    pub fn pos(&self) -> OpPos {
+        use BinaryOp::*;
+        match self {
+            Plus() | Sub() | Mult() | Div() | Modulo() | Pow() | StrConcat() | Eq()
+            | LessThan() | LessOrEq() | GreaterThan() | GreaterOrEq() | DynExtend()
+            | DynRemove() | DynAccess() | ArrayConcat() | Merge() => OpPos::Infix,
+            _ => OpPos::Prefix,
+        }
+    }
 }
 
 /// Primitive n-ary operators. Unary and binary operator make up for most of operators and are
@@ -961,7 +1023,7 @@ impl RichTerm {
     /// Erase recursively the positional information.
     ///
     /// It allows to use rust `Eq` trait to compare the values of the underlying terms.
-    #[cfg(test)]
+    //#[cfg(test)]
     pub fn without_pos(mut self) -> Self {
         fn clean_pos(rt: &mut RichTerm) {
             rt.pos = TermPos::None;


Test patch:
diff --git a/tests/pretty.rs b/tests/pretty.rs
new file mode 100644
index 00000000..ad4ed55d
--- /dev/null
+++ b/tests/pretty.rs
@@ -0,0 +1,140 @@
+use nickel_lang::pretty::*;
+use nickel_lang::term::RichTerm;
+use nickel_lang_utilities::parse;
+use pretty::BoxAllocator;
+
+use std::io::{Cursor, Read};
+use std::path::PathBuf;
+
+fn diff(s1: &str, s2: &str) {
+    use similar::*;
+    let diff = TextDiff::from_lines(s1, s2);
+    let mut nb_diff = 0;
+
+    for change in diff.iter_all_changes() {
+        match change.tag() {
+            ChangeTag::Delete => {
+                nb_diff += 1;
+                println!("- {}", change);
+            }
+            ChangeTag::Insert => {
+                nb_diff += 1;
+                println!("+ {}", change);
+            }
+            ChangeTag::Equal => (),
+        };
+    }
+    assert!(nb_diff == 0);
+}
+
+fn pretty(rt: &RichTerm) -> String {
+    let allocator = BoxAllocator;
+    let mut ret = Vec::new();
+    let mut rt_pretty = Cursor::new(&mut ret);
+
+    let doc: DocBuilder<_, ()> = rt.clone().pretty(&allocator);
+    doc.render(80, &mut rt_pretty).unwrap();
+    String::from_utf8_lossy(&ret).into_owned()
+}
+
+fn check_file(file: &str) {
+    let mut buffer = String::new();
+    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+    path.push(format!("tests/pass/{}", file));
+    std::fs::File::open(&path)
+        .and_then(|mut file| file.read_to_string(&mut buffer))
+        .unwrap();
+
+    let rt = parse(&buffer).unwrap();
+    let pretty_rt = pretty(&rt);
+    let double_pretty = pretty(&parse(&pretty_rt).unwrap());
+
+    diff(&pretty_rt, &double_pretty);
+}
+
+#[test]
+fn basics() {
+    check_file("basics.ncl");
+}
+
+#[test]
+fn builtins() {
+    check_file("builtins.ncl");
+}
+
+#[test]
+fn complete() {
+    check_file("complete.ncl");
+}
+
+#[test]
+fn eq() {
+    check_file("eq.ncl")
+}
+
+#[test]
+fn functions() {
+    check_file("functions.ncl");
+}
+
+#[test]
+fn arrays() {
+    check_file("arrays.ncl");
+}
+// TODO: Maybe fix the issue with transformation of `let A = Num` form
+// in `let A = $num` which is not parsable.
+//#[test]
+//fn metavalues() {
+//    check_file("metavalues.ncl");
+//}
+//
+//#[test]
+//fn contracts() {
+//    check_file("contracts.ncl");
+//}
+
+#[test]
+fn records() {
+    check_file("records.ncl");
+}
+
+#[test]
+fn record_defs() {
+    check_file("record-defs.ncl");
+}
+
+#[test]
+fn strings() {
+    check_file("strings.ncl");
+}
+
+#[test]
+fn typechecking() {
+    check_file("typechecking.ncl");
+}
+
+#[test]
+fn types() {
+    check_file("types.ncl");
+}
+
+#[test]
+fn serialize() {
+    check_file("serialize.ncl");
+    check_file("serialize-package.ncl");
+}
+
+#[test]
+fn annot_parsing() {
+    check_file("annotations.ncl");
+}
+
+#[test]
+fn importing() {
+    check_file("import.ncl");
+}
+
+#[test]
+fn overriding() {
+    check_file("overriding.ncl");
+}