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