tvl-depot/tvix/eval/src/errors.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

680 lines
24 KiB
Rust
Raw Normal View History

use crate::spans::ToSpan;
use crate::value::CoercionKind;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::{fmt::Display, num::ParseIntError};
use codemap::{File, Span};
use codemap_diagnostic::{ColorConfig, Diagnostic, Emitter, Level, SpanLabel, SpanStyle};
use smol_str::SmolStr;
use crate::{SourceCode, 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(Value),
AttributeNotFound {
name: String,
},
/// Attempted to index into a list beyond its boundaries.
IndexOutOfBounds {
index: i64,
},
/// Attempted to call `builtins.tail` on an empty list.
TailEmptyList,
TypeError {
expected: &'static str,
actual: &'static str,
},
Incomparable {
lhs: &'static str,
rhs: &'static str,
},
/// Resolving a user-supplied path literal failed in some way.
PathResolution(String),
/// Dynamic keys are not allowed in some scopes.
DynamicKeyInScope(&'static str),
/// Unknown variable in statically known scope.
UnknownStaticVariable,
/// Unknown variable in dynamic scope (with, rec, ...).
UnknownDynamicVariable(String),
/// User is defining the same variable twice at the same depth.
VariableAlreadyDefined(Span),
/// Attempt to call something that is not callable.
NotCallable(&'static str),
/// Infinite recursion encountered while forcing thunks.
InfiniteRecursion,
ParseErrors(Vec<rnix::parser::ParseError>),
/// An error occured while forcing a thunk, and needs to be
/// chained up.
ThunkForce(Box<Error>),
/// Given type can't be coerced to a string in the respective context
NotCoercibleToString {
from: &'static str,
kind: CoercionKind,
},
/// The given string doesn't represent an absolute path
NotAnAbsolutePath(PathBuf),
/// An error occurred when parsing an integer
ParseIntError(ParseIntError),
/// A negative integer was used as a value representing length.
NegativeLength {
length: i64,
},
// Errors specific to nested attribute sets and merges thereof.
/// Nested attributes can not be merged with an inherited value.
UnmergeableInherit {
name: SmolStr,
},
/// Nested attributes can not be merged with values that are not
/// literal attribute sets.
UnmergeableValue,
/// Tvix failed to read a file from disk for some reason.
ReadFileError {
path: PathBuf,
error: Rc<std::io::Error>,
},
/// Parse errors occured while importing a file.
ImportParseError {
path: PathBuf,
file: Arc<File>,
errors: Vec<rnix::parser::ParseError>,
},
/// Compilation errors occured while importing a file.
ImportCompilerError {
path: PathBuf,
errors: Vec<Error>,
},
/// Tvix internal warning for features triggered by users that are
/// not actually implemented yet, and without which eval can not
/// proceed.
NotImplemented(&'static str),
}
impl From<ParseIntError> for ErrorKind {
fn from(e: ParseIntError) -> Self {
Self::ParseIntError(e)
}
}
/// Implementation used if errors occur while forcing thunks (which
/// can potentially be threaded through a few contexts, i.e. nested
/// thunks).
impl From<Error> for ErrorKind {
fn from(e: Error) -> Self {
Self::ThunkForce(Box::new(e))
}
}
#[derive(Clone, Debug)]
pub struct Error {
pub kind: ErrorKind,
pub span: Span,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(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,
format!(
"this code exceeds the parser's recursion limit, please report a Tvix bug"
),
),
// 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, source: &SourceCode) -> String {
let mut out = vec![];
Emitter::vec(&mut out, Some(&*source.codemap())).emit(&[self.diagnostic(source)]);
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, source: &SourceCode) {
Emitter::stderr(ColorConfig::Auto, Some(&*source.codemap()))
.emit(&[self.diagnostic(source)]);
}
/// 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::PathResolution(_) => "in this path literal",
// The spans for some errors don't have any more descriptive stuff
// in them, or we don't utilise it yet.
ErrorKind::Throw(_)
| ErrorKind::Abort(_)
| ErrorKind::AssertionFailed
| ErrorKind::AttributeNotFound { .. }
| ErrorKind::IndexOutOfBounds { .. }
| ErrorKind::TailEmptyList
| ErrorKind::TypeError { .. }
| ErrorKind::Incomparable { .. }
| ErrorKind::DynamicKeyInScope(_)
| ErrorKind::UnknownStaticVariable
| ErrorKind::UnknownDynamicVariable(_)
| ErrorKind::VariableAlreadyDefined(_)
| ErrorKind::NotCallable(_)
| ErrorKind::InfiniteRecursion
| ErrorKind::ParseErrors(_)
| ErrorKind::ThunkForce(_)
| ErrorKind::NotCoercibleToString { .. }
| ErrorKind::NotAnAbsolutePath(_)
| ErrorKind::ParseIntError(_)
| ErrorKind::NegativeLength { .. }
| ErrorKind::UnmergeableInherit { .. }
| ErrorKind::UnmergeableValue
| ErrorKind::ReadFileError { .. }
| ErrorKind::ImportParseError { .. }
| ErrorKind::ImportCompilerError { .. }
| ErrorKind::NotImplemented(_) => return None,
};
Some(label.into())
}
/// Create the primary error message displayed to users.
fn message(&self) -> 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::IndexOutOfBounds { index } => {
format!("list index '{}' is out of bounds", index)
}
ErrorKind::TailEmptyList => "'tail' called on an empty list".to_string(),
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::DynamicKeyInScope(scope) => {
format!("dynamically evaluated keys are not allowed in {}", scope)
}
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(other_type) => {
format!(
"only functions and builtins can be called, but this is a '{}'",
other_type
)
}
ErrorKind::InfiniteRecursion => "infinite recursion encountered".to_string(),
// Errors themselves ignored here & handled in Self::spans instead
ErrorKind::ParseErrors(_) => format!("failed to parse Nix code:"),
// 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(),
ErrorKind::NotCoercibleToString { kind, from } => {
let kindly = match kind {
CoercionKind::Strong => "strongly",
CoercionKind::Weak => "weakly",
};
let hint = if *from == "set" {
", missing a `__toString` or `outPath` attribute"
} else {
""
};
format!("cannot ({kindly}) coerce {from} to a string{hint}")
}
ErrorKind::NotAnAbsolutePath(given) => {
format!(
"string '{}' does not represent an absolute path",
given.to_string_lossy()
)
}
ErrorKind::ParseIntError(err) => {
format!("invalid integer: {}", err)
}
ErrorKind::NegativeLength { length } => {
format!(
"cannot use a negative integer, {}, for a value representing length",
length
)
}
ErrorKind::UnmergeableInherit { name } => {
format!(
"cannot merge a nested attribute set into the inherited entry '{}'",
name
)
}
ErrorKind::UnmergeableValue => {
"nested attribute sets or keys can only be merged with literal attribute sets"
.into()
}
ErrorKind::ReadFileError { path, error } => {
format!(
"failed to read file '{}': {}",
path.to_string_lossy(),
error
)
}
// Errors themselves ignored here & handled in Self::spans instead
ErrorKind::ImportParseError { path, .. } => {
format!(
"parse errors occured while importing '{}'",
path.to_string_lossy()
)
}
ErrorKind::ImportCompilerError { errors, path } => {
// TODO: chain display of these errors, though this is
// probably not the right place for that (should
// branch into a more elaborate diagnostic() call
// below).
format!(
"{} errors occured while importing '{}'",
errors.len(),
path.to_string_lossy()
)
}
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::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::NegativeLength { .. } => "E022",
ErrorKind::TailEmptyList { .. } => "E023",
ErrorKind::UnmergeableInherit { .. } => "E024",
ErrorKind::UnmergeableValue => "E025",
ErrorKind::ReadFileError { .. } => "E026",
ErrorKind::ImportParseError { .. } => "E027",
ErrorKind::ImportCompilerError { .. } => "E028",
// Placeholder error while Tvix is under construction.
ErrorKind::NotImplemented(_) => "E999",
// TODO: thunk force errors should yield a chained
// diagnostic, but until then we just forward the error
// code from the inner error.
//
// The error code for thunk forces is E017.
ErrorKind::ThunkForce(ref err) => err.code(),
}
}
fn spans(&self, source: &SourceCode) -> Vec<SpanLabel> {
match &self.kind {
ErrorKind::ImportParseError { errors, file, .. } => {
spans_for_parse_errors(&file, errors)
}
ErrorKind::ParseErrors(errors) => {
let file = source.get_file(self.span);
spans_for_parse_errors(&file, errors)
}
// Unwrap thunk errors to the innermost one
// TODO: limit the number of intermediates!
ErrorKind::ThunkForce(err) => {
let mut labels = err.spans(source);
// Only add this thunk to the "cause chain" if it span isn't
// exactly identical to the next-higher level, which is very
// common for the last thunk in a chain.
if let Some(label) = labels.last() {
if label.span != self.span {
labels.push(SpanLabel {
label: Some("while evaluating this".into()),
span: self.span,
style: SpanStyle::Secondary,
});
}
}
labels
}
// All other errors pretty much have the same shape.
_ => {
vec![SpanLabel {
label: self.span_label(),
span: self.span,
style: SpanStyle::Primary,
}]
}
}
}
fn diagnostic(&self, source: &SourceCode) -> Diagnostic {
Diagnostic {
level: Level::Error,
message: self.message(),
spans: self.spans(source),
code: Some(self.code().into()),
}
}
}