Skip to main content

ion_core/
error.rs

1use std::fmt;
2
3#[derive(Debug, Clone)]
4pub struct IonError {
5    pub kind: ErrorKind,
6    pub message: String,
7    pub line: usize,
8    pub col: usize,
9    /// Additional errors (for multi-error reporting from parser).
10    pub additional: Vec<IonError>,
11}
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum ErrorKind {
15    LexError,
16    ParseError,
17    RuntimeError,
18    TypeError,
19    NameError,
20    PropagatedErr,
21    PropagatedNone,
22}
23
24impl IonError {
25    /// Format error with source context showing the offending line.
26    pub fn format_with_source(&self, source: &str) -> String {
27        let mut out = Self::format_single(self, source);
28        for extra in &self.additional {
29            out.push('\n');
30            out.push_str(&Self::format_single(extra, source));
31        }
32        out
33    }
34
35    fn format_single(err: &IonError, source: &str) -> String {
36        let mut out = String::new();
37        // Header
38        out.push_str(&format!(
39            "\x1b[1;31merror[{}]\x1b[0m: {}\n",
40            err.kind_str(),
41            err.message
42        ));
43        // Source context
44        if err.line > 0 {
45            let lines: Vec<&str> = source.lines().collect();
46            if err.line <= lines.len() {
47                let line_str = lines[err.line - 1];
48                let line_num = format!("{}", err.line);
49                let padding = " ".repeat(line_num.len());
50                out.push_str(&format!(" {} \x1b[34m|\x1b[0m\n", padding));
51                out.push_str(&format!(" \x1b[34m{} |\x1b[0m {}\n", line_num, line_str));
52                out.push_str(&format!(" {} \x1b[34m|\x1b[0m ", padding));
53                if err.col > 0 && err.col <= line_str.len() + 1 {
54                    out.push_str(&" ".repeat(err.col - 1));
55                    out.push_str("\x1b[1;31m^\x1b[0m");
56                }
57                out.push('\n');
58            }
59        }
60        // Suggestion hint
61        if let Some(hint) = Self::suggest_hint(&err.kind, &err.message) {
62            out.push_str(&format!(" \x1b[1;36mhelp\x1b[0m: {}\n", hint));
63        }
64        out
65    }
66
67    fn suggest_hint(kind: &ErrorKind, msg: &str) -> Option<&'static str> {
68        match kind {
69            ErrorKind::NameError => {
70                if msg.contains(&*ion_str!("undefined variable")) {
71                    Some(ion_static_str!("check spelling, or ensure the variable is declared with `let` before use"))
72                } else {
73                    None
74                }
75            }
76            ErrorKind::TypeError => {
77                if msg.contains(&*ion_str!("cannot assign to immutable")) {
78                    Some(ion_static_str!("declare with `let mut` to allow reassignment"))
79                } else if msg.contains(&*ion_str!("cannot add"))
80                    || msg.contains(&*ion_str!("cannot subtract"))
81                {
82                    Some(ion_static_str!("Ion has no implicit type coercions \u{2014} convert explicitly with `int()`, `float()`, or `str()`"))
83                } else if msg.contains(&*ion_str!("no method")) {
84                    Some(ion_static_str!("use `.to_string()` to inspect the value's type, or check LANGUAGE.md for available methods"))
85                } else {
86                    None
87                }
88            }
89            ErrorKind::ParseError => {
90                if msg.contains(&*ion_str!("expected ';'")) {
91                    Some(ion_static_str!("Ion requires semicolons after statements"))
92                } else if msg.contains(&*ion_str!("expected '}'")) {
93                    Some(ion_static_str!("check for unmatched `{` braces"))
94                } else {
95                    None
96                }
97            }
98            ErrorKind::RuntimeError => {
99                if msg.contains(&*ion_str!("division by zero")) {
100                    Some(ion_static_str!("check the divisor before dividing, or use a try/catch block"))
101                } else if msg.contains(&*ion_str!("stack overflow")) {
102                    Some(ion_static_str!("check for infinite recursion, or increase the stack depth limit"))
103                } else if msg.contains(&*ion_str!("index out of bounds")) {
104                    Some(ion_static_str!("use `.len()` to check the collection size, or `.get()` for safe access"))
105                } else {
106                    None
107                }
108            }
109            _ => None,
110        }
111    }
112
113    fn kind_str(&self) -> &str {
114        match &self.kind {
115            ErrorKind::LexError => ion_static_str!("lex"),
116            ErrorKind::ParseError => ion_static_str!("parse"),
117            ErrorKind::RuntimeError => ion_static_str!("runtime"),
118            ErrorKind::TypeError => ion_static_str!("type"),
119            ErrorKind::NameError => ion_static_str!("name"),
120            ErrorKind::PropagatedErr => ion_static_str!("propagated_err"),
121            ErrorKind::PropagatedNone => ion_static_str!("propagated_none"),
122        }
123    }
124
125    pub fn lex(message: impl Into<String>, line: usize, col: usize) -> Self {
126        Self {
127            kind: ErrorKind::LexError,
128            message: message.into(),
129            line,
130            col,
131            additional: Vec::new(),
132        }
133    }
134
135    pub fn parse(message: impl Into<String>, line: usize, col: usize) -> Self {
136        Self {
137            kind: ErrorKind::ParseError,
138            message: message.into(),
139            line,
140            col,
141            additional: Vec::new(),
142        }
143    }
144
145    pub fn runtime(message: impl Into<String>, line: usize, col: usize) -> Self {
146        Self {
147            kind: ErrorKind::RuntimeError,
148            message: message.into(),
149            line,
150            col,
151            additional: Vec::new(),
152        }
153    }
154
155    pub fn type_err(message: impl Into<String>, line: usize, col: usize) -> Self {
156        Self {
157            kind: ErrorKind::TypeError,
158            message: message.into(),
159            line,
160            col,
161            additional: Vec::new(),
162        }
163    }
164
165    pub fn name(message: impl Into<String>, line: usize, col: usize) -> Self {
166        Self {
167            kind: ErrorKind::NameError,
168            message: message.into(),
169            line,
170            col,
171            additional: Vec::new(),
172        }
173    }
174
175    pub fn propagated_err(message: impl Into<String>, line: usize, col: usize) -> Self {
176        Self {
177            kind: ErrorKind::PropagatedErr,
178            message: message.into(),
179            line,
180            col,
181            additional: Vec::new(),
182        }
183    }
184
185    pub fn propagated_none(line: usize, col: usize) -> Self {
186        Self {
187            kind: ErrorKind::PropagatedNone,
188            message: String::new(),
189            line,
190            col,
191            additional: Vec::new(),
192        }
193    }
194}
195
196impl fmt::Display for IonError {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        let kind = match &self.kind {
199            ErrorKind::LexError => ion_str!("LexError"),
200            ErrorKind::ParseError => ion_str!("ParseError"),
201            ErrorKind::RuntimeError => ion_str!("RuntimeError"),
202            ErrorKind::TypeError => ion_str!("TypeError"),
203            ErrorKind::NameError => ion_str!("NameError"),
204            ErrorKind::PropagatedErr => ion_str!("PropagatedErr"),
205            ErrorKind::PropagatedNone => ion_str!("PropagatedNone"),
206        };
207        write!(
208            f,
209            "{} at {}:{}: {}",
210            kind, self.line, self.col, self.message
211        )
212    }
213}
214
215impl std::error::Error for IonError {}