1use miette::{Diagnostic, NamedSource, SourceSpan};
4use thiserror::Error as ThisError;
5use varpulis_core::Span;
6
7#[derive(Debug, Clone)]
9pub struct SourceLocation {
10 pub line: usize,
12 pub column: usize,
14 pub position: usize,
16}
17
18impl SourceLocation {
19 pub fn from_position(source: &str, position: usize) -> Self {
21 let mut line = 1;
22 let mut column = 1;
23
24 for (i, ch) in source.chars().enumerate() {
25 if i >= position {
26 break;
27 }
28 if ch == '\n' {
29 line += 1;
30 column = 1;
31 } else {
32 column += 1;
33 }
34 }
35
36 Self {
37 line,
38 column,
39 position,
40 }
41 }
42}
43
44#[derive(Debug, ThisError, Clone)]
46pub enum ParseError {
47 #[error("Line {line}, column {column}: {message}")]
49 Located {
50 line: usize,
52 column: usize,
54 position: usize,
56 message: String,
58 hint: Option<String>,
60 },
61
62 #[error("Unexpected token at position {position}: expected {expected}, found {found}")]
64 UnexpectedToken {
65 position: usize,
67 expected: String,
69 found: String,
71 },
72
73 #[error("Unexpected end of input")]
75 UnexpectedEof,
76
77 #[error("Invalid token at position {position}: {message}")]
79 InvalidToken {
80 position: usize,
82 message: String,
84 },
85
86 #[error("Invalid number literal: {0}")]
88 InvalidNumber(String),
89
90 #[error("Invalid duration literal: {0}")]
92 InvalidDuration(String),
93
94 #[error("Invalid timestamp literal: {0}")]
96 InvalidTimestamp(String),
97
98 #[error("Unterminated string starting at position {0}")]
100 UnterminatedString(
101 usize,
103 ),
104
105 #[error("Invalid escape sequence: {0}")]
107 InvalidEscape(String),
108
109 #[error("{message}")]
111 Custom {
112 span: Span,
114 message: String,
116 },
117}
118
119impl ParseError {
120 pub fn custom(span: Span, message: impl Into<String>) -> Self {
122 Self::Custom {
123 span,
124 message: message.into(),
125 }
126 }
127
128 pub fn at_location(
130 source: &str,
131 position: usize,
132 message: impl Into<String>,
133 hint: Option<String>,
134 ) -> Self {
135 let loc = SourceLocation::from_position(source, position);
136 Self::Located {
137 line: loc.line,
138 column: loc.column,
139 position,
140 message: message.into(),
141 hint,
142 }
143 }
144
145 pub fn source_span(&self, source: &str) -> (usize, usize) {
150 match self {
151 Self::Located { position, .. }
152 | Self::UnexpectedToken { position, .. }
153 | Self::InvalidToken { position, .. }
154 | Self::UnterminatedString(position) => {
155 let pos = (*position).min(source.len());
156 let rest = &source[pos..];
158 let len = rest.find(['\n', '\r']).unwrap_or(rest.len()).max(1);
159 (pos, len)
160 }
161 Self::Custom { span, .. } => {
162 let start = span.start.min(source.len());
163 let end = span.end.min(source.len());
164 (start, (end - start).max(1))
165 }
166 Self::UnexpectedEof
168 | Self::InvalidNumber(_)
169 | Self::InvalidDuration(_)
170 | Self::InvalidTimestamp(_)
171 | Self::InvalidEscape(_) => (source.len().saturating_sub(1), 1),
172 }
173 }
174
175 pub fn hint(&self) -> Option<&str> {
177 match self {
178 Self::Located { hint, .. } => hint.as_deref(),
179 _ => None,
180 }
181 }
182}
183
184#[derive(Debug)]
191pub struct RichParseError {
192 inner: ParseError,
193 src: NamedSource<String>,
194 span: SourceSpan,
195 help: Option<String>,
196}
197
198impl std::fmt::Display for RichParseError {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 write!(f, "{}", self.inner)
201 }
202}
203
204impl std::error::Error for RichParseError {}
205
206impl Diagnostic for RichParseError {
207 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
208 self.help
209 .as_ref()
210 .map(|h| Box::new(h.clone()) as Box<dyn std::fmt::Display>)
211 }
212
213 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
214 Some(&self.src)
215 }
216
217 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
218 Some(Box::new(std::iter::once(
219 miette::LabeledSpan::new_primary_with_span(Some("here".to_string()), self.span),
220 )))
221 }
222}
223
224impl RichParseError {
225 pub fn new(error: ParseError, source: &str, filename: &str) -> Self {
228 let (offset, len) = error.source_span(source);
229 let help = error.hint().map(String::from);
230 Self {
231 src: NamedSource::new(filename, source.to_string()),
232 span: SourceSpan::new(offset.into(), len),
233 help,
234 inner: error,
235 }
236 }
237}
238
239pub fn suggest_fix(token: &str) -> Option<String> {
241 match token.to_lowercase().as_str() {
242 "string" => Some("Did you mean 'str'? VPL uses 'str' for string types.".to_string()),
243 "integer" => Some("Did you mean 'int'? VPL uses 'int' for integer types.".to_string()),
244 "boolean" => Some("Did you mean 'bool'? VPL uses 'bool' for boolean types.".to_string()),
245 "&&" => Some("Use 'and' instead of '&&' for logical AND.".to_string()),
246 "||" => Some("Use 'or' instead of '||' for logical OR.".to_string()),
247 "!" => Some("Use 'not' instead of '!' for logical NOT.".to_string()),
248 "function" | "func" | "def" => Some("Use 'fn' to declare functions.".to_string()),
249 "class" | "struct" => Some("Use 'event' to declare event types.".to_string()),
250 _ => None,
251 }
252}
253
254pub type ParseResult<T> = Result<T, ParseError>;