1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Span {
13 pub line: usize,
15 pub col: usize,
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct Token {
22 pub kind: TokenKind,
24 pub span: Span,
26}
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum TokenKind {
31 Ident(String),
33 Const,
43 Input,
48 Extern,
50 Shared,
52 Volatile,
55 Cursor,
57 Over,
63 Pragma,
68 For(String),
75 Tile,
78 TileBody(String, crate::ast::TileBodyKind),
80 Dot,
82 IntLit(u64),
84 FloatLit(f64),
86 StringLit(String),
88 ColonEq,
91 Eq,
95 LParen,
97 RParen,
99 LBracket,
101 RBracket,
103 LBrace,
105 RBrace,
107 Comma,
109 Colon,
111 Arrow,
113 Plus,
115 Minus,
117 Star,
119 Slash,
121 Percent,
123 Caret,
125 StarStar,
127 ShiftLeft,
129 ShiftRight,
131 Ampersand,
133 AmpAmp,
135 Pipe,
137 PipePipe,
139 Bang,
141 Lt,
143 Gt,
145 EqEq,
147 BangEq,
149 LtEq,
151 GtEq,
153 Eof,
155}
156
157pub fn lex(source: &str) -> Result<Vec<Token>, String> {
159 let mut tokens = Vec::new();
160 let chars: Vec<char> = source.chars().collect();
161 let mut pos = 0;
162 let mut line = 1;
163 let mut col = 1;
164 let mut tile_pending = false;
166
167 while pos < chars.len() {
168 let c = chars[pos];
169
170 if c == '\n' {
172 line += 1;
173 col = 1;
174 pos += 1;
175 tile_pending = false;
179 continue;
180 }
181 if c.is_ascii_whitespace() {
182 col += 1;
183 pos += 1;
184 continue;
185 }
186
187 if c == '#' {
189 while pos < chars.len() && chars[pos] != '\n' {
190 pos += 1;
191 }
192 continue;
193 }
194
195 if c == '/' && pos + 1 < chars.len() {
197 if chars[pos + 1] == '/' {
198 while pos < chars.len() && chars[pos] != '\n' {
200 pos += 1;
201 }
202 continue;
203 }
204 if chars[pos + 1] == '*' {
205 pos += 2;
207 col += 2;
208 while pos + 1 < chars.len() {
209 if chars[pos] == '\n' {
210 line += 1;
211 col = 1;
212 }
213 if chars[pos] == '*' && chars[pos + 1] == '/' {
214 pos += 2;
215 col += 2;
216 break;
217 }
218 pos += 1;
219 col += 1;
220 }
221 continue;
222 }
223 }
224
225 if c == '/' {
227 let span = Span { line, col };
228 tokens.push(Token {
229 kind: TokenKind::Slash,
230 span,
231 });
232 pos += 1;
233 col += 1;
234 continue;
235 }
236
237 let span = Span { line, col };
238
239 if !tile_pending
248 && chars[pos..].starts_with(&['<', '<', '<'])
249 && let Some((body, _, consumed, newlines, end_col)) =
250 capture_tile_body(&chars, pos, col)?
251 {
252 tokens.push(Token {
253 kind: TokenKind::StringLit(body),
254 span,
255 });
256 pos += consumed;
257 if newlines > 0 {
258 line += newlines;
259 col = end_col;
260 } else {
261 col += consumed;
262 }
263 continue;
264 }
265
266 if c == ':' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
268 tokens.push(Token {
269 kind: TokenKind::ColonEq,
270 span,
271 });
272 pos += 2;
273 col += 2;
274 if tile_pending {
275 tile_pending = false;
276 if let Some((body, kind, consumed, newlines, end_col)) =
277 capture_tile_body(&chars, pos, col)?
278 {
279 tokens.push(Token {
280 kind: TokenKind::TileBody(body, kind),
281 span: Span { line, col },
282 });
283 pos += consumed;
284 if newlines > 0 {
285 line += newlines;
286 col = end_col;
287 } else {
288 col += consumed;
289 }
290 }
291 }
292 continue;
293 }
294 if c == '-' && pos + 1 < chars.len() && chars[pos + 1] == '>' {
295 tokens.push(Token {
296 kind: TokenKind::Arrow,
297 span,
298 });
299 pos += 2;
300 col += 2;
301 continue;
302 }
303
304 if c.is_ascii_digit() {
308 let start = pos;
309
310 if pos + 1 < chars.len()
312 && chars[pos] == '0'
313 && (chars[pos + 1] == 'x' || chars[pos + 1] == 'X')
314 {
315 pos += 2;
316 col += 2;
317 let hex_start = pos;
318 while pos < chars.len() && chars[pos].is_ascii_hexdigit() {
319 pos += 1;
320 col += 1;
321 }
322 let hex: String = chars[hex_start..pos].iter().collect();
323 let val = u64::from_str_radix(&hex, 16).map_err(|e| {
324 format!(
325 "invalid hex literal at line {}, col {}: {e}",
326 span.line, span.col
327 )
328 })?;
329 tokens.push(Token {
330 kind: TokenKind::IntLit(val),
331 span,
332 });
333 continue;
334 }
335
336 while pos < chars.len() && chars[pos].is_ascii_digit() {
337 pos += 1;
338 col += 1;
339 }
340
341 let mut is_float = false;
343 if pos < chars.len()
344 && chars[pos] == '.'
345 && pos + 1 < chars.len()
346 && chars[pos + 1].is_ascii_digit()
347 {
348 is_float = true;
349 pos += 1;
350 col += 1;
351 while pos < chars.len() && chars[pos].is_ascii_digit() {
352 pos += 1;
353 col += 1;
354 }
355 if pos < chars.len() && (chars[pos] == 'e' || chars[pos] == 'E') {
357 pos += 1;
358 col += 1;
359 if pos < chars.len() && (chars[pos] == '+' || chars[pos] == '-') {
360 pos += 1;
361 col += 1;
362 }
363 while pos < chars.len() && chars[pos].is_ascii_digit() {
364 pos += 1;
365 col += 1;
366 }
367 }
368 } else if pos < chars.len() && (chars[pos] == 'e' || chars[pos] == 'E') {
369 is_float = true;
371 pos += 1;
372 col += 1;
373 if pos < chars.len() && (chars[pos] == '+' || chars[pos] == '-') {
374 pos += 1;
375 col += 1;
376 }
377 while pos < chars.len() && chars[pos].is_ascii_digit() {
378 pos += 1;
379 col += 1;
380 }
381 }
382
383 let suffix_consumed = match peek_si_suffix(&chars, pos) {
391 Some((multiplier, len, is_subunit)) => {
392 pos += len;
393 col += len;
394 Some((multiplier, is_subunit))
395 }
396 None => None,
397 };
398
399 if is_float {
400 let num: String = chars[start..pos - suffix_len_consumed(suffix_consumed)]
401 .iter()
402 .collect();
403 let mut val: f64 = num.parse().map_err(|e| {
404 format!("invalid float at line {}, col {}: {e}", span.line, span.col)
405 })?;
406 if let Some((mult, is_sub)) = suffix_consumed {
407 if is_sub {
408 val /= mult as f64;
409 } else {
410 val *= mult as f64;
411 }
412 }
413 if suffix_consumed.is_some()
418 && val.fract() == 0.0
419 && val >= 0.0
420 && val <= u64::MAX as f64
421 {
422 tokens.push(Token {
423 kind: TokenKind::IntLit(val as u64),
424 span,
425 });
426 } else {
427 tokens.push(Token {
428 kind: TokenKind::FloatLit(val),
429 span,
430 });
431 }
432 } else {
433 let num_end = pos - suffix_len_consumed(suffix_consumed);
435 let num: String = chars[start..num_end]
436 .iter()
437 .filter(|c| **c != '_')
438 .collect();
439 let val: u64 = num.parse().map_err(|e| {
440 format!(
441 "invalid integer at line {}, col {}: {e}",
442 span.line, span.col
443 )
444 })?;
445 match suffix_consumed {
446 Some((mult, true)) => {
447 let f = val as f64 / mult as f64;
450 tokens.push(Token {
451 kind: TokenKind::FloatLit(f),
452 span,
453 });
454 }
455 Some((mult, false)) => {
456 let v = val.checked_mul(mult).ok_or_else(|| {
457 format!(
458 "integer literal with SI suffix overflows u64 at line {}, col {}",
459 span.line, span.col
460 )
461 })?;
462 tokens.push(Token {
463 kind: TokenKind::IntLit(v),
464 span,
465 });
466 }
467 None => {
468 tokens.push(Token {
469 kind: TokenKind::IntLit(val),
470 span,
471 });
472 }
473 }
474 }
475 continue;
476 }
477
478 match c {
480 '(' => {
481 tokens.push(Token {
482 kind: TokenKind::LParen,
483 span,
484 });
485 pos += 1;
486 col += 1;
487 continue;
488 }
489 ')' => {
490 tokens.push(Token {
491 kind: TokenKind::RParen,
492 span,
493 });
494 pos += 1;
495 col += 1;
496 continue;
497 }
498 '[' => {
499 tokens.push(Token {
500 kind: TokenKind::LBracket,
501 span,
502 });
503 pos += 1;
504 col += 1;
505 continue;
506 }
507 ']' => {
508 tokens.push(Token {
509 kind: TokenKind::RBracket,
510 span,
511 });
512 pos += 1;
513 col += 1;
514 continue;
515 }
516 '{' => {
517 tokens.push(Token {
518 kind: TokenKind::LBrace,
519 span,
520 });
521 pos += 1;
522 col += 1;
523 continue;
524 }
525 '}' => {
526 tokens.push(Token {
527 kind: TokenKind::RBrace,
528 span,
529 });
530 pos += 1;
531 col += 1;
532 continue;
533 }
534 ',' => {
535 tokens.push(Token {
536 kind: TokenKind::Comma,
537 span,
538 });
539 pos += 1;
540 col += 1;
541 continue;
542 }
543 '=' => {
544 if pos + 1 < chars.len() && chars[pos + 1] == '=' {
545 tokens.push(Token {
546 kind: TokenKind::EqEq,
547 span,
548 });
549 pos += 2;
550 col += 2;
551 } else {
552 tokens.push(Token {
553 kind: TokenKind::Eq,
554 span,
555 });
556 pos += 1;
557 col += 1;
558 }
559 continue;
560 }
561 ':' => {
562 tokens.push(Token {
563 kind: TokenKind::Colon,
564 span,
565 });
566 pos += 1;
567 col += 1;
568 continue;
569 }
570 '+' => {
571 tokens.push(Token {
572 kind: TokenKind::Plus,
573 span,
574 });
575 pos += 1;
576 col += 1;
577 continue;
578 }
579 '-' => {
580 tokens.push(Token {
581 kind: TokenKind::Minus,
582 span,
583 });
584 pos += 1;
585 col += 1;
586 continue;
587 }
588 '*' => {
589 if pos + 1 < chars.len() && chars[pos + 1] == '*' {
590 tokens.push(Token {
591 kind: TokenKind::StarStar,
592 span,
593 });
594 pos += 2;
595 col += 2;
596 } else {
597 tokens.push(Token {
598 kind: TokenKind::Star,
599 span,
600 });
601 pos += 1;
602 col += 1;
603 }
604 continue;
605 }
606 '%' => {
607 tokens.push(Token {
608 kind: TokenKind::Percent,
609 span,
610 });
611 pos += 1;
612 col += 1;
613 continue;
614 }
615 '^' => {
616 tokens.push(Token {
617 kind: TokenKind::Caret,
618 span,
619 });
620 pos += 1;
621 col += 1;
622 continue;
623 }
624 '<' => {
625 if pos + 1 < chars.len() && chars[pos + 1] == '<' {
626 tokens.push(Token {
627 kind: TokenKind::ShiftLeft,
628 span,
629 });
630 pos += 2;
631 col += 2;
632 } else if pos + 1 < chars.len() && chars[pos + 1] == '=' {
633 tokens.push(Token {
634 kind: TokenKind::LtEq,
635 span,
636 });
637 pos += 2;
638 col += 2;
639 } else {
640 tokens.push(Token {
641 kind: TokenKind::Lt,
642 span,
643 });
644 pos += 1;
645 col += 1;
646 }
647 continue;
648 }
649 '>' => {
650 if pos + 1 < chars.len() && chars[pos + 1] == '>' {
651 tokens.push(Token {
652 kind: TokenKind::ShiftRight,
653 span,
654 });
655 pos += 2;
656 col += 2;
657 } else if pos + 1 < chars.len() && chars[pos + 1] == '=' {
658 tokens.push(Token {
659 kind: TokenKind::GtEq,
660 span,
661 });
662 pos += 2;
663 col += 2;
664 } else {
665 tokens.push(Token {
666 kind: TokenKind::Gt,
667 span,
668 });
669 pos += 1;
670 col += 1;
671 }
672 continue;
673 }
674 '.' => {
675 tokens.push(Token {
676 kind: TokenKind::Dot,
677 span,
678 });
679 pos += 1;
680 col += 1;
681 continue;
682 }
683 '&' => {
684 if pos + 1 < chars.len() && chars[pos + 1] == '&' {
685 tokens.push(Token {
686 kind: TokenKind::AmpAmp,
687 span,
688 });
689 pos += 2;
690 col += 2;
691 continue;
692 }
693 tokens.push(Token {
694 kind: TokenKind::Ampersand,
695 span,
696 });
697 pos += 1;
698 col += 1;
699 continue;
700 }
701 '|' => {
702 if pos + 1 < chars.len() && chars[pos + 1] == '|' {
703 tokens.push(Token {
704 kind: TokenKind::PipePipe,
705 span,
706 });
707 pos += 2;
708 col += 2;
709 continue;
710 }
711 tokens.push(Token {
712 kind: TokenKind::Pipe,
713 span,
714 });
715 pos += 1;
716 col += 1;
717 continue;
718 }
719 '!' => {
720 if pos + 1 < chars.len() && chars[pos + 1] == '=' {
721 tokens.push(Token {
722 kind: TokenKind::BangEq,
723 span,
724 });
725 pos += 2;
726 col += 2;
727 } else {
728 tokens.push(Token {
729 kind: TokenKind::Bang,
730 span,
731 });
732 pos += 1;
733 col += 1;
734 }
735 continue;
736 }
737 _ => {}
738 }
739
740 if c == '"' || c == '\'' {
742 let quote = c;
743 pos += 1;
744 col += 1;
745 let mut s = String::new();
746 while pos < chars.len() && chars[pos] != quote {
747 if chars[pos] == '\\' && pos + 1 < chars.len() {
748 pos += 1;
749 col += 1;
750 match chars[pos] {
751 'n' => s.push('\n'),
752 't' => s.push('\t'),
753 '\\' => s.push('\\'),
754 c if c == quote => s.push(c),
755 other => {
756 s.push('\\');
757 s.push(other);
758 }
759 }
760 } else {
761 s.push(chars[pos]);
762 }
763 pos += 1;
764 col += 1;
765 }
766 if pos < chars.len() {
767 pos += 1; col += 1;
769 } else {
770 return Err(format!(
771 "unterminated string at line {}, col {}",
772 span.line, span.col
773 ));
774 }
775 tokens.push(Token {
776 kind: TokenKind::StringLit(s),
777 span,
778 });
779 continue;
780 }
781
782 if c.is_ascii_alphabetic() || c == '_' {
784 let start = pos;
785 while pos < chars.len() && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
786 pos += 1;
787 col += 1;
788 }
789 let word: String = chars[start..pos].iter().collect();
790 let kind = match word.as_str() {
791 "const" => TokenKind::Const,
792 "input" => TokenKind::Input,
793 "extern" => TokenKind::Extern,
794 "shared" => TokenKind::Shared,
795 "volatile" => TokenKind::Volatile,
796 "cursor" => TokenKind::Cursor,
797 "over" => TokenKind::Over,
798 "pragma" => TokenKind::Pragma,
799 "tile" => {
800 tile_pending = true;
801 TokenKind::Tile
802 }
803 "for" => {
804 let (text, consumed) = capture_for_text(&chars, pos);
805 for c in &chars[pos..pos + consumed] {
808 if *c == '\n' {
809 line += 1;
810 col = 1;
811 } else {
812 col += 1;
813 }
814 }
815 pos += consumed;
816 tokens.push(Token {
817 kind: TokenKind::For(text),
818 span,
819 });
820 continue;
821 }
822 _ => TokenKind::Ident(word),
823 };
824 tokens.push(Token { kind, span });
825 continue;
826 }
827
828 return Err(format!(
829 "unexpected character '{}' at line {}, col {}",
830 c, line, col
831 ));
832 }
833
834 tokens.push(Token {
835 kind: TokenKind::Eof,
836 span: Span { line, col },
837 });
838 Ok(tokens)
839}
840
841type TileBodyCapture = (String, crate::ast::TileBodyKind, usize, usize, usize);
851
852fn capture_tile_body(
853 chars: &[char],
854 start: usize,
855 start_col: usize,
856) -> Result<Option<TileBodyCapture>, String> {
857 use crate::ast::TileBodyKind;
858 let mut pos = start;
859 let mut col = start_col;
860 let mut newlines = 0;
861 while pos < chars.len() && chars[pos].is_whitespace() {
862 if chars[pos] == '\n' {
863 newlines += 1;
864 col = 1;
865 } else {
866 col += 1;
867 }
868 pos += 1;
869 }
870 if pos >= chars.len() {
871 return Ok(None);
872 }
873 let body_start = pos;
874 let kind;
875 match chars[pos] {
876 '{' | '[' => {
877 kind = TileBodyKind::Block;
878 let mut depth = 0i32;
879 let mut quote: Option<char> = None;
880 loop {
881 if pos >= chars.len() {
882 return Err("unterminated tile body: block never closed".to_string());
883 }
884 let c = chars[pos];
885 if c == '\n' {
886 newlines += 1;
887 col = 0;
888 }
889 if let Some(q) = quote {
890 if c == '\\' && pos + 1 < chars.len() {
891 pos += 2;
892 col += 2;
893 continue;
894 }
895 if c == q {
896 quote = None;
897 }
898 } else {
899 match c {
900 '"' => quote = Some(c),
901 '{' | '[' => depth += 1,
902 '}' | ']' => depth -= 1,
903 _ => {}
904 }
905 }
906 pos += 1;
907 col += 1;
908 if depth == 0 && quote.is_none() {
909 break;
910 }
911 }
912 }
913 '<' if chars[pos..].starts_with(&['<', '<', '<']) => {
914 kind = TileBodyKind::Heredoc;
915 pos += 3;
916 col += 3;
917 let text_start = pos;
918 loop {
919 if pos + 2 >= chars.len() {
920 return Err(
921 "unterminated tile body: heredoc never closed with `>>>`".to_string()
922 );
923 }
924 if chars[pos..].starts_with(&['>', '>', '>']) {
925 break;
926 }
927 if chars[pos] == '\n' {
928 newlines += 1;
929 col = 0;
930 }
931 pos += 1;
932 col += 1;
933 }
934 let mut text: String = chars[text_start..pos].iter().collect();
935 if let Some(t) = text.strip_prefix('\n') {
936 text = t.to_string();
937 }
938 if let Some(t) = text.strip_suffix('\n') {
939 text = t.to_string();
940 }
941 pos += 3;
942 col += 3;
943 return Ok(Some((text, kind, pos - start, newlines, col)));
944 }
945 _ => return Ok(None),
946 }
947 let text: String = chars[body_start..pos].iter().collect();
948 Ok(Some((
949 dedent_block(&text),
950 kind,
951 pos - start,
952 newlines,
953 col,
954 )))
955}
956
957fn dedent_block(text: &str) -> String {
962 let mut lines = text.split('\n');
963 let first = lines.next().unwrap_or("");
964 let rest: Vec<&str> = lines.collect();
965 let indent = rest
966 .iter()
967 .filter(|l| !l.trim().is_empty())
968 .map(|l| l.chars().take_while(|c| *c == ' ' || *c == '\t').count())
969 .min()
970 .unwrap_or(0);
971 if indent == 0 {
972 return text.to_string();
973 }
974 let mut out = String::with_capacity(text.len());
975 out.push_str(first);
976 for line in rest {
977 out.push('\n');
978 let skip = line
979 .chars()
980 .take_while(|c| *c == ' ' || *c == '\t')
981 .count()
982 .min(indent);
983 out.push_str(&line.chars().skip(skip).collect::<String>());
984 }
985 out
986}
987
988fn capture_for_text(chars: &[char], start: usize) -> (String, usize) {
996 let mut pos = start;
997 let mut depth = 0usize;
998 let mut text = String::new();
999 while pos < chars.len() {
1000 let c = chars[pos];
1001 match c {
1002 '\n' if depth == 0 => break,
1003 '\n' => {
1004 text.push(' ');
1007 pos += 1;
1008 continue;
1009 }
1010 '{' if depth == 0 => {
1011 match placeholder_len(chars, pos) {
1016 Some(n) => {
1017 text.extend(chars[pos..pos + n].iter());
1018 pos += n;
1019 continue;
1020 }
1021 None => break,
1022 }
1023 }
1024 '#' if depth == 0 => break,
1025 '/' if depth == 0 && pos + 1 < chars.len() && chars[pos + 1] == '/' => break,
1026 '#' => {
1027 skip_comment(chars, &mut pos);
1028 continue;
1029 }
1030 '/' if pos + 1 < chars.len() && chars[pos + 1] == '/' => {
1031 skip_comment(chars, &mut pos);
1032 continue;
1033 }
1034 '"' | '\'' => {
1035 let quote = c;
1036 text.push(c);
1037 pos += 1;
1038 while pos < chars.len() && chars[pos] != quote && chars[pos] != '\n' {
1039 if chars[pos] == '\\' && pos + 1 < chars.len() {
1040 text.push(chars[pos]);
1041 pos += 1;
1042 }
1043 text.push(chars[pos]);
1044 pos += 1;
1045 }
1046 if pos < chars.len() && chars[pos] == quote {
1047 text.push(quote);
1048 pos += 1;
1049 }
1050 continue;
1051 }
1052 '(' | '[' => depth += 1,
1053 ')' | ']' => depth = depth.saturating_sub(1),
1054 _ => {}
1055 }
1056 text.push(c);
1057 pos += 1;
1058 }
1059 (text.trim().to_string(), pos - start)
1060}
1061
1062fn placeholder_len(chars: &[char], pos: usize) -> Option<usize> {
1065 let mut i = pos + 1;
1066 let first = *chars.get(i)?;
1067 if !(first.is_ascii_alphabetic() || first == '_') {
1068 return None;
1069 }
1070 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
1071 i += 1;
1072 }
1073 (chars.get(i) == Some(&'}')).then_some(i + 1 - pos)
1074}
1075
1076fn peek_si_suffix(chars: &[char], pos: usize) -> Option<(u64, usize, bool)> {
1088 if pos >= chars.len() {
1089 return None;
1090 }
1091 if pos + 1 < chars.len() && chars[pos + 1] == 'i' {
1093 let mult = match chars[pos] {
1094 'K' => 1u64 << 10,
1095 'M' => 1u64 << 20,
1096 'G' => 1u64 << 30,
1097 'T' => 1u64 << 40,
1098 'P' => 1u64 << 50,
1099 _ => 0,
1100 };
1101 if mult > 0 {
1102 let next = chars.get(pos + 2);
1104 if !next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
1105 return Some((mult, 2, false));
1106 }
1107 }
1108 }
1109 let (mult, is_subunit) = match chars[pos] {
1111 'K' => (1_000u64, false),
1112 'M' => (1_000_000u64, false),
1113 'G' => (1_000_000_000u64, false),
1114 'T' => (1_000_000_000_000u64, false),
1115 'P' => (1_000_000_000_000_000u64, false),
1116 'm' => (1_000u64, true), 'u' => (1_000_000u64, true), 'n' => (1_000_000_000u64, true), _ => return None,
1120 };
1121 let next = chars.get(pos + 1);
1122 if !next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
1123 Some((mult, 1, is_subunit))
1124 } else {
1125 None
1126 }
1127}
1128
1129fn suffix_len_consumed(suffix: Option<(u64, bool)>) -> usize {
1134 match suffix {
1135 Some((m, _)) => {
1140 if m.is_power_of_two() && m >= (1 << 10) {
1141 2
1142 } else {
1143 1
1144 }
1145 }
1146 None => 0,
1147 }
1148}
1149
1150fn skip_comment(chars: &[char], pos: &mut usize) {
1153 while *pos < chars.len() && chars[*pos] != '\n' {
1154 *pos += 1;
1155 }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161
1162 #[test]
1163 fn lex_cycle_binding() {
1164 let tokens = lex("seed := hash(cycle)").unwrap();
1165 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
1166 assert!(matches!(tokens[1].kind, TokenKind::ColonEq));
1167 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "hash"));
1168 assert!(matches!(tokens[3].kind, TokenKind::LParen));
1169 assert!(matches!(tokens[4].kind, TokenKind::Ident(ref s) if s == "cycle"));
1170 assert!(matches!(tokens[5].kind, TokenKind::RParen));
1171 }
1172
1173 #[test]
1174 fn lex_const_binding() {
1175 let tokens = lex("const lut := dist_normal(72.0, 5.0)").unwrap();
1176 assert!(matches!(tokens[0].kind, TokenKind::Const));
1177 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "lut"));
1178 assert!(matches!(tokens[2].kind, TokenKind::ColonEq));
1179 assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "dist_normal"));
1180 assert!(matches!(tokens[5].kind, TokenKind::FloatLit(v) if v == 72.0));
1181 assert!(matches!(tokens[7].kind, TokenKind::FloatLit(v) if v == 5.0));
1182 }
1183
1184 #[test]
1185 fn lex_input_keyword_tuple() {
1186 let tokens = lex("input (cycle: u64, thread: u64)").unwrap();
1187 assert!(matches!(tokens[0].kind, TokenKind::Input));
1188 assert!(matches!(tokens[1].kind, TokenKind::LParen));
1189 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "cycle"));
1190 }
1191
1192 #[test]
1193 fn lex_destructuring() {
1194 let tokens = lex("(a, b, c) := mixed_radix(cycle, 100, 1000, 0)").unwrap();
1195 assert!(matches!(tokens[0].kind, TokenKind::LParen));
1196 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "a"));
1197 }
1198
1199 #[test]
1200 fn lex_string_with_interpolation() {
1201 let tokens = lex(r#"id := "{code}-{seq}""#).unwrap();
1202 assert!(matches!(tokens[2].kind, TokenKind::StringLit(ref s) if s == "{code}-{seq}"));
1204 }
1205
1206 #[test]
1207 fn lex_named_args() {
1208 let tokens = lex("dist_normal(mean: 72.0, stddev: 5.0)").unwrap();
1209 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "mean"));
1210 assert!(matches!(tokens[3].kind, TokenKind::Colon));
1211 assert!(matches!(tokens[4].kind, TokenKind::FloatLit(v) if v == 72.0));
1212 }
1213
1214 #[test]
1215 fn lex_array_literal() {
1216 let tokens = lex("[60.0, 20.0, 15.0, 5.0]").unwrap();
1217 assert!(matches!(tokens[0].kind, TokenKind::LBracket));
1218 assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if v == 60.0));
1219 assert!(matches!(tokens[8].kind, TokenKind::RBracket));
1220 }
1221
1222 #[test]
1223 fn lex_comments_stripped() {
1224 let tokens = lex("// this is a comment\nseed := hash(cycle)").unwrap();
1225 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
1226 }
1227
1228 #[test]
1229 fn lex_arrow() {
1230 let tokens = lex("(x: u64) -> (y: u64)").unwrap();
1231 assert!(matches!(tokens[5].kind, TokenKind::Arrow));
1234 }
1235
1236 #[test]
1237 fn lex_large_int() {
1238 let tokens = lex("1710000000000").unwrap();
1239 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1710000000000)));
1240 }
1241
1242 #[test]
1243 fn lex_hex_int() {
1244 let tokens = lex("0xFF").unwrap();
1245 assert!(matches!(tokens[0].kind, TokenKind::IntLit(255)));
1246 }
1247
1248 #[test]
1249 fn lex_block_comment() {
1250 let tokens = lex("a := /* skip this */ hash(b)").unwrap();
1251 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1252 assert!(matches!(tokens[1].kind, TokenKind::ColonEq));
1253 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "hash"));
1254 }
1255
1256 #[test]
1257 fn lex_block_comment_multiline() {
1258 let tokens = lex("a := 42\n/* this\nis\na\nblock */\nb := 7").unwrap();
1259 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1260 assert!(matches!(tokens[2].kind, TokenKind::IntLit(42)));
1261 assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "b"));
1262 }
1263
1264 #[test]
1265 fn lex_doc_comment() {
1266 let tokens = lex("/// doc comment\nseed := hash(cycle)").unwrap();
1268 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
1269 }
1270
1271 #[test]
1272 fn lex_input_keyword_bare() {
1273 let tokens = lex("input cycle: u64").unwrap();
1274 assert!(matches!(tokens[0].kind, TokenKind::Input));
1275 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "cycle"));
1276 assert!(matches!(tokens[2].kind, TokenKind::Colon));
1277 assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "u64"));
1278 }
1279
1280 #[test]
1281 fn lex_arithmetic_operators() {
1282 let tokens = lex("a + b * 2.0 - c / d % e ^ f").unwrap();
1283 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1284 assert!(matches!(tokens[1].kind, TokenKind::Plus));
1285 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "b"));
1286 assert!(matches!(tokens[3].kind, TokenKind::Star));
1287 assert!(matches!(tokens[4].kind, TokenKind::FloatLit(v) if v == 2.0));
1288 assert!(matches!(tokens[5].kind, TokenKind::Minus));
1289 assert!(matches!(tokens[6].kind, TokenKind::Ident(ref s) if s == "c"));
1290 assert!(matches!(tokens[7].kind, TokenKind::Slash));
1291 assert!(matches!(tokens[8].kind, TokenKind::Ident(ref s) if s == "d"));
1292 assert!(matches!(tokens[9].kind, TokenKind::Percent));
1293 assert!(matches!(tokens[10].kind, TokenKind::Ident(ref s) if s == "e"));
1294 assert!(matches!(tokens[11].kind, TokenKind::Caret));
1295 assert!(matches!(tokens[12].kind, TokenKind::Ident(ref s) if s == "f"));
1296 }
1297
1298 #[test]
1299 fn lex_minus_binary_vs_negative_literal() {
1300 let tokens = lex("x - 3").unwrap();
1302 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "x"));
1303 assert!(matches!(tokens[1].kind, TokenKind::Minus));
1304 assert!(matches!(tokens[2].kind, TokenKind::IntLit(3)));
1305 }
1306
1307 #[test]
1308 fn lex_negative_via_minus_token() {
1309 let tokens = lex("-3.0").unwrap();
1312 assert!(matches!(tokens[0].kind, TokenKind::Minus));
1313 assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if v == 3.0));
1314
1315 let tokens = lex("-3").unwrap();
1317 assert!(matches!(tokens[0].kind, TokenKind::Minus));
1318 assert!(matches!(tokens[1].kind, TokenKind::IntLit(3)));
1319 }
1320
1321 #[test]
1322 fn lex_scientific_notation() {
1323 let tokens = lex("1e10").unwrap();
1324 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
1325 }
1326
1327 #[test]
1328 fn lex_scientific_notation_negative_exponent() {
1329 let tokens = lex("1e-10").unwrap();
1330 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if *v > 0.0 && *v < 1e-5));
1331 }
1332
1333 #[test]
1334 fn lex_scientific_notation_with_decimal() {
1335 let tokens = lex("2.5e3").unwrap();
1336 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 2500.0).abs() < 0.1));
1337 }
1338
1339 #[test]
1340 fn lex_scientific_notation_positive_exponent() {
1341 let tokens = lex("3E+5").unwrap();
1342 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 3e5).abs() < 1.0));
1343 }
1344
1345 #[test]
1346 fn lex_sci_uppercase_e() {
1347 let t = lex("2.5E3").unwrap();
1348 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 2500.0).abs() < 0.1));
1349 }
1350
1351 #[test]
1352 fn lex_sci_explicit_positive_exp() {
1353 let t = lex("1e+10").unwrap();
1354 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
1355 }
1356
1357 #[test]
1358 fn lex_sci_decimal_negative_exp() {
1359 let t = lex("3.14e-2").unwrap();
1360 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 0.0314).abs() < 0.001));
1361 }
1362
1363 #[test]
1364 fn lex_sci_zero_exponent() {
1365 let t = lex("0.5e0").unwrap();
1366 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 0.5).abs() < 0.001));
1367 }
1368
1369 #[test]
1370 fn lex_sci_very_small() {
1371 let t = lex("1e-300").unwrap();
1372 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if *v > 0.0 && *v < 1e-299));
1373 }
1374
1375 #[test]
1376 fn lex_sci_uppercase_no_decimal() {
1377 let t = lex("1E10").unwrap();
1378 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
1379 }
1380
1381 #[test]
1382 fn lex_slash_vs_comment() {
1383 let tokens = lex("a / b // comment").unwrap();
1385 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1386 assert!(matches!(tokens[1].kind, TokenKind::Slash));
1387 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "b"));
1388 assert!(matches!(tokens[3].kind, TokenKind::Eof));
1389 }
1390
1391 #[test]
1394 fn lex_si_decimal_k_m_g_t_p() {
1395 let tokens = lex("1K 1M 1G 1T 1P").unwrap();
1396 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1_000)));
1397 assert!(matches!(tokens[1].kind, TokenKind::IntLit(1_000_000)));
1398 assert!(matches!(tokens[2].kind, TokenKind::IntLit(1_000_000_000)));
1399 assert!(matches!(
1400 tokens[3].kind,
1401 TokenKind::IntLit(1_000_000_000_000)
1402 ));
1403 assert!(matches!(
1404 tokens[4].kind,
1405 TokenKind::IntLit(1_000_000_000_000_000)
1406 ));
1407 }
1408
1409 #[test]
1410 fn lex_si_binary_ki_mi_gi_ti_pi() {
1411 let tokens = lex("1Ki 1Mi 1Gi 1Ti 1Pi").unwrap();
1412 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1024)));
1413 assert!(matches!(tokens[1].kind, TokenKind::IntLit(1_048_576)));
1414 assert!(matches!(tokens[2].kind, TokenKind::IntLit(1_073_741_824)));
1415 assert!(matches!(
1416 tokens[3].kind,
1417 TokenKind::IntLit(1_099_511_627_776)
1418 ));
1419 assert!(matches!(
1420 tokens[4].kind,
1421 TokenKind::IntLit(1_125_899_906_842_624)
1422 ));
1423 }
1424
1425 #[test]
1426 fn lex_si_subunit_m_u_n() {
1427 let tokens = lex("5m 5u 5n").unwrap();
1429 assert!(matches!(tokens[0].kind, TokenKind::FloatLit(v) if (v - 0.005).abs() < 1e-12));
1430 assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if (v - 0.000_005).abs() < 1e-15));
1431 assert!(
1432 matches!(tokens[2].kind, TokenKind::FloatLit(v) if (v - 0.000_000_005).abs() < 1e-18)
1433 );
1434 }
1435
1436 #[test]
1437 fn lex_si_float_base_with_decimal_suffix() {
1438 let tokens = lex("1.5K").unwrap();
1440 assert!(
1441 matches!(tokens[0].kind, TokenKind::IntLit(1_500)),
1442 "1.5K should be IntLit(1500), got {:?}",
1443 tokens[0].kind
1444 );
1445 }
1446
1447 #[test]
1448 fn lex_si_float_base_non_integral_stays_float() {
1449 let tokens = lex("1.5m").unwrap();
1456 assert!(matches!(tokens[0].kind, TokenKind::FloatLit(v) if (v - 0.0015).abs() < 1e-12));
1457 }
1458
1459 #[test]
1460 fn lex_si_kilometers_stays_identifier() {
1461 let tokens = lex("1 Kilometers").unwrap();
1464 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1)));
1465 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "Kilometers"));
1466 }
1467
1468 #[test]
1469 fn lex_si_overflow_errors_loud() {
1470 let err = lex("100000P").unwrap_err();
1474 assert!(err.contains("overflows u64"), "{err}");
1475 }
1476
1477 #[test]
1478 fn lex_si_in_range_expression() {
1479 let tokens = lex("1K..1M..100K").unwrap();
1483 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1_000)));
1484 let int_lits: Vec<u64> = tokens
1488 .iter()
1489 .filter_map(|t| match &t.kind {
1490 TokenKind::IntLit(v) => Some(*v),
1491 _ => None,
1492 })
1493 .collect();
1494 assert_eq!(int_lits, vec![1_000, 1_000_000, 100_000]);
1495 }
1496
1497 #[test]
1498 fn lex_si_disambiguation_two_char_first() {
1499 let tokens = lex("1Ki").unwrap();
1502 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1024)));
1503 assert!(matches!(tokens[1].kind, TokenKind::Eof));
1505 }
1506
1507 #[test]
1508 fn lex_si_followed_by_operator_applies_suffix() {
1509 let tokens = lex("1K+5").unwrap();
1511 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1000)));
1512 assert!(matches!(tokens[1].kind, TokenKind::Plus));
1513 assert!(matches!(tokens[2].kind, TokenKind::IntLit(5)));
1514 }
1515}