1use thiserror::Error;
4use varpulis_core::Span;
5
6#[derive(Debug, Clone)]
8pub struct SourceLocation {
9 pub line: usize,
11 pub column: usize,
13 pub position: usize,
15}
16
17impl SourceLocation {
18 pub fn from_position(source: &str, position: usize) -> Self {
20 let mut line = 1;
21 let mut column = 1;
22
23 for (i, ch) in source.chars().enumerate() {
24 if i >= position {
25 break;
26 }
27 if ch == '\n' {
28 line += 1;
29 column = 1;
30 } else {
31 column += 1;
32 }
33 }
34
35 Self {
36 line,
37 column,
38 position,
39 }
40 }
41}
42
43#[derive(Debug, Error, Clone)]
45pub enum ParseError {
46 #[error("Line {line}, column {column}: {message}")]
48 Located {
49 line: usize,
51 column: usize,
53 position: usize,
55 message: String,
57 hint: Option<String>,
59 },
60
61 #[error("Unexpected token at position {position}: expected {expected}, found {found}")]
63 UnexpectedToken {
64 position: usize,
66 expected: String,
68 found: String,
70 },
71
72 #[error("Unexpected end of input")]
74 UnexpectedEof,
75
76 #[error("Invalid token at position {position}: {message}")]
78 InvalidToken {
79 position: usize,
81 message: String,
83 },
84
85 #[error("Invalid number literal: {0}")]
87 InvalidNumber(String),
88
89 #[error("Invalid duration literal: {0}")]
91 InvalidDuration(String),
92
93 #[error("Invalid timestamp literal: {0}")]
95 InvalidTimestamp(String),
96
97 #[error("Unterminated string starting at position {0}")]
99 UnterminatedString(
100 usize,
102 ),
103
104 #[error("Invalid escape sequence: {0}")]
106 InvalidEscape(String),
107
108 #[error("{message}")]
110 Custom {
111 span: Span,
113 message: String,
115 },
116}
117
118impl ParseError {
119 pub fn custom(span: Span, message: impl Into<String>) -> Self {
121 Self::Custom {
122 span,
123 message: message.into(),
124 }
125 }
126
127 pub fn at_location(
129 source: &str,
130 position: usize,
131 message: impl Into<String>,
132 hint: Option<String>,
133 ) -> Self {
134 let loc = SourceLocation::from_position(source, position);
135 Self::Located {
136 line: loc.line,
137 column: loc.column,
138 position,
139 message: message.into(),
140 hint,
141 }
142 }
143}
144
145pub fn suggest_fix(token: &str) -> Option<String> {
147 match token.to_lowercase().as_str() {
148 "string" => Some("Did you mean 'str'? VPL uses 'str' for string types.".to_string()),
149 "integer" => Some("Did you mean 'int'? VPL uses 'int' for integer types.".to_string()),
150 "boolean" => Some("Did you mean 'bool'? VPL uses 'bool' for boolean types.".to_string()),
151 "&&" => Some("Use 'and' instead of '&&' for logical AND.".to_string()),
152 "||" => Some("Use 'or' instead of '||' for logical OR.".to_string()),
153 "!" => Some("Use 'not' instead of '!' for logical NOT.".to_string()),
154 "function" | "func" | "def" => Some("Use 'fn' to declare functions.".to_string()),
155 "class" | "struct" => Some("Use 'event' to declare event types.".to_string()),
156 _ => None,
157 }
158}
159
160pub type ParseResult<T> = Result<T, ParseError>;