Skip to main content

stryke/
error.rs

1use std::fmt;
2
3use crate::value::StrykeValue;
4/// `StrykeError` — see fields for layout.
5#[derive(Debug, Clone)]
6pub struct StrykeError {
7    /// `kind` field.
8    pub kind: ErrorKind,
9    /// `message` field.
10    pub message: String,
11    /// `line` field.
12    pub line: usize,
13    /// `file` field.
14    pub file: String,
15    /// When `die` is called with a ref argument, the original value is preserved here
16    /// so that `$@` can hold the ref (not just its stringification).
17    pub die_value: Option<StrykeValue>,
18    /// For Syntax errors, optional suffix shown after `at -e line N`. Perl
19    /// emits `, near "TOKEN"` or `, at EOF` depending on where the parse
20    /// failed; absent (`None`) yields the standard period-terminated form.
21    pub near: Option<String>,
22}
23/// `ErrorKind` — see variants.
24#[derive(Debug, Clone, PartialEq)]
25pub enum ErrorKind {
26    /// `Syntax` variant.
27    Syntax,
28    /// `Runtime` variant.
29    Runtime,
30    /// `Type` variant.
31    Type,
32    /// `UndefinedVariable` variant.
33    UndefinedVariable,
34    /// `UndefinedSubroutine` variant.
35    UndefinedSubroutine,
36    /// `FileNotFound` variant.
37    FileNotFound,
38    /// `IO` variant.
39    IO,
40    /// `Regex` variant.
41    Regex,
42    /// `DivisionByZero` variant.
43    DivisionByZero,
44    /// `Die` variant.
45    Die,
46    /// `Exit` variant.
47    Exit(i32),
48}
49
50impl StrykeError {
51    /// `new` — see implementation.
52    pub fn new(
53        kind: ErrorKind,
54        message: impl Into<String>,
55        line: usize,
56        file: impl Into<String>,
57    ) -> Self {
58        Self {
59            kind,
60            message: message.into(),
61            line,
62            file: file.into(),
63            die_value: None,
64            near: None,
65        }
66    }
67
68    /// Attach a `, near "..."` or `, at EOF` suffix used by Perl's
69    /// compile-error formatter. Only takes effect for `ErrorKind::Syntax`.
70    pub fn with_near(mut self, near: impl Into<String>) -> Self {
71        self.near = Some(near.into());
72        self
73    }
74    /// `syntax` — see implementation.
75    pub fn syntax(message: impl Into<String>, line: usize) -> Self {
76        Self::new(ErrorKind::Syntax, message, line, "-e")
77    }
78    /// `runtime` — see implementation.
79    pub fn runtime(message: impl Into<String>, line: usize) -> Self {
80        Self::new(ErrorKind::Runtime, message, line, "-e")
81    }
82    /// `type_error` — see implementation.
83    pub fn type_error(message: impl Into<String>, line: usize) -> Self {
84        Self::new(ErrorKind::Type, message, line, "-e")
85    }
86
87    /// Replace line number (e.g. map VM op line onto an error).
88    pub fn at_line(mut self, line: usize) -> Self {
89        self.line = line;
90        self
91    }
92    /// `die` — see implementation.
93    pub fn die(message: impl Into<String>, line: usize) -> Self {
94        Self::new(ErrorKind::Die, message, line, "-e")
95    }
96
97    /// Constructor for division-by-zero errors. The user-visible message
98    /// and Display formatting are unchanged from `runtime(...)`; only the
99    /// `kind` differs, so callers (try/catch, error filters) can match
100    /// `ErrorKind::DivisionByZero` specifically.
101    pub fn division_by_zero(message: impl Into<String>, line: usize) -> Self {
102        Self::new(ErrorKind::DivisionByZero, message, line, "-e")
103    }
104    /// `die_with_value` — see implementation.
105    pub fn die_with_value(value: StrykeValue, message: String, line: usize) -> Self {
106        let mut e = Self::new(ErrorKind::Die, message, line, "-e");
107        e.die_value = Some(value);
108        e
109    }
110}
111
112impl fmt::Display for StrykeError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self.kind {
115            ErrorKind::Die => write!(f, "{}", self.message),
116            ErrorKind::Exit(_) => write!(f, ""),
117            // Compile-time syntax errors get the `, near "..."` / `, at EOF`
118            // suffix (when set) plus the trailing
119            // `Execution of <file> aborted due to compilation errors.` line
120            // — matches stock perl for `stryke --compat` parity.
121            ErrorKind::Syntax => {
122                if let Some(near) = &self.near {
123                    write!(
124                        f,
125                        "{} at {} line {}, {}\nExecution of {} aborted due to compilation errors.",
126                        self.message, self.file, self.line, near, self.file,
127                    )
128                } else {
129                    write!(
130                        f,
131                        "{} at {} line {}.\nExecution of {} aborted due to compilation errors.",
132                        self.message, self.file, self.line, self.file,
133                    )
134                }
135            }
136            // Perl 5 ends runtime errors with `.` after the line number
137            // (`Illegal division by zero at -e line 1.`). Matches stock
138            // perl for `stryke --compat` parity — see `tests/suite/error_parity.rs`.
139            _ => write!(f, "{} at {} line {}.", self.message, self.file, self.line),
140        }
141    }
142}
143
144impl std::error::Error for StrykeError {}
145/// `StrykeResult` type alias.
146pub type StrykeResult<T> = Result<T, StrykeError>;
147
148/// Long-form hints for `stryke --explain CODE` (rustc-style).
149pub fn explain_error(code: &str) -> Option<&'static str> {
150    match code {
151        "E0001" => Some(
152            "Undefined subroutine: no `sub name` or builtin exists for this bare call. \
153Declare the sub, use the correct package (`Foo::bar`), or import via `use Module qw(name)`.",
154        ),
155        "E0002" => Some(
156            "Runtime error from `die`, a failed builtin, or an I/O/regex/sqlite failure. \
157Check the message above; use `try { } catch ($e) { }` to recover.",
158        ),
159        "E0003" => Some(
160            "pmap_reduce / preduce require an associative reduce op: order of pairwise combines is not fixed. \
161Do not use for non-associative operations.",
162        ),
163        _ => None,
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn syntax_error_display_includes_message_and_line() {
173        let e = StrykeError::syntax("bad token", 7);
174        let s = e.to_string();
175        assert!(s.contains("bad token"));
176        assert!(s.contains("line 7"));
177    }
178
179    #[test]
180    fn die_error_display_is_message_only() {
181        let e = StrykeError::die("halt", 1);
182        assert_eq!(e.to_string(), "halt");
183    }
184
185    #[test]
186    fn exit_error_display_is_empty() {
187        let e = StrykeError::new(ErrorKind::Exit(0), "ignored", 1, "-e");
188        assert_eq!(e.to_string(), "");
189    }
190
191    #[test]
192    fn runtime_error_display_includes_file_and_line() {
193        let e = StrykeError::runtime("boom", 3);
194        let s = e.to_string();
195        assert!(s.contains("boom"));
196        assert!(s.contains("-e"));
197        assert!(s.contains("line 3"));
198    }
199
200    #[test]
201    fn division_by_zero_kind_matches_message_display() {
202        let e = StrykeError::new(ErrorKind::DivisionByZero, "divide by zero", 2, "t.pl");
203        assert_eq!(e.kind, ErrorKind::DivisionByZero);
204        let s = e.to_string();
205        assert!(s.contains("divide by zero"));
206        assert!(s.contains("t.pl"));
207        assert!(s.contains("line 2"));
208    }
209
210    #[test]
211    fn type_error_display_matches_runtime_shape() {
212        let e = StrykeError::type_error("expected array", 9);
213        assert_eq!(e.kind, ErrorKind::Type);
214        let s = e.to_string();
215        assert!(s.contains("expected array"));
216        assert!(s.contains("line 9"));
217    }
218
219    #[test]
220    fn at_line_overrides_line_number() {
221        let e = StrykeError::runtime("x", 1).at_line(99);
222        assert_eq!(e.line, 99);
223        assert!(e.to_string().contains("line 99"));
224    }
225
226    #[test]
227    fn explain_error_known_codes() {
228        assert!(explain_error("E0001").is_some());
229        assert!(explain_error("E0002").is_some());
230        assert!(explain_error("E0003").is_some());
231    }
232
233    #[test]
234    fn explain_error_unknown_returns_none() {
235        assert!(explain_error("E9999").is_none());
236        assert!(explain_error("").is_none());
237    }
238
239    #[test]
240    fn perl_error_implements_std_error() {
241        let e: Box<dyn std::error::Error> = Box::new(StrykeError::syntax("x", 1));
242        assert!(!e.to_string().is_empty());
243    }
244}