Skip to main content

conjure_cp_essence_parser/
errors.rs

1pub use conjure_cp_core::error::Error as ConjureParseError;
2use conjure_cp_core::error::Error;
3use serde_json::Error as JsonError;
4use thiserror::Error as ThisError;
5
6#[derive(Debug, ThisError)]
7pub enum FatalParseError {
8    #[error("Failed to read Essence file `{path}`: {source}")]
9    FileRead {
10        path: String,
11        #[source]
12        source: std::io::Error,
13    },
14    #[error("Could not parse Essence AST: {0}")]
15    TreeSitterError(String),
16    #[error("Error running `conjure pretty`: {0}")]
17    ConjurePrettyError(String),
18    #[error("Internal parser error: {msg}{}\nThis indicates a bug in the parser or syntax validator. Please report this issue.",
19        match range {
20            Some(range) => format!(" at {}-{}", range.start_point, range.end_point),
21            None => "".to_string(),
22        }
23    )]
24    InternalError {
25        msg: String,
26        range: Option<tree_sitter::Range>,
27    },
28    #[error("JSON Error: {0}")]
29    JsonError(#[from] JsonError),
30    #[error("Error: {0} is not yet implemented.")]
31    NotImplemented(String),
32    #[error("Error: {0}")]
33    Other(Error),
34}
35
36impl FatalParseError {
37    pub fn internal_error(msg: String, range: Option<tree_sitter::Range>) -> Self {
38        FatalParseError::InternalError { msg, range }
39    }
40}
41
42impl From<ConjureParseError> for FatalParseError {
43    fn from(value: ConjureParseError) -> Self {
44        match value {
45            Error::Parse(msg) => FatalParseError::internal_error(msg, None),
46            Error::NotImplemented(msg) => FatalParseError::NotImplemented(msg),
47            Error::Json(err) => FatalParseError::JsonError(err),
48            Error::Other(err) => FatalParseError::Other(err.into()),
49        }
50    }
51}
52
53#[derive(Debug, Clone)]
54pub struct RecoverableParseError {
55    pub msg: String,
56    pub range: Option<tree_sitter::Range>,
57    pub file_name: Option<String>,
58    pub source_code: Option<String>,
59}
60
61impl RecoverableParseError {
62    pub fn new(msg: String, range: Option<tree_sitter::Range>) -> Self {
63        Self {
64            msg,
65            range,
66            file_name: None,
67            source_code: None,
68        }
69    }
70
71    pub fn enrich(mut self, file_name: Option<String>, source_code: Option<String>) -> Self {
72        self.file_name = file_name;
73        self.source_code = source_code;
74        self
75    }
76}
77
78impl std::fmt::Display for RecoverableParseError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        // If we have all the info, format nicely with source context
81        if let (Some(range), Some(file_name), Some(source_code)) =
82            (&self.range, &self.file_name, &self.source_code)
83        {
84            let line_num = range.start_point.row + 1; // tree-sitter uses 0-indexed rows
85            let col_num = range.start_point.column + 1; // tree-sitter uses 0-indexed columns
86
87            // Get the specific line from source code
88            let lines: Vec<&str> = source_code.lines().collect();
89            let line_content = lines.get(range.start_point.row).unwrap_or(&"");
90
91            // Build the pointer line (spaces + ^)
92            let pointer = " ".repeat(range.start_point.column) + "^";
93
94            write!(
95                f,
96                "{}:{}:{}:\n  |\n{} | {}\n  | {}\n{}",
97                file_name, line_num, col_num, line_num, line_content, pointer, self.msg
98            )
99        } else {
100            // Fall back to simple format without context
101            write!(f, "Essence syntax error: {}", self.msg)?;
102            if let Some(range) = &self.range {
103                write!(f, " at {}-{}", range.start_point, range.end_point)?;
104            }
105            Ok(())
106        }
107    }
108}
109
110/// Error type for issues during model instantiation (when applying parameters to a problem model).
111#[derive(Debug)]
112pub struct InstantiateModelError {
113    pub msg: String,
114}
115
116impl std::fmt::Display for InstantiateModelError {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(f, "{}", self.msg)
119    }
120}
121
122impl std::error::Error for InstantiateModelError {}
123/// Collection of parse errors
124#[derive(Debug)]
125pub enum ParseErrorCollection {
126    /// A single fatal error that stops parsing entirely
127    Fatal(FatalParseError),
128    /// Multiple recoverable errors accumulated during parsing
129    Multiple {
130        errors: Vec<RecoverableParseError>,
131    },
132
133    InstantiateModel(InstantiateModelError),
134}
135
136impl ParseErrorCollection {
137    /// Create a fatal error collection from a single fatal error
138    pub fn fatal(error: FatalParseError) -> Self {
139        ParseErrorCollection::Fatal(error)
140    }
141
142    /// Create a multiple error collection from recoverable errors
143    /// This enriches all errors with file_name and source_code
144    pub fn multiple(
145        errors: Vec<RecoverableParseError>,
146        source_code: Option<String>,
147        file_name: Option<String>,
148    ) -> Self {
149        let enriched_errors = errors
150            .into_iter()
151            .map(|err| err.enrich(file_name.clone(), source_code.clone()))
152            .collect();
153        ParseErrorCollection::Multiple {
154            errors: enriched_errors,
155        }
156    }
157}
158
159impl std::fmt::Display for ParseErrorCollection {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        match self {
162            ParseErrorCollection::Fatal(error) => write!(f, "{}", error),
163            ParseErrorCollection::InstantiateModel(error) => write!(f, "{}", error),
164            ParseErrorCollection::Multiple { errors } => {
165                // Create indices sorted by line and column
166                let mut indices: Vec<usize> = (0..errors.len()).collect();
167                indices.sort_by(|&a, &b| {
168                    match (&errors[a], &errors[b]) {
169                        (
170                            RecoverableParseError {
171                                range: Some(r1), ..
172                            },
173                            RecoverableParseError {
174                                range: Some(r2), ..
175                            },
176                        ) => {
177                            // Compare by row first, then by column
178                            match r1.start_point.row.cmp(&r2.start_point.row) {
179                                std::cmp::Ordering::Equal => {
180                                    r1.start_point.column.cmp(&r2.start_point.column)
181                                }
182                                other => other,
183                            }
184                        }
185                        // Errors without ranges go last
186                        (RecoverableParseError { range: Some(_), .. }, _) => {
187                            std::cmp::Ordering::Less
188                        }
189                        (_, RecoverableParseError { range: Some(_), .. }) => {
190                            std::cmp::Ordering::Greater
191                        }
192                        _ => std::cmp::Ordering::Equal,
193                    }
194                });
195
196                // Print out each error using Display
197                for (i, &idx) in indices.iter().enumerate() {
198                    if i > 0 {
199                        write!(f, "\n\n")?;
200                    }
201                    write!(f, "{}", errors[idx])?;
202                }
203                Ok(())
204            }
205        }
206    }
207}
208
209impl std::error::Error for ParseErrorCollection {}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn instantiate_model_error_display_and_error_trait() {
217        let err = InstantiateModelError {
218            msg: "hello".to_string(),
219        };
220
221        assert_eq!(err.to_string(), "hello");
222        let _as_error: &dyn std::error::Error = &err;
223    }
224
225    #[test]
226    fn parse_error_collection_instantiate_model_variant_is_displayed() {
227        let err = ParseErrorCollection::InstantiateModel(InstantiateModelError {
228            msg: "missing param".to_string(),
229        });
230        assert_eq!(err.to_string(), "missing param");
231    }
232
233    #[test]
234    fn parse_error_collection_multiple_constructor_works() {
235        let err = ParseErrorCollection::multiple(
236            vec![RecoverableParseError::new("bad token".to_string(), None)],
237            None,
238            None,
239        );
240        let formatted = err.to_string();
241        assert!(formatted.contains("bad token"));
242    }
243}