Repository: tweag/nickel
Pull request #1055 (2023-01-19 11:57:20)

Add a %trace% primop
Add a `%trace%` builtin which prints a message to standard output when encountered by the evaluator, together with a standard libary function `builtin.trace`.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a2fe25b0..842c6f44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1705,7 +1705,6 @@ dependencies = [
 name = "pyckel"
 version = "0.3.1"
 dependencies = [
- "codespan-reporting",
  "nickel-lang",
  "pyo3",
 ]
diff --git a/pyckel/Cargo.toml b/pyckel/Cargo.toml
index 7eeb6e0d..9869bfcc 100644
--- a/pyckel/Cargo.toml
+++ b/pyckel/Cargo.toml
@@ -13,7 +13,6 @@ edition = "2018"
 [dependencies]
 nickel-lang = {default-features = false, path = "../", version = "0.3.1" }
 pyo3 = { version = "0.17.3", features = ["extension-module"] }
-codespan-reporting = "0.11"
 
 [lib]
 crate-type = ["cdylib", "rlib"]
diff --git a/pyckel/src/lib.rs b/pyckel/src/lib.rs
index b2d44eee..0fb18093 100644
--- a/pyckel/src/lib.rs
+++ b/pyckel/src/lib.rs
@@ -1,8 +1,8 @@
 use std::io::Cursor;
 
 use nickel_lang::{
-    error::Error,
-    eval::cache::{CBNCache, Cache},
+    error::{Error, SerializationError},
+    eval::cache::CBNCache,
     program::Program,
     serialize,
 };
@@ -11,9 +11,38 @@ use pyo3::{create_exception, exceptions::PyException, prelude::*};
 
 create_exception!(pyckel, NickelException, PyException);
 
-/// Turn an internal Nickel error into a PyErr with a fancy diagnostic message
-fn error_to_exception<E: Into<Error>, EC: Cache>(error: E, program: &mut Program<EC>) -> PyErr {
-    NickelException::new_err(program.report_as_str(error.into()))
+// see https://pyo3.rs/v0.17.3/function/error_handling.html#foreign-rust-error-types
+struct NickelError(Error);
+struct NickelSerializationError(SerializationError);
+
+impl From<Error> for NickelError {
+    fn from(value: Error) -> Self {
+        Self(value)
+    }
+}
+
+impl std::convert::From<NickelError> for PyErr {
+    fn from(err: NickelError) -> PyErr {
+        match err {
+            // TODO better exceptions
+            NickelError(error) => NickelException::new_err(format!("{error:?}")),
+        }
+    }
+}
+
+impl From<SerializationError> for NickelSerializationError {
+    fn from(value: SerializationError) -> Self {
+        Self(value)
+    }
+}
+
+impl std::convert::From<NickelSerializationError> for PyErr {
+    fn from(err: NickelSerializationError) -> PyErr {
+        match err {
+            // TODO better exceptions
+            NickelSerializationError(error) => NickelException::new_err(format!("{error:?}")),
+        }
+    }
 }
 
 /// Evaluate from a Python str of a Nickel expression to a Python str of the resulting JSON.
@@ -22,15 +51,12 @@ pub fn run(s: String) -> PyResult<String> {
     let mut program: Program<CBNCache> =
         Program::new_from_source(Cursor::new(s.to_string()), "python")?;
 
-    let term = program
-        .eval_full()
-        .map_err(|error| error_to_exception(error, &mut program))?;
+    let term = program.eval_full().map_err(NickelError)?;
 
-    serialize::validate(serialize::ExportFormat::Json, &term)
-        .map_err(|error| error_to_exception(error, &mut program))?;
+    serialize::validate(serialize::ExportFormat::Json, &term).map_err(NickelSerializationError)?;
 
     let json_string = serialize::to_string(serialize::ExportFormat::Json, &term)
-        .map_err(|error| error_to_exception(error, &mut program))?;
+        .map_err(NickelSerializationError)?;
 
     Ok(json_string)
 }
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 8a7d050d..6b8c8194 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -672,17 +672,11 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     }
                 }
             }
-            UnaryOp::Seq() => {
-                if self.stack.count_args() >= 1 {
-                    let (next, ..) = self
-                        .stack
-                        .pop_arg(&self.cache)
-                        .expect("Condition already checked.");
-                    Ok(next)
-                } else {
-                    Err(EvalError::NotEnoughArgs(2, String::from("seq"), pos_op))
-                }
-            }
+            UnaryOp::Seq() => self
+                .stack
+                .pop_arg(&self.cache)
+                .map(|(next, ..)| next)
+                .ok_or_else(|| EvalError::NotEnoughArgs(2, String::from("seq"), pos_op)),
             UnaryOp::DeepSeq(_) => {
                 /// Build a RichTerm that forces a given list of terms, and at the end resumes the
                 /// evaluation of the argument on the top of the stack. The argument must iterate over
@@ -1258,6 +1252,24 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     ))
                 }
             },
+            UnaryOp::Trace() => {
+                if let Term::Str(s) = &*t {
+                    eprintln!("builtin.trace: {s}");
+                    Ok(())
+                } else {
+                    Err(EvalError::TypeError(
+                        String::from("Str"),
+                        String::from("trace"),
+                        arg_pos,
+                        RichTerm { term: t, pos },
+                    ))
+                }?;
+
+                self.stack
+                    .pop_arg(&self.cache)
+                    .map(|(next, ..)| next)
+                    .ok_or_else(|| EvalError::NotEnoughArgs(2, String::from("trace"), pos_op))
+            }
         }
     }
 
diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop
index 06310566..b754c3ba 100644
--- a/src/parser/grammar.lalrpop
+++ b/src/parser/grammar.lalrpop
@@ -596,6 +596,7 @@ UOp: UnaryOp = {
     "rec_force_op" => UnaryOp::RecForce(),
     "rec_default_op" => UnaryOp::RecDefault(),
     "record_empty_with_tail" => UnaryOp::RecordEmptyWithTail(),
+    "trace" => UnaryOp::Trace(),
 };
 
 MatchCase: MatchCase = {
@@ -937,6 +938,7 @@ extern {
         "pow" => Token::Normal(NormalToken::Pow),
         "rec_force_op" => Token::Normal(NormalToken::RecForceOp),
         "rec_default_op" => Token::Normal(NormalToken::RecDefaultOp),
+        "trace" => Token::Normal(NormalToken::Trace),
 
         "has_field" => Token::Normal(NormalToken::HasField),
         "map" => Token::Normal(NormalToken::Map),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 9dd785ed..89ece5a0 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -223,6 +223,8 @@ pub enum NormalToken<'input> {
     ValuesOf,
     #[token("%pow%")]
     Pow,
+    #[token("%trace%")]
+    Trace,
 
     #[token("%has_field%")]
     HasField,
diff --git a/src/program.rs b/src/program.rs
index 726cc679..64775129 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -29,9 +29,9 @@ use crate::parser::lexer::Lexer;
 use crate::term::{RichTerm, Term};
 use crate::{eval, parser};
 use codespan::FileId;
-use codespan_reporting::term::termcolor::{Ansi, ColorChoice, StandardStream};
+use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
 use std::ffi::OsString;
-use std::io::{self, Cursor, Read};
+use std::io::{self, Read};
 use std::result::Result;
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -163,28 +163,6 @@ impl<EC: EvalCache> Program<EC> {
         report(self.vm.import_resolver_mut(), error, self.color_opt)
     }
 
-    /// Build an error report as a string and return it.
-    pub fn report_as_str<E>(&mut self, error: E) -> String
-    where
-        E: ToDiagnostic<FileId>,
-    {
-        let cache = self.vm.import_resolver_mut();
-        let contracts_id = cache.id_of("<stdlib/contract.ncl>");
-        let diagnostics = error.to_diagnostic(cache.files_mut(), contracts_id);
-        let mut buffer = Ansi::new(Cursor::new(Vec::new()));
-        let config = codespan_reporting::term::Config::default();
-        // write to `buffer`
-        diagnostics
-            .iter()
-            .try_for_each(|d| {
-                codespan_reporting::term::emit(&mut buffer, &config, cache.files_mut(), d)
-            })
-            // safe because writing to a cursor in memory
-            .unwrap();
-        // unwrap(): emit() should only print valid utf8 to the the buffer
-        String::from_utf8(buffer.into_inner().into_inner()).unwrap()
-    }
-
     /// Create a markdown file with documentation for the specified program in `.nickel/doc/program_main_file_name.md`
     #[cfg(feature = "doc")]
     pub fn output_doc(&mut self, out: &mut dyn std::io::Write) -> Result<(), Error> {
diff --git a/src/term/mod.rs b/src/term/mod.rs
index 92a8fb7f..1cc11be4 100644
--- a/src/term/mod.rs
+++ b/src/term/mod.rs
@@ -1047,6 +1047,11 @@ pub enum UnaryOp {
     /// define a `field_diff` function that preserves the sealed polymorphic
     /// tail of its argument.
     RecordEmptyWithTail(),
+
+    /// Print a message when encountered during evaluation and proceed with the evaluation of the argument
+    /// on the top of the stack. Operationally the same as the identity
+    /// function
+    Trace(),
 }
 
 // See: https://github.com/rust-lang/regex/issues/178
diff --git a/src/typecheck/operation.rs b/src/typecheck/operation.rs
index eaa25a3f..0dc67b18 100644
--- a/src/typecheck/operation.rs
+++ b/src/typecheck/operation.rs
@@ -211,6 +211,12 @@ pub fn get_uop_type(
             (ty.clone(), ty)
         }
         UnaryOp::RecordEmptyWithTail() => (mk_uniftype::dynamic(), mk_uniftype::dynamic()),
+
+        // forall a. Str -> a -> a
+        UnaryOp::Trace() => {
+            let ty = UnifType::UnifVar(state.table.fresh_type_var_id());
+            (mk_uniftype::str(), mk_uty_arrow!(ty.clone(), ty))
+        }
     })
 }
 
diff --git a/stdlib/builtin.ncl b/stdlib/builtin.ncl
index b35d7f7b..404e2747 100644
--- a/stdlib/builtin.ncl
+++ b/stdlib/builtin.ncl
@@ -207,8 +207,22 @@
         "Foo"
       from null =>
         "null"
+      ```
       "%
     = fun x => %to_str% x,
 
+    trace : forall a. Str -> a -> a
+    | doc m%"
+      `builtin.trace msg x` prints `msg` to standard output when encountered by the evaluator,
+      and proceed with the evaluation of `x`.
+
+      For example:
+      ```nickel
+      builtin.trace "Hello, world!" true =>
+        builtin.trace: Hello, world!
+        true
+      ```
+      "%
+    = fun msg x => %trace% msg x,
   },
 }


Test patch:
diff --git a/tests/snapshot/inputs/export/trace.ncl b/tests/snapshot/inputs/export/trace.ncl
new file mode 100644
index 00000000..f18fbcde
--- /dev/null
+++ b/tests/snapshot/inputs/export/trace.ncl
@@ -0,0 +1,1 @@
+builtin.trace "Hello, world!" true
diff --git a/tests/snapshot/snapshots/snapshot__export_trace.ncl.snap b/tests/snapshot/snapshots/snapshot__export_trace.ncl.snap
new file mode 100644
index 00000000..109fa87b
--- /dev/null
+++ b/tests/snapshot/snapshots/snapshot__export_trace.ncl.snap
@@ -0,0 +1,5 @@
+---
+source: tests/snapshot/main.rs
+expression: snapshot
+---
+true