Skip to main content

rigsql_parser/
context.rs

1use rigsql_core::{Token, TokenKind};
2
3/// Parser context: a cursor over the token stream.
4pub struct ParseContext<'a> {
5    tokens: &'a [Token],
6    pos: usize,
7    source: &'a str,
8}
9
10impl<'a> ParseContext<'a> {
11    pub fn new(tokens: &'a [Token], source: &'a str) -> Self {
12        Self {
13            tokens,
14            pos: 0,
15            source,
16        }
17    }
18
19    pub fn source(&self) -> &'a str {
20        self.source
21    }
22
23    /// Current position in the token stream.
24    pub fn pos(&self) -> usize {
25        self.pos
26    }
27
28    /// Save cursor position for backtracking.
29    pub fn save(&self) -> usize {
30        self.pos
31    }
32
33    /// Restore cursor to a saved position.
34    pub fn restore(&mut self, pos: usize) {
35        self.pos = pos;
36    }
37
38    /// Peek at the current token without consuming.
39    pub fn peek(&self) -> Option<&'a Token> {
40        self.tokens.get(self.pos)
41    }
42
43    /// Peek at the current non-trivia token kind.
44    pub fn peek_kind(&self) -> Option<TokenKind> {
45        self.peek().map(|t| t.kind)
46    }
47
48    /// Peek at the next non-trivia token (skipping whitespace/comments).
49    pub fn peek_non_trivia(&self) -> Option<&'a Token> {
50        let mut i = self.pos;
51        while i < self.tokens.len() {
52            if !self.tokens[i].kind.is_trivia() {
53                return Some(&self.tokens[i]);
54            }
55            i += 1;
56        }
57        None
58    }
59
60    /// Check if the next non-trivia token is a keyword matching `kw` (case-insensitive).
61    pub fn peek_keyword(&self, kw: &str) -> bool {
62        self.peek_non_trivia()
63            .is_some_and(|t| t.kind == TokenKind::Word && t.text.eq_ignore_ascii_case(kw))
64    }
65
66    /// Check if next non-trivia tokens form a keyword sequence (e.g. "GROUP", "BY").
67    pub fn peek_keywords(&self, kws: &[&str]) -> bool {
68        let mut i = self.pos;
69        for kw in kws {
70            // skip trivia
71            while i < self.tokens.len() && self.tokens[i].kind.is_trivia() {
72                i += 1;
73            }
74            if i >= self.tokens.len() {
75                return false;
76            }
77            let t = &self.tokens[i];
78            if t.kind != TokenKind::Word || !t.text.eq_ignore_ascii_case(kw) {
79                return false;
80            }
81            i += 1;
82        }
83        true
84    }
85
86    /// Consume and return the current token, advancing the cursor.
87    pub fn advance(&mut self) -> Option<&'a Token> {
88        if self.pos < self.tokens.len() {
89            let token = &self.tokens[self.pos];
90            self.pos += 1;
91            Some(token)
92        } else {
93            None
94        }
95    }
96
97    /// Consume all leading trivia tokens and return them.
98    pub fn eat_trivia(&mut self) -> Vec<&'a Token> {
99        let mut trivia = Vec::new();
100        while self.pos < self.tokens.len() && self.tokens[self.pos].kind.is_trivia() {
101            trivia.push(&self.tokens[self.pos]);
102            self.pos += 1;
103        }
104        trivia
105    }
106
107    /// Try to consume a keyword (case-insensitive). Returns the token if matched.
108    pub fn eat_keyword(&mut self, kw: &str) -> Option<&'a Token> {
109        if self.pos < self.tokens.len()
110            && self.tokens[self.pos].kind == TokenKind::Word
111            && self.tokens[self.pos].text.eq_ignore_ascii_case(kw)
112        {
113            let token = &self.tokens[self.pos];
114            self.pos += 1;
115            Some(token)
116        } else {
117            None
118        }
119    }
120
121    /// Try to consume a specific token kind.
122    pub fn eat_kind(&mut self, kind: TokenKind) -> Option<&'a Token> {
123        if self.pos < self.tokens.len() && self.tokens[self.pos].kind == kind {
124            let token = &self.tokens[self.pos];
125            self.pos += 1;
126            Some(token)
127        } else {
128            None
129        }
130    }
131
132    /// Are we at EOF?
133    pub fn at_eof(&self) -> bool {
134        self.pos >= self.tokens.len() || self.tokens[self.pos].kind == TokenKind::Eof
135    }
136
137    /// Remaining tokens from current position.
138    pub fn remaining(&self) -> &'a [Token] {
139        &self.tokens[self.pos..]
140    }
141}