Skip to main content

rigsql_parser/
context.rs

1use rigsql_core::{Token, TokenKind};
2
3/// A parse error recorded during error recovery.
4#[derive(Debug, Clone)]
5pub struct ParseDiagnostic {
6    /// Byte offset in the source where the error was detected.
7    pub offset: u32,
8    /// Human-readable description of what went wrong.
9    pub message: String,
10}
11
12/// Parser context: a cursor over the token stream.
13pub struct ParseContext<'a> {
14    tokens: &'a [Token],
15    pos: usize,
16    source: &'a str,
17    /// Diagnostics collected during error-recovery passes.
18    diagnostics: Vec<ParseDiagnostic>,
19}
20
21impl<'a> ParseContext<'a> {
22    pub fn new(tokens: &'a [Token], source: &'a str) -> Self {
23        Self {
24            tokens,
25            pos: 0,
26            source,
27            diagnostics: Vec::new(),
28        }
29    }
30
31    pub fn source(&self) -> &'a str {
32        self.source
33    }
34
35    /// Record a parse error at the current position.
36    pub fn record_error(&mut self, message: &str) {
37        let offset = self
38            .peek()
39            .map(|t| t.span.start)
40            .unwrap_or_else(|| self.source.len() as u32);
41        self.diagnostics.push(ParseDiagnostic {
42            offset,
43            message: message.to_string(),
44        });
45    }
46
47    /// Record a parse error at a specific byte offset.
48    pub fn record_error_at(&mut self, offset: u32, message: &str) {
49        self.diagnostics.push(ParseDiagnostic {
50            offset,
51            message: message.to_string(),
52        });
53    }
54
55    /// Take collected diagnostics, leaving the internal list empty.
56    pub fn take_diagnostics(&mut self) -> Vec<ParseDiagnostic> {
57        std::mem::take(&mut self.diagnostics)
58    }
59
60    /// Current position in the token stream.
61    pub fn pos(&self) -> usize {
62        self.pos
63    }
64
65    /// Save cursor position for backtracking.
66    pub fn save(&self) -> usize {
67        self.pos
68    }
69
70    /// Restore cursor to a saved position.
71    pub fn restore(&mut self, pos: usize) {
72        self.pos = pos;
73    }
74
75    /// Peek at the current token without consuming.
76    pub fn peek(&self) -> Option<&'a Token> {
77        self.tokens.get(self.pos)
78    }
79
80    /// Peek at the current non-trivia token kind.
81    pub fn peek_kind(&self) -> Option<TokenKind> {
82        self.peek().map(|t| t.kind)
83    }
84
85    /// Peek at the next non-trivia token (skipping whitespace/comments).
86    pub fn peek_non_trivia(&self) -> Option<&'a Token> {
87        let mut i = self.pos;
88        while i < self.tokens.len() {
89            if !self.tokens[i].kind.is_trivia() {
90                return Some(&self.tokens[i]);
91            }
92            i += 1;
93        }
94        None
95    }
96
97    /// Check if the next non-trivia token is a keyword matching `kw` (case-insensitive).
98    pub fn peek_keyword(&self, kw: &str) -> bool {
99        self.peek_non_trivia()
100            .is_some_and(|t| t.kind == TokenKind::Word && t.text.eq_ignore_ascii_case(kw))
101    }
102
103    /// Check if next non-trivia tokens form a keyword sequence (e.g. "GROUP", "BY").
104    pub fn peek_keywords(&self, kws: &[&str]) -> bool {
105        let mut i = self.pos;
106        for kw in kws {
107            // skip trivia
108            while i < self.tokens.len() && self.tokens[i].kind.is_trivia() {
109                i += 1;
110            }
111            if i >= self.tokens.len() {
112                return false;
113            }
114            let t = &self.tokens[i];
115            if t.kind != TokenKind::Word || !t.text.eq_ignore_ascii_case(kw) {
116                return false;
117            }
118            i += 1;
119        }
120        true
121    }
122
123    /// Consume and return the current token, advancing the cursor.
124    pub fn advance(&mut self) -> Option<&'a Token> {
125        if self.pos < self.tokens.len() {
126            let token = &self.tokens[self.pos];
127            self.pos += 1;
128            Some(token)
129        } else {
130            None
131        }
132    }
133
134    /// Consume all leading trivia tokens and return them.
135    pub fn eat_trivia(&mut self) -> Vec<&'a Token> {
136        let mut trivia = Vec::new();
137        while self.pos < self.tokens.len() && self.tokens[self.pos].kind.is_trivia() {
138            trivia.push(&self.tokens[self.pos]);
139            self.pos += 1;
140        }
141        trivia
142    }
143
144    /// Try to consume a keyword (case-insensitive). Returns the token if matched.
145    pub fn eat_keyword(&mut self, kw: &str) -> Option<&'a Token> {
146        if self.pos < self.tokens.len()
147            && self.tokens[self.pos].kind == TokenKind::Word
148            && self.tokens[self.pos].text.eq_ignore_ascii_case(kw)
149        {
150            let token = &self.tokens[self.pos];
151            self.pos += 1;
152            Some(token)
153        } else {
154            None
155        }
156    }
157
158    /// Try to consume a specific token kind.
159    pub fn eat_kind(&mut self, kind: TokenKind) -> Option<&'a Token> {
160        if self.pos < self.tokens.len() && self.tokens[self.pos].kind == kind {
161            let token = &self.tokens[self.pos];
162            self.pos += 1;
163            Some(token)
164        } else {
165            None
166        }
167    }
168
169    /// Are we at EOF?
170    pub fn at_eof(&self) -> bool {
171        self.pos >= self.tokens.len() || self.tokens[self.pos].kind == TokenKind::Eof
172    }
173
174    /// Remaining tokens from current position.
175    pub fn remaining(&self) -> &'a [Token] {
176        &self.tokens[self.pos..]
177    }
178}