use std::fmt;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use chumsky::error::Error as ChumskyError;
use chumsky::input::ValueInput;
use chumsky::label::LabelError;
use chumsky::text::Char;
use chumsky::util::MaybeRef;
use chumsky::DefaultExpected;
use itertools::Itertools;
use simplicity::elements;
use crate::lexer::Token;
use crate::parse::MatchPattern;
use crate::resolution::SourceFile;
use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName};
use crate::types::{ResolvedType, UIntType};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
#[cfg(feature = "arbitrary")]
pub(crate) const DUMMY: Self = Self::new(0, 0);
pub const fn new(start: usize, end: usize) -> Self {
assert!(start <= end, "Start cannot come after end");
Self { start, end }
}
pub fn to_slice<'a>(&self, file: &'a str) -> Option<&'a str> {
file.get(self.start..self.end)
}
}
impl chumsky::span::Span for Span {
type Context = ();
type Offset = usize;
fn new((): Self::Context, range: Range<Self::Offset>) -> Self {
Self {
start: range.start,
end: range.end,
}
}
fn context(&self) -> Self::Context {}
fn start(&self) -> Self::Offset {
self.start
}
fn end(&self) -> Self::Offset {
self.end
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}..{}", self.start, self.end)?;
Ok(())
}
}
impl From<chumsky::span::SimpleSpan> for Span {
fn from(span: chumsky::span::SimpleSpan) -> Self {
Self {
start: span.start,
end: span.end,
}
}
}
impl From<Range<usize>> for Span {
fn from(range: Range<usize>) -> Self {
Self::new(range.start, range.end)
}
}
impl From<&str> for Span {
fn from(s: &str) -> Self {
Span::new(0, s.len())
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Span {
fn arbitrary(_: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Self::DUMMY)
}
}
pub trait WithSpan<T> {
fn with_span<S: Into<Span>>(self, span: S) -> Result<T, RichError>;
}
impl<T, E: Into<Error>> WithSpan<T> for Result<T, E> {
fn with_span<S: Into<Span>>(self, span: S) -> Result<T, RichError> {
self.map_err(|e| e.into().with_span(span.into()))
}
}
pub trait WithContent<T> {
fn with_content<C: Into<Arc<str>>>(self, content: C) -> Result<T, RichError>;
}
impl<T> WithContent<T> for Result<T, RichError> {
fn with_content<C: Into<Arc<str>>>(self, content: C) -> Result<T, RichError> {
self.map_err(|e| e.with_content(content.into()))
}
}
pub trait WithSource<T> {
fn with_source<S: Into<SourceFile>>(self, source: S) -> Result<T, RichError>;
}
impl<T> WithSource<T> for Result<T, RichError> {
fn with_source<S: Into<SourceFile>>(self, source: S) -> Result<T, RichError> {
self.map_err(|e| e.with_source(source.into()))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct RichError {
error: Box<Error>,
span: Span,
source: Option<SourceFile>,
}
impl RichError {
pub fn new(error: Error, span: Span) -> RichError {
RichError {
error: Box::new(error),
span,
source: None,
}
}
pub fn with_content(self, program_content: Arc<str>) -> Self {
Self {
error: self.error,
span: self.span,
source: Some(SourceFile::anonymous(program_content)),
}
}
pub fn with_source(self, source: impl Into<SourceFile>) -> Self {
Self {
error: self.error,
span: self.span,
source: Some(source.into()),
}
}
pub fn parsing_error(reason: &str) -> Self {
Self {
error: Box::new(Error::CannotParse(reason.to_string())),
span: Span::new(0, 0),
source: None,
}
}
pub fn source(&self) -> &Option<SourceFile> {
&self.source
}
pub fn error(&self) -> &Error {
&self.error
}
pub fn span(&self) -> &Span {
&self.span
}
}
impl fmt::Display for RichError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn get_line_col(file: &str, offset: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 0;
let slice = file.get(0..offset).unwrap_or_default();
for char in slice.chars() {
if char.is_newline() {
line += 1;
col = 0;
} else {
col += char.len_utf16();
}
}
(line, col + 1)
}
let Some(source) = &self.source else {
return write!(f, "{}", self.error);
};
let content = source.content();
if content.is_empty() {
return write!(f, "{}", self.error);
}
let (start_line, start_col) = get_line_col(&content, self.span.start);
let (end_line, end_col) = get_line_col(&content, self.span.end);
let start_line_index = start_line - 1;
let n_spanned_lines = end_line - start_line_index;
let line_num_width = end_line.to_string().len();
if let Some(name) = source.name() {
writeln!(
f,
"{:>width$}--> {}:{}:{}",
"",
name.display(),
start_line,
start_col,
width = line_num_width
)?;
}
writeln!(f, "{:width$} |", " ", width = line_num_width)?;
let mut lines = content
.split(|c: char| c.is_newline())
.skip(start_line_index)
.peekable();
let start_line_len = lines
.peek()
.map_or(0, |l| l.chars().map(char::len_utf16).sum());
for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
let line_num = start_line_index + relative_line_index + 1;
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
}
let is_multiline = end_line > start_line;
let (underline_start, underline_length) = match is_multiline {
true => (0, start_line_len),
false => (start_col, (end_col - start_col).max(1)),
};
write!(f, "{:width$} |", " ", width = line_num_width)?;
write!(f, "{:width$}", " ", width = underline_start)?;
write!(f, "{:^<width$} ", "", width = underline_length)?;
write!(f, "{}", self.error)
}
}
impl std::error::Error for RichError {}
impl From<RichError> for Error {
fn from(error: RichError) -> Self {
*error.error
}
}
impl From<RichError> for String {
fn from(error: RichError) -> Self {
error.to_string()
}
}
impl<'tokens, 'src: 'tokens, I> ChumskyError<'tokens, I> for RichError
where
I: ValueInput<'tokens, Token = Token<'src>, Span = Span>,
{
fn merge(self, other: Self) -> Self {
match (self.error.as_ref(), other.error.as_ref()) {
(Error::Grammar(_), Error::Grammar(_)) => other,
(Error::Grammar(_), _) => other,
(_, Error::Grammar(_)) => self,
_ => other,
}
}
}
impl<'tokens, 'src: 'tokens, I> LabelError<'tokens, I, DefaultExpected<'tokens, Token<'src>>>
for RichError
where
I: ValueInput<'tokens, Token = Token<'src>, Span = Span>,
{
fn expected_found<E>(
expected: E,
found: Option<MaybeRef<'tokens, Token<'src>>>,
span: Span,
) -> Self
where
E: IntoIterator<Item = DefaultExpected<'tokens, Token<'src>>>,
{
let expected_tokens: Vec<String> = expected
.into_iter()
.map(|t| match t {
DefaultExpected::Token(maybe) => maybe.to_string(),
DefaultExpected::Any => "anything".to_string(),
DefaultExpected::SomethingElse => "something else".to_string(),
DefaultExpected::EndOfInput => "end of input".to_string(),
_ => "UNEXPECTED_TOKEN".to_string(),
})
.collect();
let found_string = found.map(|t| t.to_string());
Self {
error: Box::new(Error::Syntax {
expected: expected_tokens,
label: None,
found: found_string,
}),
span,
source: None,
}
}
}
impl<'tokens, 'src: 'tokens, I> LabelError<'tokens, I, &'tokens str> for RichError
where
I: ValueInput<'tokens, Token = Token<'src>, Span = Span>,
{
fn expected_found<E>(
expected: E,
found: Option<MaybeRef<'tokens, Token<'src>>>,
span: Span,
) -> Self
where
E: IntoIterator<Item = &'tokens str>,
{
let expected_strings: Vec<String> = expected.into_iter().map(|s| s.to_string()).collect();
let found_string = found.map(|t| t.to_string());
Self {
error: Box::new(Error::Syntax {
expected: expected_strings,
label: None,
found: found_string,
}),
span,
source: None,
}
}
fn label_with(&mut self, label: &'tokens str) {
if let Error::Syntax {
label: ref mut l, ..
} = self.error.as_mut()
{
*l = Some(label.to_string());
}
}
}
#[derive(Debug, Clone, Hash)]
pub struct ErrorCollector {
errors: Vec<RichError>,
}
impl Default for ErrorCollector {
fn default() -> Self {
Self::new()
}
}
impl ErrorCollector {
pub fn new() -> Self {
Self { errors: Vec::new() }
}
pub fn push(&mut self, error: RichError) {
self.errors.push(error);
}
pub fn extend(
&mut self,
source: impl Into<SourceFile> + Clone,
errors: impl IntoIterator<Item = RichError>,
) {
let new_errors = errors
.into_iter()
.map(|err| err.with_source(source.clone()));
self.errors.extend(new_errors);
}
pub fn extend_with_handler(
&mut self,
source: impl Into<SourceFile> + Clone,
handler: &ErrorCollector,
) {
self.extend(source, handler.errors.iter().cloned());
}
pub fn get(&self) -> &[RichError] {
&self.errors
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
impl fmt::Display for ErrorCollector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for err in self.get() {
writeln!(f, "{err}\n")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Error {
Internal(String),
UnknownLibrary(String),
ArraySizeNonZero(usize),
ListBoundPow2(usize),
BitStringPow2(usize),
CannotParse(String),
Grammar(String),
Syntax {
expected: Vec<String>,
label: Option<String>,
found: Option<String>,
},
IncompatibleMatchArms(MatchPattern, MatchPattern),
CannotCompile(String),
JetDoesNotExist(JetName),
InvalidCast(ResolvedType, ResolvedType),
FileNotFound(PathBuf),
RedefinedItem(String),
UnresolvedItem(String),
PrivateItem(String),
MainNoInputs,
MainNoOutput,
MainRequired,
MainOutOfEntryFile,
MainCannotBePublic,
MainCannotBeAlias,
FunctionRedefined(FunctionName),
FunctionUndefined(FunctionName),
InvalidNumberOfArguments(usize, usize),
FunctionNotFoldable(FunctionName),
FunctionNotLoopable(FunctionName),
ExpressionUnexpectedType(ResolvedType),
ExpressionTypeMismatch(ResolvedType, ResolvedType),
ExpressionNotConstant,
IntegerOutOfBounds(UIntType),
UndefinedVariable(Identifier),
RedefinedAlias(AliasName),
RedefinedAliasAsBuiltin(AliasName),
UndefinedAlias(AliasName),
DuplicateAlias(String),
VariableReuseInPattern(Identifier),
WitnessReused(WitnessName),
WitnessTypeMismatch(WitnessName, ResolvedType, ResolvedType),
WitnessReassigned(WitnessName),
WitnessOutsideMain,
ModuleRedefined(ModuleName),
ArgumentMissing(WitnessName),
ArgumentTypeMismatch(WitnessName, ResolvedType, ResolvedType),
}
#[rustfmt::skip]
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Internal(err) => write!(
f,
"INTERNAL ERROR: {err}"
),
Error::UnknownLibrary(name) => write!(
f,
"Unknown module or library '{name}'"
),
Error::ArraySizeNonZero(size) => write!(
f,
"Expected a non-negative integer as array size, found {size}"
),
Error::ListBoundPow2(bound) => write!(
f,
"Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found {bound}"
),
Error::BitStringPow2(len) => write!(
f,
"Expected a valid bit string length (1, 2, 4, 8, 16, 32, 64, 128, 256), found {len}"
),
Error::CannotParse(description) => write!(
f,
"Cannot parse: {description}"
),
Error::Grammar(description) => write!(
f,
"Grammar error: {description}"
),
Error::FileNotFound(path) => write!(
f,
"File `{}` not found", path.to_string_lossy()
),
Error::Syntax { expected, label, found } => {
let found_text = found.clone().unwrap_or("end of input".to_string());
match (label, expected.len()) {
(Some(l), _) => write!(f, "Expected {}, found {}", l, found_text),
(None, 1) => {
let exp_text = expected.first().unwrap();
write!(f, "Expected '{}', found '{}'", exp_text, found_text)
}
(None, 0) => write!(f, "Unexpected {}", found_text),
(None, _) => {
let exp_text = expected.iter().map(|s| format!("'{}'", s)).join(", ");
write!(f, "Expected one of {}, found '{}'", exp_text, found_text)
}
}
}
Error::IncompatibleMatchArms(pattern1, pattern2) => write!(
f,
"Match arm `{pattern1}` is incompatible with arm `{pattern2}`"
),
Error::CannotCompile(description) => write!(
f,
"Failed to compile to Simplicity: {description}"
),
Error::JetDoesNotExist(name) => write!(
f,
"Jet `{name}` does not exist"
),
Error::InvalidCast(source, target) => write!(
f,
"Cannot cast values of type `{source}` as values of type `{target}`"
),
Error::MainNoInputs => write!(
f,
"Main function takes no input parameters"
),
Error::MainNoOutput => write!(
f,
"Main function produces no output"
),
Error::MainRequired => write!(
f,
"Main function is required"
),
Error::MainOutOfEntryFile => write!(
f,
"The 'main' function must be defined in the entry point file"
),
Error::MainCannotBePublic => write!(
f,
"Main function cannot be public"
),
Error::MainCannotBeAlias => write!(
f,
"Main function cannot be alias",
),
Error::FunctionRedefined(name) => write!(
f,
"Function `{name}` was defined multiple times"
),
Error::FunctionUndefined(name) => write!(
f,
"Function `{name}` was called but not defined"
),
Error::RedefinedItem(name) => write!(
f,
"Item `{name}` was defined multiple times"
),
Error::UnresolvedItem(name) => write!(
f,
"Item `{name}` could not be found"
),
Error::PrivateItem(name) => write!(
f,
"Item `{name}` is private"
),
Error::InvalidNumberOfArguments(expected, found) => write!(
f,
"Expected {expected} arguments, found {found} arguments"
),
Error::FunctionNotFoldable(name) => write!(
f,
"Expected a signature like `fn {name}(element: E, accumulator: A) -> A` for a fold"
),
Error::FunctionNotLoopable(name) => write!(
f,
"Expected a signature like `fn {name}(accumulator: A, context: C, counter u{{1,2,4,8,16}}) -> Either<B, A>` for a for-while loop"
),
Error::ExpressionUnexpectedType(ty) => write!(
f,
"Expected expression of type `{ty}`; found something else"
),
Error::ExpressionTypeMismatch(expected, found) => write!(
f,
"Expected expression of type `{expected}`, found type `{found}`"
),
Error::ExpressionNotConstant => write!(
f,
"Expression cannot be evaluated at compile time"
),
Error::IntegerOutOfBounds(ty) => write!(
f,
"Value is out of bounds for type `{ty}`"
),
Error::UndefinedVariable(identifier) => write!(
f,
"Variable `{identifier}` is not defined"
),
Error::RedefinedAlias(identifier) => write!(
f,
"Type alias `{identifier}` was defined multiple times"
),
Error::RedefinedAliasAsBuiltin(identifier) => write!(
f,
"Type alias `{identifier}` is already exists as built-in alias"
),
Error::UndefinedAlias(identifier) => write!(
f,
"Type alias `{identifier}` is not defined"
),
Error::DuplicateAlias(name) => write!(
f,
"The alias `{name}` was defined multiple times"
),
Error::VariableReuseInPattern(identifier) => write!(
f,
"Variable `{identifier}` is used twice in the pattern"
),
Error::WitnessReused(name) => write!(
f,
"Witness `{name}` has been used before somewhere in the program"
),
Error::WitnessTypeMismatch(name, declared, assigned) => write!(
f,
"Witness `{name}` was declared with type `{declared}` but its assigned value is of type `{assigned}`"
),
Error::WitnessReassigned(name) => write!(
f,
"Witness `{name}` has already been assigned a value"
),
Error::WitnessOutsideMain => write!(
f,
"Witness expressions are not allowed outside the `main` function"
),
Error::ModuleRedefined(name) => write!(
f,
"Module `{name}` is defined twice"
),
Error::ArgumentMissing(name) => write!(
f,
"Parameter `{name}` is missing an argument"
),
Error::ArgumentTypeMismatch(name, declared, assigned) => write!(
f,
"Parameter `{name}` was declared with type `{declared}` but its assigned argument is of type `{assigned}`"
),
}
}
}
impl std::error::Error for Error {}
impl Error {
pub fn with_span(self, span: Span) -> RichError {
RichError::new(self, span)
}
}
impl From<elements::hex::Error> for Error {
fn from(error: elements::hex::Error) -> Self {
Self::CannotParse(error.to_string())
}
}
impl From<std::num::ParseIntError> for Error {
fn from(error: std::num::ParseIntError) -> Self {
Self::CannotParse(error.to_string())
}
}
impl From<crate::num::ParseIntError> for Error {
fn from(error: crate::num::ParseIntError) -> Self {
Self::CannotParse(error.to_string())
}
}
impl From<simplicity::types::Error> for Error {
fn from(error: simplicity::types::Error) -> Self {
Self::CannotCompile(error.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
const CONTENT: &str = r#"let a1: List<u32, 5> = None;
let x: u32 = Left(
Right(0)
);"#;
const EMPTY_FILE: &str = "";
#[test]
fn display_single_line() {
let error = Error::ListBoundPow2(5)
.with_span(Span::new(13, 19))
.with_content(Arc::from(CONTENT));
let expected = r#"
|
1 | let a1: List<u32, 5> = None;
| ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_multi_line() {
let error = Error::CannotParse(
"Expected value of type `u32`, got `Either<Either<_, u32>, _>`".to_string(),
)
.with_span(Span::new(41, CONTENT.len()))
.with_content(Arc::from(CONTENT));
let expected = r#"
|
2 | let x: u32 = Left(
3 | Right(0)
4 | );
| ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either<Either<_, u32>, _>`"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_entire_file() {
let error = Error::CannotParse("This span covers the entire file".to_string())
.with_span(Span::from(CONTENT))
.with_content(Arc::from(CONTENT));
let expected = r#"
|
1 | let a1: List<u32, 5> = None;
2 | let x: u32 = Left(
3 | Right(0)
4 | );
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_no_file() {
let error = Error::CannotParse("This error has no file".to_string())
.with_span(Span::from(EMPTY_FILE));
let expected = "Cannot parse: This error has no file";
assert_eq!(&expected, &error.to_string());
let error =
Error::CannotParse("This error has no file".to_string()).with_span(Span::new(5, 10));
assert_eq!(&expected, &error.to_string());
}
#[test]
fn display_empty_file() {
let error = Error::CannotParse("This error has an empty file".to_string())
.with_span(Span::from(EMPTY_FILE))
.with_content(Arc::from(EMPTY_FILE));
let expected = "Cannot parse: This error has an empty file";
assert_eq!(&expected, &error.to_string());
}
#[test]
fn display_with_utf16_chars() {
let file = "/*😀*/ let a: u8 = 65536;";
let error = Error::CannotParse("number too large to fit in target type".to_string())
.with_span(Span::new(21, 26))
.with_content(Arc::from(file));
let expected = r#"
|
1 | /*😀*/ let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn multiline_display_with_utf16_chars() {
let file = r#"/*😀 this symbol should not break the rendering*/
let a: u8 = 65536;
let x: u32 = Left(
Right(0)
);"#;
let error = Error::CannotParse("This span covers the entire file".to_string())
.with_span(Span::from(file))
.with_content(Arc::from(file));
let expected = r#"
|
1 | /*😀 this symbol should not break the rendering*/
2 | let a: u8 = 65536;
3 | let x: u32 = Left(
4 | Right(0)
5 | );
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_with_unicode_separator() {
let file = "let a: u8 = 65536;\u{2028}let b: u8 = 0;";
let error = Error::CannotParse("number too large to fit in target type".to_string())
.with_span(Span::new(12, 17))
.with_content(Arc::from(file));
let expected = r#"
|
1 | let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_span_as_point() {
let file = "fn main()";
let error = Error::Grammar("Error span at (0,0)".to_string())
.with_span(Span::new(0, 0))
.with_content(Arc::from(file));
let expected = r#"
|
1 | fn main()
| ^ Grammar error: Error span at (0,0)"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_span_as_point_on_trailing_empty_line() {
let file = "fn main(){\n let a:\n";
let error = Error::CannotParse("eof".to_string())
.with_span(Span::new(file.len(), file.len()))
.with_content(Arc::from(file));
let expected = r#"
|
3 |
| ^ Cannot parse: eof"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_single_line_with_file() {
let source = SourceFile::new(std::path::Path::new("src/main.simf"), Arc::from(CONTENT));
let error = Error::ListBoundPow2(5)
.with_span(Span::new(13, 19))
.with_source(source);
let expected = r#"
--> src/main.simf:1:14
|
1 | let a1: List<u32, 5> = None;
| ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_multi_line_with_file() {
let source = SourceFile::new(std::path::Path::new("lib/parser.simf"), Arc::from(CONTENT));
let error = Error::CannotParse(
"Expected value of type `u32`, got `Either<Either<_, u32>, _>`".to_string(),
)
.with_span(Span::new(41, CONTENT.len()))
.with_source(source);
let expected = r#"
--> lib/parser.simf:2:13
|
2 | let x: u32 = Left(
3 | Right(0)
4 | );
| ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either<Either<_, u32>, _>`"#;
assert_eq!(&expected[1..], &error.to_string());
}
#[test]
fn display_entire_file_with_file() {
let source = SourceFile::new(
std::path::Path::new("tests/integration.simf"),
Arc::from(CONTENT),
);
let error = Error::CannotParse("This span covers the entire file".to_string())
.with_span(Span::from(CONTENT))
.with_source(source);
let expected = r#"
--> tests/integration.simf:1:1
|
1 | let a1: List<u32, 5> = None;
2 | let x: u32 = Left(
3 | Right(0)
4 | );
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#;
assert_eq!(&expected[1..], &error.to_string());
}
}