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    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            // Perl 5 ends runtime errors with `.` after the line number
82            // (`Illegal division by zero at -e line 1.`). Matches stock
83            // perl for `stryke --compat` parity — see `tests/suite/error_parity.rs`.
84            _ => 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
93/// Long-form hints for `stryke --explain CODE` (rustc-style).
94pub 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}