Repository: tweag/nickel
Pull request #811 (2022-08-30 09:11:24)

Add capped pretty printing function
Implement the display trait for terms using the pretty printer, and add an helper to pretty print a term within a given limited width. Preliminary work for solving #724 (used for reporting failed unification of contracts).


Patch:
diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml
index 65e6074c..1056fa2a 100644
--- a/.github/workflows/update-flake-lock.yml
+++ b/.github/workflows/update-flake-lock.yml
@@ -16,4 +16,4 @@ jobs:
           extra_nix_config: |
             access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
       - name: Update flake.lock
-        uses: DeterminateSystems/update-flake-lock@v13
+        uses: DeterminateSystems/update-flake-lock@v12
diff --git a/benches/arrays.rs b/benches/arrays.rs
index f4d14a9e..4427688c 100644
--- a/benches/arrays.rs
+++ b/benches/arrays.rs
@@ -1,29 +1,6 @@
 use criterion::{criterion_main, Criterion};
-use nickel_lang::term::{ArrayAttrs, RichTerm, Term};
 use nickel_lang_utilities::{ncl_bench_group, EvalMode};
 use pprof::criterion::{Output, PProfProfiler};
-use pretty::{BoxAllocator, DocBuilder, Pretty};
-
-/// Generates a pseaudo-random Nickel array as a string.
-fn ncl_random_array(len: usize) -> String {
-    let m = 2_u64.pow(32);
-    let a = 1664525;
-    let c = 1013904223;
-
-    let mut numbers = Vec::with_capacity(len);
-    let mut acc = 1337;
-
-    for _ in 0..len {
-        acc = (a * acc + c) % m;
-        numbers.push(RichTerm::from(Term::Num(acc as f64)));
-    }
-
-    let xs = RichTerm::from(Term::Array(numbers, ArrayAttrs::default()));
-    let doc: DocBuilder<_, ()> = xs.pretty(&BoxAllocator);
-    let mut out = Vec::new();
-    doc.render(80, &mut out).unwrap();
-    String::from_utf8(out).unwrap()
-}
 
 ncl_bench_group! {
 name = benches;
@@ -131,7 +108,7 @@ config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flam
     }, {
         name = "sort normal",
         path = "arrays/sort",
-        args = (ncl_random_array(50)),
+        args = (20),
     }, {
         name = "sum normal 50",
         path = "arrays/sum",
@@ -150,15 +127,6 @@ config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flam
         path = "arrays/primes",
         args = (30),
         eval_mode = EvalMode::DeepSeq,
-    }, {
-        name = "random normal",
-        path = "arrays/random",
-        args = (50),
-    }, {
-        name = "random deepseq",
-        path = "arrays/random",
-        args = (50),
-        eval_mode = EvalMode::DeepSeq,
     }
 }
 criterion_main!(benches);
diff --git a/benches/arrays/random.ncl b/benches/arrays/random.ncl
deleted file mode 100644
index 48a0772e..00000000
--- a/benches/arrays/random.ncl
+++ /dev/null
@@ -1,22 +0,0 @@
-let mgc 
-  | doc "A multiplicative congruential pseudorandom number generator (MCG)."
-  = fun params x => (params.a * x + params.c) % params.m in
-
-# NOTE: These parameters are from "Table 7: Good multipliers for MCGs with m = 2^32"
-# of the 2021 paper "Computationally Easy, Spectrally Good Multipliers for
-# Congruential Pseudorandom Number Generators", by Guy Steele and Sebastiano Vigna.
-# But, they cannot be used because they require bit-shifting to select the 32 MSBs.
-# let params = { m = num.pow 2 32, a = 2480367069 } in
-
-# These parameters are from the "Numerical Recipes" book.
-let params = { m = num.pow 2 32, a = 1664525, c = 1013904223 } in
-
-let rec array_random = fun init n =>
-  if n == 0 then [init]
-  else 
-    let next = mgc params init in
-    [next] @ array_random next (n - 1)
-in
-
-{ run = array_random 42 }
-
diff --git a/benches/arrays/sort.ncl b/benches/arrays/sort.ncl
index 3204d762..e0a9f834 100644
--- a/benches/arrays/sort.ncl
+++ b/benches/arrays/sort.ncl
@@ -1,7 +1,5 @@
-let ascending = fun x y => 
-  if x < y then `Lesser 
-  else if x == y then `Equal 
-  else `Greater 
-in
-
-{ run = array.sort ascending }
+let nums = [3288, 2438, 1462, 437, 17, 1929, 2595, 2702, 831, 3685, 1906, 420, 2646, 1906, 2539, 184, 299, 3455, 2320, 1471, 1812, 3837, 1000, 2097, 945, 1973, 1090, 2285, 2272, 1045, 848, 3194, 2113, 537, 1953, 561, 2082, 2416, 3182, 3660, 3379, 1458, 39, 2720, 2469, 811, 837, 548, 3021, 2961, 2860, 2092, 942, 2511, 1769, 168, 130, 2755, 2552, 1057, 2619, 1848, 2373, 2012, 2339, 2521, 3593, 2784, 3094, 2260, 1994, 690, 828, 1298, 3434, 2929, 2188, 1285, 3783, 3635, 971, 3334, 87, 1578, 1363, 533, 3634, 1874, 830, 3976, 1140, 789, 1483, 1966, 1626, 3552, 2968, 3301, 749, 2576] in
+let ascending = fun x y => if x < y then `Less else if (x == y) then `Equal else `Greater in
+{
+    run = fun n => array.sort ascending nums
+}
diff --git a/src/cache.rs b/src/cache.rs
index 20d6fbe2..91f82562 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -846,8 +846,8 @@ impl Cache {
     }
 
     /// Load, parse, and apply program transformations to the standard library. Do not typecheck
-    /// for performance reasons: this is done in the test suite. Return an initial environment
-    /// containing both the evaluation and type environments. If you only need the type environment, use
+    /// for performance reason: this is done in the test suite. Return an initial environment
+    /// containing both eval and type environment. If you need only the type environment, use
     /// `load_stdlib` then `mk_type_env` to avoid transformations and evaluation preparation.
     pub fn prepare_stdlib(&mut self) -> Result<Envs, Error> {
         #[cfg(debug_assertions)]
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index ea7c6f8c..62429b73 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -280,7 +280,7 @@ where
 /// # Arguments
 ///
 /// - `clos`: the closure to evaluate
-/// - `initial_env`: the initial environment containing the stdlib items.
+/// - `initial_env`: the initial environment containing the builtin functions of the language.
 ///   Accessible from anywhere in the program.
 /// - `resolver`: the interface to fetch imports.
 /// - `enriched_strict`: if evaluation is strict with respect to enriched values (metavalues).
diff --git a/src/term.rs b/src/term.rs
index 5988522d..d952a2fd 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -1298,6 +1298,28 @@ impl RichTerm {
             TraverseOrder::BottomUp => f(result, state),
         }
     }
+
+    /// Pretty print a term capped to a given max length (in characters). Useful to limit the size
+    /// of terms reported e.g. in typechecking errors. If the output of pretty printing is greater
+    /// than the bound, the string is truncated to `max_width` and the last character after
+    /// truncate is replaced by the ellipsis unicode character U+2026.
+    pub fn pretty_print_cap(&self, max_width: usize) -> String {
+        let output = format!("{}", self);
+
+        if output.len() <= max_width {
+            output
+        } else {
+            let (end, _) = output.char_indices().nth(max_width).unwrap();
+            let mut truncated = String::from(&output[..end]);
+
+            if max_width >= 2 {
+                truncated.pop();
+                truncated.push('\u{2026}');
+            }
+
+            truncated
+        }
+    }
 }
 
 impl From<RichTerm> for Term {
@@ -1321,6 +1343,18 @@ impl From<Term> for RichTerm {
     }
 }
 
+impl std::fmt::Display for RichTerm {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        use crate::pretty::*;
+        use pretty::BoxAllocator;
+
+        let allocator = BoxAllocator;
+
+        let doc: DocBuilder<_, ()> = self.clone().pretty(&allocator);
+        doc.render_fmt(80, f)
+    }
+}
+
 /// Allows to match on SharedTerm without taking ownership of the matched part until the match.
 /// In the `else` clause, we haven't taken ownership yet, so we can still use the richterm at that point.
 ///
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 7faff74d..d7646c82 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -150,7 +150,7 @@ pub struct State<'a> {
 /// Typecheck a term.
 ///
 /// Return the inferred type in case of success. This is just a wrapper that calls
-/// `type_check_linearize` with a blanket implementation for the linearizer.
+/// `type_check_linearize` with an blanket implementation for the linearizer.
 ///
 /// Note that this function doesn't recursively typecheck imports (anymore), but just the current
 /// file. It however still needs the resolver to get the apparent type of imports.
diff --git a/stdlib/array.ncl b/stdlib/array.ncl
index 10e9ab92..75b3788a 100644
--- a/stdlib/array.ncl
+++ b/stdlib/array.ncl
@@ -260,13 +260,13 @@
 
         For example:
         ```nickel
-          sort (fun x y => if x < y then `Lesser else if (x == y) then `Equal else `Greater) [ 4, 5, 1, 2 ] =>
+          sort (fun x y => if x < y then `Less else if (x == y) then `Equal else `Greater) [ 4, 5, 1, 2 ] =>
             [ 1, 2, 4, 5 ]
         ```
         "%m
       = fun cmp l =>
         let first = %head% l in
-        let parts = partition (fun x => (cmp x first == `Lesser)) (%tail% l) in
+        let parts = partition (fun x => (cmp x first == `Less)) (%tail% l) in
         if %length% l <= 1 then
           l
         else


Test patch:
diff --git a/tests/pass/arrays.ncl b/tests/pass/arrays.ncl
index 6933a786..55cc54a2 100644
--- a/tests/pass/arrays.ncl
+++ b/tests/pass/arrays.ncl
@@ -10,13 +10,6 @@ let Assert = fun l x => x || %blame% l in
   array.length [1,2,3] == 3,
   array.length ([] @ [1,2] @ [3,4] @ []) == 4,
 
-  # sort
-  let cmp = fun x y =>
-    if x < y then `Lesser
-    else if x == y then `Equal
-    else `Greater
-  in array.sort cmp [3, 42, -1, -5] == [-5, -1, 3, 42],
-
   # Test case added after https://github.com/tweag/nickel/issues/154
   let x = 1 in let l = [x] @ [2] in %head% l == 1,