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,
69 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 pos += consumed;
806 col += consumed;
807 tokens.push(Token {
808 kind: TokenKind::For(text),
809 span,
810 });
811 continue;
812 }
813 _ => TokenKind::Ident(word),
814 };
815 tokens.push(Token { kind, span });
816 continue;
817 }
818
819 return Err(format!(
820 "unexpected character '{}' at line {}, col {}",
821 c, line, col
822 ));
823 }
824
825 tokens.push(Token {
826 kind: TokenKind::Eof,
827 span: Span { line, col },
828 });
829 Ok(tokens)
830}
831
832type TileBodyCapture = (String, crate::ast::TileBodyKind, usize, usize, usize);
842
843fn capture_tile_body(
844 chars: &[char],
845 start: usize,
846 start_col: usize,
847) -> Result<Option<TileBodyCapture>, String> {
848 use crate::ast::TileBodyKind;
849 let mut pos = start;
850 let mut col = start_col;
851 let mut newlines = 0;
852 while pos < chars.len() && chars[pos].is_whitespace() {
853 if chars[pos] == '\n' {
854 newlines += 1;
855 col = 1;
856 } else {
857 col += 1;
858 }
859 pos += 1;
860 }
861 if pos >= chars.len() {
862 return Ok(None);
863 }
864 let body_start = pos;
865 let kind;
866 match chars[pos] {
867 '{' | '[' => {
868 kind = TileBodyKind::Block;
869 let mut depth = 0i32;
870 let mut quote: Option<char> = None;
871 loop {
872 if pos >= chars.len() {
873 return Err("unterminated tile body: block never closed".to_string());
874 }
875 let c = chars[pos];
876 if c == '\n' {
877 newlines += 1;
878 col = 0;
879 }
880 if let Some(q) = quote {
881 if c == '\\' && pos + 1 < chars.len() {
882 pos += 2;
883 col += 2;
884 continue;
885 }
886 if c == q {
887 quote = None;
888 }
889 } else {
890 match c {
891 '"' => quote = Some(c),
892 '{' | '[' => depth += 1,
893 '}' | ']' => depth -= 1,
894 _ => {}
895 }
896 }
897 pos += 1;
898 col += 1;
899 if depth == 0 && quote.is_none() {
900 break;
901 }
902 }
903 }
904 '<' if chars[pos..].starts_with(&['<', '<', '<']) => {
905 kind = TileBodyKind::Heredoc;
906 pos += 3;
907 col += 3;
908 let text_start = pos;
909 loop {
910 if pos + 2 >= chars.len() {
911 return Err(
912 "unterminated tile body: heredoc never closed with `>>>`".to_string()
913 );
914 }
915 if chars[pos..].starts_with(&['>', '>', '>']) {
916 break;
917 }
918 if chars[pos] == '\n' {
919 newlines += 1;
920 col = 0;
921 }
922 pos += 1;
923 col += 1;
924 }
925 let mut text: String = chars[text_start..pos].iter().collect();
926 if let Some(t) = text.strip_prefix('\n') {
927 text = t.to_string();
928 }
929 if let Some(t) = text.strip_suffix('\n') {
930 text = t.to_string();
931 }
932 pos += 3;
933 col += 3;
934 return Ok(Some((text, kind, pos - start, newlines, col)));
935 }
936 _ => return Ok(None),
937 }
938 let text: String = chars[body_start..pos].iter().collect();
939 Ok(Some((
940 dedent_block(&text),
941 kind,
942 pos - start,
943 newlines,
944 col,
945 )))
946}
947
948fn dedent_block(text: &str) -> String {
953 let mut lines = text.split('\n');
954 let first = lines.next().unwrap_or("");
955 let rest: Vec<&str> = lines.collect();
956 let indent = rest
957 .iter()
958 .filter(|l| !l.trim().is_empty())
959 .map(|l| l.chars().take_while(|c| *c == ' ' || *c == '\t').count())
960 .min()
961 .unwrap_or(0);
962 if indent == 0 {
963 return text.to_string();
964 }
965 let mut out = String::with_capacity(text.len());
966 out.push_str(first);
967 for line in rest {
968 out.push('\n');
969 let skip = line
970 .chars()
971 .take_while(|c| *c == ' ' || *c == '\t')
972 .count()
973 .min(indent);
974 out.push_str(&line.chars().skip(skip).collect::<String>());
975 }
976 out
977}
978
979fn capture_for_text(chars: &[char], start: usize) -> (String, usize) {
987 let mut pos = start;
988 let mut depth = 0usize;
989 let mut text = String::new();
990 while pos < chars.len() {
991 let c = chars[pos];
992 match c {
993 '\n' => break,
994 '{' if depth == 0 => {
995 match placeholder_len(chars, pos) {
1000 Some(n) => {
1001 text.extend(chars[pos..pos + n].iter());
1002 pos += n;
1003 continue;
1004 }
1005 None => break,
1006 }
1007 }
1008 '#' => break,
1009 '/' if pos + 1 < chars.len() && chars[pos + 1] == '/' => break,
1010 '"' | '\'' => {
1011 let quote = c;
1012 text.push(c);
1013 pos += 1;
1014 while pos < chars.len() && chars[pos] != quote && chars[pos] != '\n' {
1015 if chars[pos] == '\\' && pos + 1 < chars.len() {
1016 text.push(chars[pos]);
1017 pos += 1;
1018 }
1019 text.push(chars[pos]);
1020 pos += 1;
1021 }
1022 if pos < chars.len() && chars[pos] == quote {
1023 text.push(quote);
1024 pos += 1;
1025 }
1026 continue;
1027 }
1028 '(' | '[' => depth += 1,
1029 ')' | ']' => depth = depth.saturating_sub(1),
1030 _ => {}
1031 }
1032 text.push(c);
1033 pos += 1;
1034 }
1035 (text.trim().to_string(), pos - start)
1036}
1037
1038fn placeholder_len(chars: &[char], pos: usize) -> Option<usize> {
1041 let mut i = pos + 1;
1042 let first = *chars.get(i)?;
1043 if !(first.is_ascii_alphabetic() || first == '_') {
1044 return None;
1045 }
1046 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
1047 i += 1;
1048 }
1049 (chars.get(i) == Some(&'}')).then_some(i + 1 - pos)
1050}
1051
1052fn peek_si_suffix(chars: &[char], pos: usize) -> Option<(u64, usize, bool)> {
1065 if pos >= chars.len() {
1066 return None;
1067 }
1068 if pos + 1 < chars.len() && chars[pos + 1] == 'i' {
1070 let mult = match chars[pos] {
1071 'K' => 1u64 << 10,
1072 'M' => 1u64 << 20,
1073 'G' => 1u64 << 30,
1074 'T' => 1u64 << 40,
1075 'P' => 1u64 << 50,
1076 _ => 0,
1077 };
1078 if mult > 0 {
1079 let next = chars.get(pos + 2);
1081 if !next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
1082 return Some((mult, 2, false));
1083 }
1084 }
1085 }
1086 let (mult, is_subunit) = match chars[pos] {
1088 'K' => (1_000u64, false),
1089 'M' => (1_000_000u64, false),
1090 'G' => (1_000_000_000u64, false),
1091 'T' => (1_000_000_000_000u64, false),
1092 'P' => (1_000_000_000_000_000u64, false),
1093 'm' => (1_000u64, true), 'u' => (1_000_000u64, true), 'n' => (1_000_000_000u64, true), _ => return None,
1097 };
1098 let next = chars.get(pos + 1);
1099 if !next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
1100 Some((mult, 1, is_subunit))
1101 } else {
1102 None
1103 }
1104}
1105
1106fn suffix_len_consumed(suffix: Option<(u64, bool)>) -> usize {
1111 match suffix {
1112 Some((m, _)) => {
1117 if m.is_power_of_two() && m >= (1 << 10) {
1118 2
1119 } else {
1120 1
1121 }
1122 }
1123 None => 0,
1124 }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use super::*;
1130
1131 #[test]
1132 fn lex_cycle_binding() {
1133 let tokens = lex("seed := hash(cycle)").unwrap();
1134 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
1135 assert!(matches!(tokens[1].kind, TokenKind::ColonEq));
1136 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "hash"));
1137 assert!(matches!(tokens[3].kind, TokenKind::LParen));
1138 assert!(matches!(tokens[4].kind, TokenKind::Ident(ref s) if s == "cycle"));
1139 assert!(matches!(tokens[5].kind, TokenKind::RParen));
1140 }
1141
1142 #[test]
1143 fn lex_const_binding() {
1144 let tokens = lex("const lut := dist_normal(72.0, 5.0)").unwrap();
1145 assert!(matches!(tokens[0].kind, TokenKind::Const));
1146 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "lut"));
1147 assert!(matches!(tokens[2].kind, TokenKind::ColonEq));
1148 assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "dist_normal"));
1149 assert!(matches!(tokens[5].kind, TokenKind::FloatLit(v) if v == 72.0));
1150 assert!(matches!(tokens[7].kind, TokenKind::FloatLit(v) if v == 5.0));
1151 }
1152
1153 #[test]
1154 fn lex_input_keyword_tuple() {
1155 let tokens = lex("input (cycle: u64, thread: u64)").unwrap();
1156 assert!(matches!(tokens[0].kind, TokenKind::Input));
1157 assert!(matches!(tokens[1].kind, TokenKind::LParen));
1158 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "cycle"));
1159 }
1160
1161 #[test]
1162 fn lex_destructuring() {
1163 let tokens = lex("(a, b, c) := mixed_radix(cycle, 100, 1000, 0)").unwrap();
1164 assert!(matches!(tokens[0].kind, TokenKind::LParen));
1165 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "a"));
1166 }
1167
1168 #[test]
1169 fn lex_string_with_interpolation() {
1170 let tokens = lex(r#"id := "{code}-{seq}""#).unwrap();
1171 assert!(matches!(tokens[2].kind, TokenKind::StringLit(ref s) if s == "{code}-{seq}"));
1173 }
1174
1175 #[test]
1176 fn lex_named_args() {
1177 let tokens = lex("dist_normal(mean: 72.0, stddev: 5.0)").unwrap();
1178 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "mean"));
1179 assert!(matches!(tokens[3].kind, TokenKind::Colon));
1180 assert!(matches!(tokens[4].kind, TokenKind::FloatLit(v) if v == 72.0));
1181 }
1182
1183 #[test]
1184 fn lex_array_literal() {
1185 let tokens = lex("[60.0, 20.0, 15.0, 5.0]").unwrap();
1186 assert!(matches!(tokens[0].kind, TokenKind::LBracket));
1187 assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if v == 60.0));
1188 assert!(matches!(tokens[8].kind, TokenKind::RBracket));
1189 }
1190
1191 #[test]
1192 fn lex_comments_stripped() {
1193 let tokens = lex("// this is a comment\nseed := hash(cycle)").unwrap();
1194 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
1195 }
1196
1197 #[test]
1198 fn lex_arrow() {
1199 let tokens = lex("(x: u64) -> (y: u64)").unwrap();
1200 assert!(matches!(tokens[5].kind, TokenKind::Arrow));
1203 }
1204
1205 #[test]
1206 fn lex_large_int() {
1207 let tokens = lex("1710000000000").unwrap();
1208 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1710000000000)));
1209 }
1210
1211 #[test]
1212 fn lex_hex_int() {
1213 let tokens = lex("0xFF").unwrap();
1214 assert!(matches!(tokens[0].kind, TokenKind::IntLit(255)));
1215 }
1216
1217 #[test]
1218 fn lex_block_comment() {
1219 let tokens = lex("a := /* skip this */ hash(b)").unwrap();
1220 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1221 assert!(matches!(tokens[1].kind, TokenKind::ColonEq));
1222 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "hash"));
1223 }
1224
1225 #[test]
1226 fn lex_block_comment_multiline() {
1227 let tokens = lex("a := 42\n/* this\nis\na\nblock */\nb := 7").unwrap();
1228 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1229 assert!(matches!(tokens[2].kind, TokenKind::IntLit(42)));
1230 assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "b"));
1231 }
1232
1233 #[test]
1234 fn lex_doc_comment() {
1235 let tokens = lex("/// doc comment\nseed := hash(cycle)").unwrap();
1237 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
1238 }
1239
1240 #[test]
1241 fn lex_input_keyword_bare() {
1242 let tokens = lex("input cycle: u64").unwrap();
1243 assert!(matches!(tokens[0].kind, TokenKind::Input));
1244 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "cycle"));
1245 assert!(matches!(tokens[2].kind, TokenKind::Colon));
1246 assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "u64"));
1247 }
1248
1249 #[test]
1250 fn lex_arithmetic_operators() {
1251 let tokens = lex("a + b * 2.0 - c / d % e ^ f").unwrap();
1252 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1253 assert!(matches!(tokens[1].kind, TokenKind::Plus));
1254 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "b"));
1255 assert!(matches!(tokens[3].kind, TokenKind::Star));
1256 assert!(matches!(tokens[4].kind, TokenKind::FloatLit(v) if v == 2.0));
1257 assert!(matches!(tokens[5].kind, TokenKind::Minus));
1258 assert!(matches!(tokens[6].kind, TokenKind::Ident(ref s) if s == "c"));
1259 assert!(matches!(tokens[7].kind, TokenKind::Slash));
1260 assert!(matches!(tokens[8].kind, TokenKind::Ident(ref s) if s == "d"));
1261 assert!(matches!(tokens[9].kind, TokenKind::Percent));
1262 assert!(matches!(tokens[10].kind, TokenKind::Ident(ref s) if s == "e"));
1263 assert!(matches!(tokens[11].kind, TokenKind::Caret));
1264 assert!(matches!(tokens[12].kind, TokenKind::Ident(ref s) if s == "f"));
1265 }
1266
1267 #[test]
1268 fn lex_minus_binary_vs_negative_literal() {
1269 let tokens = lex("x - 3").unwrap();
1271 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "x"));
1272 assert!(matches!(tokens[1].kind, TokenKind::Minus));
1273 assert!(matches!(tokens[2].kind, TokenKind::IntLit(3)));
1274 }
1275
1276 #[test]
1277 fn lex_negative_via_minus_token() {
1278 let tokens = lex("-3.0").unwrap();
1281 assert!(matches!(tokens[0].kind, TokenKind::Minus));
1282 assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if v == 3.0));
1283
1284 let tokens = lex("-3").unwrap();
1286 assert!(matches!(tokens[0].kind, TokenKind::Minus));
1287 assert!(matches!(tokens[1].kind, TokenKind::IntLit(3)));
1288 }
1289
1290 #[test]
1291 fn lex_scientific_notation() {
1292 let tokens = lex("1e10").unwrap();
1293 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
1294 }
1295
1296 #[test]
1297 fn lex_scientific_notation_negative_exponent() {
1298 let tokens = lex("1e-10").unwrap();
1299 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if *v > 0.0 && *v < 1e-5));
1300 }
1301
1302 #[test]
1303 fn lex_scientific_notation_with_decimal() {
1304 let tokens = lex("2.5e3").unwrap();
1305 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 2500.0).abs() < 0.1));
1306 }
1307
1308 #[test]
1309 fn lex_scientific_notation_positive_exponent() {
1310 let tokens = lex("3E+5").unwrap();
1311 assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 3e5).abs() < 1.0));
1312 }
1313
1314 #[test]
1315 fn lex_sci_uppercase_e() {
1316 let t = lex("2.5E3").unwrap();
1317 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 2500.0).abs() < 0.1));
1318 }
1319
1320 #[test]
1321 fn lex_sci_explicit_positive_exp() {
1322 let t = lex("1e+10").unwrap();
1323 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
1324 }
1325
1326 #[test]
1327 fn lex_sci_decimal_negative_exp() {
1328 let t = lex("3.14e-2").unwrap();
1329 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 0.0314).abs() < 0.001));
1330 }
1331
1332 #[test]
1333 fn lex_sci_zero_exponent() {
1334 let t = lex("0.5e0").unwrap();
1335 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 0.5).abs() < 0.001));
1336 }
1337
1338 #[test]
1339 fn lex_sci_very_small() {
1340 let t = lex("1e-300").unwrap();
1341 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if *v > 0.0 && *v < 1e-299));
1342 }
1343
1344 #[test]
1345 fn lex_sci_uppercase_no_decimal() {
1346 let t = lex("1E10").unwrap();
1347 assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
1348 }
1349
1350 #[test]
1351 fn lex_slash_vs_comment() {
1352 let tokens = lex("a / b // comment").unwrap();
1354 assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
1355 assert!(matches!(tokens[1].kind, TokenKind::Slash));
1356 assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "b"));
1357 assert!(matches!(tokens[3].kind, TokenKind::Eof));
1358 }
1359
1360 #[test]
1363 fn lex_si_decimal_k_m_g_t_p() {
1364 let tokens = lex("1K 1M 1G 1T 1P").unwrap();
1365 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1_000)));
1366 assert!(matches!(tokens[1].kind, TokenKind::IntLit(1_000_000)));
1367 assert!(matches!(tokens[2].kind, TokenKind::IntLit(1_000_000_000)));
1368 assert!(matches!(
1369 tokens[3].kind,
1370 TokenKind::IntLit(1_000_000_000_000)
1371 ));
1372 assert!(matches!(
1373 tokens[4].kind,
1374 TokenKind::IntLit(1_000_000_000_000_000)
1375 ));
1376 }
1377
1378 #[test]
1379 fn lex_si_binary_ki_mi_gi_ti_pi() {
1380 let tokens = lex("1Ki 1Mi 1Gi 1Ti 1Pi").unwrap();
1381 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1024)));
1382 assert!(matches!(tokens[1].kind, TokenKind::IntLit(1_048_576)));
1383 assert!(matches!(tokens[2].kind, TokenKind::IntLit(1_073_741_824)));
1384 assert!(matches!(
1385 tokens[3].kind,
1386 TokenKind::IntLit(1_099_511_627_776)
1387 ));
1388 assert!(matches!(
1389 tokens[4].kind,
1390 TokenKind::IntLit(1_125_899_906_842_624)
1391 ));
1392 }
1393
1394 #[test]
1395 fn lex_si_subunit_m_u_n() {
1396 let tokens = lex("5m 5u 5n").unwrap();
1398 assert!(matches!(tokens[0].kind, TokenKind::FloatLit(v) if (v - 0.005).abs() < 1e-12));
1399 assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if (v - 0.000_005).abs() < 1e-15));
1400 assert!(
1401 matches!(tokens[2].kind, TokenKind::FloatLit(v) if (v - 0.000_000_005).abs() < 1e-18)
1402 );
1403 }
1404
1405 #[test]
1406 fn lex_si_float_base_with_decimal_suffix() {
1407 let tokens = lex("1.5K").unwrap();
1409 assert!(
1410 matches!(tokens[0].kind, TokenKind::IntLit(1_500)),
1411 "1.5K should be IntLit(1500), got {:?}",
1412 tokens[0].kind
1413 );
1414 }
1415
1416 #[test]
1417 fn lex_si_float_base_non_integral_stays_float() {
1418 let tokens = lex("1.5m").unwrap();
1425 assert!(matches!(tokens[0].kind, TokenKind::FloatLit(v) if (v - 0.0015).abs() < 1e-12));
1426 }
1427
1428 #[test]
1429 fn lex_si_kilometers_stays_identifier() {
1430 let tokens = lex("1 Kilometers").unwrap();
1433 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1)));
1434 assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "Kilometers"));
1435 }
1436
1437 #[test]
1438 fn lex_si_overflow_errors_loud() {
1439 let err = lex("100000P").unwrap_err();
1443 assert!(err.contains("overflows u64"), "{err}");
1444 }
1445
1446 #[test]
1447 fn lex_si_in_range_expression() {
1448 let tokens = lex("1K..1M..100K").unwrap();
1453 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1_000)));
1454 let int_lits: Vec<u64> = tokens
1460 .iter()
1461 .filter_map(|t| match &t.kind {
1462 TokenKind::IntLit(v) => Some(*v),
1463 _ => None,
1464 })
1465 .collect();
1466 assert_eq!(int_lits, vec![1_000, 1_000_000, 100_000]);
1467 }
1468
1469 #[test]
1470 fn lex_si_disambiguation_two_char_first() {
1471 let tokens = lex("1Ki").unwrap();
1474 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1024)));
1475 assert!(matches!(tokens[1].kind, TokenKind::Eof));
1477 }
1478
1479 #[test]
1480 fn lex_si_followed_by_operator_applies_suffix() {
1481 let tokens = lex("1K+5").unwrap();
1483 assert!(matches!(tokens[0].kind, TokenKind::IntLit(1000)));
1484 assert!(matches!(tokens[1].kind, TokenKind::Plus));
1485 assert!(matches!(tokens[2].kind, TokenKind::IntLit(5)));
1486 }
1487}