Skip to main content

xmt_lib/
error.rs

1// Copyright Pierre Carbonnelle, 2025.
2
3use peg::{error::ParseError, str::LineCol};
4use rusqlite::Error as SqlError;
5use thiserror::Error;
6
7use crate::ast::{Identifier, Term};
8use crate::ast::L;
9
10/// The number of characters since the begin of a source file.
11///
12/// Used for error reporting.
13#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
14pub struct Offset(pub usize);
15
16#[derive(Error, Debug, PartialEq)]
17pub enum SolverError {
18
19    #[error("{0}")]
20    ParseError(#[from] ParseError<LineCol>),
21
22    #[error("{0}")]
23    ExprError(String),
24
25    #[error("{0}: {1}")]
26    TermError(&'static str, L<Term>),
27
28    #[error("{0}: {1}")]
29    IdentifierError(&'static str, L<Identifier>),
30
31    #[error("Database error: {0}")]
32    DatabaseError(#[from] SqlError),
33
34    #[error("{0}")]
35    InternalError(usize),  // can't be fixed by the user
36}
37
38use crate::error::SolverError::*;
39
40
41/// Show the error in the context of the relevant source code.
42pub fn format_error(input: &str, e: SolverError) -> String {
43    match e {
44
45        ParseError(e) =>
46            pretty_print(input, e.location, format!("Expected: {}\n", e.expected)),
47
48        DatabaseError(e) => format!("****** Database Error: {}\n", e),
49
50        ExprError(msg) => format!("****** Error: {}\n", msg),
51
52        TermError(msg, term) => {
53            match offset_to_line_col_utf8(&input, term.start()) {
54                None => format!("****** Error: {}\n", msg),
55                Some(location) =>
56                    pretty_print(input, location, msg.to_string())
57            }
58        },
59
60        IdentifierError(msg, id) => {
61            let msg = if id.start() != Offset(0) { msg.to_string() }
62                else { format!("{msg} for {id}") };
63            match offset_to_line_col_utf8(&input, id.start()) {
64                None => format!("****** Error: {}\n", msg),
65                Some(location) =>
66                    pretty_print(input, location, msg.to_string())
67            }
68        },
69
70        InternalError(n) => format!("****** Internal Error: {}\n", n)
71    }
72}
73
74
75/// Show the error in the context of the `input` source code.
76fn pretty_print(input: &str, location: LineCol, msg: String) -> String {
77    if let Some(source) = input.lines().nth(location.line-1) {
78        chic::Error::new(format!("at position ({}, {}): {}", location.line, location.column, msg))
79            .error(
80                location.line,
81                location.column - 1,
82                location.column,
83                &source,
84                msg,
85            )
86            .to_string()
87    } else {
88        format!("****** Error: {msg} at {location}\n")
89    }
90
91}
92
93fn offset_to_line_col_utf8(s: &str, offset: Offset) -> Option<LineCol> {
94    if offset.0 > s.len() {
95        return None;
96    }
97
98    let mut current_offset = 0;
99
100    for (line_number, line) in s.split('\n').enumerate() {
101        let char_indices: Vec<_> = line.char_indices().collect();
102        let line_length = line.len() + 1; // Include the newline character
103
104        if current_offset + line_length > offset.0 {
105            for (column, (byte_index, _)) in char_indices.iter().enumerate() {
106                if current_offset + byte_index >= offset.0 {
107                    return Some(LineCol{column: column + 1, line: line_number + 1, offset:offset.0});
108                }
109            }
110        }
111
112        current_offset += line_length;
113    }
114
115    None
116}