1use pine_core::PineVersion;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum LexerError {
6 #[error("Unterminated string at line {line}, column {column}")]
7 UnterminatedString { line: usize, column: usize },
8
9 #[error("Invalid hex color format '{value}' at line {line}, column {column}")]
10 InvalidHexColor {
11 value: String,
12 line: usize,
13 column: usize,
14 },
15
16 #[error("Unexpected character '{ch}' at line {line}, column {column}")]
17 UnexpectedCharacter {
18 ch: char,
19 line: usize,
20 column: usize,
21 },
22
23 #[error("Indentation error at line {line}")]
24 IndentationError { line: usize },
25
26 #[error("Invalid number '{value}' at line {line}, column {column}")]
27 InvalidNumber {
28 value: String,
29 line: usize,
30 column: usize,
31 },
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum TokenType {
37 IntLiteral(i64),
38 Number(f64),
39 String(String),
40 Bool(bool),
41 HexColor(String), Comment(String),
46 BlankLine,
49
50 Ident(String),
52 Var,
53 Varip,
54 Const,
55 Type,
56 Enum,
57 Method,
58 Export,
59 Import,
60 If,
61 Else,
62 For,
63 While,
64 Break,
65 Continue,
66 To,
67 In,
68 Switch, Int,
70 Float, Na, And,
73 Or,
74 Not, Plus,
78 Minus,
79 Star,
80 Slash,
81 Percent,
82 Equal,
83 NotEqual,
84 Less,
85 Greater,
86 LessEqual,
87 GreaterEqual,
88 Assign,
89 ColonAssign,
90 Arrow, PlusAssign,
92 MinusAssign,
93 StarAssign,
94 SlashAssign, LParen,
98 RParen,
99 LBracket,
100 RBracket,
101 Comma,
102 Dot,
103 Colon,
104 Question,
105 Newline,
106 Indent,
107 Dedent,
108
109 Eof,
110}
111
112#[derive(Debug, Clone)]
113pub struct Token {
114 pub typ: TokenType,
115 pub lexeme: String,
116 pub line: usize,
117 pub column: usize,
118}
119
120pub struct Lexer {
121 input: Vec<char>,
122 current: usize,
123 line: usize,
124 column: usize,
125 indent_stack: Vec<usize>, pending_tokens: Vec<Token>, paren_depth: usize, version: PineVersion, }
130
131impl Lexer {
132 pub fn new(input: &str) -> Self {
136 Self::with_version(input, PineVersion::LATEST)
137 }
138
139 pub fn with_version(input: &str, version: PineVersion) -> Self {
140 Self {
141 input: input.chars().collect(),
142 current: 0,
143 line: 1,
144 column: 1,
145 indent_stack: vec![0], pending_tokens: vec![],
147 paren_depth: 0,
148 version,
149 }
150 }
151
152 fn peek(&self) -> Option<char> {
153 self.input.get(self.current).copied()
154 }
155
156 fn advance(&mut self) -> Option<char> {
157 let ch = self.peek()?;
158 self.current += 1;
159 if ch == '\n' {
160 self.line += 1;
161 self.column = 1;
162 } else {
163 self.column += 1;
164 }
165 Some(ch)
166 }
167
168 fn skip_whitespace(&mut self) {
169 while let Some(ch) = self.peek() {
170 if ch == ' ' || ch == '\t' || ch == '\r' {
171 self.advance();
172 } else {
173 break;
174 }
175 }
176 }
177
178 fn scan_number(&mut self) -> Result<Token, LexerError> {
179 let start_line = self.line;
180 let start_col = self.column;
181 let mut num_str = String::new();
182
183 if self.peek() == Some('.') {
185 num_str.push('.');
186 self.advance();
187 }
188
189 while let Some(ch) = self.peek() {
190 if ch.is_numeric() {
191 num_str.push(ch);
192 self.advance();
193 } else if ch == '.' && !num_str.contains('.') {
194 if let Some(next_ch) = self.input.get(self.current + 1) {
196 if next_ch.is_numeric() {
197 num_str.push(ch);
198 self.advance();
199 } else {
200 break;
201 }
202 } else {
203 break;
204 }
205 } else {
206 break;
207 }
208 }
209
210 let typ =
214 if num_str.contains('.') {
215 TokenType::Number(num_str.parse::<f64>().map_err(|_| {
216 LexerError::InvalidNumber {
217 value: num_str.clone(),
218 line: start_line,
219 column: start_col,
220 }
221 })?)
222 } else {
223 TokenType::IntLiteral(num_str.parse::<i64>().map_err(|_| {
224 LexerError::InvalidNumber {
225 value: num_str.clone(),
226 line: start_line,
227 column: start_col,
228 }
229 })?)
230 };
231 Ok(Token {
232 typ,
233 lexeme: num_str,
234 line: start_line,
235 column: start_col,
236 })
237 }
238
239 fn scan_identifier(&mut self) -> Token {
240 let start_line = self.line;
241 let start_col = self.column;
242 let mut ident = String::new();
243
244 while let Some(ch) = self.peek() {
245 if ch.is_alphanumeric() || ch == '_' {
246 ident.push(ch);
247 self.advance();
248 } else {
249 break;
250 }
251 }
252
253 let typ = match ident.as_str() {
255 "var" => TokenType::Var,
256 "varip" => TokenType::Varip,
257 "const" => TokenType::Const,
258 "type" if self.version >= PineVersion::V5 => TokenType::Type,
261 "enum" => TokenType::Enum,
262 "method" => TokenType::Method,
263 "export" => TokenType::Export,
264 "import" => TokenType::Import,
265 "if" => TokenType::If,
266 "else" => TokenType::Else,
267 "true" => TokenType::Bool(true),
268 "false" => TokenType::Bool(false),
269 "for" => TokenType::For,
270 "while" => TokenType::While,
271 "break" => TokenType::Break,
272 "continue" => TokenType::Continue,
273 "to" => TokenType::To,
274 "in" => TokenType::In,
275 "switch" => TokenType::Switch,
276 "int" => TokenType::Int,
277 "float" => TokenType::Float,
278 "na" => TokenType::Na,
279 "and" => TokenType::And,
280 "or" => TokenType::Or,
281 "not" => TokenType::Not,
282 _ => TokenType::Ident(ident.clone()),
283 };
284
285 Token {
286 typ,
287 lexeme: ident,
288 line: start_line,
289 column: start_col,
290 }
291 }
292
293 fn scan_string(&mut self, quote_char: char) -> Result<Token, LexerError> {
294 let start_line = self.line;
295 let start_col = self.column;
296
297 self.advance(); let mut string = String::new();
299
300 while let Some(ch) = self.peek() {
301 if ch == quote_char {
302 self.advance();
303 return Ok(Token {
304 typ: TokenType::String(string.clone()),
305 lexeme: format!("{}{}{}", quote_char, string, quote_char),
306 line: start_line,
307 column: start_col,
308 });
309 } else if ch == '\\' {
310 self.advance();
311 if let Some(escaped) = self.advance() {
312 string.push(match escaped {
313 'n' => '\n',
314 't' => '\t',
315 '"' => '"',
316 '\'' => '\'',
317 '\\' => '\\',
318 _ => escaped,
319 });
320 }
321 } else {
322 string.push(ch);
323 self.advance();
324 }
325 }
326
327 Err(LexerError::UnterminatedString {
328 line: start_line,
329 column: start_col,
330 })
331 }
332
333 fn scan_hex_color(&mut self) -> Result<Token, LexerError> {
334 let start_line = self.line;
335 let start_col = self.column;
336
337 self.advance(); let mut hex = String::from("#");
339
340 while let Some(ch) = self.peek() {
342 if ch.is_ascii_hexdigit() {
343 hex.push(ch);
344 self.advance();
345 } else {
346 break;
347 }
348 }
349
350 let hex_len = hex.len() - 1;
352 if hex_len != 6 && hex_len != 8 {
353 return Err(LexerError::InvalidHexColor {
354 value: hex,
355 line: start_line,
356 column: start_col,
357 });
358 }
359
360 Ok(Token {
361 typ: TokenType::HexColor(hex.clone()),
362 lexeme: hex,
363 line: start_line,
364 column: start_col,
365 })
366 }
367
368 fn next_token(&mut self) -> Result<Token, LexerError> {
369 self.skip_whitespace();
370
371 let ch = match self.peek() {
372 Some(c) => c,
373 None => {
374 return Ok(Token {
375 typ: TokenType::Eof,
376 lexeme: String::new(),
377 line: self.line,
378 column: self.column,
379 });
380 }
381 };
382
383 let line = self.line;
384 let col = self.column;
385
386 let token = match ch {
387 '+' => {
388 self.advance();
389 if self.peek() == Some('=') {
390 self.advance();
391 Token {
392 typ: TokenType::PlusAssign,
393 lexeme: "+=".to_string(),
394 line,
395 column: col,
396 }
397 } else {
398 Token {
399 typ: TokenType::Plus,
400 lexeme: "+".to_string(),
401 line,
402 column: col,
403 }
404 }
405 }
406 '-' => {
407 self.advance();
408 if self.peek() == Some('=') {
409 self.advance();
410 Token {
411 typ: TokenType::MinusAssign,
412 lexeme: "-=".to_string(),
413 line,
414 column: col,
415 }
416 } else {
417 Token {
418 typ: TokenType::Minus,
419 lexeme: "-".to_string(),
420 line,
421 column: col,
422 }
423 }
424 }
425 '*' => {
426 self.advance();
427 if self.peek() == Some('=') {
428 self.advance();
429 Token {
430 typ: TokenType::StarAssign,
431 lexeme: "*=".to_string(),
432 line,
433 column: col,
434 }
435 } else {
436 Token {
437 typ: TokenType::Star,
438 lexeme: "*".to_string(),
439 line,
440 column: col,
441 }
442 }
443 }
444 '/' => {
445 self.advance();
446 if self.peek() == Some('/') {
447 self.advance(); let mut text = String::new();
449 while self.peek().is_some() && self.peek() != Some('\n') {
450 text.push(self.advance().expect("peeked Some"));
451 }
452 Token {
453 typ: TokenType::Comment(text.clone()),
454 lexeme: format!("//{text}"),
455 line,
456 column: col,
457 }
458 } else if self.peek() == Some('=') {
459 self.advance();
460 Token {
461 typ: TokenType::SlashAssign,
462 lexeme: "/=".to_string(),
463 line,
464 column: col,
465 }
466 } else {
467 Token {
468 typ: TokenType::Slash,
469 lexeme: "/".to_string(),
470 line,
471 column: col,
472 }
473 }
474 }
475 '%' => {
476 self.advance();
477 Token {
478 typ: TokenType::Percent,
479 lexeme: "%".to_string(),
480 line,
481 column: col,
482 }
483 }
484 '=' => {
485 self.advance();
486 if self.peek() == Some('=') {
487 self.advance();
488 Token {
489 typ: TokenType::Equal,
490 lexeme: "==".to_string(),
491 line,
492 column: col,
493 }
494 } else if self.peek() == Some('>') {
495 self.advance();
496 Token {
497 typ: TokenType::Arrow,
498 lexeme: "=>".to_string(),
499 line,
500 column: col,
501 }
502 } else {
503 Token {
504 typ: TokenType::Assign,
505 lexeme: "=".to_string(),
506 line,
507 column: col,
508 }
509 }
510 }
511 '!' => {
512 self.advance();
513 if self.peek() == Some('=') {
514 self.advance();
515 Token {
516 typ: TokenType::NotEqual,
517 lexeme: "!=".to_string(),
518 line,
519 column: col,
520 }
521 } else {
522 return Err(LexerError::UnexpectedCharacter {
523 ch: '!',
524 line,
525 column: col,
526 });
527 }
528 }
529 '<' => {
530 self.advance();
531 if self.peek() == Some('=') {
532 self.advance();
533 Token {
534 typ: TokenType::LessEqual,
535 lexeme: "<=".to_string(),
536 line,
537 column: col,
538 }
539 } else {
540 Token {
541 typ: TokenType::Less,
542 lexeme: "<".to_string(),
543 line,
544 column: col,
545 }
546 }
547 }
548 '>' => {
549 self.advance();
550 if self.peek() == Some('=') {
551 self.advance();
552 Token {
553 typ: TokenType::GreaterEqual,
554 lexeme: ">=".to_string(),
555 line,
556 column: col,
557 }
558 } else {
559 Token {
560 typ: TokenType::Greater,
561 lexeme: ">".to_string(),
562 line,
563 column: col,
564 }
565 }
566 }
567 '(' => {
568 self.advance();
569 Token {
570 typ: TokenType::LParen,
571 lexeme: "(".to_string(),
572 line,
573 column: col,
574 }
575 }
576 ')' => {
577 self.advance();
578 Token {
579 typ: TokenType::RParen,
580 lexeme: ")".to_string(),
581 line,
582 column: col,
583 }
584 }
585 '[' => {
586 self.advance();
587 Token {
588 typ: TokenType::LBracket,
589 lexeme: "[".to_string(),
590 line,
591 column: col,
592 }
593 }
594 ']' => {
595 self.advance();
596 Token {
597 typ: TokenType::RBracket,
598 lexeme: "]".to_string(),
599 line,
600 column: col,
601 }
602 }
603 ',' => {
604 self.advance();
605 Token {
606 typ: TokenType::Comma,
607 lexeme: ",".to_string(),
608 line,
609 column: col,
610 }
611 }
612 '.' => {
613 if let Some(next_ch) = self.input.get(self.current + 1) {
615 if next_ch.is_numeric() {
616 return self.scan_number();
618 }
619 }
620 self.advance();
621 Token {
622 typ: TokenType::Dot,
623 lexeme: ".".to_string(),
624 line,
625 column: col,
626 }
627 }
628 ':' => {
629 self.advance();
630 if self.peek() == Some('=') {
631 self.advance();
632 Token {
633 typ: TokenType::ColonAssign,
634 lexeme: ":=".to_string(),
635 line,
636 column: col,
637 }
638 } else {
639 Token {
640 typ: TokenType::Colon,
641 lexeme: ":".to_string(),
642 line,
643 column: col,
644 }
645 }
646 }
647 '?' => {
648 self.advance();
649 Token {
650 typ: TokenType::Question,
651 lexeme: "?".to_string(),
652 line,
653 column: col,
654 }
655 }
656 '\n' => {
657 self.advance();
658 Token {
659 typ: TokenType::Newline,
660 lexeme: "\\n".to_string(),
661 line,
662 column: col,
663 }
664 }
665 '"' => return self.scan_string('"'),
666 '\'' => return self.scan_string('\''),
667 '#' => return self.scan_hex_color(),
668 _ if ch.is_numeric() => return self.scan_number(),
669 _ if ch.is_alphabetic() || ch == '_' => self.scan_identifier(),
670 _ => {
671 return Err(LexerError::UnexpectedCharacter {
672 ch,
673 line,
674 column: col,
675 })
676 }
677 };
678
679 Ok(token)
680 }
681
682 pub fn tokenize(&mut self) -> Result<Vec<Token>, LexerError> {
683 let mut tokens = vec![];
684 let mut at_line_start = true;
685
686 loop {
687 if !self.pending_tokens.is_empty() {
689 tokens.push(self.pending_tokens.remove(0));
690 continue;
691 }
692
693 if at_line_start {
695 at_line_start = false;
696
697 let saved_line = self.line;
699 let saved_col = self.column;
700
701 let mut indent_level = 0;
703 while let Some(ch) = self.peek() {
704 if ch == ' ' {
705 indent_level += 1;
706 self.advance();
707 } else if ch == '\t' {
708 indent_level += 4; self.advance();
710 } else {
711 break;
712 }
713 }
714
715 if let Some(ch) = self.peek() {
717 if ch == '\n' || ch == '\r' {
718 tokens.push(Token {
720 typ: TokenType::BlankLine,
721 lexeme: String::new(),
722 line: self.line,
723 column: self.column,
724 });
725 self.advance();
726 at_line_start = true;
727 continue;
728 } else if ch == '/' && self.peek_ahead(1) == Some('/') {
729 let comment_line = self.line;
732 let comment_col = self.column;
733 self.advance();
734 self.advance();
735 let mut text = String::new();
736 while let Some(c) = self.peek() {
737 if c == '\n' {
738 break;
739 }
740 text.push(c);
741 self.advance();
742 }
743 tokens.push(Token {
744 typ: TokenType::Comment(text.clone()),
745 lexeme: format!("//{text}"),
746 line: comment_line,
747 column: comment_col,
748 });
749 if self.peek() == Some('\n') {
750 self.advance();
751 }
752 at_line_start = true;
753 continue;
754 }
755 } else {
756 let current_line = self.line;
758 let current_col = self.column;
759 while self.indent_stack.len() > 1 {
760 self.indent_stack.pop();
761 tokens.push(Token {
762 typ: TokenType::Dedent,
763 lexeme: String::new(),
764 line: current_line,
765 column: current_col,
766 });
767 }
768 tokens.push(Token {
769 typ: TokenType::Eof,
770 lexeme: String::new(),
771 line: current_line,
772 column: current_col,
773 });
774 break;
775 }
776
777 if self.paren_depth > 0 {
778 } else if indent_level % 4 != 0 {
784 if matches!(
790 tokens.last(),
791 Some(Token {
792 typ: TokenType::Newline,
793 ..
794 })
795 ) {
796 tokens.pop();
797 }
798 } else {
799 let current_indent = *self.indent_stack.last().unwrap();
802 let line = saved_line;
803 let col = saved_col;
804
805 if indent_level > current_indent {
806 self.indent_stack.push(indent_level);
808 tokens.push(Token {
809 typ: TokenType::Indent,
810 lexeme: String::new(),
811 line,
812 column: col,
813 });
814 } else if indent_level < current_indent {
815 while self.indent_stack.len() > 1
818 && *self.indent_stack.last().unwrap() > indent_level
819 {
820 self.indent_stack.pop();
821 tokens.push(Token {
822 typ: TokenType::Dedent,
823 lexeme: String::new(),
824 line,
825 column: col,
826 });
827 }
828
829 if *self.indent_stack.last().unwrap() != indent_level {
832 return Err(LexerError::IndentationError { line });
833 }
834 }
835 }
836 }
837
838 let token = self.next_token()?;
840
841 match token.typ {
844 TokenType::LParen => self.paren_depth += 1,
845 TokenType::RParen => self.paren_depth = self.paren_depth.saturating_sub(1),
846 _ => {}
847 }
848
849 if matches!(token.typ, TokenType::Newline) {
851 at_line_start = true;
852 if self.paren_depth == 0 {
856 tokens.push(token);
857 }
858 } else if matches!(token.typ, TokenType::Eof) {
859 while self.indent_stack.len() > 1 {
861 self.indent_stack.pop();
862 tokens.push(Token {
863 typ: TokenType::Dedent,
864 lexeme: String::new(),
865 line: token.line,
866 column: token.column,
867 });
868 }
869 tokens.push(token);
870 break;
871 } else {
872 tokens.push(token);
873 }
874 }
875
876 Ok(tokens)
877 }
878
879 fn peek_ahead(&self, offset: usize) -> Option<char> {
880 self.input.get(self.current + offset).copied()
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[test]
889 fn test_line_wrapping_non_multiple_of_4_joins_lines() -> eyre::Result<()> {
890 let mut lexer = Lexer::new("a = x < 2\n and y\nb = 1");
895 let tokens = lexer.tokenize()?;
896 assert!(
897 !tokens
898 .iter()
899 .any(|t| matches!(t.typ, TokenType::Indent | TokenType::Dedent)),
900 "wrapped continuation must not emit Indent/Dedent: {:?}",
901 tokens.iter().map(|t| &t.typ).collect::<Vec<_>>()
902 );
903 let and_pos = tokens
906 .iter()
907 .position(|t| matches!(t.typ, TokenType::And))
908 .expect("And token present");
909 assert!(
910 !tokens[..and_pos]
911 .iter()
912 .any(|t| matches!(t.typ, TokenType::Newline)),
913 "no Newline may precede the continuation's `and`: {:?}",
914 tokens.iter().map(|t| &t.typ).collect::<Vec<_>>()
915 );
916 Ok(())
917 }
918
919 #[test]
920 fn test_block_indent_multiple_of_4_still_indents() -> eyre::Result<()> {
921 let mut lexer = Lexer::new("if cond\n x = 1\ny = 2");
922 let tokens = lexer.tokenize()?;
923 assert!(
924 tokens.iter().any(|t| matches!(t.typ, TokenType::Indent)),
925 "4-space block body must still emit Indent"
926 );
927 assert!(
928 tokens.iter().any(|t| matches!(t.typ, TokenType::Dedent)),
929 "return to column 0 must still emit Dedent"
930 );
931 Ok(())
932 }
933
934 #[test]
935 fn test_parens_suppress_layout_at_any_indent() -> eyre::Result<()> {
936 let mut lexer = Lexer::new("plot(\n a,\n b\n)");
940 let tokens = lexer.tokenize()?;
941 assert!(matches!(&tokens[0].typ, TokenType::Ident(s) if s == "plot"));
942 assert!(matches!(tokens[1].typ, TokenType::LParen));
943 assert!(matches!(&tokens[2].typ, TokenType::Ident(s) if s == "a"));
944 assert!(matches!(tokens[3].typ, TokenType::Comma));
945 assert!(matches!(&tokens[4].typ, TokenType::Ident(s) if s == "b"));
946 assert!(matches!(tokens[5].typ, TokenType::RParen));
947 assert!(matches!(tokens[6].typ, TokenType::Eof));
948 Ok(())
949 }
950
951 #[test]
952 fn test_newline_after_closing_paren_terminates() -> eyre::Result<()> {
953 let mut lexer = Lexer::new("x = f(\n a\n)\ny = 1");
956 let tokens = lexer.tokenize()?;
957 assert!(matches!(tokens[5].typ, TokenType::RParen));
958 assert!(matches!(tokens[6].typ, TokenType::Newline));
959 assert!(matches!(&tokens[7].typ, TokenType::Ident(s) if s == "y"));
960 Ok(())
961 }
962
963 #[test]
964 fn test_literals() -> eyre::Result<()> {
965 let mut lexer = Lexer::new("42 3.15");
967 let tokens = lexer.tokenize()?;
968 assert!(matches!(tokens[0].typ, TokenType::IntLiteral(n) if n == 42));
969 assert!(matches!(tokens[1].typ, TokenType::Number(n) if n == 3.15));
970
971 let mut lexer = Lexer::new(r#""hello" "world\n""#);
973 let tokens = lexer.tokenize()?;
974 assert!(matches!(&tokens[0].typ, TokenType::String(s) if s == "hello"));
975 assert!(matches!(&tokens[1].typ, TokenType::String(s) if s == "world\n"));
976
977 let mut lexer = Lexer::new("true false");
979 let tokens = lexer.tokenize()?;
980 assert!(matches!(tokens[0].typ, TokenType::Bool(true)));
981 assert!(matches!(tokens[1].typ, TokenType::Bool(false)));
982 Ok(())
983 }
984
985 #[test]
986 fn test_identifiers_and_keywords() -> eyre::Result<()> {
987 let mut lexer = Lexer::new("my_var var if else for while int float na");
988 let tokens = lexer.tokenize()?;
989 assert!(matches!(&tokens[0].typ, TokenType::Ident(s) if s == "my_var"));
990 assert!(matches!(tokens[1].typ, TokenType::Var));
991 assert!(matches!(tokens[2].typ, TokenType::If));
992 assert!(matches!(tokens[3].typ, TokenType::Else));
993 assert!(matches!(tokens[4].typ, TokenType::For));
994 assert!(matches!(tokens[5].typ, TokenType::While));
995 assert!(matches!(tokens[6].typ, TokenType::Int));
996 assert!(matches!(tokens[7].typ, TokenType::Float));
997 assert!(matches!(tokens[8].typ, TokenType::Na));
998 Ok(())
999 }
1000
1001 #[test]
1002 fn test_type_is_a_keyword_only_from_v5() -> eyre::Result<()> {
1003 let tokens = Lexer::with_version("type", PineVersion::V4).tokenize()?;
1006 assert!(matches!(&tokens[0].typ, TokenType::Ident(s) if s == "type"));
1007
1008 let tokens = Lexer::with_version("type", PineVersion::V5).tokenize()?;
1009 assert!(matches!(tokens[0].typ, TokenType::Type));
1010 Ok(())
1011 }
1012
1013 #[test]
1014 fn test_operators() -> eyre::Result<()> {
1015 let mut lexer = Lexer::new("+ - * / = == < >");
1016 let tokens = lexer.tokenize()?;
1017 assert!(matches!(tokens[0].typ, TokenType::Plus));
1018 assert!(matches!(tokens[1].typ, TokenType::Minus));
1019 assert!(matches!(tokens[2].typ, TokenType::Star));
1020 assert!(matches!(tokens[3].typ, TokenType::Slash));
1021 assert!(matches!(tokens[4].typ, TokenType::Assign));
1022 assert!(matches!(tokens[5].typ, TokenType::Equal));
1023 assert!(matches!(tokens[6].typ, TokenType::Less));
1024 assert!(matches!(tokens[7].typ, TokenType::Greater));
1025 Ok(())
1026 }
1027
1028 #[test]
1029 fn test_delimiters() -> eyre::Result<()> {
1030 let mut lexer = Lexer::new("( ) [ ] , . : ? \n");
1031 let tokens = lexer.tokenize()?;
1032 assert!(matches!(tokens[0].typ, TokenType::LParen));
1033 assert!(matches!(tokens[1].typ, TokenType::RParen));
1034 assert!(matches!(tokens[2].typ, TokenType::LBracket));
1035 assert!(matches!(tokens[3].typ, TokenType::RBracket));
1036 assert!(matches!(tokens[4].typ, TokenType::Comma));
1037 assert!(matches!(tokens[5].typ, TokenType::Dot));
1038 assert!(matches!(tokens[6].typ, TokenType::Colon));
1039 assert!(matches!(tokens[7].typ, TokenType::Question));
1040 assert!(matches!(tokens[8].typ, TokenType::Newline));
1041 Ok(())
1042 }
1043
1044 #[test]
1045 fn test_member_access() -> eyre::Result<()> {
1046 let mut lexer = Lexer::new("input.int ta.stoch");
1047 let tokens = lexer.tokenize()?;
1048 assert!(matches!(&tokens[0].typ, TokenType::Ident(s) if s == "input"));
1049 assert!(matches!(tokens[1].typ, TokenType::Dot));
1050 assert!(matches!(tokens[2].typ, TokenType::Int)); assert!(matches!(&tokens[3].typ, TokenType::Ident(s) if s == "ta"));
1052 assert!(matches!(tokens[4].typ, TokenType::Dot));
1053 assert!(matches!(&tokens[5].typ, TokenType::Ident(s) if s == "stoch"));
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn test_comments() -> eyre::Result<()> {
1059 let mut lexer = Lexer::new("42 // comment\n10");
1061 let tokens = lexer.tokenize()?;
1062 assert!(matches!(tokens[0].typ, TokenType::IntLiteral(n) if n == 42));
1063 assert!(matches!(&tokens[1].typ, TokenType::Comment(c) if c == " comment"));
1064 assert!(matches!(tokens[2].typ, TokenType::Newline));
1065 assert!(matches!(tokens[3].typ, TokenType::IntLiteral(n) if n == 10));
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn test_whole_line_comment_is_trivia() -> eyre::Result<()> {
1071 let mut lexer = Lexer::new("// header\n42");
1073 let tokens = lexer.tokenize()?;
1074 assert!(matches!(&tokens[0].typ, TokenType::Comment(c) if c == " header"));
1075 assert!(matches!(tokens[1].typ, TokenType::IntLiteral(n) if n == 42));
1076 assert!(!tokens
1077 .iter()
1078 .any(|t| matches!(t.typ, TokenType::Indent | TokenType::Dedent)));
1079 Ok(())
1080 }
1081
1082 #[test]
1083 fn test_errors() {
1084 let mut lexer = Lexer::new(r#""hello"#);
1086 assert!(lexer.tokenize().is_err());
1087
1088 let mut lexer = Lexer::new("@");
1090 assert!(lexer.tokenize().is_err());
1091 }
1092
1093 #[test]
1094 fn test_complex_expressions() -> eyre::Result<()> {
1095 let mut lexer = Lexer::new("var x = 10");
1097 let tokens = lexer.tokenize()?;
1098 assert!(matches!(tokens[0].typ, TokenType::Var));
1099 assert!(matches!(&tokens[1].typ, TokenType::Ident(s) if s == "x"));
1100 assert!(matches!(tokens[2].typ, TokenType::Assign));
1101 assert!(matches!(tokens[3].typ, TokenType::IntLiteral(n) if n == 10));
1102
1103 let mut lexer = Lexer::new("close[1]");
1105 let tokens = lexer.tokenize()?;
1106 assert!(matches!(&tokens[0].typ, TokenType::Ident(s) if s == "close"));
1107 assert!(matches!(tokens[1].typ, TokenType::LBracket));
1108 assert!(matches!(tokens[2].typ, TokenType::IntLiteral(n) if n == 1));
1109 assert!(matches!(tokens[3].typ, TokenType::RBracket));
1110
1111 let mut lexer = Lexer::new("x > 5");
1113 let tokens = lexer.tokenize()?;
1114 assert!(matches!(&tokens[0].typ, TokenType::Ident(s) if s == "x"));
1115 assert!(matches!(tokens[1].typ, TokenType::Greater));
1116 assert!(matches!(tokens[2].typ, TokenType::IntLiteral(n) if n == 5));
1117 Ok(())
1118 }
1119}