simfony/
error.rs

1use std::fmt;
2use std::num::NonZeroUsize;
3use std::sync::Arc;
4
5use simplicity::hashes::{sha256, Hash, HashEngine};
6use simplicity::{elements, Cmr};
7
8use crate::parse::{MatchPattern, Rule};
9use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName};
10use crate::types::{ResolvedType, UIntType};
11
12/// Position of an object inside a file.
13///
14/// [`pest::Position<'i>`] forces us to track lifetimes, so we introduce our own struct.
15#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
16pub struct Position {
17    /// Line where the object is located.
18    ///
19    /// Starts at 1.
20    pub line: NonZeroUsize,
21    /// Column where the object is located.
22    ///
23    /// Starts at 1.
24    pub col: NonZeroUsize,
25}
26
27impl Position {
28    /// A dummy position.
29    #[cfg(feature = "arbitrary")]
30    pub(crate) const DUMMY: Self = Self::new(1, 1);
31
32    /// Create a new position.
33    ///
34    /// ## Panics
35    ///
36    /// Line or column are zero.
37    pub const fn new(line: usize, col: usize) -> Self {
38        if line == 0 {
39            panic!("Line must not be zero");
40        }
41        // Safety: Checked above
42        let line = unsafe { NonZeroUsize::new_unchecked(line) };
43        if col == 0 {
44            panic!("Column must not be zero");
45        }
46        // Safety: Checked above
47        let col = unsafe { NonZeroUsize::new_unchecked(col) };
48        Self { line, col }
49    }
50}
51
52/// Area that an object spans inside a file.
53///
54/// The area cannot be empty.
55///
56/// [`pest::Span<'i>`] forces us to track lifetimes, so we introduce our own struct.
57#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
58pub struct Span {
59    /// Position where the object starts, inclusively.
60    pub start: Position,
61    /// Position where the object ends, inclusively.
62    pub end: Position,
63}
64
65impl Span {
66    /// A dummy span.
67    #[cfg(feature = "arbitrary")]
68    pub(crate) const DUMMY: Self = Self::new(Position::DUMMY, Position::DUMMY);
69
70    /// Create a new span.
71    ///
72    /// ## Panics
73    ///
74    /// Start comes after end.
75    pub const fn new(start: Position, end: Position) -> Self {
76        // NonZeroUsize does not implement const comparisons (yet)
77        // So we call NonZeroUsize:get() to compare usize in const
78        assert!(
79            start.line.get() <= end.line.get(),
80            "Start cannot come after end"
81        );
82        assert!(
83            start.line.get() < end.line.get() || start.col.get() <= end.col.get(),
84            "Start cannot come after end"
85        );
86        Self { start, end }
87    }
88
89    /// Check if the span covers more than one line.
90    pub const fn is_multiline(&self) -> bool {
91        self.start.line.get() < self.end.line.get()
92    }
93
94    /// Return the CMR of the span.
95    pub fn cmr(&self) -> Cmr {
96        let mut hasher = sha256::HashEngine::default();
97        hasher.input(&self.start.line.get().to_be_bytes());
98        hasher.input(&self.start.col.get().to_be_bytes());
99        hasher.input(&self.end.line.get().to_be_bytes());
100        hasher.input(&self.end.col.get().to_be_bytes());
101        let hash = sha256::Hash::from_engine(hasher);
102        Cmr::from_byte_array(hash.to_byte_array())
103    }
104
105    /// Return a slice from the given `file` that corresponds to the span.
106    ///
107    /// Return `None` if the span runs out of bounds.
108    pub fn to_slice<'a>(&self, file: &'a str) -> Option<&'a str> {
109        let mut current_line = 1;
110        let mut current_col = 1;
111        let mut start_index = None;
112
113        for (i, c) in file.char_indices() {
114            if current_line == self.start.line.get() && current_col == self.start.col.get() {
115                start_index = Some(i);
116            }
117            if current_line == self.end.line.get() && current_col == self.end.col.get() {
118                let start_index = start_index.expect("start comes before end");
119                let end_index = i;
120                return Some(&file[start_index..end_index]);
121            }
122            if c == '\n' {
123                current_line += 1;
124                current_col = 1;
125            } else {
126                current_col += 1;
127            }
128        }
129
130        None
131    }
132}
133
134impl<'a> From<&'a pest::iterators::Pair<'_, Rule>> for Span {
135    fn from(pair: &'a pest::iterators::Pair<Rule>) -> Self {
136        let (line, col) = pair.line_col();
137        let start = Position::new(line, col);
138        // end_pos().line_col() is O(n) in file length
139        // https://github.com/pest-parser/pest/issues/560
140        // We should generate `Span`s only on error paths
141        let (line, col) = pair.as_span().end_pos().line_col();
142        let end = Position::new(line, col);
143        Self::new(start, end)
144    }
145}
146
147impl From<&str> for Span {
148    fn from(s: &str) -> Self {
149        let start = Position::new(1, 1);
150        let end_line = std::cmp::max(1, s.lines().count());
151        let end_col = std::cmp::max(1, s.lines().next_back().unwrap_or("").len());
152        let end = Position::new(end_line, end_col);
153        debug_assert!(start.line <= end.line);
154        debug_assert!(start.line < end.line || start.col <= end.col);
155        Span::new(start, end)
156    }
157}
158
159#[cfg(feature = "arbitrary")]
160impl<'a> arbitrary::Arbitrary<'a> for Span {
161    fn arbitrary(_: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
162        Ok(Self::DUMMY)
163    }
164}
165
166/// Helper trait to convert `Result<T, E>` into `Result<T, RichError>`.
167pub trait WithSpan<T> {
168    /// Update the result with the affected span.
169    fn with_span<S: Into<Span>>(self, span: S) -> Result<T, RichError>;
170}
171
172impl<T, E: Into<Error>> WithSpan<T> for Result<T, E> {
173    fn with_span<S: Into<Span>>(self, span: S) -> Result<T, RichError> {
174        self.map_err(|e| e.into().with_span(span.into()))
175    }
176}
177
178/// Helper trait to update `Result<A, RichError>` with the affected source file.
179pub trait WithFile<T> {
180    /// Update the result with the affected source file.
181    ///
182    /// Enable pretty errors.
183    fn with_file<F: Into<Arc<str>>>(self, file: F) -> Result<T, RichError>;
184}
185
186impl<T> WithFile<T> for Result<T, RichError> {
187    fn with_file<F: Into<Arc<str>>>(self, file: F) -> Result<T, RichError> {
188        self.map_err(|e| e.with_file(file.into()))
189    }
190}
191
192/// An error enriched with context.
193///
194/// Records _what_ happened and _where_.
195#[derive(Debug, Clone, Eq, PartialEq, Hash)]
196pub struct RichError {
197    /// The error that occurred.
198    error: Error,
199    /// Area that the error spans inside the file.
200    span: Span,
201    /// File in which the error occurred.
202    ///
203    /// Required to print pretty errors.
204    file: Option<Arc<str>>,
205}
206
207impl RichError {
208    /// Create a new error with context.
209    pub fn new(error: Error, span: Span) -> RichError {
210        RichError {
211            error,
212            span,
213            file: None,
214        }
215    }
216
217    /// Add the source file where the error occurred.
218    ///
219    /// Enable pretty errors.
220    pub fn with_file(self, file: Arc<str>) -> Self {
221        Self {
222            error: self.error,
223            span: self.span,
224            file: Some(file),
225        }
226    }
227}
228
229impl fmt::Display for RichError {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self.file {
232            Some(ref file) if !file.is_empty() => {
233                let start_line_index = self.span.start.line.get() - 1;
234                let n_spanned_lines = self.span.end.line.get() - start_line_index;
235                let line_num_width = self.span.end.line.get().to_string().len();
236                writeln!(f, "{:width$} |", " ", width = line_num_width)?;
237
238                let mut lines = file.lines().skip(start_line_index).peekable();
239                let start_line_len = lines.peek().map(|l| l.len()).unwrap_or(0);
240
241                for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
242                    let line_num = start_line_index + relative_line_index + 1;
243                    writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
244                }
245
246                let (underline_start, underline_length) = match self.span.is_multiline() {
247                    true => (0, start_line_len),
248                    false => (
249                        self.span.start.col.get(),
250                        self.span.end.col.get() - self.span.start.col.get(),
251                    ),
252                };
253                write!(f, "{:width$} |", " ", width = line_num_width)?;
254                write!(f, "{:width$}", " ", width = underline_start)?;
255                write!(f, "{:^<width$} ", "", width = underline_length)?;
256                write!(f, "{}", self.error)
257            }
258            _ => {
259                write!(f, "{}", self.error)
260            }
261        }
262    }
263}
264
265impl std::error::Error for RichError {}
266
267impl From<RichError> for Error {
268    fn from(error: RichError) -> Self {
269        error.error
270    }
271}
272
273impl From<RichError> for String {
274    fn from(error: RichError) -> Self {
275        error.to_string()
276    }
277}
278
279impl From<pest::error::Error<Rule>> for RichError {
280    fn from(error: pest::error::Error<Rule>) -> Self {
281        let description = error.variant.message().to_string();
282        let (start, end) = match error.line_col {
283            pest::error::LineColLocation::Pos((line, col)) => {
284                (Position::new(line, col), Position::new(line, col + 1))
285            }
286            pest::error::LineColLocation::Span((line, col), (line_end, col_end)) => {
287                (Position::new(line, col), Position::new(line_end, col_end))
288            }
289        };
290        let span = Span::new(start, end);
291        Self::new(Error::Grammar(description), span)
292    }
293}
294
295/// An individual error.
296///
297/// Records _what_ happened but not where.
298#[derive(Debug, Clone, Eq, PartialEq, Hash)]
299pub enum Error {
300    ListBoundPow2(usize),
301    BitStringPow2(usize),
302    HexStringLen(usize),
303    ForWhileWidthPow2(usize),
304    CannotParse(String),
305    Grammar(String),
306    IncompatibleMatchArms(MatchPattern, MatchPattern),
307    // TODO: Remove CompileError once Simfony has a type system
308    // The Simfony compiler should never produce ill-typed Simplicity code
309    // The compiler can only be this precise if it knows a type system at least as expressive as Simplicity's
310    CannotCompile(String),
311    JetDoesNotExist(JetName),
312    InvalidCast(ResolvedType, ResolvedType),
313    MainNoInputs,
314    MainNoOutput,
315    MainRequired,
316    FunctionRedefined(FunctionName),
317    FunctionUndefined(FunctionName),
318    InvalidNumberOfArguments(usize, usize),
319    FunctionNotFoldable(FunctionName),
320    FunctionNotLoopable(FunctionName),
321    ExpressionUnexpectedType(ResolvedType),
322    ExpressionTypeMismatch(ResolvedType, ResolvedType),
323    ExpressionNotConstant,
324    IntegerOutOfBounds(UIntType),
325    UndefinedVariable(Identifier),
326    UndefinedAlias(AliasName),
327    VariableReuseInPattern(Identifier),
328    WitnessReused(WitnessName),
329    WitnessTypeMismatch(WitnessName, ResolvedType, ResolvedType),
330    WitnessReassigned(WitnessName),
331    WitnessOutsideMain,
332    ModuleRequired(ModuleName),
333    ModuleRedefined(ModuleName),
334    ArgumentMissing(WitnessName),
335    ArgumentTypeMismatch(WitnessName, ResolvedType, ResolvedType),
336}
337
338#[rustfmt::skip]
339impl fmt::Display for Error {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        match self {
342            Error::ListBoundPow2(bound) => write!(
343                f,
344                "Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found {bound}"
345            ),
346            Error::BitStringPow2(len) => write!(
347                f,
348                "Expected a valid bit string length (1, 2, 4, 8, 16, 32, 64, 128, 256), found {len}"
349            ),
350            Error::HexStringLen(len) => write!(
351                f,
352                "Expected an even hex string length (0, 2, 4, 6, 8, ...), found {len}"
353            ),
354            Error::ForWhileWidthPow2(bit_width) => write!(
355                f,
356                "Expected a power of two (1, 2, 4, 8, 16, ...) as for-while bit width, found {bit_width}"
357            ),
358            Error::CannotParse(description) => write!(
359                f,
360                "Cannot parse: {description}"
361            ),
362            Error::Grammar(description) => write!(
363                f,
364                "Grammar error: {description}"
365            ),
366            Error::IncompatibleMatchArms(pattern1, pattern2) => write!(
367                f,
368                "Match arm `{pattern1}` is incompatible with arm `{pattern2}`"
369            ),
370            Error::CannotCompile(description) => write!(
371                f,
372                "Failed to compile to Simplicity: {description}"
373            ),
374            Error::JetDoesNotExist(name) => write!(
375                f,
376                "Jet `{name}` does not exist"
377            ),
378            Error::InvalidCast(source, target) => write!(
379                f,
380                "Cannot cast values of type `{source}` as values of type `{target}`"
381            ),
382            Error::MainNoInputs => write!(
383                f,
384                "Main function takes no input parameters"
385            ),
386            Error::MainNoOutput => write!(
387                f,
388                "Main function produces no output"
389            ),
390            Error::MainRequired => write!(
391                f,
392                "Main function is required"
393            ),
394            Error::FunctionRedefined(name) => write!(
395                f,
396                "Function `{name}` was defined multiple times"
397            ),
398            Error::FunctionUndefined(name) => write!(
399                f,
400                "Function `{name}` was called but not defined"
401            ),
402            Error::InvalidNumberOfArguments(expected, found) => write!(
403                f,
404                "Expected {expected} arguments, found {found} arguments"
405            ),
406            Error::FunctionNotFoldable(name) => write!(
407                f,
408                "Expected a signature like `fn {name}(element: E, accumulator: A) -> A` for a fold"
409            ),
410            Error::FunctionNotLoopable(name) => write!(
411                f,
412                "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"
413            ),
414            Error::ExpressionUnexpectedType(ty) => write!(
415                f,
416                "Expected expression of type `{ty}`; found something else"
417            ),
418            Error::ExpressionTypeMismatch(expected, found) => write!(
419                f,
420                "Expected expression of type `{expected}`, found type `{found}`"
421            ),
422            Error::ExpressionNotConstant => write!(
423                f,
424                "Expression cannot be evaluated at compile time"
425            ),
426            Error::IntegerOutOfBounds(ty) => write!(
427                f,
428                "Value is out of bounds for type `{ty}`"
429            ),
430            Error::UndefinedVariable(identifier) => write!(
431                f,
432                "Variable `{identifier}` is not defined"
433            ),
434            Error::UndefinedAlias(identifier) => write!(
435                f,
436                "Type alias `{identifier}` is not defined"
437            ),
438            Error::VariableReuseInPattern(identifier) => write!(
439                f,
440                "Variable `{identifier}` is used twice in the pattern"
441            ),
442            Error::WitnessReused(name) => write!(
443                f,
444                "Witness `{name}` has been used before somewhere in the program"
445            ),
446            Error::WitnessTypeMismatch(name, declared, assigned) => write!(
447                f,
448                "Witness `{name}` was declared with type `{declared}` but its assigned value is of type `{assigned}`"
449            ),
450            Error::WitnessReassigned(name) => write!(
451                f,
452                "Witness `{name}` has already been assigned a value"
453            ),
454            Error::WitnessOutsideMain => write!(
455                f,
456                "Witness expressions are not allowed outside the `main` function"
457            ),
458            Error::ModuleRequired(name) => write!(
459                f,
460                "Required module `{name}` is missing"
461            ),
462            Error::ModuleRedefined(name) => write!(
463                f,
464                "Module `{name}` is defined twice"
465            ),
466            Error::ArgumentMissing(name) => write!(
467                f,
468                "Parameter `{name}` is missing an argument"
469            ),
470            Error::ArgumentTypeMismatch(name, declared, assigned) => write!(
471                f,
472                "Parameter `{name}` was declared with type `{declared}` but its assigned argument is of type `{assigned}`"
473            ),
474        }
475    }
476}
477
478impl std::error::Error for Error {}
479
480impl Error {
481    /// Update the error with the affected span.
482    pub fn with_span(self, span: Span) -> RichError {
483        RichError::new(self, span)
484    }
485}
486
487impl From<elements::hex::Error> for Error {
488    fn from(error: elements::hex::Error) -> Self {
489        Self::CannotParse(error.to_string())
490    }
491}
492
493impl From<std::num::ParseIntError> for Error {
494    fn from(error: std::num::ParseIntError) -> Self {
495        Self::CannotParse(error.to_string())
496    }
497}
498
499impl From<crate::num::ParseIntError> for Error {
500    fn from(error: crate::num::ParseIntError) -> Self {
501        Self::CannotParse(error.to_string())
502    }
503}
504
505impl From<simplicity::types::Error> for Error {
506    fn from(error: simplicity::types::Error) -> Self {
507        Self::CannotCompile(error.to_string())
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    const FILE: &str = r#"let a1: List<u32, 5> = None;
516let x: u32 = Left(
517    Right(0)
518);"#;
519    const EMPTY_FILE: &str = "";
520
521    #[test]
522    fn display_single_line() {
523        let error = Error::ListBoundPow2(5)
524            .with_span(Span::new(Position::new(1, 14), Position::new(1, 20)))
525            .with_file(Arc::from(FILE));
526        let expected = r#"
527  |
5281 | let a1: List<u32, 5> = None;
529  |              ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#;
530        assert_eq!(&expected[1..], &error.to_string());
531    }
532
533    #[test]
534    fn display_multi_line() {
535        let error = Error::CannotParse(
536            "Expected value of type `u32`, got `Either<Either<_, u32>, _>`".to_string(),
537        )
538        .with_span(Span::new(Position::new(2, 21), Position::new(4, 2)))
539        .with_file(Arc::from(FILE));
540        let expected = r#"
541  |
5422 | let x: u32 = Left(
5433 |     Right(0)
5444 | );
545  | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either<Either<_, u32>, _>`"#;
546        assert_eq!(&expected[1..], &error.to_string());
547    }
548
549    #[test]
550    fn display_entire_file() {
551        let error = Error::CannotParse("This span covers the entire file".to_string())
552            .with_span(Span::from(FILE))
553            .with_file(Arc::from(FILE));
554        let expected = r#"
555  |
5561 | let a1: List<u32, 5> = None;
5572 | let x: u32 = Left(
5583 |     Right(0)
5594 | );
560  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#;
561        assert_eq!(&expected[1..], &error.to_string());
562    }
563
564    #[test]
565    fn display_no_file() {
566        let error = Error::CannotParse("This error has no file".to_string())
567            .with_span(Span::from(EMPTY_FILE));
568        let expected = "Cannot parse: This error has no file";
569        assert_eq!(&expected, &error.to_string());
570
571        let error = Error::CannotParse("This error has no file".to_string())
572            .with_span(Span::new(Position::new(1, 1), Position::new(2, 2)));
573        assert_eq!(&expected, &error.to_string());
574    }
575
576    #[test]
577    fn display_empty_file() {
578        let error = Error::CannotParse("This error has an empty file".to_string())
579            .with_span(Span::from(EMPTY_FILE))
580            .with_file(Arc::from(EMPTY_FILE));
581        let expected = "Cannot parse: This error has an empty file";
582        assert_eq!(&expected, &error.to_string());
583
584        let error = Error::CannotParse("This error has an empty file".to_string())
585            .with_span(Span::new(Position::new(1, 1), Position::new(2, 2)))
586            .with_file(Arc::from(EMPTY_FILE));
587        assert_eq!(&expected, &error.to_string());
588    }
589}