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