1use thiserror::Error;
4use varpulis_core::Span;
5
6#[derive(Debug, Clone)]
8pub struct SourceLocation {
9 pub line: usize,
10 pub column: usize,
11 pub position: usize,
12}
13
14impl SourceLocation {
15 pub fn from_position(source: &str, position: usize) -> Self {
17 let mut line = 1;
18 let mut column = 1;
19
20 for (i, ch) in source.chars().enumerate() {
21 if i >= position {
22 break;
23 }
24 if ch == '\n' {
25 line += 1;
26 column = 1;
27 } else {
28 column += 1;
29 }
30 }
31
32 SourceLocation {
33 line,
34 column,
35 position,
36 }
37 }
38}
39
40#[derive(Debug, Error, Clone)]
41pub enum ParseError {
42 #[error("Line {line}, column {column}: {message}")]
43 Located {
44 line: usize,
45 column: usize,
46 position: usize,
47 message: String,
48 hint: Option<String>,
49 },
50
51 #[error("Unexpected token at position {position}: expected {expected}, found {found}")]
52 UnexpectedToken {
53 position: usize,
54 expected: String,
55 found: String,
56 },
57
58 #[error("Unexpected end of input")]
59 UnexpectedEof,
60
61 #[error("Invalid token at position {position}: {message}")]
62 InvalidToken { position: usize, message: String },
63
64 #[error("Invalid number literal: {0}")]
65 InvalidNumber(String),
66
67 #[error("Invalid duration literal: {0}")]
68 InvalidDuration(String),
69
70 #[error("Invalid timestamp literal: {0}")]
71 InvalidTimestamp(String),
72
73 #[error("Unterminated string starting at position {0}")]
74 UnterminatedString(usize),
75
76 #[error("Invalid escape sequence: {0}")]
77 InvalidEscape(String),
78
79 #[error("{message}")]
80 Custom { span: Span, message: String },
81}
82
83impl ParseError {
84 pub fn custom(span: Span, message: impl Into<String>) -> Self {
85 ParseError::Custom {
86 span,
87 message: message.into(),
88 }
89 }
90
91 pub fn at_location(
93 source: &str,
94 position: usize,
95 message: impl Into<String>,
96 hint: Option<String>,
97 ) -> Self {
98 let loc = SourceLocation::from_position(source, position);
99 ParseError::Located {
100 line: loc.line,
101 column: loc.column,
102 position,
103 message: message.into(),
104 hint,
105 }
106 }
107}
108
109pub fn suggest_fix(token: &str) -> Option<String> {
111 match token.to_lowercase().as_str() {
112 "string" => Some("Did you mean 'str'? VPL uses 'str' for string types.".to_string()),
113 "integer" => Some("Did you mean 'int'? VPL uses 'int' for integer types.".to_string()),
114 "boolean" => Some("Did you mean 'bool'? VPL uses 'bool' for boolean types.".to_string()),
115 "&&" => Some("Use 'and' instead of '&&' for logical AND.".to_string()),
116 "||" => Some("Use 'or' instead of '||' for logical OR.".to_string()),
117 "!" => Some("Use 'not' instead of '!' for logical NOT.".to_string()),
118 "function" | "func" | "def" => Some("Use 'fn' to declare functions.".to_string()),
119 "class" | "struct" => Some("Use 'event' to declare event types.".to_string()),
120 _ => None,
121 }
122}
123
124pub type ParseResult<T> = Result<T, ParseError>;