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