about summary refs log tree commit diff
path: root/tvix/eval
diff options
context:
space:
mode:
authorIlan Joselevich <personal@ilanjoselevich.com>2024-09-07T19·29+0300
committerclbot <clbot@tvl.fyi>2024-09-25T18·18+0000
commitc1e69e260dc9025e7c1ff04131f336cd45ca72e9 (patch)
treea531f875d3dd8d1cde0caca5d96e83b615801807 /tvix/eval
parent1bdf5c0c118020e897d8810df7a4e70c613e3c7f (diff)
feat(tvix/eval): Use thiserror for ErrorKind and CatchableErrorKind r/8713
thiserror is much more easier to maintain than manually implementing Error and Display.

Change-Id: Ibf13e2d8a96fba69c8acb362b7515274a593dfd6
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12452
Reviewed-by: flokli <flokli@flokli.de>
Autosubmit: Ilan Joselevich <personal@ilanjoselevich.com>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/eval')
-rw-r--r--tvix/eval/Cargo.toml1
-rw-r--r--tvix/eval/src/errors.rs336
2 files changed, 95 insertions, 242 deletions
diff --git a/tvix/eval/Cargo.toml b/tvix/eval/Cargo.toml
index c99bea4f7125..29862f97d8e4 100644
--- a/tvix/eval/Cargo.toml
+++ b/tvix/eval/Cargo.toml
@@ -36,6 +36,7 @@ data-encoding = { workspace = true }
 rustc-hash = { workspace = true }
 nohash-hasher = { workspace = true }
 vu128 = { workspace = true }
+thiserror.workspace = true
 
 [dev-dependencies]
 criterion = { workspace = true }
diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs
index ee55552c7ac5..9c3383fc5d94 100644
--- a/tvix/eval/src/errors.rs
+++ b/tvix/eval/src/errors.rs
@@ -35,96 +35,109 @@ use crate::{SourceCode, Value};
 /// because Rust's magic `?`-syntax does not work on nested Result
 /// values like this.
 // TODO(amjoseph): investigate result<T,Either<CatchableErrorKind,ErrorKind>>
-#[derive(Clone, Debug)]
+#[derive(thiserror::Error, Clone, Debug)]
 pub enum CatchableErrorKind {
+    #[error("error thrown: {0}")]
     Throw(Box<str>),
+
+    #[error("assertion failed")]
     AssertionFailed,
+
+    #[error("feature {0} is not implemented yet")]
     UnimplementedFeature(Box<str>),
+
     /// Resolving a user-supplied angle brackets path literal failed in some way.
+    #[error("Nix path entry could not be resolved: {0}")]
     NixPathResolution(Box<str>),
 }
 
-impl Display for CatchableErrorKind {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            CatchableErrorKind::Throw(s) => write!(f, "error thrown: {}", s),
-            CatchableErrorKind::AssertionFailed => write!(f, "assertion failed"),
-            CatchableErrorKind::UnimplementedFeature(s) => {
-                write!(f, "feature {} is not implemented yet", s)
-            }
-            CatchableErrorKind::NixPathResolution(s) => {
-                write!(f, "Nix path entry could not be resolved: {}", s)
-            }
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
+#[derive(thiserror::Error, Clone, Debug)]
 pub enum ErrorKind {
     /// These are user-generated errors through builtins.
+    #[error("evaluation aborted: {0}")]
     Abort(String),
 
+    #[error("division by zero")]
     DivisionByZero,
 
-    DuplicateAttrsKey {
-        key: String,
-    },
+    #[error("attribute key '{key}' already defined")]
+    DuplicateAttrsKey { key: String },
 
     /// Attempted to specify an invalid key type (e.g. integer) in a
     /// dynamic attribute name.
+    #[error(
+        "found attribute name '{0}' of type '{}', but attribute names must be strings",
+        .0.type_of()
+    )]
     InvalidAttributeName(Value),
 
-    AttributeNotFound {
-        name: String,
-    },
+    #[error("attribute with name '{name}' could not be found in the set")]
+    AttributeNotFound { name: String },
 
     /// Attempted to index into a list beyond its boundaries.
-    IndexOutOfBounds {
-        index: i64,
-    },
+    #[error("list index '{index}' is out of bounds")]
+    IndexOutOfBounds { index: i64 },
 
     /// Attempted to call `builtins.tail` on an empty list.
+    #[error("'tail' called on an empty list")]
     TailEmptyList,
 
+    #[error("expected value of type '{expected}', but found a '{actual}'")]
     TypeError {
         expected: &'static str,
         actual: &'static str,
     },
 
+    #[error("can not compare a {lhs} with a {rhs}")]
     Incomparable {
         lhs: &'static str,
         rhs: &'static str,
     },
 
     /// Resolving a user-supplied relative or home-relative path literal failed in some way.
+    #[error("could not resolve path: {0}")]
     RelativePathResolution(String),
 
     /// Dynamic keys are not allowed in some scopes.
+    #[error("dynamically evaluated keys are not allowed in {0}")]
     DynamicKeyInScope(&'static str),
 
     /// Unknown variable in statically known scope.
+    #[error("variable not found")]
     UnknownStaticVariable,
 
     /// Unknown variable in dynamic scope (with, rec, ...).
+    #[error(
+        r#"variable '{0}' 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`."#
+    )]
     UnknownDynamicVariable(String),
 
     /// User is defining the same variable twice at the same depth.
+    #[error("variable has already been defined")]
     VariableAlreadyDefined(Option<Span>),
 
     /// Attempt to call something that is not callable.
+    #[error("only functions and builtins can be called, but this is a '{0}'")]
     NotCallable(&'static str),
 
     /// Infinite recursion encountered while forcing thunks.
+    #[error("infinite recursion encountered")]
     InfiniteRecursion {
         first_force: Span,
         suspended_at: Option<Span>,
         content_span: Option<Span>,
     },
 
+    // Errors themselves ignored here & handled in Self::spans instead
+    #[error("failed to parse Nix code:")]
     ParseErrors(Vec<rnix::parser::ParseError>),
 
     /// An error occured while executing some native code (e.g. a
     /// builtin), and needs to be chained up.
+    #[error("while evaluating this as native code ({gen_type})")]
     NativeError {
         gen_type: &'static str,
         err: Box<Error>,
@@ -132,31 +145,44 @@ pub enum ErrorKind {
 
     /// An error occured while executing Tvix bytecode, but needs to
     /// be chained up.
+    #[error("while evaluating this Nix code")]
     BytecodeError(Box<Error>),
 
     /// Given type can't be coerced to a string in the respective context
+    #[error("cannot ({}) coerce {from} to a string{}", 
+        (if .kind.strong { "strongly" } else { "weakly" }),
+        (if *.from == "set" {
+            ", missing a `__toString` or `outPath` attribute"
+        } else {
+            ""
+        })
+    )]
     NotCoercibleToString {
         from: &'static str,
         kind: CoercionKind,
     },
 
     /// The given string doesn't represent an absolute path
+    #[error("string '{}' does not represent an absolute path", .0.to_string_lossy())]
     NotAnAbsolutePath(PathBuf),
 
     /// An error occurred when parsing an integer
+    #[error("invalid integer: {0}")]
     ParseIntError(ParseIntError),
 
     // Errors specific to nested attribute sets and merges thereof.
     /// Nested attributes can not be merged with an inherited value.
-    UnmergeableInherit {
-        name: SmolStr,
-    },
+    #[error("cannot merge a nested attribute set into the inherited entry '{name}'")]
+    UnmergeableInherit { name: SmolStr },
 
     /// Nested attributes can not be merged with values that are not
     /// literal attribute sets.
+    #[error("nested attribute sets or keys can only be merged with literal attribute sets")]
     UnmergeableValue,
 
+    // Errors themselves ignored here & handled in Self::spans instead
     /// Parse errors occured while importing a file.
+    #[error("parse errors occured while importing '{}'", .path.to_string_lossy())]
     ImportParseError {
         path: PathBuf,
         file: Arc<File>,
@@ -164,44 +190,70 @@ pub enum ErrorKind {
     },
 
     /// Compilation errors occured while importing a file.
-    ImportCompilerError {
-        path: PathBuf,
-        errors: Vec<Error>,
-    },
+    #[error("compiler errors occured while importing '{}'", .path.to_string_lossy())]
+    ImportCompilerError { path: PathBuf, errors: Vec<Error> },
 
     /// I/O errors
+    #[error("I/O error: {}",
+        ({
+            let mut msg = String::new();
+
+            if let Some(path) = .path {
+                msg.push_str(&format!("{}: ", path.display()));
+            }
+
+            msg.push_str(&.error.to_string());
+
+            msg
+        })
+    )]
     IO {
         path: Option<PathBuf>,
         error: Rc<io::Error>,
     },
 
     /// Errors parsing JSON, or serializing as JSON.
+    #[error("Error converting JSON to a Nix value or back: {0}")]
     JsonError(String),
 
     /// Nix value that can not be serialised to JSON.
+    #[error("a {0} cannot be converted to JSON")]
     NotSerialisableToJson(&'static str),
 
     /// Errors converting TOML to a value
+    #[error("Error converting TOML to a Nix value: {0}")]
     FromTomlError(String),
 
     /// An unexpected argument was supplied to a builtin
+    #[error("Unexpected agrument `{0}` passed to builtin")]
     UnexpectedArgumentBuiltin(NixString),
 
     /// An unexpected argument was supplied to a function that takes formal parameters
-    UnexpectedArgumentFormals {
-        arg: NixString,
-        formals_span: Span,
-    },
+    #[error("Unexpected argument `{arg}` supplied to function")]
+    UnexpectedArgumentFormals { arg: NixString, formals_span: Span },
 
     /// Invalid UTF-8 was encoutered somewhere
+    #[error("Invalid UTF-8 in string")]
     Utf8,
 
     /// Variant for errors that bubble up to eval from other Tvix
     /// components.
+    #[error("{0}")]
     TvixError(Rc<dyn error::Error>),
 
     /// Variant for code paths that are known bugs in Tvix (usually
     /// issues with the compiler/VM interaction).
+    #[error("{}",
+        ({
+            let mut disp = format!("Tvix bug: {}", .msg);
+
+            if let Some(metadata) = .metadata {
+                disp.push_str(&format!("; metadata: {:?}", metadata));
+            }
+
+            disp
+        })
+    )]
     TvixBug {
         msg: &'static str,
         metadata: Option<Rc<dyn Debug>>,
@@ -210,15 +262,18 @@ pub enum ErrorKind {
     /// Tvix internal warning for features triggered by users that are
     /// not actually implemented yet, and without which eval can not
     /// proceed.
+    #[error("feature not yet implemented in Tvix: {0}")]
     NotImplemented(&'static str),
 
     /// Internal variant which should disappear during error construction.
+    #[error("internal ErrorKind::WithContext variant leaked")]
     WithContext {
         context: String,
         underlying: Box<ErrorKind>,
     },
 
     /// Unexpected context string
+    #[error("unexpected context string")]
     UnexpectedContext,
 
     /// Top-level evaluation result was a catchable Nix error, and
@@ -227,10 +282,12 @@ pub enum ErrorKind {
     /// This variant **must** only be used at the top-level of
     /// tvix-eval when returning a result to the user, never inside of
     /// eval code.
+    #[error("{0}")]
     CatchableError(CatchableErrorKind),
 
     /// Invalid hash type specified, must be one of "md5", "sha1", "sha256"
     /// or "sha512"
+    #[error("unknown hash type '{0}'")]
     UnknownHashType(String),
 }
 
@@ -334,211 +391,6 @@ impl Error {
     }
 }
 
-impl Display for ErrorKind {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match &self {
-            ErrorKind::Abort(msg) => write!(f, "evaluation aborted: {}", msg),
-
-            ErrorKind::DivisionByZero => write!(f, "division by zero"),
-
-            ErrorKind::DuplicateAttrsKey { key } => {
-                write!(f, "attribute key '{}' already defined", key)
-            }
-
-            ErrorKind::InvalidAttributeName(val) => write!(
-                f,
-                "found attribute name '{}' of type '{}', but attribute names must be strings",
-                val,
-                val.type_of()
-            ),
-
-            ErrorKind::AttributeNotFound { name } => write!(
-                f,
-                "attribute with name '{}' could not be found in the set",
-                name
-            ),
-
-            ErrorKind::IndexOutOfBounds { index } => {
-                write!(f, "list index '{}' is out of bounds", index)
-            }
-
-            ErrorKind::TailEmptyList => write!(f, "'tail' called on an empty list"),
-
-            ErrorKind::TypeError { expected, actual } => write!(
-                f,
-                "expected value of type '{}', but found a '{}'",
-                expected, actual
-            ),
-
-            ErrorKind::Incomparable { lhs, rhs } => {
-                write!(f, "can not compare a {} with a {}", lhs, rhs)
-            }
-
-            ErrorKind::RelativePathResolution(err) => {
-                write!(f, "could not resolve path: {}", err)
-            }
-
-            ErrorKind::DynamicKeyInScope(scope) => {
-                write!(f, "dynamically evaluated keys are not allowed in {}", scope)
-            }
-
-            ErrorKind::UnknownStaticVariable => write!(f, "variable not found"),
-
-            ErrorKind::UnknownDynamicVariable(name) => write!(
-                f,
-                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(_) => write!(f, "variable has already been defined"),
-
-            ErrorKind::NotCallable(other_type) => {
-                write!(
-                    f,
-                    "only functions and builtins can be called, but this is a '{}'",
-                    other_type
-                )
-            }
-
-            ErrorKind::InfiniteRecursion { .. } => write!(f, "infinite recursion encountered"),
-
-            // Errors themselves ignored here & handled in Self::spans instead
-            ErrorKind::ParseErrors(_) => write!(f, "failed to parse Nix code:"),
-
-            ErrorKind::NativeError { gen_type, .. } => {
-                write!(f, "while evaluating this as native code ({})", gen_type)
-            }
-
-            ErrorKind::BytecodeError(_) => write!(f, "while evaluating this Nix code"),
-
-            ErrorKind::NotCoercibleToString { kind, from } => {
-                let kindly = if kind.strong { "strongly" } else { "weakly" };
-
-                let hint = if *from == "set" {
-                    ", missing a `__toString` or `outPath` attribute"
-                } else {
-                    ""
-                };
-
-                write!(f, "cannot ({kindly}) coerce {from} to a string{hint}")
-            }
-
-            ErrorKind::NotAnAbsolutePath(given) => {
-                write!(
-                    f,
-                    "string '{}' does not represent an absolute path",
-                    given.to_string_lossy()
-                )
-            }
-
-            ErrorKind::ParseIntError(err) => {
-                write!(f, "invalid integer: {}", err)
-            }
-
-            ErrorKind::UnmergeableInherit { name } => {
-                write!(
-                    f,
-                    "cannot merge a nested attribute set into the inherited entry '{}'",
-                    name
-                )
-            }
-
-            ErrorKind::UnmergeableValue => {
-                write!(
-                    f,
-                    "nested attribute sets or keys can only be merged with literal attribute sets"
-                )
-            }
-
-            // Errors themselves ignored here & handled in Self::spans instead
-            ErrorKind::ImportParseError { path, .. } => {
-                write!(
-                    f,
-                    "parse errors occured while importing '{}'",
-                    path.to_string_lossy()
-                )
-            }
-
-            ErrorKind::ImportCompilerError { path, .. } => {
-                writeln!(
-                    f,
-                    "compiler errors occured while importing '{}'",
-                    path.to_string_lossy()
-                )
-            }
-
-            ErrorKind::IO { path, error } => {
-                write!(f, "I/O error: ")?;
-                if let Some(path) = path {
-                    write!(f, "{}: ", path.display())?;
-                }
-                write!(f, "{error}")
-            }
-
-            ErrorKind::JsonError(msg) => {
-                write!(f, "Error converting JSON to a Nix value or back: {msg}")
-            }
-
-            ErrorKind::NotSerialisableToJson(_type) => {
-                write!(f, "a {} cannot be converted to JSON", _type)
-            }
-
-            ErrorKind::FromTomlError(msg) => {
-                write!(f, "Error converting TOML to a Nix value: {msg}")
-            }
-
-            ErrorKind::UnexpectedArgumentBuiltin(arg) => {
-                write!(f, "Unexpected agrument `{arg}` passed to builtin",)
-            }
-
-            ErrorKind::UnexpectedArgumentFormals { arg, .. } => {
-                write!(f, "Unexpected argument `{arg}` supplied to function",)
-            }
-
-            ErrorKind::Utf8 => {
-                write!(f, "Invalid UTF-8 in string")
-            }
-
-            ErrorKind::TvixError(inner_error) => {
-                write!(f, "{inner_error}")
-            }
-
-            ErrorKind::TvixBug { msg, metadata } => {
-                write!(f, "Tvix bug: {}", msg)?;
-
-                if let Some(metadata) = metadata {
-                    write!(f, "; metadata: {:?}", metadata)?;
-                }
-
-                Ok(())
-            }
-
-            ErrorKind::NotImplemented(feature) => {
-                write!(f, "feature not yet implemented in Tvix: {}", feature)
-            }
-
-            ErrorKind::WithContext { .. } => {
-                panic!("internal ErrorKind::WithContext variant leaked")
-            }
-
-            ErrorKind::UnexpectedContext => {
-                write!(f, "unexpected context string")
-            }
-
-            ErrorKind::CatchableError(inner) => {
-                write!(f, "{}", inner)
-            }
-
-            ErrorKind::UnknownHashType(hash_type) => {
-                write!(f, "unknown hash type '{}'", hash_type)
-            }
-        }
-    }
-}
-
 impl Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", self.kind)