Skip to main content

varpulis_parser/
error.rs

1//! Parser error types
2
3use thiserror::Error;
4use varpulis_core::Span;
5
6/// Location in source code with line and column
7#[derive(Debug, Clone)]
8pub struct SourceLocation {
9    pub line: usize,
10    pub column: usize,
11    pub position: usize,
12}
13
14impl SourceLocation {
15    /// Convert a byte position to line/column using the source text
16    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    /// Create an error with source location and optional hint
92    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
109/// Suggestions for common mistakes
110pub 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>;