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    /// 1-based line number.
10    pub line: usize,
11    /// 1-based column number.
12    pub column: usize,
13    /// 0-based byte offset in the source string.
14    pub position: usize,
15}
16
17impl SourceLocation {
18    /// Convert a byte position to line/column using the source text
19    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/// Errors that can occur during VPL parsing.
44#[derive(Debug, Error, Clone)]
45pub enum ParseError {
46    /// Error with precise source location and an optional hint for the user.
47    #[error("Line {line}, column {column}: {message}")]
48    Located {
49        /// 1-based line number where the error occurred.
50        line: usize,
51        /// 1-based column number where the error occurred.
52        column: usize,
53        /// 0-based byte offset in the source string.
54        position: usize,
55        /// Human-readable description of the error.
56        message: String,
57        /// Optional suggestion for how to fix the error.
58        hint: Option<String>,
59    },
60
61    /// An unexpected token was encountered during parsing.
62    #[error("Unexpected token at position {position}: expected {expected}, found {found}")]
63    UnexpectedToken {
64        /// 0-based byte offset of the unexpected token.
65        position: usize,
66        /// Description of what was expected.
67        expected: String,
68        /// Description of what was found instead.
69        found: String,
70    },
71
72    /// The input ended unexpectedly (e.g., unclosed parenthesis).
73    #[error("Unexpected end of input")]
74    UnexpectedEof,
75
76    /// A token that does not belong to the VPL grammar was found.
77    #[error("Invalid token at position {position}: {message}")]
78    InvalidToken {
79        /// 0-based byte offset of the invalid token.
80        position: usize,
81        /// Description of why the token is invalid.
82        message: String,
83    },
84
85    /// A numeric literal could not be parsed.
86    #[error("Invalid number literal: {0}")]
87    InvalidNumber(String),
88
89    /// A duration literal (e.g., `5s`, `100ms`) could not be parsed.
90    #[error("Invalid duration literal: {0}")]
91    InvalidDuration(String),
92
93    /// A timestamp literal (e.g., `@2024-01-15`) could not be parsed.
94    #[error("Invalid timestamp literal: {0}")]
95    InvalidTimestamp(String),
96
97    /// A string literal was not closed before end of input.
98    #[error("Unterminated string starting at position {0}")]
99    UnterminatedString(
100        /// 0-based byte offset where the string started.
101        usize,
102    ),
103
104    /// An unrecognized escape sequence was found inside a string literal.
105    #[error("Invalid escape sequence: {0}")]
106    InvalidEscape(String),
107
108    /// A custom error with an associated source span.
109    #[error("{message}")]
110    Custom {
111        /// Source span where the error occurred.
112        span: Span,
113        /// Human-readable error message.
114        message: String,
115    },
116}
117
118impl ParseError {
119    /// Create a custom error from a source span and message.
120    pub fn custom(span: Span, message: impl Into<String>) -> Self {
121        Self::Custom {
122            span,
123            message: message.into(),
124        }
125    }
126
127    /// Create an error with source location and optional hint
128    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
145/// Suggestions for common mistakes
146pub 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
160/// Convenience alias for `Result<T, ParseError>`.
161pub type ParseResult<T> = Result<T, ParseError>;