1use std::fmt;
2
3use crate::value::StrykeValue;
4#[derive(Debug, Clone)]
6pub struct StrykeError {
7 pub kind: ErrorKind,
9 pub message: String,
11 pub line: usize,
13 pub file: String,
15 pub die_value: Option<StrykeValue>,
18 pub near: Option<String>,
22}
23#[derive(Debug, Clone, PartialEq)]
25pub enum ErrorKind {
26 Syntax,
28 Runtime,
30 Type,
32 UndefinedVariable,
34 UndefinedSubroutine,
36 FileNotFound,
38 IO,
40 Regex,
42 DivisionByZero,
44 Die,
46 Exit(i32),
48}
49
50impl StrykeError {
51 pub fn new(
53 kind: ErrorKind,
54 message: impl Into<String>,
55 line: usize,
56 file: impl Into<String>,
57 ) -> Self {
58 Self {
59 kind,
60 message: message.into(),
61 line,
62 file: file.into(),
63 die_value: None,
64 near: None,
65 }
66 }
67
68 pub fn with_near(mut self, near: impl Into<String>) -> Self {
71 self.near = Some(near.into());
72 self
73 }
74 pub fn syntax(message: impl Into<String>, line: usize) -> Self {
76 Self::new(ErrorKind::Syntax, message, line, "-e")
77 }
78 pub fn runtime(message: impl Into<String>, line: usize) -> Self {
80 Self::new(ErrorKind::Runtime, message, line, "-e")
81 }
82 pub fn type_error(message: impl Into<String>, line: usize) -> Self {
84 Self::new(ErrorKind::Type, message, line, "-e")
85 }
86
87 pub fn at_line(mut self, line: usize) -> Self {
89 self.line = line;
90 self
91 }
92 pub fn die(message: impl Into<String>, line: usize) -> Self {
94 Self::new(ErrorKind::Die, message, line, "-e")
95 }
96
97 pub fn division_by_zero(message: impl Into<String>, line: usize) -> Self {
102 Self::new(ErrorKind::DivisionByZero, message, line, "-e")
103 }
104 pub fn die_with_value(value: StrykeValue, message: String, line: usize) -> Self {
106 let mut e = Self::new(ErrorKind::Die, message, line, "-e");
107 e.die_value = Some(value);
108 e
109 }
110}
111
112impl fmt::Display for StrykeError {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self.kind {
115 ErrorKind::Die => write!(f, "{}", self.message),
116 ErrorKind::Exit(_) => write!(f, ""),
117 ErrorKind::Syntax => {
122 if let Some(near) = &self.near {
123 write!(
124 f,
125 "{} at {} line {}, {}\nExecution of {} aborted due to compilation errors.",
126 self.message, self.file, self.line, near, self.file,
127 )
128 } else {
129 write!(
130 f,
131 "{} at {} line {}.\nExecution of {} aborted due to compilation errors.",
132 self.message, self.file, self.line, self.file,
133 )
134 }
135 }
136 _ => write!(f, "{} at {} line {}.", self.message, self.file, self.line),
140 }
141 }
142}
143
144impl std::error::Error for StrykeError {}
145pub type StrykeResult<T> = Result<T, StrykeError>;
147
148pub fn explain_error(code: &str) -> Option<&'static str> {
150 match code {
151 "E0001" => Some(
152 "Undefined subroutine: no `sub name` or builtin exists for this bare call. \
153Declare the sub, use the correct package (`Foo::bar`), or import via `use Module qw(name)`.",
154 ),
155 "E0002" => Some(
156 "Runtime error from `die`, a failed builtin, or an I/O/regex/sqlite failure. \
157Check the message above; use `try { } catch ($e) { }` to recover.",
158 ),
159 "E0003" => Some(
160 "pmap_reduce / preduce require an associative reduce op: order of pairwise combines is not fixed. \
161Do not use for non-associative operations.",
162 ),
163 _ => None,
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn syntax_error_display_includes_message_and_line() {
173 let e = StrykeError::syntax("bad token", 7);
174 let s = e.to_string();
175 assert!(s.contains("bad token"));
176 assert!(s.contains("line 7"));
177 }
178
179 #[test]
180 fn die_error_display_is_message_only() {
181 let e = StrykeError::die("halt", 1);
182 assert_eq!(e.to_string(), "halt");
183 }
184
185 #[test]
186 fn exit_error_display_is_empty() {
187 let e = StrykeError::new(ErrorKind::Exit(0), "ignored", 1, "-e");
188 assert_eq!(e.to_string(), "");
189 }
190
191 #[test]
192 fn runtime_error_display_includes_file_and_line() {
193 let e = StrykeError::runtime("boom", 3);
194 let s = e.to_string();
195 assert!(s.contains("boom"));
196 assert!(s.contains("-e"));
197 assert!(s.contains("line 3"));
198 }
199
200 #[test]
201 fn division_by_zero_kind_matches_message_display() {
202 let e = StrykeError::new(ErrorKind::DivisionByZero, "divide by zero", 2, "t.pl");
203 assert_eq!(e.kind, ErrorKind::DivisionByZero);
204 let s = e.to_string();
205 assert!(s.contains("divide by zero"));
206 assert!(s.contains("t.pl"));
207 assert!(s.contains("line 2"));
208 }
209
210 #[test]
211 fn type_error_display_matches_runtime_shape() {
212 let e = StrykeError::type_error("expected array", 9);
213 assert_eq!(e.kind, ErrorKind::Type);
214 let s = e.to_string();
215 assert!(s.contains("expected array"));
216 assert!(s.contains("line 9"));
217 }
218
219 #[test]
220 fn at_line_overrides_line_number() {
221 let e = StrykeError::runtime("x", 1).at_line(99);
222 assert_eq!(e.line, 99);
223 assert!(e.to_string().contains("line 99"));
224 }
225
226 #[test]
227 fn explain_error_known_codes() {
228 assert!(explain_error("E0001").is_some());
229 assert!(explain_error("E0002").is_some());
230 assert!(explain_error("E0003").is_some());
231 }
232
233 #[test]
234 fn explain_error_unknown_returns_none() {
235 assert!(explain_error("E9999").is_none());
236 assert!(explain_error("").is_none());
237 }
238
239 #[test]
240 fn perl_error_implements_std_error() {
241 let e: Box<dyn std::error::Error> = Box::new(StrykeError::syntax("x", 1));
242 assert!(!e.to_string().is_empty());
243 }
244}