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

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

961 lines
34 KiB
Rust
Raw Normal View History

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 },
fix(tvix): Represent strings as byte arrays C++ nix uses C-style zero-terminated char pointers to represent strings internally - however, up to this point, tvix has used Rust `String` and `str` for string values. Since those are required to be valid utf-8, we haven't been able to properly represent all the string values that Nix supports. To fix that, this change converts the internal representation of the NixString struct from `Box<str>` to `BString`, from the `bstr` crate - this is a wrapper around a `Vec<u8>` with extra functions for treating that byte vector as a "morally string-like" value, which is basically exactly what we need. Since this changes a pretty fundamental assumption about a pretty core type, there are a *lot* of changes in a lot of places to make this work, but I've tried to keep the general philosophy and intent of most of the code in most places intact. Most notably, there's nothing that's been done to make the derivation stuff in //tvix/glue work with non-utf8 strings everywhere, instead opting to just convert to String/str when passing things into that - there *might* be something to be done there, but I don't know what the rules should be and I don't want to figure them out in this change. To deal with OS-native paths in a way that also works in WASM for tvixbolt, this also adds a dependency on the "os_str_bytes" crate. Fixes: b/189 Fixes: b/337 Change-Id: I5e6eb29c62f47dd91af954f5e12bfc3d186f5526 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10200 Reviewed-by: tazjin <tazjin@tvl.su> Reviewed-by: flokli <flokli@flokli.de> Reviewed-by: sterni <sternenseemann@systemli.org> Autosubmit: aspen <root@gws.fyi> Tested-by: BuildkiteCI
2023-12-05 23:25:52 +01:00
/// Invalid UTF-8 was encoutered somewhere
#[error("Invalid UTF-8 in string")]
fix(tvix): Represent strings as byte arrays C++ nix uses C-style zero-terminated char pointers to represent strings internally - however, up to this point, tvix has used Rust `String` and `str` for string values. Since those are required to be valid utf-8, we haven't been able to properly represent all the string values that Nix supports. To fix that, this change converts the internal representation of the NixString struct from `Box<str>` to `BString`, from the `bstr` crate - this is a wrapper around a `Vec<u8>` with extra functions for treating that byte vector as a "morally string-like" value, which is basically exactly what we need. Since this changes a pretty fundamental assumption about a pretty core type, there are a *lot* of changes in a lot of places to make this work, but I've tried to keep the general philosophy and intent of most of the code in most places intact. Most notably, there's nothing that's been done to make the derivation stuff in //tvix/glue work with non-utf8 strings everywhere, instead opting to just convert to String/str when passing things into that - there *might* be something to be done there, but I don't know what the rules should be and I don't want to figure them out in this change. To deal with OS-native paths in a way that also works in WASM for tvixbolt, this also adds a dependency on the "os_str_bytes" crate. Fixes: b/189 Fixes: b/337 Change-Id: I5e6eb29c62f47dd91af954f5e12bfc3d186f5526 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10200 Reviewed-by: tazjin <tazjin@tvl.su> Reviewed-by: flokli <flokli@flokli.de> Reviewed-by: sterni <sternenseemann@systemli.org> Autosubmit: aspen <root@gws.fyi> Tested-by: BuildkiteCI
2023-12-05 23:25:52 +01:00
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")
}
}
fix(tvix): Represent strings as byte arrays C++ nix uses C-style zero-terminated char pointers to represent strings internally - however, up to this point, tvix has used Rust `String` and `str` for string values. Since those are required to be valid utf-8, we haven't been able to properly represent all the string values that Nix supports. To fix that, this change converts the internal representation of the NixString struct from `Box<str>` to `BString`, from the `bstr` crate - this is a wrapper around a `Vec<u8>` with extra functions for treating that byte vector as a "morally string-like" value, which is basically exactly what we need. Since this changes a pretty fundamental assumption about a pretty core type, there are a *lot* of changes in a lot of places to make this work, but I've tried to keep the general philosophy and intent of most of the code in most places intact. Most notably, there's nothing that's been done to make the derivation stuff in //tvix/glue work with non-utf8 strings everywhere, instead opting to just convert to String/str when passing things into that - there *might* be something to be done there, but I don't know what the rules should be and I don't want to figure them out in this change. To deal with OS-native paths in a way that also works in WASM for tvixbolt, this also adds a dependency on the "os_str_bytes" crate. Fixes: b/189 Fixes: b/337 Change-Id: I5e6eb29c62f47dd91af954f5e12bfc3d186f5526 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10200 Reviewed-by: tazjin <tazjin@tvl.su> Reviewed-by: flokli <flokli@flokli.de> Reviewed-by: sterni <sternenseemann@systemli.org> Autosubmit: aspen <root@gws.fyi> Tested-by: BuildkiteCI
2023-12-05 23:25:52 +01:00
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(_)
fix(tvix): Represent strings as byte arrays C++ nix uses C-style zero-terminated char pointers to represent strings internally - however, up to this point, tvix has used Rust `String` and `str` for string values. Since those are required to be valid utf-8, we haven't been able to properly represent all the string values that Nix supports. To fix that, this change converts the internal representation of the NixString struct from `Box<str>` to `BString`, from the `bstr` crate - this is a wrapper around a `Vec<u8>` with extra functions for treating that byte vector as a "morally string-like" value, which is basically exactly what we need. Since this changes a pretty fundamental assumption about a pretty core type, there are a *lot* of changes in a lot of places to make this work, but I've tried to keep the general philosophy and intent of most of the code in most places intact. Most notably, there's nothing that's been done to make the derivation stuff in //tvix/glue work with non-utf8 strings everywhere, instead opting to just convert to String/str when passing things into that - there *might* be something to be done there, but I don't know what the rules should be and I don't want to figure them out in this change. To deal with OS-native paths in a way that also works in WASM for tvixbolt, this also adds a dependency on the "os_str_bytes" crate. Fixes: b/189 Fixes: b/337 Change-Id: I5e6eb29c62f47dd91af954f5e12bfc3d186f5526 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10200 Reviewed-by: tazjin <tazjin@tvl.su> Reviewed-by: flokli <flokli@flokli.de> Reviewed-by: sterni <sternenseemann@systemli.org> Autosubmit: aspen <root@gws.fyi> Tested-by: BuildkiteCI
2023-12-05 23:25:52 +01:00
| 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",
fix(tvix): Represent strings as byte arrays C++ nix uses C-style zero-terminated char pointers to represent strings internally - however, up to this point, tvix has used Rust `String` and `str` for string values. Since those are required to be valid utf-8, we haven't been able to properly represent all the string values that Nix supports. To fix that, this change converts the internal representation of the NixString struct from `Box<str>` to `BString`, from the `bstr` crate - this is a wrapper around a `Vec<u8>` with extra functions for treating that byte vector as a "morally string-like" value, which is basically exactly what we need. Since this changes a pretty fundamental assumption about a pretty core type, there are a *lot* of changes in a lot of places to make this work, but I've tried to keep the general philosophy and intent of most of the code in most places intact. Most notably, there's nothing that's been done to make the derivation stuff in //tvix/glue work with non-utf8 strings everywhere, instead opting to just convert to String/str when passing things into that - there *might* be something to be done there, but I don't know what the rules should be and I don't want to figure them out in this change. To deal with OS-native paths in a way that also works in WASM for tvixbolt, this also adds a dependency on the "os_str_bytes" crate. Fixes: b/189 Fixes: b/337 Change-Id: I5e6eb29c62f47dd91af954f5e12bfc3d186f5526 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10200 Reviewed-by: tazjin <tazjin@tvl.su> Reviewed-by: flokli <flokli@flokli.de> Reviewed-by: sterni <sternenseemann@systemli.org> Autosubmit: aspen <root@gws.fyi> Tested-by: BuildkiteCI
2023-12-05 23:25:52 +01:00
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
})
}
}