Skip to main content

lp_parser_rs/
error.rs

1use std::io;
2
3use thiserror::Error;
4
5use crate::lexer::{LexerError, Token};
6
7/// This error type provides detailed context about parsing failures,
8/// including location information and specific error conditions.
9#[derive(Error, Debug, Clone, PartialEq, Eq)]
10pub enum LpParseError {
11    /// Invalid or malformed constraint syntax
12    #[error("Invalid constraint syntax at position {position}: {context}")]
13    ConstraintSyntax { position: usize, context: String },
14
15    /// Invalid or malformed objective syntax
16    #[error("Invalid objective syntax at position {position}: {context}")]
17    ObjectiveSyntax { position: usize, context: String },
18
19    /// Unknown or invalid variable type specification
20    #[error("Unknown variable type '{var_type}' for variable '{variable}'")]
21    UnknownVariableType { variable: String, var_type: String },
22
23    /// Reference to an undefined variable
24    #[error("Undefined variable '{variable}' referenced in {context}")]
25    UndefinedVariable { variable: String, context: String },
26
27    /// Duplicate definition of a component
28    #[error("Duplicate {component_type} '{name}' defined")]
29    DuplicateDefinition { component_type: String, name: String },
30
31    /// Invalid numerical value or format
32    #[error("Invalid number format '{value}' at position {position}")]
33    InvalidNumber { value: String, position: usize },
34
35    /// Missing required section in LP file
36    #[error("Missing required section: {section}")]
37    MissingSection { section: String },
38
39    /// Invalid bound specification
40    #[error("Invalid bounds for variable '{variable}': {details}")]
41    InvalidBounds { variable: String, details: String },
42
43    /// Invalid SOS constraint specification
44    #[error("Invalid SOS constraint '{constraint}': {details}")]
45    InvalidSosConstraint { constraint: String, details: String },
46
47    /// Validation error for logical consistency
48    #[error("Validation error: {message}")]
49    ValidationError { message: String },
50
51    /// Generic parsing error with context
52    #[error("Parse error at position {position}: {message}")]
53    ParseError { position: usize, message: String },
54
55    /// File I/O related errors
56    #[error("File I/O error: {message}")]
57    IoError { message: String },
58
59    /// Internal parser state errors
60    #[error("Internal parser error: {message}")]
61    InternalError { message: String },
62}
63
64impl LpParseError {
65    /// Create a new constraint syntax error
66    pub fn constraint_syntax(position: usize, context: impl Into<String>) -> Self {
67        Self::ConstraintSyntax { position, context: context.into() }
68    }
69
70    /// Create a new objective syntax error
71    pub fn objective_syntax(position: usize, context: impl Into<String>) -> Self {
72        Self::ObjectiveSyntax { position, context: context.into() }
73    }
74
75    /// Create a new unknown variable type error
76    pub fn unknown_variable_type(variable: impl Into<String>, var_type: impl Into<String>) -> Self {
77        Self::UnknownVariableType { variable: variable.into(), var_type: var_type.into() }
78    }
79
80    /// Create a new undefined variable error
81    pub fn undefined_variable(variable: impl Into<String>, context: impl Into<String>) -> Self {
82        Self::UndefinedVariable { variable: variable.into(), context: context.into() }
83    }
84
85    /// Create a new duplicate definition error
86    pub fn duplicate_definition(component_type: impl Into<String>, name: impl Into<String>) -> Self {
87        Self::DuplicateDefinition { component_type: component_type.into(), name: name.into() }
88    }
89
90    /// Create a new invalid number error
91    pub fn invalid_number(value: impl Into<String>, position: usize) -> Self {
92        Self::InvalidNumber { value: value.into(), position }
93    }
94
95    /// Create a new missing section error
96    pub fn missing_section(section: impl Into<String>) -> Self {
97        Self::MissingSection { section: section.into() }
98    }
99
100    /// Create a new invalid bounds error
101    pub fn invalid_bounds(variable: impl Into<String>, details: impl Into<String>) -> Self {
102        Self::InvalidBounds { variable: variable.into(), details: details.into() }
103    }
104
105    /// Create a new invalid SOS constraint error
106    pub fn invalid_sos_constraint(constraint: impl Into<String>, details: impl Into<String>) -> Self {
107        Self::InvalidSosConstraint { constraint: constraint.into(), details: details.into() }
108    }
109
110    /// Create a new validation error
111    pub fn validation_error(message: impl Into<String>) -> Self {
112        Self::ValidationError { message: message.into() }
113    }
114
115    /// Create a new parse error
116    pub fn parse_error(position: usize, message: impl Into<String>) -> Self {
117        Self::ParseError { position, message: message.into() }
118    }
119
120    /// Create a new I/O error
121    pub fn io_error(message: impl Into<String>) -> Self {
122        Self::IoError { message: message.into() }
123    }
124
125    /// Create a new internal error
126    pub fn internal_error(message: impl Into<String>) -> Self {
127        Self::InternalError { message: message.into() }
128    }
129}
130
131/// Convert from LALRPOP parsing errors to our custom error type.
132impl<'input> From<lalrpop_util::ParseError<usize, Token<'input>, LexerError>> for LpParseError {
133    fn from(err: lalrpop_util::ParseError<usize, Token<'input>, LexerError>) -> Self {
134        match err {
135            lalrpop_util::ParseError::InvalidToken { location } => Self::parse_error(location, "Invalid token"),
136            lalrpop_util::ParseError::UnrecognizedEof { location, expected } => {
137                let expected_str = if expected.is_empty() { String::new() } else { format!(", expected one of: {}", expected.join(", ")) };
138                Self::parse_error(location, format!("Unexpected end of input{expected_str}"))
139            }
140            lalrpop_util::ParseError::UnrecognizedToken { token: (start, tok, _), expected } => {
141                let expected_str = if expected.is_empty() { String::new() } else { format!(", expected one of: {}", expected.join(", ")) };
142                Self::parse_error(start, format!("Unexpected token {tok:?}{expected_str}"))
143            }
144            lalrpop_util::ParseError::ExtraToken { token: (start, tok, _) } => Self::parse_error(start, format!("Extra token {tok:?}")),
145            lalrpop_util::ParseError::User { error } => Self::parse_error(0, format!("Lexer error: {error:?}")),
146        }
147    }
148}
149
150/// Convert from standard I/O errors
151impl From<io::Error> for LpParseError {
152    fn from(err: io::Error) -> Self {
153        Self::io_error(err.to_string())
154    }
155}
156
157/// Convert from boxed errors (used by CSV module)
158impl From<Box<dyn std::error::Error + 'static>> for LpParseError {
159    fn from(err: Box<dyn std::error::Error + 'static>) -> Self {
160        Self::io_error(err.to_string())
161    }
162}
163
164/// Result type alias for LP parsing operations
165pub type LpResult<T> = Result<T, LpParseError>;
166
167/// Context extension trait for adding location information to errors
168pub trait ErrorContext<T> {
169    /// Add position context to an error
170    ///
171    /// # Errors
172    ///
173    /// Propagates the original error with updated position information
174    fn with_position(self, position: usize) -> LpResult<T>;
175
176    /// Add general context to an error
177    ///
178    /// # Errors
179    ///
180    /// Propagates the original error with added context message
181    fn with_context(self, context: &str) -> LpResult<T>;
182}
183
184impl<T> ErrorContext<T> for Result<T, LpParseError> {
185    fn with_position(self, position: usize) -> Self {
186        self.map_err(|mut err| {
187            if let LpParseError::ParseError { position: pos, .. } = &mut err {
188                *pos = position;
189            }
190            err
191        })
192    }
193
194    fn with_context(self, context: &str) -> Self {
195        self.map_err(|err| match err {
196            LpParseError::ParseError { position, message } => LpParseError::parse_error(position, format!("{context}: {message}")),
197            other => other,
198        })
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_error_creation() {
208        let err = LpParseError::constraint_syntax(42, "missing operator");
209        assert_eq!(err.to_string(), "Invalid constraint syntax at position 42: missing operator");
210    }
211
212    #[test]
213    fn test_error_context() {
214        let result: LpResult<()> = Err(LpParseError::parse_error(10, "test error"));
215        let with_context = result.with_context("parsing constraint");
216
217        assert!(with_context.is_err());
218        assert!(with_context.unwrap_err().to_string().contains("parsing constraint"));
219    }
220
221    #[test]
222    fn test_io_error_conversion() {
223        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
224        let lp_err: LpParseError = io_err.into();
225
226        match lp_err {
227            LpParseError::IoError { message } => assert!(message.contains("file not found")),
228            _ => panic!("Expected IoError"),
229        }
230    }
231}