Skip to main content

stryke/
error.rs

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    /// When `die` is called with a ref argument, the original value is preserved here
12    /// so that `$@` can hold the ref (not just its stringification).
13    pub die_value: Option<PerlValue>,
14    /// For Syntax errors, optional suffix shown after `at -e line N`. Perl
15    /// emits `, near "TOKEN"` or `, at EOF` depending on where the parse
16    /// failed; absent (`None`) yields the standard period-terminated form.
17    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    /// Attach a `, near "..."` or `, at EOF` suffix used by Perl's
53    /// compile-error formatter. Only takes effect for `ErrorKind::Syntax`.
54    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    /// Replace line number (e.g. map VM op line onto an error).
72    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    /// Constructor for division-by-zero errors. The user-visible message
82    /// and Display formatting are unchanged from `runtime(...)`; only the
83    /// `kind` differs, so callers (try/catch, error filters) can match
84    /// `ErrorKind::DivisionByZero` specifically.
85    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            // Compile-time syntax errors get the `, near "..."` / `, at EOF`
102            // suffix (when set) plus the trailing
103            // `Execution of <file> aborted due to compilation errors.` line
104            // — matches stock perl for `stryke --compat` parity.
105            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            // Perl 5 ends runtime errors with `.` after the line number
121            // (`Illegal division by zero at -e line 1.`). Matches stock
122            // perl for `stryke --compat` parity — see `tests/suite/error_parity.rs`.
123            _ => 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
132/// Long-form hints for `stryke --explain CODE` (rustc-style).
133pub 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}