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