use std::error;
use std::io;
use std::path::PathBuf;
use std::rc::Rc;
use std::str::Utf8Error;
use std::string::FromUtf8Error;
use std::sync::Arc;
use std::{fmt::Debug, fmt::Display, num::ParseIntError};
use codemap::{File, Span};
use codemap_diagnostic::{ColorConfig, Diagnostic, Emitter, Level, SpanLabel, SpanStyle};
use smol_str::SmolStr;
use crate::spans::ToSpan;
use crate::value::{CoercionKind, NixString};
use crate::{SourceCode, Value};
/// "CatchableErrorKind" errors -- those which can be detected by
/// `builtins.tryEval`.
///
/// Note: this type is deliberately *not* incorporated as a variant
/// of ErrorKind, because then Result<Value,ErrorKind> would have
/// redundant representations for catchable errors, which would make
/// it too easy to handle errors incorrectly:
///
/// - Ok(Value::Catchable(cek))
/// - Err(ErrorKind::ThisVariantDoesNotExist(cek))
///
/// Because CatchableErrorKind is not a variant of ErrorKind, you
/// will often see functions which return a type like:
///
/// Result<Result<T,CatchableErrorKind>,ErrorKind>
///
/// ... where T is any type other than Value. This is unfortunate,
/// because Rust's magic `?`-syntax does not work on nested Result
/// values like this.
// TODO(amjoseph): investigate result<T,Either<CatchableErrorKind,ErrorKind>>
#[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>),
}
#[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,
#[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),
#[error("attribute with name '{name}' could not be found in the set")]
AttributeNotFound { name: String },
/// Attempted to index into a list beyond its boundaries.
#[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>,
},
/// 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.
#[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>,
errors: Vec<rnix::parser::ParseError>,
},
/// Compilation errors occured while importing a file.
#[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
#[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,
#[error("Invalid hash: {0}")]
InvalidHash(String),
/// 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>>,
},
/// 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
/// should fail the evaluation.
///
/// 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),
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.kind {
ErrorKind::NativeError { err, .. } | ErrorKind::BytecodeError(err) => err.source(),
ErrorKind::ParseErrors(err) => err.first().map(|e| e as &dyn error::Error),
ErrorKind::ParseIntError(err) => Some(err),
ErrorKind::ImportParseError { errors, .. } => {
errors.first().map(|e| e as &dyn error::Error)
}
ErrorKind::ImportCompilerError { errors, .. } => {
errors.first().map(|e| e as &dyn error::Error)
}
ErrorKind::IO { error, .. } => Some(error.as_ref()),
ErrorKind::TvixError(error) => Some(error.as_ref()),
_ => None,
}
}
}
impl From<ParseIntError> for ErrorKind {
fn from(e: ParseIntError) -> Self {
Self::ParseIntError(e)
}
}
impl From<Utf8Error> for ErrorKind {
fn from(_: Utf8Error) -> Self {
Self::NotImplemented("FromUtf8Error not handled: https://b.tvl.fyi/issues/189")
}
}
impl From<FromUtf8Error> for ErrorKind {
fn from(_: FromUtf8Error) -> Self {
Self::NotImplemented("FromUtf8Error not handled: https://b.tvl.fyi/issues/189")
}
}
impl From<bstr::Utf8Error> for ErrorKind {
fn from(_: bstr::Utf8Error) -> Self {
Self::Utf8
}
}
impl From<bstr::FromUtf8Error> for ErrorKind {
fn from(_value: bstr::FromUtf8Error) -> Self {
Self::Utf8
}
}
impl From<io::Error> for ErrorKind {
fn from(e: io::Error) -> Self {
ErrorKind::IO {
path: None,
error: Rc::new(e),
}
}
}
impl From<serde_json::Error> for ErrorKind {
fn from(err: serde_json::Error) -> Self {
// Can't just put the `serde_json::Error` in the ErrorKind since it doesn't impl `Clone`
Self::JsonError(err.to_string())
}
}
impl From<toml::de::Error> for ErrorKind {
fn from(err: toml::de::Error) -> Self {
Self::FromTomlError(format!("error in TOML serialization: {err}"))
}
}
#[derive(Clone, Debug)]
pub struct Error {
pub kind: ErrorKind,
pub span: Span,
pub contexts: Vec<String>,
pub source: SourceCode,
}
impl Error {
pub fn new(mut kind: ErrorKind, span: Span, source: SourceCode) -> Self {
let mut contexts = vec![];
while let ErrorKind::WithContext {
context,
underlying,
} = kind
{
kind = *underlying;
contexts.push(context);
}
Error {
kind,
span,
contexts,
source,
}
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.kind)
}
}
pub type EvalResult<T> = Result<T, Error>;
/// Human-readable names for rnix syntaxes.
fn name_for_syntax(syntax: &rnix::SyntaxKind) -> &'static str {
match syntax {
rnix::SyntaxKind::TOKEN_COMMENT => "a comment",
rnix::SyntaxKind::TOKEN_WHITESPACE => "whitespace",
rnix::SyntaxKind::TOKEN_ASSERT => "`assert`-keyword",
rnix::SyntaxKind::TOKEN_ELSE => "`else`-keyword",
rnix::SyntaxKind::TOKEN_IN => "`in`-keyword",
rnix::SyntaxKind::TOKEN_IF => "`if`-keyword",
rnix::SyntaxKind::TOKEN_INHERIT => "`inherit`-keyword",
rnix::SyntaxKind::TOKEN_LET => "`let`-keyword",
rnix::SyntaxKind::TOKEN_OR => "`or`-keyword",
rnix::SyntaxKind::TOKEN_REC => "`rec`-keyword",
rnix::SyntaxKind::TOKEN_THEN => "`then`-keyword",
rnix::SyntaxKind::TOKEN_WITH => "`with`-keyword",
rnix::SyntaxKind::TOKEN_L_BRACE => "{",
rnix::SyntaxKind::TOKEN_R_BRACE => "}",
rnix::SyntaxKind::TOKEN_L_BRACK => "[",
rnix::SyntaxKind::TOKEN_R_BRACK => "]",
rnix::SyntaxKind::TOKEN_ASSIGN => "=",
rnix::SyntaxKind::TOKEN_AT => "@",
rnix::SyntaxKind::TOKEN_COLON => ":",
rnix::SyntaxKind::TOKEN_COMMA => "`,`",
rnix::SyntaxKind::TOKEN_DOT => ".",
rnix::SyntaxKind::TOKEN_ELLIPSIS => "...",
rnix::SyntaxKind::TOKEN_QUESTION => "?",
rnix::SyntaxKind::TOKEN_SEMICOLON => ";",
rnix::SyntaxKind::TOKEN_L_PAREN => "(",
rnix::SyntaxKind::TOKEN_R_PAREN => ")",
rnix::SyntaxKind::TOKEN_CONCAT => "++",
rnix::SyntaxKind::TOKEN_INVERT => "!",
rnix::SyntaxKind::TOKEN_UPDATE => "//",
rnix::SyntaxKind::TOKEN_ADD => "+",
rnix::SyntaxKind::TOKEN_SUB => "-",
rnix::SyntaxKind::TOKEN_MUL => "*",
rnix::SyntaxKind::TOKEN_DIV => "/",
rnix::SyntaxKind::TOKEN_AND_AND => "&&",
rnix::SyntaxKind::TOKEN_EQUAL => "==",
rnix::SyntaxKind::TOKEN_IMPLICATION => "->",
rnix::SyntaxKind::TOKEN_LESS => "<",
rnix::SyntaxKind::TOKEN_LESS_OR_EQ => "<=",
rnix::SyntaxKind::TOKEN_MORE => ">",
rnix::SyntaxKind::TOKEN_MORE_OR_EQ => ">=",
rnix::SyntaxKind::TOKEN_NOT_EQUAL => "!=",
rnix::SyntaxKind::TOKEN_OR_OR => "||",
rnix::SyntaxKind::TOKEN_FLOAT => "a float",
rnix::SyntaxKind::TOKEN_IDENT => "an identifier",
rnix::SyntaxKind::TOKEN_INTEGER => "an integer",
rnix::SyntaxKind::TOKEN_INTERPOL_END => "}",
rnix::SyntaxKind::TOKEN_INTERPOL_START => "${",
rnix::SyntaxKind::TOKEN_PATH => "a path",
rnix::SyntaxKind::TOKEN_URI => "a literal URI",
rnix::SyntaxKind::TOKEN_STRING_CONTENT => "content of a string",
rnix::SyntaxKind::TOKEN_STRING_END => "\"",
rnix::SyntaxKind::TOKEN_STRING_START => "\"",
rnix::SyntaxKind::NODE_APPLY => "a function application",
rnix::SyntaxKind::NODE_ASSERT => "an assertion",
rnix::SyntaxKind::NODE_ATTRPATH => "an attribute path",
rnix::SyntaxKind::NODE_DYNAMIC => "a dynamic identifier",
rnix::SyntaxKind::NODE_IDENT => "an identifier",
rnix::SyntaxKind::NODE_IF_ELSE => "an `if`-expression",
rnix::SyntaxKind::NODE_SELECT => "a `select`-expression",
rnix::SyntaxKind::NODE_INHERIT => "inherited values",
rnix::SyntaxKind::NODE_INHERIT_FROM => "inherited values",
rnix::SyntaxKind::NODE_STRING => "a string",
rnix::SyntaxKind::NODE_INTERPOL => "an interpolation",
rnix::SyntaxKind::NODE_LAMBDA => "a function",
rnix::SyntaxKind::NODE_IDENT_PARAM => "a function parameter",
rnix::SyntaxKind::NODE_LEGACY_LET => "a legacy `let`-expression",
rnix::SyntaxKind::NODE_LET_IN => "a `let`-expression",
rnix::SyntaxKind::NODE_LIST => "a list",
rnix::SyntaxKind::NODE_BIN_OP => "a binary operator",
rnix::SyntaxKind::NODE_PAREN => "a parenthesised expression",
rnix::SyntaxKind::NODE_PATTERN => "a function argument pattern",
rnix::SyntaxKind::NODE_PAT_BIND => "an argument pattern binding",
rnix::SyntaxKind::NODE_PAT_ENTRY => "an argument pattern entry",
rnix::SyntaxKind::NODE_ROOT => "a Nix expression",
rnix::SyntaxKind::NODE_ATTR_SET => "an attribute set",
rnix::SyntaxKind::NODE_ATTRPATH_VALUE => "an attribute set entry",
rnix::SyntaxKind::NODE_UNARY_OP => "a unary operator",
rnix::SyntaxKind::NODE_LITERAL => "a literal value",
rnix::SyntaxKind::NODE_WITH => "a `with`-expression",
rnix::SyntaxKind::NODE_PATH => "a path",
rnix::SyntaxKind::NODE_HAS_ATTR => "`?`-operator",
// TODO(tazjin): unsure what these variants are, lets crash!
rnix::SyntaxKind::NODE_ERROR => todo!("NODE_ERROR found, tell tazjin!"),
rnix::SyntaxKind::TOKEN_ERROR => todo!("TOKEN_ERROR found, tell tazjin!"),
_ => todo!(),
}
}
/// Construct the string representation for a list of expected parser tokens.
fn expected_syntax(one_of: &[rnix::SyntaxKind]) -> String {
match one_of.len() {
0 => "nothing".into(),
1 => format!("'{}'", name_for_syntax(&one_of[0])),
_ => {
let mut out: String = "one of: ".into();
let end = one_of.len() - 1;
for (idx, item) in one_of.iter().enumerate() {
if idx != 0 {
out.push_str(", ");
} else if idx == end {
out.push_str(", or ");
};
out.push_str(name_for_syntax(item));
}
out
}
}
}
/// Process a list of parse errors into a set of span labels, annotating parse
/// errors.
fn spans_for_parse_errors(file: &File, errors: &[rnix::parser::ParseError]) -> Vec<SpanLabel> {
// rnix has a tendency to emit some identical errors more than once, but
// they do not enhance the user experience necessarily, so we filter them
// out
let mut had_eof = false;
errors
.iter()
.enumerate()
.filter_map(|(idx, err)| {
let (span, label): (Span, String) = match err {
rnix::parser::ParseError::Unexpected(range) => (
range.span_for(file),
"found an unexpected syntax element here".into(),
),
rnix::parser::ParseError::UnexpectedExtra(range) => (
range.span_for(file),
"found unexpected extra elements at the root of the expression".into(),
),
rnix::parser::ParseError::UnexpectedWanted(found, range, wanted) => {
let span = range.span_for(file);
(
span,
format!(
"found '{}', but expected {}",
name_for_syntax(found),
expected_syntax(wanted),
),
)
}
rnix::parser::ParseError::UnexpectedEOF => {
if had_eof {
return None;
}
had_eof = true;
(
file.span,
"code ended unexpectedly while the parser still expected more".into(),
)
}
rnix::parser::ParseError::UnexpectedEOFWanted(wanted) => {
had_eof = true;
(
file.span,
format!(
"code ended unexpectedly, but wanted {}",
expected_syntax(wanted)
),
)
}
rnix::parser::ParseError::DuplicatedArgs(range, name) => (
range.span_for(file),
format!(
"the function argument pattern '{}' was bound more than once",
name
),
),
rnix::parser::ParseError::RecursionLimitExceeded => (
file.span,
"this code exceeds the parser's recursion limit, please report a Tvix bug"
.to_string(),
),
// TODO: can rnix even still throw this? it's semantic!
rnix::parser::ParseError::UnexpectedDoubleBind(range) => (
range.span_for(file),
"this pattern was bound more than once".into(),
),
// The error enum is marked as `#[non_exhaustive]` in rnix,
// which disables the compiler error for missing a variant. This
// feature makes it possible for users to miss critical updates
// of enum variants for a more exciting runtime experience.
new => todo!("new parse error variant: {}", new),
};
Some(SpanLabel {
span,
label: Some(label),
style: if idx == 0 {
SpanStyle::Primary
} else {
SpanStyle::Secondary
},
})
})
.collect()
}
impl Error {
pub fn fancy_format_str(&self) -> String {
let mut out = vec![];
Emitter::vec(&mut out, Some(&*self.source.codemap())).emit(&self.diagnostics());
String::from_utf8_lossy(&out).to_string()
}
/// Render a fancy, human-readable output of this error and print
/// it to stderr.
pub fn fancy_format_stderr(&self) {
Emitter::stderr(ColorConfig::Auto, Some(&*self.source.codemap())).emit(&self.diagnostics());
}
/// Create the optional span label displayed as an annotation on
/// the underlined span of the error.
fn span_label(&self) -> Option<String> {
let label = match &self.kind {
ErrorKind::DuplicateAttrsKey { .. } => "in this attribute set",
ErrorKind::InvalidAttributeName(_) => "in this attribute set",
ErrorKind::RelativePathResolution(_) => "in this path literal",
ErrorKind::UnexpectedArgumentBuiltin { .. } => "while calling this builtin",
ErrorKind::UnexpectedArgumentFormals { .. } => "in this function call",
ErrorKind::UnexpectedContext => "in this string",
// The spans for some errors don't have any more descriptive stuff
// in them, or we don't utilise it yet.
ErrorKind::Abort(_)
| ErrorKind::AttributeNotFound { .. }
| ErrorKind::IndexOutOfBounds { .. }
| ErrorKind::TailEmptyList
| ErrorKind::TypeError { .. }
| ErrorKind::Incomparable { .. }
| ErrorKind::DivisionByZero
| ErrorKind::DynamicKeyInScope(_)
| ErrorKind::UnknownStaticVariable
| ErrorKind::UnknownDynamicVariable(_)
| ErrorKind::VariableAlreadyDefined(_)
| ErrorKind::NotCallable(_)
| ErrorKind::InfiniteRecursion { .. }
| ErrorKind::ParseErrors(_)
| ErrorKind::NativeError { .. }
| ErrorKind::BytecodeError(_)
| ErrorKind::NotCoercibleToString { .. }
| ErrorKind::NotAnAbsolutePath(_)
| ErrorKind::ParseIntError(_)
| ErrorKind::UnmergeableInherit { .. }
| ErrorKind::UnmergeableValue
| ErrorKind::ImportParseError { .. }
| ErrorKind::ImportCompilerError { .. }
| ErrorKind::IO { .. }
| ErrorKind::JsonError(_)
| ErrorKind::NotSerialisableToJson(_)
| ErrorKind::FromTomlError(_)
| ErrorKind::Utf8
| ErrorKind::TvixError(_)
| ErrorKind::TvixBug { .. }
| ErrorKind::NotImplemented(_)
| ErrorKind::WithContext { .. }
| ErrorKind::UnknownHashType(_)
| ErrorKind::InvalidHash(_)
| ErrorKind::CatchableError(_) => return None,
};
Some(label.into())
}
/// 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::CatchableError(CatchableErrorKind::Throw(_)) => "E001",
ErrorKind::Abort(_) => "E002",
ErrorKind::CatchableError(CatchableErrorKind::AssertionFailed) => "E003",
ErrorKind::InvalidAttributeName { .. } => "E004",
ErrorKind::AttributeNotFound { .. } => "E005",
ErrorKind::TypeError { .. } => "E006",
ErrorKind::Incomparable { .. } => "E007",
ErrorKind::CatchableError(CatchableErrorKind::NixPathResolution(_)) => "E008",
ErrorKind::DynamicKeyInScope(_) => "E009",
ErrorKind::UnknownStaticVariable => "E010",
ErrorKind::UnknownDynamicVariable(_) => "E011",
ErrorKind::VariableAlreadyDefined(_) => "E012",
ErrorKind::NotCallable(_) => "E013",
ErrorKind::InfiniteRecursion { .. } => "E014",
ErrorKind::ParseErrors(_) => "E015",
ErrorKind::DuplicateAttrsKey { .. } => "E016",
ErrorKind::NotCoercibleToString { .. } => "E018",
ErrorKind::IndexOutOfBounds { .. } => "E019",
ErrorKind::NotAnAbsolutePath(_) => "E020",
ErrorKind::ParseIntError(_) => "E021",
ErrorKind::TailEmptyList { .. } => "E023",
ErrorKind::UnmergeableInherit { .. } => "E024",
ErrorKind::UnmergeableValue => "E025",
ErrorKind::ImportParseError { .. } => "E027",
ErrorKind::ImportCompilerError { .. } => "E028",
ErrorKind::IO { .. } => "E029",
ErrorKind::JsonError { .. } => "E030",
ErrorKind::UnexpectedArgumentFormals { .. } => "E031",
ErrorKind::RelativePathResolution(_) => "E032",
ErrorKind::DivisionByZero => "E033",
ErrorKind::FromTomlError(_) => "E035",
ErrorKind::NotSerialisableToJson(_) => "E036",
ErrorKind::UnexpectedContext => "E037",
ErrorKind::Utf8 => "E038",
ErrorKind::UnknownHashType(_) => "E039",
ErrorKind::UnexpectedArgumentBuiltin { .. } => "E040",
ErrorKind::InvalidHash(_) => "E041",
// Special error code for errors from other Tvix
// components. We may want to introduce a code namespacing
// system to have these errors pass codes through.
ErrorKind::TvixError(_) => "E997",
// Special error code that is not part of the normal
// ordering.
ErrorKind::TvixBug { .. } => "E998",
// Placeholder error while Tvix is under construction.
ErrorKind::CatchableError(CatchableErrorKind::UnimplementedFeature(_))
| ErrorKind::NotImplemented(_) => "E999",
// Chained errors should yield the code of the innermost
// error.
ErrorKind::NativeError { ref err, .. } | ErrorKind::BytecodeError(ref err) => {
err.code()
}
ErrorKind::WithContext { .. } => {
panic!("internal ErrorKind::WithContext variant leaked")
}
}
}
fn spans(&self) -> Vec<SpanLabel> {
let mut spans = match &self.kind {
ErrorKind::ImportParseError { errors, file, .. } => {
spans_for_parse_errors(file, errors)
}
ErrorKind::ParseErrors(errors) => {
let file = self.source.get_file(self.span);
spans_for_parse_errors(&file, errors)
}
ErrorKind::UnexpectedArgumentFormals { formals_span, .. } => {
vec![
SpanLabel {
label: self.span_label(),
span: self.span,
style: SpanStyle::Primary,
},
SpanLabel {
label: Some("the accepted arguments".into()),
span: *formals_span,
style: SpanStyle::Secondary,
},
]
}
ErrorKind::InfiniteRecursion {
first_force,
suspended_at,
content_span,
} => {
let mut spans = vec![];
if let Some(content_span) = content_span {
spans.push(SpanLabel {
label: Some("this lazily-evaluated code".into()),
span: *content_span,
style: SpanStyle::Secondary,
})
}
if let Some(suspended_at) = suspended_at {
spans.push(SpanLabel {
label: Some("which was instantiated here".into()),
span: *suspended_at,
style: SpanStyle::Secondary,
})
}
spans.push(SpanLabel {
label: Some("was first requested to be evaluated here".into()),
span: *first_force,
style: SpanStyle::Secondary,
});
spans.push(SpanLabel {
label: Some("but then requested again here during its own evaluation".into()),
span: self.span,
style: SpanStyle::Primary,
});
spans
}
// All other errors pretty much have the same shape.
_ => {
vec![SpanLabel {
label: self.span_label(),
span: self.span,
style: SpanStyle::Primary,
}]
}
};
for ctx in &self.contexts {
spans.push(SpanLabel {
label: Some(format!("while {}", ctx)),
span: self.span,
style: SpanStyle::Secondary,
});
}
spans
}
/// Create the primary diagnostic for a given error.
fn diagnostic(&self) -> Diagnostic {
Diagnostic {
level: Level::Error,
message: self.to_string(),
spans: self.spans(),
code: Some(self.code().into()),
}
}
/// Return the primary diagnostic and all further associated diagnostics (if
/// any) of an error.
fn diagnostics(&self) -> Vec<Diagnostic> {
match &self.kind {
ErrorKind::ImportCompilerError { errors, .. } => {
let mut out = vec![self.diagnostic()];
out.extend(errors.iter().map(|e| e.diagnostic()));
out
}
// When encountering either of these error kinds, we are dealing
// with the top of an error chain.
//
// An error chain creates a list of diagnostics which provide trace
// information.
//
// We don't know how deep this chain is, so we avoid recursing in
// this function while unrolling the chain.
ErrorKind::NativeError { err: next, .. } | ErrorKind::BytecodeError(next) => {
// Accumulated diagnostics to return.
let mut diagnostics: Vec<Diagnostic> = vec![];
// The next (inner) error to add to the diagnostics, after this
// one.
let mut next = *next.clone();
// Diagnostic message for *this* error.
let mut this_message = self.to_string();
// Primary span for *this* error.
let mut this_span = self.span;
// Diagnostic spans for *this* error.
let mut this_spans = self.spans();
loop {
if is_new_span(
this_span,
diagnostics.last().and_then(|last| last.spans.last()),
) {
diagnostics.push(Diagnostic {
level: Level::Note,
message: this_message,
spans: this_spans,
code: None, // only the top-level error has one
});
}
this_message = next.to_string();
this_span = next.span;
this_spans = next.spans();
match next.kind {
ErrorKind::NativeError { err: inner, .. }
| ErrorKind::BytecodeError(inner) => {
next = *inner;
continue;
}
_ => {
diagnostics.extend(next.diagnostics());
break;
}
}
}
diagnostics
}
_ => vec![self.diagnostic()],
}
}
}
// Check if this error is in a different span from its immediate ancestor.
fn is_new_span(this_span: Span, parent: Option<&SpanLabel>) -> bool {
match parent {
None => true,
Some(parent) => parent.span != this_span,
}
}
// Convenience methods to add context on other types.
pub trait AddContext {
/// Add context to the error-carrying type.
fn context<S: Into<String>>(self, ctx: S) -> Self;
}
impl AddContext for ErrorKind {
fn context<S: Into<String>>(self, ctx: S) -> Self {
ErrorKind::WithContext {
context: ctx.into(),
underlying: Box::new(self),
}
}
}
impl<T> AddContext for Result<T, ErrorKind> {
fn context<S: Into<String>>(self, ctx: S) -> Self {
self.map_err(|kind| kind.context(ctx))
}
}
impl<T> AddContext for Result<T, Error> {
fn context<S: Into<String>>(self, ctx: S) -> Self {
self.map_err(|err| Error {
kind: err.kind.context(ctx),
..err
})
}
}