1use std::fmt;
2use thiserror::Error;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct SourceLocation {
7 pub line: usize,
8 pub column: usize,
9}
10
11impl SourceLocation {
12 pub fn new(line: usize, column: usize) -> Self {
13 Self { line, column }
14 }
15}
16
17impl fmt::Display for SourceLocation {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(f, "line {}, column {}", self.line, self.column)
20 }
21}
22
23#[derive(Error, Debug)]
25pub enum Error {
26 #[error("lexer error at {location}: {message}")]
27 Lexer {
28 message: String,
29 location: SourceLocation,
30 },
31
32 #[error("parser error at {location}: {message}")]
33 Parser {
34 message: String,
35 location: SourceLocation,
36 },
37
38 #[error("runtime error: {message}")]
39 Runtime { message: String },
40
41 #[error("runtime error at {location}: {message}")]
42 RuntimeWithLocation {
43 message: String,
44 location: SourceLocation,
45 },
46
47 #[error("I/O error: {0}")]
48 Io(#[from] std::io::Error),
49
50 #[error("regex error: {0}")]
51 Regex(#[from] regex::Error),
52}
53
54impl Error {
55 pub fn lexer(message: impl Into<String>, line: usize, column: usize) -> Self {
56 Self::Lexer {
57 message: message.into(),
58 location: SourceLocation::new(line, column),
59 }
60 }
61
62 pub fn parser(message: impl Into<String>, line: usize, column: usize) -> Self {
63 Self::Parser {
64 message: message.into(),
65 location: SourceLocation::new(line, column),
66 }
67 }
68
69 pub fn runtime(message: impl Into<String>) -> Self {
70 Self::Runtime {
71 message: message.into(),
72 }
73 }
74
75 pub fn runtime_at(message: impl Into<String>, line: usize, column: usize) -> Self {
76 Self::RuntimeWithLocation {
77 message: message.into(),
78 location: SourceLocation::new(line, column),
79 }
80 }
81}
82
83pub type Result<T> = std::result::Result<T, Error>;
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_source_location() {
92 let loc = SourceLocation::new(10, 5);
93 assert_eq!(loc.line, 10);
94 assert_eq!(loc.column, 5);
95 assert_eq!(format!("{}", loc), "line 10, column 5");
96 }
97
98 #[test]
99 fn test_lexer_error() {
100 let err = Error::lexer("unexpected character", 1, 5);
101 assert!(matches!(err, Error::Lexer { .. }));
102 let msg = format!("{}", err);
103 assert!(msg.contains("lexer error"));
104 assert!(msg.contains("unexpected character"));
105 }
106
107 #[test]
108 fn test_parser_error() {
109 let err = Error::parser("expected expression", 2, 10);
110 assert!(matches!(err, Error::Parser { .. }));
111 let msg = format!("{}", err);
112 assert!(msg.contains("parser error"));
113 }
114
115 #[test]
116 fn test_runtime_error() {
117 let err = Error::runtime("division by zero");
118 assert!(matches!(err, Error::Runtime { .. }));
119 let msg = format!("{}", err);
120 assert!(msg.contains("runtime error"));
121 assert!(msg.contains("division by zero"));
122 }
123
124 #[test]
125 fn test_runtime_error_with_location() {
126 let err = Error::runtime_at("undefined variable", 5, 3);
127 assert!(matches!(err, Error::RuntimeWithLocation { .. }));
128 let msg = format!("{}", err);
129 assert!(msg.contains("runtime error"));
130 assert!(msg.contains("line 5"));
131 }
132
133 #[test]
134 fn test_io_error() {
135 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
136 let err: Error = io_err.into();
137 assert!(matches!(err, Error::Io(_)));
138 let msg = format!("{}", err);
139 assert!(msg.contains("I/O error"));
140 }
141
142 #[test]
143 #[allow(clippy::invalid_regex)]
144 fn test_regex_error() {
145 let re_err = regex::Regex::new("[invalid").unwrap_err();
146 let err: Error = re_err.into();
147 assert!(matches!(err, Error::Regex(_)));
148 let msg = format!("{}", err);
149 assert!(msg.contains("regex error"));
150 }
151}