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