1use crate::error::{AamlError, ErrorDiagnostics};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Token<'a> {
14 pub kind: TokenKind,
15 pub line: usize,
16 pub column: usize,
17 pub text: std::borrow::Cow<'a, str>,
18 pub start: usize,
19 pub end: usize,
20 pub source: &'a str,
21}
22
23impl<'a> Token<'a> {
24 pub fn new(
25 kind: TokenKind,
26 line: usize,
27 column: usize,
28 text: impl Into<std::borrow::Cow<'a, str>>,
29 start: usize,
30 end: usize,
31 source: &'a str,
32 ) -> Self {
33 Self {
34 kind,
35 line,
36 column,
37 text: text.into(),
38 start,
39 end,
40 source,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum TokenKind {
48 Identifier,
50 Assign,
52 String,
54 Number,
56 Boolean,
58 LeftBrace,
60 RightBrace,
62 LeftBracket,
64 RightBracket,
66 Comma,
68 At,
70 Newline,
72 Comment,
74}
75
76pub trait Lexer: Send + Sync {
78 fn tokenize<'a>(&self, content: &'a str) -> Result<Vec<Token<'a>>, AamlError>;
84}
85
86pub struct DefaultLexer;
88
89impl DefaultLexer {
90 #[must_use]
91 pub const fn new() -> Self {
92 Self
93 }
94
95 const fn is_whitespace(c: char) -> bool {
97 c == ' ' || c == '\t' || c == '\r'
98 }
99
100 fn is_id_start(c: char) -> bool {
102 c.is_alphabetic() || c == '_' || c == '@' || c == '#' || c == '/'
103 }
104
105 fn is_id_cont(c: char) -> bool {
107 c.is_alphanumeric()
108 || c == '_'
109 || c == ':'
110 || c == '.'
111 || c == '*'
112 || c == '#'
113 || c == '-'
114 || c == '/'
115 || c == '<'
116 || c == '>'
117 }
118
119 fn is_comment_start(chars: &std::iter::Peekable<std::str::Chars>) -> bool {
121 chars.clone().nth(1).is_some_and(char::is_whitespace)
122 }
123
124 const fn is_digit(c: char) -> bool {
126 c.is_ascii_digit()
127 }
128
129 const fn is_number_part(c: char) -> bool {
131 c.is_ascii_digit() || c == '.' || c == '-' || c == 'e' || c == 'E'
132 }
133}
134
135impl Default for DefaultLexer {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl Lexer for DefaultLexer {
142 #[allow(clippy::too_many_lines)]
143 fn tokenize<'a>(&self, content: &'a str) -> Result<Vec<Token<'a>>, AamlError> {
144 let mut tokens = Vec::new();
145 let mut line = 1;
146 let mut column = 1;
147 let mut byte_offset = 0usize;
148 let mut chars = content.chars().peekable();
149
150 while let Some(&ch) = chars.peek() {
151 match ch {
152 '\n' => {
153 Self::handle_newline(
154 &mut tokens,
155 &mut chars,
156 &mut line,
157 &mut column,
158 &mut byte_offset,
159 content,
160 );
161 }
162 c if Self::is_whitespace(c) => {
163 chars.next();
164 column += 1;
165 byte_offset += c.len_utf8();
166 }
167 '#' => {
168 if Self::is_comment_start(&chars) {
169 Self::handle_comment(
170 &mut tokens,
171 &mut chars,
172 line,
173 &mut column,
174 &mut byte_offset,
175 content,
176 );
177 } else {
178 Self::handle_identifier(
179 &mut tokens,
180 &mut chars,
181 line,
182 &mut column,
183 &mut byte_offset,
184 content,
185 );
186 }
187 }
188 '=' => Self::push_single_token(
189 &mut tokens,
190 TokenKind::Assign,
191 line,
192 column,
193 "=",
194 &mut chars,
195 &mut column,
196 &mut byte_offset,
197 content,
198 ),
199 '{' => Self::push_single_token(
200 &mut tokens,
201 TokenKind::LeftBrace,
202 line,
203 column,
204 "{",
205 &mut chars,
206 &mut column,
207 &mut byte_offset,
208 content,
209 ),
210 '}' => Self::push_single_token(
211 &mut tokens,
212 TokenKind::RightBrace,
213 line,
214 column,
215 "}",
216 &mut chars,
217 &mut column,
218 &mut byte_offset,
219 content,
220 ),
221 '[' => Self::push_single_token(
222 &mut tokens,
223 TokenKind::LeftBracket,
224 line,
225 column,
226 "[",
227 &mut chars,
228 &mut column,
229 &mut byte_offset,
230 content,
231 ),
232 ']' => Self::push_single_token(
233 &mut tokens,
234 TokenKind::RightBracket,
235 line,
236 column,
237 "]",
238 &mut chars,
239 &mut column,
240 &mut byte_offset,
241 content,
242 ),
243 ',' => Self::push_single_token(
244 &mut tokens,
245 TokenKind::Comma,
246 line,
247 column,
248 ",",
249 &mut chars,
250 &mut column,
251 &mut byte_offset,
252 content,
253 ),
254 '@' => Self::push_single_token(
255 &mut tokens,
256 TokenKind::At,
257 line,
258 column,
259 "@",
260 &mut chars,
261 &mut column,
262 &mut byte_offset,
263 content,
264 ),
265 '"' | '\'' => {
266 Self::handle_string(
267 &mut tokens,
268 &mut chars,
269 ch,
270 line,
271 &mut column,
272 &mut line,
273 &mut byte_offset,
274 content,
275 );
276 }
277 _ if Self::is_digit(ch)
278 || (ch == '-' && chars.clone().nth(1).is_some_and(Self::is_digit)) =>
279 {
280 Self::handle_number(
281 &mut tokens,
282 &mut chars,
283 ch,
284 line,
285 &mut column,
286 &mut byte_offset,
287 content,
288 );
289 }
290 _ if Self::is_id_start(ch) => {
291 Self::handle_identifier(
292 &mut tokens,
293 &mut chars,
294 line,
295 &mut column,
296 &mut byte_offset,
297 content,
298 );
299 }
300 _ => {
301 return Err(AamlError::LexError {
302 line,
303 column,
304 character: ch.to_string(),
305 diagnostics: Some(Box::new(ErrorDiagnostics::new(
306 "Invalid character in input",
307 format!("Unexpected character '{ch}' at {line}:{column}"),
308 "Check for typos or unsupported characters",
309 ))),
310 });
311 }
312 }
313 }
314
315 if tokens.is_empty() || tokens.last().is_none_or(|t| t.kind != TokenKind::Newline) {
317 tokens.push(Token::new(
318 TokenKind::Newline,
319 line,
320 column,
321 "\n".to_string(),
322 byte_offset,
323 byte_offset + 1,
324 content,
325 ));
326 }
327
328 Ok(tokens)
329 }
330}
331
332impl DefaultLexer {
333 fn handle_newline<'a>(
334 tokens: &mut Vec<Token<'a>>,
335 chars: &mut std::iter::Peekable<std::str::Chars>,
336 line: &mut usize,
337 column: &mut usize,
338 byte_offset: &mut usize,
339 source: &'a str,
340 ) {
341 let start = *byte_offset;
342 tokens.push(Token::new(
343 TokenKind::Newline,
344 *line,
345 *column,
346 "\n".to_string(),
347 start,
348 start + 1,
349 source,
350 ));
351 chars.next();
352 *line += 1;
353 *column = 1;
354 *byte_offset += 1;
355 }
356
357 fn handle_comment<'a>(
358 tokens: &mut Vec<Token<'a>>,
359 chars: &mut std::iter::Peekable<std::str::Chars>,
360 line: usize,
361 column: &mut usize,
362 byte_offset: &mut usize,
363 source: &'a str,
364 ) {
365 let col = *column;
366 let start = *byte_offset;
367 let mut text = String::new();
368 while let Some(&c) = chars.peek() {
369 if c == '\n' {
370 break;
371 }
372 text.push(c);
373 chars.next();
374 *column += 1;
375 *byte_offset += c.len_utf8();
376 }
377 tokens.push(Token::new(
378 TokenKind::Comment,
379 line,
380 col,
381 text,
382 start,
383 *byte_offset,
384 source,
385 ));
386 }
387
388 #[allow(clippy::too_many_arguments)]
389 fn push_single_token<'a>(
390 tokens: &mut Vec<Token<'a>>,
391 kind: TokenKind,
392 line: usize,
393 column: usize,
394 text: &str,
395 chars: &mut std::iter::Peekable<std::str::Chars>,
396 col_ref: &mut usize,
397 byte_offset: &mut usize,
398 source: &'a str,
399 ) {
400 let start = *byte_offset;
401 let end = start + text.len();
402 tokens.push(Token::new(
403 kind,
404 line,
405 column,
406 text.to_string(),
407 start,
408 end,
409 source,
410 ));
411 chars.next();
412 *col_ref += 1;
413 *byte_offset = end;
414 }
415
416 const fn update_string_scan_state(
417 c: char,
418 quote: char,
419 escaped: &mut bool,
420 line: &mut usize,
421 column: &mut usize,
422 ) -> bool {
423 if *escaped {
424 *escaped = false;
425 return false;
426 }
427
428 if c == '\\' {
429 *escaped = true;
430 return false;
431 }
432
433 if c == quote {
434 return true;
435 }
436
437 if c == '\n' {
438 *line += 1;
439 *column = 1;
440 }
441
442 false
443 }
444
445 #[allow(clippy::too_many_arguments)]
446 fn handle_string<'a>(
447 tokens: &mut Vec<Token<'a>>,
448 chars: &mut std::iter::Peekable<std::str::Chars>,
449 quote: char,
450 mut line: usize,
451 column: &mut usize,
452 line_ref: &mut usize,
453 byte_offset: &mut usize,
454 source: &'a str,
455 ) {
456 let col = *column;
457 let start = *byte_offset;
458 chars.next();
459 *column += 1;
460 *byte_offset += quote.len_utf8();
461 let mut text = String::from(quote);
462 let mut escaped = false;
463
464 while let Some(&c) = chars.peek() {
465 text.push(c);
466 chars.next();
467 *column += 1;
468 *byte_offset += c.len_utf8();
469
470 if Self::update_string_scan_state(c, quote, &mut escaped, &mut line, column) {
471 break;
472 }
473 }
474
475 tokens.push(Token::new(
476 TokenKind::String,
477 line,
478 col,
479 text,
480 start,
481 *byte_offset,
482 source,
483 ));
484 *line_ref = line;
485 }
486
487 fn handle_number<'a>(
488 tokens: &mut Vec<Token<'a>>,
489 chars: &mut std::iter::Peekable<std::str::Chars>,
490 first_ch: char,
491 line: usize,
492 column: &mut usize,
493 byte_offset: &mut usize,
494 source: &'a str,
495 ) {
496 let col = *column;
497 let start = *byte_offset;
498 let mut text = String::new();
499
500 if first_ch == '-' {
501 text.push('-');
502 chars.next();
503 *column += 1;
504 *byte_offset += '-'.len_utf8();
505 }
506
507 while let Some(&c) = chars.peek() {
508 if Self::is_number_part(c) {
509 text.push(c);
510 chars.next();
511 *column += 1;
512 *byte_offset += c.len_utf8();
513 } else {
514 break;
515 }
516 }
517
518 let kind = if text == "true" || text == "false" {
519 TokenKind::Boolean
520 } else {
521 TokenKind::Number
522 };
523
524 tokens.push(Token::new(
525 kind,
526 line,
527 col,
528 text,
529 start,
530 *byte_offset,
531 source,
532 ));
533 }
534
535 fn handle_identifier<'a>(
536 tokens: &mut Vec<Token<'a>>,
537 chars: &mut std::iter::Peekable<std::str::Chars>,
538 line: usize,
539 column: &mut usize,
540 byte_offset: &mut usize,
541 source: &'a str,
542 ) {
543 let col = *column;
544 let start = *byte_offset;
545 let mut text = String::new();
546
547 while let Some(&c) = chars.peek() {
548 if Self::is_id_cont(c) {
549 text.push(c);
550 chars.next();
551 *column += 1;
552 *byte_offset += c.len_utf8();
553 } else {
554 break;
555 }
556 }
557
558 let kind = match text.as_str() {
559 "true" | "false" => TokenKind::Boolean,
560 _ => TokenKind::Identifier,
561 };
562
563 tokens.push(Token::new(
564 kind,
565 line,
566 col,
567 text,
568 start,
569 *byte_offset,
570 source,
571 ));
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn test_simple_assignment() {
581 let lexer = DefaultLexer::new();
582 let tokens = lexer.tokenize("host = localhost").unwrap();
583
584 assert_eq!(tokens.len(), 4); assert_eq!(tokens[0].kind, TokenKind::Identifier);
586 assert_eq!(tokens[1].kind, TokenKind::Assign);
587 assert_eq!(tokens[2].kind, TokenKind::Identifier);
588 assert_eq!(tokens[3].kind, TokenKind::Newline);
589 }
590
591 #[test]
592 fn test_quoted_string() {
593 let lexer = DefaultLexer::new();
594 let tokens = lexer.tokenize("name = \"John Doe\"").unwrap();
595
596 assert!(tokens.iter().any(|t| t.kind == TokenKind::String));
597 }
598
599 #[test]
600 fn test_number_literal() {
601 let lexer = DefaultLexer::new();
602 let tokens = lexer.tokenize("port = 8080").unwrap();
603
604 assert!(tokens.iter().any(|t| t.kind == TokenKind::Number));
605 }
606
607 #[test]
608 fn test_boolean_literal() {
609 let lexer = DefaultLexer::new();
610 let tokens = lexer.tokenize("enabled = true").unwrap();
611
612 assert!(tokens.iter().any(|t| t.kind == TokenKind::Boolean));
613 }
614
615 #[test]
616 fn test_braces_and_brackets() {
617 let lexer = DefaultLexer::new();
618 let tokens = lexer.tokenize("obj = { key = val }").unwrap();
619
620 assert!(tokens.iter().any(|t| t.kind == TokenKind::LeftBrace));
621 assert!(tokens.iter().any(|t| t.kind == TokenKind::RightBrace));
622 }
623
624 #[test]
625 fn test_directive() {
626 let lexer = DefaultLexer::new();
627 let tokens = lexer.tokenize("@import base.aam").unwrap();
628
629 assert_eq!(tokens[0].kind, TokenKind::At);
630 assert_eq!(tokens[1].kind, TokenKind::Identifier);
631 }
632}