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 division_by_zero(message: impl Into<String>, line: usize) -> Self {
74 Self::new(ErrorKind::DivisionByZero, message, line, "-e")
75 }
76
77 pub fn die_with_value(value: PerlValue, message: String, line: usize) -> Self {
78 let mut e = Self::new(ErrorKind::Die, message, line, "-e");
79 e.die_value = Some(value);
80 e
81 }
82}
83
84impl fmt::Display for PerlError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self.kind {
87 ErrorKind::Die => write!(f, "{}", self.message),
88 ErrorKind::Exit(_) => write!(f, ""),
89 _ => write!(f, "{} at {} line {}.", self.message, self.file, self.line),
93 }
94 }
95}
96
97impl std::error::Error for PerlError {}
98
99pub type PerlResult<T> = Result<T, PerlError>;
100
101pub fn explain_error(code: &str) -> Option<&'static str> {
103 match code {
104 "E0001" => Some(
105 "Undefined subroutine: no `sub name` or builtin exists for this bare call. \
106Declare the sub, use the correct package (`Foo::bar`), or import via `use Module qw(name)`.",
107 ),
108 "E0002" => Some(
109 "Runtime error from `die`, a failed builtin, or an I/O/regex/sqlite failure. \
110Check the message above; use `try { } catch ($e) { }` to recover.",
111 ),
112 "E0003" => Some(
113 "pmap_reduce / preduce require an associative reduce op: order of pairwise combines is not fixed. \
114Do not use for non-associative operations.",
115 ),
116 _ => None,
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn syntax_error_display_includes_message_and_line() {
126 let e = PerlError::syntax("bad token", 7);
127 let s = e.to_string();
128 assert!(s.contains("bad token"));
129 assert!(s.contains("line 7"));
130 }
131
132 #[test]
133 fn die_error_display_is_message_only() {
134 let e = PerlError::die("halt", 1);
135 assert_eq!(e.to_string(), "halt");
136 }
137
138 #[test]
139 fn exit_error_display_is_empty() {
140 let e = PerlError::new(ErrorKind::Exit(0), "ignored", 1, "-e");
141 assert_eq!(e.to_string(), "");
142 }
143
144 #[test]
145 fn runtime_error_display_includes_file_and_line() {
146 let e = PerlError::runtime("boom", 3);
147 let s = e.to_string();
148 assert!(s.contains("boom"));
149 assert!(s.contains("-e"));
150 assert!(s.contains("line 3"));
151 }
152
153 #[test]
154 fn division_by_zero_kind_matches_message_display() {
155 let e = PerlError::new(ErrorKind::DivisionByZero, "divide by zero", 2, "t.pl");
156 assert_eq!(e.kind, ErrorKind::DivisionByZero);
157 let s = e.to_string();
158 assert!(s.contains("divide by zero"));
159 assert!(s.contains("t.pl"));
160 assert!(s.contains("line 2"));
161 }
162
163 #[test]
164 fn type_error_display_matches_runtime_shape() {
165 let e = PerlError::type_error("expected array", 9);
166 assert_eq!(e.kind, ErrorKind::Type);
167 let s = e.to_string();
168 assert!(s.contains("expected array"));
169 assert!(s.contains("line 9"));
170 }
171
172 #[test]
173 fn at_line_overrides_line_number() {
174 let e = PerlError::runtime("x", 1).at_line(99);
175 assert_eq!(e.line, 99);
176 assert!(e.to_string().contains("line 99"));
177 }
178
179 #[test]
180 fn explain_error_known_codes() {
181 assert!(explain_error("E0001").is_some());
182 assert!(explain_error("E0002").is_some());
183 assert!(explain_error("E0003").is_some());
184 }
185
186 #[test]
187 fn explain_error_unknown_returns_none() {
188 assert!(explain_error("E9999").is_none());
189 assert!(explain_error("").is_none());
190 }
191
192 #[test]
193 fn perl_error_implements_std_error() {
194 let e: Box<dyn std::error::Error> = Box::new(PerlError::syntax("x", 1));
195 assert!(!e.to_string().is_empty());
196 }
197}