1use crate::Span;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5pub const MAX_ERRORS: usize = 20;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Severity {
14 Error,
15 Warning,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum ErrorCategory {
22 Syntax,
23 Type,
24 Invariant,
25 Capability,
26 Scope,
27 Structure,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32pub struct ErrorCode(pub u16);
33
34impl ErrorCode {
35 pub const UNEXPECTED_TOKEN: Self = Self(100);
37 pub const UNCLOSED_BRACE: Self = Self(101);
38 pub const INVALID_KEYWORD: Self = Self(102);
39
40 pub const UNKNOWN_TYPE: Self = Self(200);
42 pub const TYPE_MISMATCH: Self = Self(201);
43 pub const WRONG_ARG_COUNT: Self = Self(202);
44 pub const NON_EXHAUSTIVE_MATCH: Self = Self(210);
45
46 pub const INVARIANT_UNREACHABLE: Self = Self(300);
48 pub const INVARIANT_UNKNOWN_FIELD: Self = Self(301);
49
50 pub const UNDECLARED_CAPABILITY: Self = Self(400);
52 pub const CAPABILITY_UNAVAILABLE: Self = Self(401);
53 pub const UNKNOWN_COMPONENT: Self = Self(402);
54
55 pub const VARIABLE_ALREADY_DECLARED: Self = Self(500);
57 pub const STATE_MUTATED_OUTSIDE_ACTION: Self = Self(501);
58 pub const RECURSION_NOT_ALLOWED: Self = Self(502);
59
60 pub const BLOCK_ORDERING_VIOLATED: Self = Self(600);
62 pub const DERIVED_FIELD_MODIFIED: Self = Self(601);
63 pub const EXPRESSION_BODY_LAMBDA: Self = Self(602);
64 pub const BLOCK_COMMENT_USED: Self = Self(603);
65 pub const UNDECLARED_CREDENTIAL: Self = Self(604);
66 pub const CREDENTIAL_MODIFIED: Self = Self(605);
67 pub const EMPTY_STATE_BLOCK: Self = Self(606);
68 pub const STRUCTURAL_LIMIT_EXCEEDED: Self = Self(607);
69
70 pub fn category(self) -> ErrorCategory {
72 match self.0 {
73 100..=199 => ErrorCategory::Syntax,
74 200..=299 => ErrorCategory::Type,
75 300..=399 => ErrorCategory::Invariant,
76 400..=499 => ErrorCategory::Capability,
77 500..=599 => ErrorCategory::Scope,
78 600..=699 => ErrorCategory::Structure,
79 _ => ErrorCategory::Syntax, }
81 }
82}
83
84impl fmt::Display for ErrorCode {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "E{}", self.0)
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct PeplError {
96 pub file: String,
98 pub code: ErrorCode,
100 pub severity: Severity,
102 pub category: ErrorCategory,
104 pub message: String,
106 #[serde(flatten)]
108 pub span: Span,
109 pub source_line: String,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub suggestion: Option<String>,
114}
115
116impl PeplError {
117 pub fn new(
119 file: impl Into<String>,
120 code: ErrorCode,
121 message: impl Into<String>,
122 span: Span,
123 source_line: impl Into<String>,
124 ) -> Self {
125 Self {
126 file: file.into(),
127 code,
128 severity: Severity::Error,
129 category: code.category(),
130 message: message.into(),
131 span,
132 source_line: source_line.into(),
133 suggestion: None,
134 }
135 }
136
137 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
139 self.suggestion = Some(suggestion.into());
140 self
141 }
142}
143
144impl fmt::Display for PeplError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(
147 f,
148 "{}: {} [{}] {}",
149 self.span, self.code, self.category, self.message
150 )
151 }
152}
153
154impl std::error::Error for PeplError {}
155
156impl fmt::Display for ErrorCategory {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 Self::Syntax => write!(f, "syntax"),
160 Self::Type => write!(f, "type"),
161 Self::Invariant => write!(f, "invariant"),
162 Self::Capability => write!(f, "capability"),
163 Self::Scope => write!(f, "scope"),
164 Self::Structure => write!(f, "structure"),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct CompileErrors {
172 pub errors: Vec<PeplError>,
173 pub warnings: Vec<PeplError>,
174 pub total_errors: usize,
175 pub total_warnings: usize,
176}
177
178impl CompileErrors {
179 pub fn empty() -> Self {
181 Self {
182 errors: Vec::new(),
183 warnings: Vec::new(),
184 total_errors: 0,
185 total_warnings: 0,
186 }
187 }
188
189 pub fn has_errors(&self) -> bool {
191 self.total_errors > 0
192 }
193
194 pub fn push_error(&mut self, error: PeplError) {
196 if self.errors.len() < MAX_ERRORS {
197 self.errors.push(error);
198 }
199 self.total_errors += 1;
200 }
201
202 pub fn push_warning(&mut self, warning: PeplError) {
204 self.warnings.push(warning);
205 self.total_warnings += 1;
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_error_code_category() {
215 assert_eq!(
216 ErrorCode::UNEXPECTED_TOKEN.category(),
217 ErrorCategory::Syntax
218 );
219 assert_eq!(ErrorCode::TYPE_MISMATCH.category(), ErrorCategory::Type);
220 assert_eq!(
221 ErrorCode::INVARIANT_UNREACHABLE.category(),
222 ErrorCategory::Invariant
223 );
224 assert_eq!(
225 ErrorCode::UNDECLARED_CAPABILITY.category(),
226 ErrorCategory::Capability
227 );
228 assert_eq!(
229 ErrorCode::VARIABLE_ALREADY_DECLARED.category(),
230 ErrorCategory::Scope
231 );
232 assert_eq!(
233 ErrorCode::BLOCK_ORDERING_VIOLATED.category(),
234 ErrorCategory::Structure
235 );
236 }
237
238 #[test]
239 fn test_error_code_display() {
240 assert_eq!(format!("{}", ErrorCode::TYPE_MISMATCH), "E201");
241 assert_eq!(format!("{}", ErrorCode::UNEXPECTED_TOKEN), "E100");
242 }
243
244 #[test]
245 fn test_pepl_error_creation() {
246 let err = PeplError::new(
247 "test.pepl",
248 ErrorCode::TYPE_MISMATCH,
249 "Type mismatch: expected 'Number', found 'String'",
250 Span::new(12, 5, 12, 22),
251 " set state.count = \"hello\"",
252 );
253 assert_eq!(err.code, ErrorCode::TYPE_MISMATCH);
254 assert_eq!(err.severity, Severity::Error);
255 assert_eq!(err.category, ErrorCategory::Type);
256 }
257
258 #[test]
259 fn test_pepl_error_with_suggestion() {
260 let err = PeplError::new(
261 "test.pepl",
262 ErrorCode::TYPE_MISMATCH,
263 "Type mismatch",
264 Span::new(1, 1, 1, 10),
265 "set count = \"hello\"",
266 )
267 .with_suggestion("Use convert.to_int(value)");
268 assert_eq!(err.suggestion.as_deref(), Some("Use convert.to_int(value)"));
269 }
270
271 #[test]
272 fn test_pepl_error_json_serialization() {
273 let err = PeplError::new(
274 "WaterTracker.pepl",
275 ErrorCode::TYPE_MISMATCH,
276 "Type mismatch: expected 'Number', found 'String'",
277 Span::new(12, 5, 12, 22),
278 " set state.count = \"hello\"",
279 )
280 .with_suggestion("Use convert.to_int(value) to convert String to Number");
281
282 let json = serde_json::to_string_pretty(&err).unwrap();
283 assert!(json.contains("\"code\""));
284 assert!(json.contains("\"message\""));
285 assert!(json.contains("\"source_line\""));
286 assert!(json.contains("\"suggestion\""));
287 assert!(
289 json.contains("\"line\""),
290 "JSON must use 'line' not 'start_line'"
291 );
292 assert!(
293 json.contains("\"column\""),
294 "JSON must use 'column' not 'start_col'"
295 );
296 assert!(json.contains("\"end_line\""));
297 assert!(
298 json.contains("\"end_column\""),
299 "JSON must use 'end_column' not 'end_col'"
300 );
301
302 let deserialized: PeplError = serde_json::from_str(&json).unwrap();
304 assert_eq!(deserialized.code, err.code);
305 assert_eq!(deserialized.message, err.message);
306 }
307
308 #[test]
309 fn test_compile_errors_max_limit() {
310 let mut errs = CompileErrors::empty();
311 for i in 0..25 {
312 errs.push_error(PeplError::new(
313 "test.pepl",
314 ErrorCode::UNEXPECTED_TOKEN,
315 format!("Error {i}"),
316 Span::point(i as u32 + 1, 1),
317 "",
318 ));
319 }
320 assert_eq!(errs.errors.len(), 20);
322 assert_eq!(errs.total_errors, 25);
323 assert!(errs.has_errors());
324 }
325
326 #[test]
327 fn test_compile_errors_empty() {
328 let errs = CompileErrors::empty();
329 assert!(!errs.has_errors());
330 assert_eq!(errs.total_errors, 0);
331 assert_eq!(errs.total_warnings, 0);
332 }
333
334 #[test]
335 fn test_compile_errors_json_output() {
336 let mut errs = CompileErrors::empty();
337 errs.push_error(PeplError::new(
338 "test.pepl",
339 ErrorCode::TYPE_MISMATCH,
340 "Type mismatch",
341 Span::new(1, 1, 1, 10),
342 "set count = \"hello\"",
343 ));
344
345 let json = serde_json::to_string(&errs).unwrap();
346 assert!(json.contains("\"total_errors\":1"));
347 assert!(json.contains("\"total_warnings\":0"));
348 }
349
350 #[test]
351 fn test_error_determinism_100_iterations() {
352 let first = PeplError::new(
353 "test.pepl",
354 ErrorCode::TYPE_MISMATCH,
355 "Type mismatch",
356 Span::new(12, 5, 12, 22),
357 "set count = \"hello\"",
358 );
359 let first_json = serde_json::to_string(&first).unwrap();
360
361 for i in 0..100 {
362 let err = PeplError::new(
363 "test.pepl",
364 ErrorCode::TYPE_MISMATCH,
365 "Type mismatch",
366 Span::new(12, 5, 12, 22),
367 "set count = \"hello\"",
368 );
369 let json = serde_json::to_string(&err).unwrap();
370 assert_eq!(first_json, json, "Determinism failure at iteration {i}");
371 }
372 }
373}