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