about summary refs log tree commit diff
path: root/tvix/eval/src/errors.rs
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2022-09-12T13·12+0300
committertazjin <tazjin@tvl.su>2022-09-13T14·41+0000
commit0f59fe66015580612ee79a9e9e985eaa0fa495e6 (patch)
tree5b709005b5652d04bc20cd2dfb8a6784af6f4f8b /tvix/eval/src/errors.rs
parent7fd7a4465b4357654af51bf7914e438932141f31 (diff)
feat(tvix/eval): implement initial fancy formatting for errors r/4836
This very closely follows the way it's done for warnings, but errors
have a lot more information available in some cases which we do not
surface yet.

Note also that due to requiring the `CodeMap`, this is not yet called
from eval.rs as the way that is threaded through needs to be
refactored, so only the method for reporting these errors as strings
is implemented so far.

Next steps for this will be to add a generic diagnostics module that
reduces some of the boilerplate for this between warnings & errors,
and which will also give us a good point in the future to switch to a
fancier diagnostics crate.

Change-Id: If6bb209f8e7a568d866e516a90335b9b2afbf66d
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6534
Reviewed-by: grfn <grfn@gws.fyi>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/eval/src/errors.rs')
-rw-r--r--tvix/eval/src/errors.rs150
1 files changed, 138 insertions, 12 deletions
diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs
index b87555d06d..39b105de98 100644
--- a/tvix/eval/src/errors.rs
+++ b/tvix/eval/src/errors.rs
@@ -1,16 +1,24 @@
 use std::fmt::Display;
 
+use codemap::{CodeMap, Span};
+use codemap_diagnostic::{Diagnostic, Emitter, Level, SpanLabel, SpanStyle};
+
+use crate::Value;
+
 #[derive(Clone, Debug)]
 pub enum ErrorKind {
+    /// These are user-generated errors through builtins.
+    Throw(String),
+    Abort(String),
+    AssertionFailed,
+
     DuplicateAttrsKey {
         key: String,
     },
 
     /// Attempted to specify an invalid key type (e.g. integer) in a
     /// dynamic attribute name.
-    InvalidAttributeName {
-        given: &'static str,
-    },
+    InvalidAttributeName(Value),
 
     AttributeNotFound {
         name: String,
@@ -30,7 +38,7 @@ pub enum ErrorKind {
     PathResolution(String),
 
     /// Dynamic keys are not allowed in let.
-    DynamicKeyInLet(rnix::SyntaxNode),
+    DynamicKeyInLet,
 
     /// Unknown variable in statically known scope.
     UnknownStaticVariable,
@@ -39,7 +47,7 @@ pub enum ErrorKind {
     UnknownDynamicVariable(String),
 
     /// User is defining the same variable twice at the same depth.
-    VariableAlreadyDefined(String),
+    VariableAlreadyDefined(Span),
 
     /// Attempt to call something that is not callable.
     NotCallable,
@@ -49,12 +57,6 @@ pub enum ErrorKind {
 
     ParseErrors(Vec<rnix::parser::ParseError>),
 
-    AssertionFailed,
-
-    /// These are user-generated errors through builtins.
-    Throw(String),
-    Abort(String),
-
     /// An error occured while forcing a thunk, and needs to be
     /// chained up.
     ThunkForce(Box<Error>),
@@ -68,7 +70,7 @@ pub enum ErrorKind {
 #[derive(Clone, Debug)]
 pub struct Error {
     pub kind: ErrorKind,
-    pub span: codemap::Span,
+    pub span: Span,
 }
 
 impl Display for Error {
@@ -78,3 +80,127 @@ impl Display for Error {
 }
 
 pub type EvalResult<T> = Result<T, Error>;
+
+impl Error {
+    pub fn fancy_format_str(&self, codemap: &CodeMap) -> String {
+        let mut out = vec![];
+        Emitter::vec(&mut out, Some(codemap)).emit(&[self.diagnostic(codemap)]);
+        String::from_utf8_lossy(&out).to_string()
+    }
+
+    /// Create the optional span label displayed as an annotation on
+    /// the underlined span of the error.
+    fn span_label(&self) -> Option<String> {
+        None
+    }
+
+    /// Create the primary error message displayed to users.
+    fn message(&self, codemap: &CodeMap) -> String {
+        match &self.kind {
+            ErrorKind::Throw(msg) => format!("error thrown: {}", msg),
+            ErrorKind::Abort(msg) => format!("evaluation aborted: {}", msg),
+            ErrorKind::AssertionFailed => "assertion failed".to_string(),
+
+            ErrorKind::DuplicateAttrsKey { key } => {
+                format!("attribute key '{}' already defined", key)
+            }
+
+            ErrorKind::InvalidAttributeName(val) => format!(
+                "found attribute name '{}' of type '{}', but attribute names must be strings",
+                val,
+                val.type_of()
+            ),
+
+            ErrorKind::AttributeNotFound { name } => format!(
+                "attribute with name '{}' could not be found in the set",
+                name
+            ),
+
+            ErrorKind::TypeError { expected, actual } => format!(
+                "expected value of type '{}', but found a '{}'",
+                expected, actual
+            ),
+
+            ErrorKind::Incomparable { lhs, rhs } => {
+                format!("can not compare a {} with a {}", lhs, rhs)
+            }
+
+            ErrorKind::PathResolution(err) => format!("could not resolve path: {}", err),
+
+            ErrorKind::DynamicKeyInLet => {
+                "dynamically evaluated keys are not allowed in let-bindings".to_string()
+            }
+
+            ErrorKind::UnknownStaticVariable => "variable not found".to_string(),
+
+            ErrorKind::UnknownDynamicVariable(name) => format!(
+                r#"variable '{}' could not be found
+
+Note that this occured within a `with`-expression. The problem may be related
+to a missing value in the attribute set(s) included via `with`."#,
+                name
+            ),
+
+            ErrorKind::VariableAlreadyDefined(_) => "variable has already been defined".to_string(),
+
+            ErrorKind::NotCallable => {
+                "this value is not callable (i.e. not a function or builtin)".to_string()
+            }
+
+            ErrorKind::InfiniteRecursion => "infinite recursion encountered".to_string(),
+
+            // TODO(tazjin): these errors should actually end up with
+            // individual spans etc.
+            ErrorKind::ParseErrors(errors) => format!("failed to parse Nix code: {:?}", errors),
+
+            // TODO(tazjin): trace through the whole chain of thunk
+            // forcing errors with secondary spans, instead of just
+            // delegating to the inner error
+            ErrorKind::ThunkForce(err) => err.message(codemap),
+
+            ErrorKind::NotImplemented(feature) => {
+                format!("feature not yet implemented in Tvix: {}", feature)
+            }
+        }
+    }
+
+    /// Return the unique error code for this variant which can be
+    /// used to refer users to documentation.
+    fn code(&self) -> &'static str {
+        match self.kind {
+            ErrorKind::Throw(_) => "E001",
+            ErrorKind::Abort(_) => "E002",
+            ErrorKind::AssertionFailed => "E003",
+            ErrorKind::InvalidAttributeName { .. } => "E004",
+            ErrorKind::AttributeNotFound { .. } => "E005",
+            ErrorKind::TypeError { .. } => "E006",
+            ErrorKind::Incomparable { .. } => "E007",
+            ErrorKind::PathResolution(_) => "E008",
+            ErrorKind::DynamicKeyInLet => "E009",
+            ErrorKind::UnknownStaticVariable => "E010",
+            ErrorKind::UnknownDynamicVariable(_) => "E011",
+            ErrorKind::VariableAlreadyDefined(_) => "E012",
+            ErrorKind::NotCallable => "E013",
+            ErrorKind::InfiniteRecursion => "E014",
+            ErrorKind::ParseErrors(_) => "E015",
+            ErrorKind::DuplicateAttrsKey { .. } => "E016",
+            ErrorKind::ThunkForce(_) => "E017",
+            ErrorKind::NotImplemented(_) => "E999",
+        }
+    }
+
+    fn diagnostic(&self, codemap: &CodeMap) -> Diagnostic {
+        let span_label = SpanLabel {
+            label: self.span_label(),
+            span: self.span,
+            style: SpanStyle::Primary,
+        };
+
+        Diagnostic {
+            level: Level::Error,
+            message: self.message(codemap),
+            spans: vec![span_label],
+            code: Some(self.code().into()),
+        }
+    }
+}