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}
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    /// Replace line number (e.g. map VM op line onto an error).
60    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    /// Constructor for division-by-zero errors. The user-visible message
70    /// and Display formatting are unchanged from `runtime(...)`; only the
71    /// `kind` differs, so callers (try/catch, error filters) can match
72    /// `ErrorKind::DivisionByZero` specifically.
73    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            // Perl 5 ends runtime errors with `.` after the line number
90            // (`Illegal division by zero at -e line 1.`). Matches stock
91            // perl for `stryke --compat` parity — see `tests/suite/error_parity.rs`.
92            _ => 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
101/// Long-form hints for `stryke --explain CODE` (rustc-style).
102pub 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}