1pub use forgedb_validation::Position;
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct TokenWithPos {
7 pub token: Token,
8 pub position: Position,
9}
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum Token {
14 Ident(String),
16
17 TypeU32,
19 TypeU64,
20 TypeI32,
21 TypeI64,
22 TypeF64,
23 TypeBool,
24 TypeString,
25 TypeUuid,
26 TypeTimestamp,
27 TypeChar, KwStruct, Plus, Ampersand, Caret, Colon, LBrace, RBrace, LBracket, RBracket, Asterisk, Question, At, LParen, RParen, Comma, Semicolon, Slash, Number(i64), Str(String), Newline,
54 Eof,
55}
56
57pub struct Lexer {
58 input: Vec<char>,
59 position: usize,
60 pub line: usize,
61 pub column: usize,
62}
63
64impl Lexer {
65 pub fn new(input: &str) -> Self {
66 Lexer {
67 input: input.chars().collect(),
68 position: 0,
69 line: 1,
70 column: 1,
71 }
72 }
73
74 fn current_char(&self) -> Option<char> {
75 if self.position < self.input.len() {
76 Some(self.input[self.position])
77 } else {
78 None
79 }
80 }
81
82 fn advance(&mut self) {
83 if let Some(ch) = self.current_char() {
84 if ch == '\n' {
85 self.line += 1;
86 self.column = 1;
87 } else {
88 self.column += 1;
89 }
90 self.position += 1;
91 }
92 }
93
94 fn skip_whitespace(&mut self) {
95 while let Some(ch) = self.current_char() {
96 if ch == ' ' || ch == '\t' || ch == '\r' {
97 self.advance();
98 } else {
99 break;
100 }
101 }
102 }
103
104 fn skip_whitespace_with_flag(&mut self) -> bool {
105 let start_pos = self.position;
106 self.skip_whitespace();
107 self.position > start_pos || self.column == 1
109 }
110
111 fn skip_comment(&mut self) {
112 while let Some(ch) = self.current_char() {
114 if ch == '\n' {
115 break;
116 }
117 self.advance();
118 }
119 }
120
121 fn read_identifier(&mut self) -> String {
122 let mut ident = String::new();
123 while let Some(ch) = self.current_char() {
124 if ch.is_alphanumeric() || ch == '_' {
125 ident.push(ch);
126 self.advance();
127 } else {
128 break;
129 }
130 }
131 ident
132 }
133
134 fn read_number(&mut self) -> Result<i64, String> {
135 let mut num_str = String::new();
136 while let Some(ch) = self.current_char() {
137 if ch.is_numeric() {
138 num_str.push(ch);
139 self.advance();
140 } else {
141 break;
142 }
143 }
144 num_str.parse::<i64>().map_err(|e| {
145 format!(
146 "Numeric literal '{}' is out of range at line {}, column {}: {}",
147 num_str, self.line, self.column, e
148 )
149 })
150 }
151
152 fn read_string(&mut self) -> Result<String, String> {
156 let start_line = self.line;
157 let start_column = self.column;
158 self.advance(); let mut value = String::new();
161 loop {
162 match self.current_char() {
163 None => {
164 return Err(format!(
165 "Unterminated string literal starting at line {}, column {}",
166 start_line, start_column
167 ));
168 }
169 Some('"') => {
170 self.advance(); return Ok(value);
172 }
173 Some('\\') => {
174 self.advance();
175 match self.current_char() {
176 Some('"') => value.push('"'),
177 Some('\\') => value.push('\\'),
178 Some('n') => value.push('\n'),
179 Some('t') => value.push('\t'),
180 Some('r') => value.push('\r'),
181 Some(other) => {
182 return Err(format!(
183 "Invalid escape sequence '\\{}' in string literal at line {}, column {}",
184 other, self.line, self.column
185 ));
186 }
187 None => {
188 return Err(format!(
189 "Unterminated string literal starting at line {}, column {}",
190 start_line, start_column
191 ));
192 }
193 }
194 self.advance();
195 }
196 Some('\n') => {
197 return Err(format!(
198 "Unterminated string literal starting at line {}, column {} (newline before closing quote)",
199 start_line, start_column
200 ));
201 }
202 Some(ch) => {
203 value.push(ch);
204 self.advance();
205 }
206 }
207 }
208 }
209
210 pub fn next_token(&mut self) -> Result<Token, String> {
211 let had_whitespace = self.skip_whitespace_with_flag();
212
213 match self.current_char() {
214 None => Ok(Token::Eof),
215 Some('\n') => {
216 self.advance();
217 Ok(Token::Newline)
218 }
219 Some('/') => {
220 self.advance();
221 if self.current_char() == Some('/') && had_whitespace {
224 self.advance();
225 self.skip_comment();
226 self.next_token()
228 } else {
229 Ok(Token::Slash)
231 }
232 }
233 Some('+') => {
234 self.advance();
235 Ok(Token::Plus)
236 }
237 Some('&') => {
238 self.advance();
239 Ok(Token::Ampersand)
240 }
241 Some('^') => {
242 self.advance();
243 Ok(Token::Caret)
244 }
245 Some(':') => {
246 self.advance();
247 Ok(Token::Colon)
248 }
249 Some('{') => {
250 self.advance();
251 Ok(Token::LBrace)
252 }
253 Some('}') => {
254 self.advance();
255 Ok(Token::RBrace)
256 }
257 Some('[') => {
258 self.advance();
259 Ok(Token::LBracket)
260 }
261 Some(']') => {
262 self.advance();
263 Ok(Token::RBracket)
264 }
265 Some('*') => {
266 self.advance();
267 Ok(Token::Asterisk)
268 }
269 Some('?') => {
270 self.advance();
271 Ok(Token::Question)
272 }
273 Some('@') => {
274 self.advance();
275 Ok(Token::At)
276 }
277 Some('(') => {
278 self.advance();
279 Ok(Token::LParen)
280 }
281 Some(')') => {
282 self.advance();
283 Ok(Token::RParen)
284 }
285 Some(',') => {
286 self.advance();
287 Ok(Token::Comma)
288 }
289 Some(';') => {
290 self.advance();
291 Ok(Token::Semicolon)
292 }
293 Some('"') => {
294 let s = self.read_string()?;
295 Ok(Token::Str(s))
296 }
297 Some(ch) if ch.is_numeric() => {
298 let num = self.read_number()?;
299 Ok(Token::Number(num))
300 }
301 Some(ch) if ch.is_alphabetic() || ch == '_' => {
302 let ident = self.read_identifier();
303 let token = match ident.as_str() {
304 "u32" => Token::TypeU32,
305 "u64" => Token::TypeU64,
306 "i32" => Token::TypeI32,
307 "i64" => Token::TypeI64,
308 "f64" => Token::TypeF64,
309 "bool" => Token::TypeBool,
310 "string" => Token::TypeString,
311 "uuid" => Token::TypeUuid,
312 "timestamp" => Token::TypeTimestamp,
313 "char" => Token::TypeChar,
314 "struct" => Token::KwStruct,
315 _ => Token::Ident(ident),
316 };
317 Ok(token)
318 }
319 Some(ch) => Err(format!(
320 "Unexpected character '{}' at line {}, column {}",
321 ch, self.line, self.column
322 )),
323 }
324 }
325
326 pub fn next_token_with_pos(&mut self) -> Result<TokenWithPos, String> {
327 let pos = Position {
328 line: self.line,
329 column: self.column,
330 };
331 let token = self.next_token()?;
332 Ok(TokenWithPos {
333 token,
334 position: pos,
335 })
336 }
337
338 pub fn tokenize(&mut self) -> Result<Vec<Token>, String> {
339 let mut tokens = Vec::new();
340 loop {
341 let token = self.next_token()?;
342 if token == Token::Eof {
343 tokens.push(token);
344 break;
345 }
346 tokens.push(token);
347 }
348 Ok(tokens)
349 }
350
351 pub fn tokenize_with_pos(&mut self) -> Result<Vec<TokenWithPos>, String> {
352 let mut tokens = Vec::new();
353 loop {
354 let token_with_pos = self.next_token_with_pos()?;
355 let is_eof = token_with_pos.token == Token::Eof;
356 tokens.push(token_with_pos);
357 if is_eof {
358 break;
359 }
360 }
361 Ok(tokens)
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 fn tokens(input: &str) -> Vec<Token> {
370 Lexer::new(input).tokenize().expect("lex error")
371 }
372
373 #[test]
374 fn lexes_basic_string_literal() {
375 assert_eq!(
376 tokens("\"pending\""),
377 vec![Token::Str("pending".to_string()), Token::Eof]
378 );
379 }
380
381 #[test]
382 fn lexes_string_with_regex_metacharacters() {
383 assert_eq!(
385 tokens("\"^[a-z]+$\""),
386 vec![Token::Str("^[a-z]+$".to_string()), Token::Eof]
387 );
388 }
389
390 #[test]
391 fn lexes_string_escapes() {
392 assert_eq!(
393 tokens(r#""a\"b\\c\n\t\r""#),
394 vec![Token::Str("a\"b\\c\n\t\r".to_string()), Token::Eof]
395 );
396 }
397
398 #[test]
399 fn empty_string_literal() {
400 assert_eq!(tokens("\"\""), vec![Token::Str(String::new()), Token::Eof]);
401 }
402
403 #[test]
404 fn string_literal_in_directive_context() {
405 assert_eq!(
407 tokens("@pattern(\"^[0-9]+$\")"),
408 vec![
409 Token::At,
410 Token::Ident("pattern".to_string()),
411 Token::LParen,
412 Token::Str("^[0-9]+$".to_string()),
413 Token::RParen,
414 Token::Eof,
415 ]
416 );
417 }
418
419 #[test]
420 fn unterminated_string_is_an_error() {
421 let err = Lexer::new("\"oops").tokenize().unwrap_err();
422 assert!(err.contains("Unterminated string literal"), "got: {err}");
423 }
424
425 #[test]
426 fn newline_before_closing_quote_is_an_error() {
427 let err = Lexer::new("\"oops\n\"").tokenize().unwrap_err();
428 assert!(err.contains("Unterminated string literal"), "got: {err}");
429 }
430
431 #[test]
432 fn invalid_escape_is_an_error() {
433 let err = Lexer::new("\"a\\q\"").tokenize().unwrap_err();
434 assert!(err.contains("Invalid escape sequence"), "got: {err}");
435 }
436
437 #[test]
438 fn double_slash_comment_still_works_after_string_support() {
439 assert_eq!(tokens("u32 // trailing comment"), vec![Token::TypeU32, Token::Eof]);
441 }
442}