1use logicaffeine_base::Interner;
38use crate::lexicon::{self, Aspect, Definiteness, Lexicon, Time};
39use crate::token::{BlockType, CalendarUnit, FocusKind, MeasureKind, Span, Token, TokenType};
40
41#[derive(Debug, Clone, PartialEq)]
49pub enum LineToken {
50 Indent,
52 Dedent,
54 Newline,
56 Content { text: String, start: usize, end: usize },
58}
59
60pub struct LineLexer<'a> {
63 source: &'a str,
64 bytes: &'a [u8],
65 indent_stack: Vec<usize>,
66 pending_dedents: usize,
67 position: usize,
68 has_pending_content: bool,
70 pending_content_start: usize,
71 pending_content_end: usize,
72 pending_content_text: String,
73 finished_lines: bool,
75 emitted_indent: bool,
77 escape_body_ranges: Vec<(usize, usize)>,
79}
80
81impl<'a> LineLexer<'a> {
82 pub fn new(source: &'a str) -> Self {
83 Self {
84 source,
85 bytes: source.as_bytes(),
86 indent_stack: vec![0],
87 pending_dedents: 0,
88 position: 0,
89 has_pending_content: false,
90 pending_content_start: 0,
91 pending_content_end: 0,
92 pending_content_text: String::new(),
93 finished_lines: false,
94 emitted_indent: false,
95 escape_body_ranges: Vec::new(),
96 }
97 }
98
99 pub fn with_escape_ranges(source: &'a str, escape_body_ranges: Vec<(usize, usize)>) -> Self {
100 Self {
101 source,
102 bytes: source.as_bytes(),
103 indent_stack: vec![0],
104 pending_dedents: 0,
105 position: 0,
106 has_pending_content: false,
107 pending_content_start: 0,
108 pending_content_end: 0,
109 pending_content_text: String::new(),
110 finished_lines: false,
111 emitted_indent: false,
112 escape_body_ranges,
113 }
114 }
115
116 fn is_in_escape_body(&self, pos: usize) -> bool {
118 self.escape_body_ranges.iter().any(|(start, end)| pos >= *start && pos < *end)
119 }
120
121 fn measure_indent(&self, line_start: usize) -> (usize, usize) {
124 let mut indent = 0;
125 let mut pos = line_start;
126
127 while pos < self.bytes.len() {
128 match self.bytes[pos] {
129 b' ' => {
130 indent += 1;
131 pos += 1;
132 }
133 b'\t' => {
134 indent += 4; pos += 1;
136 }
137 _ => break,
138 }
139 }
140
141 (indent, pos)
142 }
143
144 fn read_line_content(&self, content_start: usize) -> (String, usize, usize, usize) {
147 let mut pos = content_start;
148
149 while pos < self.bytes.len() && self.bytes[pos] != b'\n' {
151 pos += 1;
152 }
153
154 let content_end = pos;
155 let text = self.source[content_start..content_end].trim_end().to_string();
156
157 let next_line_start = if pos < self.bytes.len() && self.bytes[pos] == b'\n' {
159 pos + 1
160 } else {
161 pos
162 };
163
164 (text, content_start, content_end, next_line_start)
165 }
166
167 fn is_blank_line(&self, line_start: usize) -> bool {
169 let mut pos = line_start;
170 while pos < self.bytes.len() {
171 match self.bytes[pos] {
172 b' ' | b'\t' => pos += 1,
173 b'\n' => return true,
174 _ => return false,
175 }
176 }
177 true }
179
180 fn process_next_line(&mut self) -> bool {
183 while self.position < self.bytes.len() && self.is_blank_line(self.position) {
185 while self.position < self.bytes.len() && self.bytes[self.position] != b'\n' {
187 self.position += 1;
188 }
189 if self.position < self.bytes.len() {
190 self.position += 1; }
192 }
193
194 if self.position >= self.bytes.len() {
196 self.finished_lines = true;
197 if self.indent_stack.len() > 1 {
199 self.pending_dedents = self.indent_stack.len() - 1;
200 self.indent_stack.truncate(1);
201 }
202 return self.pending_dedents > 0;
203 }
204
205 let (line_indent, content_start) = self.measure_indent(self.position);
207
208 let (text, start, end, next_pos) = self.read_line_content(content_start);
210
211 if text.is_empty() {
213 self.position = next_pos;
214 return self.process_next_line();
215 }
216
217 let current_indent = *self.indent_stack.last().unwrap();
218
219 if line_indent > current_indent {
221 self.indent_stack.push(line_indent);
223 self.emitted_indent = true;
224 self.has_pending_content = true;
226 self.pending_content_text = text;
227 self.pending_content_start = start;
228 self.pending_content_end = end;
229 self.position = next_pos;
230 return true;
232 } else if line_indent < current_indent {
233 while self.indent_stack.len() > 1 {
235 let top = *self.indent_stack.last().unwrap();
236 if line_indent < top {
237 self.indent_stack.pop();
238 self.pending_dedents += 1;
239 } else {
240 break;
241 }
242 }
243 self.has_pending_content = true;
245 self.pending_content_text = text;
246 self.pending_content_start = start;
247 self.pending_content_end = end;
248 self.position = next_pos;
249 return true;
250 } else {
251 self.has_pending_content = true;
253 self.pending_content_text = text;
254 self.pending_content_start = start;
255 self.pending_content_end = end;
256 self.position = next_pos;
257 return true;
258 }
259 }
260}
261
262impl<'a> Iterator for LineLexer<'a> {
263 type Item = LineToken;
264
265 fn next(&mut self) -> Option<LineToken> {
266 if self.pending_dedents > 0 {
268 self.pending_dedents -= 1;
269 return Some(LineToken::Dedent);
270 }
271
272 if self.has_pending_content {
274 self.has_pending_content = false;
275 let text = std::mem::take(&mut self.pending_content_text);
276 let start = self.pending_content_start;
277 let end = self.pending_content_end;
278 return Some(LineToken::Content { text, start, end });
279 }
280
281 if !self.finished_lines {
287 let had_indent = self.indent_stack.len();
288 if self.process_next_line() {
289 if self.indent_stack.len() > had_indent {
291 return Some(LineToken::Indent);
292 }
293 if self.pending_dedents > 0 {
295 self.pending_dedents -= 1;
296 return Some(LineToken::Dedent);
297 }
298 if self.has_pending_content {
300 self.has_pending_content = false;
301 let text = std::mem::take(&mut self.pending_content_text);
302 let start = self.pending_content_start;
303 let end = self.pending_content_end;
304 return Some(LineToken::Content { text, start, end });
305 }
306 } else if self.pending_dedents > 0 {
307 self.pending_dedents -= 1;
309 return Some(LineToken::Dedent);
310 }
311 }
312
313 if self.pending_dedents > 0 {
315 self.pending_dedents -= 1;
316 return Some(LineToken::Dedent);
317 }
318
319 None
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
328pub enum LexerMode {
329 #[default]
330 Declarative, Imperative, }
333
334pub struct Lexer<'a> {
335 words: Vec<WordItem>,
336 pos: usize,
337 lexicon: Lexicon,
338 interner: &'a mut Interner,
339 input_len: usize,
340 in_let_context: bool,
341 mode: LexerMode,
342 source: String,
343 escape_body_ranges: Vec<(usize, usize)>,
345}
346
347struct WordItem {
348 word: String,
349 trailing_punct: Option<char>,
350 start: usize,
351 end: usize,
352 punct_pos: Option<usize>,
353}
354
355impl<'a> Lexer<'a> {
356 pub fn new(input: &str, interner: &'a mut Interner) -> Self {
380 let escape_ranges = Self::find_escape_block_ranges(input);
381 let escape_body_ranges: Vec<(usize, usize)> = escape_ranges.iter()
382 .map(|(_, end, content_start, _)| (*content_start, *end))
383 .collect();
384 let words = Self::split_into_words(input, &escape_ranges);
385 let input_len = input.len();
386
387 Lexer {
388 words,
389 pos: 0,
390 lexicon: Lexicon::new(),
391 interner,
392 input_len,
393 in_let_context: false,
394 mode: LexerMode::Declarative,
395 source: input.to_string(),
396 escape_body_ranges,
397 }
398 }
399
400 fn find_escape_block_ranges(source: &str) -> Vec<(usize, usize, usize, String)> {
405 let mut ranges = Vec::new();
406 let lines: Vec<&str> = source.split('\n').collect();
407 let mut line_starts: Vec<usize> = Vec::with_capacity(lines.len());
408 let mut pos = 0;
409 for line in &lines {
410 line_starts.push(pos);
411 pos += line.len() + 1; }
413
414 let mut i = 0;
415 while i < lines.len() {
416 let trimmed = lines[i].trim();
417 let lower = trimmed.to_lowercase();
421 if lower == "escape to rust:" ||
422 lower.ends_with(" escape to rust:") ||
423 (lower.starts_with("escape to ") && lower.ends_with(':'))
424 {
425 let header_indent = Self::measure_indent_static(lines[i]);
427 i += 1;
428
429 let mut body_start_line = i;
431 while body_start_line < lines.len() && lines[body_start_line].trim().is_empty() {
432 body_start_line += 1;
433 }
434
435 if body_start_line >= lines.len() {
436 continue;
438 }
439
440 let base_indent = Self::measure_indent_static(lines[body_start_line]);
441 if base_indent <= header_indent {
442 continue;
444 }
445
446 let body_byte_start = line_starts[body_start_line];
448 let mut body_end_line = body_start_line;
449 let mut code_lines: Vec<String> = Vec::new();
450
451 let mut j = body_start_line;
452 while j < lines.len() {
453 let line = lines[j];
454 if line.trim().is_empty() {
455 code_lines.push(String::new());
457 body_end_line = j;
458 j += 1;
459 continue;
460 }
461 let line_indent = Self::measure_indent_static(line);
462 if line_indent < base_indent {
463 break;
464 }
465 let stripped = Self::strip_indent(line, base_indent);
467 code_lines.push(stripped);
468 body_end_line = j;
469 j += 1;
470 }
471
472 while code_lines.last().map_or(false, |l| l.is_empty()) {
474 code_lines.pop();
475 }
476
477 if !code_lines.is_empty() {
478 let body_byte_end = if body_end_line + 1 < lines.len() {
479 line_starts[body_end_line + 1]
480 } else {
481 source.len()
482 };
483 let content_start = body_byte_start + Self::leading_whitespace_bytes(lines[body_start_line]);
485 let raw_code = code_lines.join("\n");
486 ranges.push((body_byte_start, body_byte_end, content_start, raw_code));
487 }
488
489 i = j;
490 } else {
491 i += 1;
492 }
493 }
494
495 ranges
496 }
497
498 fn leading_whitespace_bytes(line: &str) -> usize {
500 let mut count = 0;
501 for c in line.chars() {
502 match c {
503 ' ' | '\t' => count += c.len_utf8(),
504 _ => break,
505 }
506 }
507 count
508 }
509
510 fn measure_indent_static(line: &str) -> usize {
512 let mut indent = 0;
513 for c in line.chars() {
514 match c {
515 ' ' => indent += 1,
516 '\t' => indent += 4,
517 _ => break,
518 }
519 }
520 indent
521 }
522
523 fn strip_indent(line: &str, count: usize) -> String {
525 let mut stripped = 0;
526 let mut byte_pos = 0;
527 for (i, c) in line.char_indices() {
528 if stripped >= count {
529 byte_pos = i;
530 break;
531 }
532 match c {
533 ' ' => { stripped += 1; byte_pos = i + 1; }
534 '\t' => { stripped += 4; byte_pos = i + 1; }
535 _ => { byte_pos = i; break; }
536 }
537 }
538 if stripped < count {
539 byte_pos = line.len();
540 }
541 line[byte_pos..].to_string()
542 }
543
544 fn split_into_words(input: &str, escape_ranges: &[(usize, usize, usize, String)]) -> Vec<WordItem> {
545 let mut items = Vec::new();
546 let mut current_word = String::new();
547 let mut word_start = 0;
548 let chars: Vec<char> = input.chars().collect();
549 let mut char_idx = 0;
550 let mut skip_count = 0;
551 let mut bracket_depth: usize = 0;
556 let mut skip_to_byte: Option<usize> = None;
558
559 for (i, c) in input.char_indices() {
560 if skip_count > 0 {
561 skip_count -= 1;
562 char_idx += 1;
563 continue;
564 }
565 if let Some(end) = skip_to_byte {
567 if i < end {
568 char_idx += 1;
569 continue;
570 }
571 skip_to_byte = None;
572 word_start = i;
573 }
574 if let Some((_, end, content_start, raw_code)) = escape_ranges.iter().find(|(s, _, _, _)| i == *s) {
576 if !current_word.is_empty() {
578 items.push(WordItem {
579 word: std::mem::take(&mut current_word),
580 trailing_punct: None,
581 start: word_start,
582 end: i,
583 punct_pos: None,
584 });
585 }
586 items.push(WordItem {
589 word: format!("\x00ESC:{}", raw_code),
590 trailing_punct: None,
591 start: *content_start,
592 end: *end,
593 punct_pos: None,
594 });
595 skip_to_byte = Some(*end);
596 word_start = *end;
597 char_idx += 1;
598 continue;
599 }
600 let next_pos = i + c.len_utf8();
601 match c {
602 ' ' if Self::is_time_space_before_ampm(¤t_word, &chars, char_idx) => {}
605 ' ' | '\t' | '\n' | '\r' => {
606 if c == '\n' {
607 bracket_depth = 0;
608 }
609 if !current_word.is_empty() {
610 items.push(WordItem {
611 word: std::mem::take(&mut current_word),
612 trailing_punct: None,
613 start: word_start,
614 end: i,
615 punct_pos: None,
616 });
617 }
618 word_start = next_pos;
619 }
620 '.' => {
621 let prev_is_digit = !current_word.is_empty()
623 && current_word.chars().last().map_or(false, |ch| ch.is_ascii_digit());
624 let next_is_digit = char_idx + 1 < chars.len()
625 && chars[char_idx + 1].is_ascii_digit();
626
627 let prev_ident = current_word
637 .chars()
638 .last()
639 .map_or(false, |ch| ch.is_alphabetic() || ch == '_');
640 let prev_close = current_word.is_empty()
641 && char_idx > 0
642 && matches!(chars[char_idx - 1], ')' | ']');
643 let next_ident = char_idx + 1 < chars.len()
644 && (chars[char_idx + 1].is_alphabetic() || chars[char_idx + 1] == '_');
645
646 if prev_is_digit && next_is_digit {
647 current_word.push(c);
649 } else if (prev_ident || prev_close) && next_ident {
650 if !current_word.is_empty() {
652 items.push(WordItem {
653 word: std::mem::take(&mut current_word),
654 trailing_punct: None,
655 start: word_start,
656 end: i,
657 punct_pos: None,
658 });
659 }
660 items.push(WordItem {
661 word: "\x00DOT".to_string(),
662 trailing_punct: None,
663 start: i,
664 end: next_pos,
665 punct_pos: None,
666 });
667 word_start = next_pos;
668 } else {
669 if !current_word.is_empty() {
671 items.push(WordItem {
672 word: std::mem::take(&mut current_word),
673 trailing_punct: Some(c),
674 start: word_start,
675 end: i,
676 punct_pos: Some(i),
677 });
678 } else {
679 items.push(WordItem {
680 word: String::new(),
681 trailing_punct: Some(c),
682 start: i,
683 end: next_pos,
684 punct_pos: Some(i),
685 });
686 }
687 word_start = next_pos;
688 }
689 }
690 '#' => {
691 if char_idx + 1 < chars.len() && chars[char_idx + 1] == '#' {
693 if !current_word.is_empty() {
696 items.push(WordItem {
697 word: std::mem::take(&mut current_word),
698 trailing_punct: None,
699 start: word_start,
700 end: i,
701 punct_pos: None,
702 });
703 }
704 let header_start = i;
706 let mut j = char_idx + 2;
707 while j < chars.len() && (chars[j] == ' ' || chars[j] == '\t') {
708 j += 1;
709 }
710 let mut block_word = String::from("##");
712 while j < chars.len() && chars[j].is_alphabetic() {
713 block_word.push(chars[j]);
714 j += 1;
715 }
716 if block_word.len() > 2 {
717 items.push(WordItem {
718 word: block_word,
719 trailing_punct: None,
720 start: header_start,
721 end: header_start + (j - char_idx),
722 punct_pos: None,
723 });
724 }
725 skip_count = j - char_idx - 1;
726 word_start = header_start + (j - char_idx);
727 } else {
728 let mut look_ahead = char_idx + 1;
732 while look_ahead < chars.len() && chars[look_ahead] != '\n' {
733 skip_count += 1;
734 look_ahead += 1;
735 }
736 if !current_word.is_empty() {
737 items.push(WordItem {
738 word: std::mem::take(&mut current_word),
739 trailing_punct: None,
740 start: word_start,
741 end: i,
742 punct_pos: None,
743 });
744 }
745 word_start = look_ahead + 1; }
747 }
748 '"' => {
750 let raw_string = current_word == "r";
755 if raw_string {
756 current_word.clear();
757 } else if !current_word.is_empty() {
758 items.push(WordItem {
759 word: std::mem::take(&mut current_word),
760 trailing_punct: None,
761 start: word_start,
762 end: i,
763 punct_pos: None,
764 });
765 }
766
767 if char_idx + 2 < chars.len() && chars[char_idx + 1] == '"' && chars[char_idx + 2] == '"' {
769 let string_start = i;
770 let mut j = char_idx + 3; if j < chars.len() && chars[j] == '\n' {
773 j += 1;
774 }
775 let mut raw_content = String::new();
776 while j < chars.len() {
778 if j + 2 < chars.len() && chars[j] == '"' && chars[j + 1] == '"' && chars[j + 2] == '"' {
779 break;
780 }
781 raw_content.push(chars[j]);
782 j += 1;
783 }
784 if raw_content.ends_with('\n') {
786 raw_content.pop();
787 }
788 let dedented = Self::dedent_triple_quote(&raw_content);
790 let end_pos = if j + 2 < chars.len() { j + 3 } else { chars.len() };
791 items.push(WordItem {
792 word: format!("\x00STR:{}", dedented),
793 trailing_punct: None,
794 start: string_start,
795 end: end_pos,
796 punct_pos: None,
797 });
798 if j + 2 < chars.len() {
800 skip_count = (j + 2) - char_idx;
801 } else {
802 skip_count = chars.len() - 1 - char_idx;
803 }
804 word_start = end_pos;
805 } else {
806 let string_start = i;
808 let mut j = char_idx + 1;
809 let mut string_content = String::new();
810 while j < chars.len() && chars[j] != '"' {
811 if chars[j] == '\\' && j + 1 < chars.len() && !raw_string {
812 j += 1;
817 match chars[j] {
818 'n' => string_content.push('\n'),
819 't' => string_content.push('\t'),
820 'r' => string_content.push('\r'),
821 '0' => string_content.push('\0'),
822 '\\' => string_content.push('\\'),
823 '"' => string_content.push('"'),
824 'u' if j + 1 < chars.len() && chars[j + 1] == '{' => {
825 let mut k = j + 2;
826 let mut hex = String::new();
827 while k < chars.len() && chars[k] != '}' {
828 hex.push(chars[k]);
829 k += 1;
830 }
831 match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
832 Some(c) if k < chars.len() => {
833 string_content.push(c);
834 j = k;
835 }
836 _ => {
837 string_content.push('\\');
838 string_content.push('u');
839 }
840 }
841 }
842 other => {
843 string_content.push('\\');
844 string_content.push(other);
845 }
846 }
847 } else {
848 string_content.push(chars[j]);
849 }
850 j += 1;
851 }
852
853 let end_char = if j < chars.len() { j + 1 } else { j };
863 let end_byte = string_start
864 + chars[char_idx..end_char].iter().map(|c| c.len_utf8()).sum::<usize>();
865 items.push(WordItem {
866 word: format!("\x00STR:{}", string_content),
867 trailing_punct: None,
868 start: string_start,
869 end: end_byte,
870 punct_pos: None,
871 });
872
873 if j < chars.len() {
875 skip_count = j - char_idx;
876 } else {
877 skip_count = j - char_idx - 1;
878 }
879 word_start = end_byte;
880 }
881 }
882 '`' => {
884 if !current_word.is_empty() {
886 items.push(WordItem {
887 word: std::mem::take(&mut current_word),
888 trailing_punct: None,
889 start: word_start,
890 end: i,
891 punct_pos: None,
892 });
893 }
894
895 let char_start = i;
897 let mut j = char_idx + 1;
898 let mut char_content = String::new();
899
900 if j < chars.len() {
901 if chars[j] == '\\' && j + 1 < chars.len() {
902 j += 1;
904 let escaped_char = match chars[j] {
905 'n' => '\n',
906 't' => '\t',
907 'r' => '\r',
908 '\\' => '\\',
909 '`' => '`',
910 '0' => '\0',
911 c => c,
912 };
913 char_content.push(escaped_char);
914 j += 1;
915 } else if chars[j] != '`' {
916 char_content.push(chars[j]);
918 j += 1;
919 }
920 }
921
922 if j < chars.len() && chars[j] == '`' {
924 j += 1; }
926
927 items.push(WordItem {
929 word: format!("\x00CHAR:{}", char_content),
930 trailing_punct: None,
931 start: char_start,
932 end: if j <= chars.len() { char_start + (j - char_idx) } else { char_start + 1 },
933 punct_pos: None,
934 });
935
936 if j > char_idx + 1 {
937 skip_count = j - char_idx - 1;
938 }
939 word_start = char_start + (j - char_idx);
940 }
941 '-' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '>' => {
943 if !current_word.is_empty() {
945 items.push(WordItem {
946 word: std::mem::take(&mut current_word),
947 trailing_punct: None,
948 start: word_start,
949 end: i,
950 punct_pos: None,
951 });
952 }
953 items.push(WordItem {
955 word: "->".to_string(),
956 trailing_punct: None,
957 start: i,
958 end: i + 2,
959 punct_pos: None,
960 });
961 skip_count = 1; word_start = i + 2;
963 }
964 '<' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
966 if !current_word.is_empty() {
967 items.push(WordItem {
968 word: std::mem::take(&mut current_word),
969 trailing_punct: None,
970 start: word_start,
971 end: i,
972 punct_pos: None,
973 });
974 }
975 items.push(WordItem {
976 word: "<=".to_string(),
977 trailing_punct: None,
978 start: i,
979 end: i + 2,
980 punct_pos: None,
981 });
982 skip_count = 1;
983 word_start = i + 2;
984 }
985 '>' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
987 if !current_word.is_empty() {
988 items.push(WordItem {
989 word: std::mem::take(&mut current_word),
990 trailing_punct: None,
991 start: word_start,
992 end: i,
993 punct_pos: None,
994 });
995 }
996 items.push(WordItem {
997 word: ">=".to_string(),
998 trailing_punct: None,
999 start: i,
1000 end: i + 2,
1001 punct_pos: None,
1002 });
1003 skip_count = 1;
1004 word_start = i + 2;
1005 }
1006 '=' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
1008 if !current_word.is_empty() {
1009 items.push(WordItem {
1010 word: std::mem::take(&mut current_word),
1011 trailing_punct: None,
1012 start: word_start,
1013 end: i,
1014 punct_pos: None,
1015 });
1016 }
1017 items.push(WordItem {
1018 word: "==".to_string(),
1019 trailing_punct: None,
1020 start: i,
1021 end: i + 2,
1022 punct_pos: None,
1023 });
1024 skip_count = 1;
1025 word_start = i + 2;
1026 }
1027 '!' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
1029 if !current_word.is_empty() {
1030 items.push(WordItem {
1031 word: std::mem::take(&mut current_word),
1032 trailing_punct: None,
1033 start: word_start,
1034 end: i,
1035 punct_pos: None,
1036 });
1037 }
1038 items.push(WordItem {
1039 word: "!=".to_string(),
1040 trailing_punct: None,
1041 start: i,
1042 end: i + 2,
1043 punct_pos: None,
1044 });
1045 skip_count = 1;
1046 word_start = i + 2;
1047 }
1048 '-' if Self::is_date_hyphen(¤t_word, &chars, char_idx) => {
1050 current_word.push(c);
1052 }
1053 ':' if Self::is_time_colon(¤t_word, &chars, char_idx) => {
1055 current_word.push(c);
1057 }
1058 '-' if char_idx > 0
1062 && chars[char_idx - 1].is_alphabetic()
1063 && char_idx + 1 < chars.len()
1064 && chars[char_idx + 1].is_alphabetic() => {
1065 current_word.push(c);
1066 }
1067 '+' | '-' if Self::is_exponent_sign(¤t_word, &chars, char_idx) => {
1069 current_word.push(c);
1070 }
1071 '-' if char_idx > 0
1077 && chars[char_idx - 1].is_alphabetic()
1078 && char_idx + 1 < chars.len()
1079 && chars[char_idx + 1].is_ascii_digit()
1080 && !current_word.is_empty()
1081 && current_word.chars().all(|ch| ch.is_ascii_uppercase()) => {
1082 current_word.push(c);
1083 }
1084 '+' if !current_word.is_empty()
1089 && current_word.chars().all(|ch| ch.is_ascii_uppercase())
1090 && (char_idx + 1 >= chars.len()
1091 || !chars[char_idx + 1].is_ascii_digit()) => {
1092 current_word.push(c);
1093 }
1094 ',' if char_idx > 0
1100 && chars[char_idx - 1].is_ascii_digit()
1101 && char_idx + 1 < chars.len()
1102 && chars[char_idx + 1].is_ascii_digit()
1103 && (bracket_depth == 0
1104 || current_word.chars().next().map_or(false, Self::is_currency_symbol)) => {
1105 current_word.push(c);
1106 }
1107 '/' if char_idx > 0
1111 && chars[char_idx - 1].is_ascii_digit()
1112 && char_idx + 1 < chars.len()
1113 && chars[char_idx + 1].is_ascii_digit() => {
1114 current_word.push(c);
1115 }
1116 '-' if char_idx > 0
1123 && chars[char_idx - 1].is_ascii_digit()
1124 && char_idx + 1 < chars.len()
1125 && chars[char_idx + 1].is_alphabetic() => {
1126 if !current_word.is_empty() {
1127 items.push(WordItem {
1128 word: std::mem::take(&mut current_word),
1129 trailing_punct: None,
1130 start: word_start,
1131 end: i,
1132 punct_pos: None,
1133 });
1134 }
1135 word_start = next_pos;
1136 }
1137 '*' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '*' => {
1140 if !current_word.is_empty() {
1141 items.push(WordItem {
1142 word: std::mem::take(&mut current_word),
1143 trailing_punct: None,
1144 start: word_start,
1145 end: i,
1146 punct_pos: None,
1147 });
1148 }
1149 items.push(WordItem {
1150 word: "**".to_string(),
1151 trailing_punct: None,
1152 start: i,
1153 end: i + 2,
1154 punct_pos: None,
1155 });
1156 skip_count = 1;
1157 word_start = i + 2;
1158 }
1159 '/' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '/' => {
1164 if !current_word.is_empty() {
1165 items.push(WordItem {
1166 word: std::mem::take(&mut current_word),
1167 trailing_punct: None,
1168 start: word_start,
1169 end: i,
1170 punct_pos: None,
1171 });
1172 }
1173 items.push(WordItem {
1174 word: "//".to_string(),
1175 trailing_punct: None,
1176 start: i,
1177 end: i + 2,
1178 punct_pos: None,
1179 });
1180 skip_count = 1;
1181 word_start = i + 2;
1182 }
1183 '+' | '-' | '*' | '/' | '%'
1187 if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' =>
1188 {
1189 if !current_word.is_empty() {
1190 items.push(WordItem {
1191 word: std::mem::take(&mut current_word),
1192 trailing_punct: None,
1193 start: word_start,
1194 end: i,
1195 punct_pos: None,
1196 });
1197 }
1198 items.push(WordItem {
1199 word: format!("{c}="),
1200 trailing_punct: None,
1201 start: i,
1202 end: i + 2,
1203 punct_pos: None,
1204 });
1205 skip_count = 1;
1206 word_start = i + 2;
1207 }
1208 '(' | ')' | '[' | ']' | '{' | '}' | '|' | '~' | '^' | ',' | '?' | '!' | ':' | '+' | '-' | '*' | '/' | '%' | '<' | '>' | '=' => {
1209 match c {
1210 '[' | '{' => bracket_depth += 1,
1213 ']' | '}' => bracket_depth = bracket_depth.saturating_sub(1),
1214 _ => {}
1215 }
1216 if !current_word.is_empty() {
1217 items.push(WordItem {
1218 word: std::mem::take(&mut current_word),
1219 trailing_punct: Some(c),
1220 start: word_start,
1221 end: i,
1222 punct_pos: Some(i),
1223 });
1224 } else {
1225 items.push(WordItem {
1226 word: String::new(),
1227 trailing_punct: Some(c),
1228 start: i,
1229 end: next_pos,
1230 punct_pos: Some(i),
1231 });
1232 }
1233 word_start = next_pos;
1234 }
1235 '\'' => {
1236 let remaining: String = chars[char_idx + 1..].iter().collect();
1238 let remaining_lower = remaining.to_lowercase();
1239
1240 if remaining_lower.starts_with("t ") || remaining_lower.starts_with("t.") ||
1241 remaining_lower.starts_with("t,") || remaining_lower == "t" ||
1242 (char_idx + 1 < chars.len() && chars[char_idx + 1] == 't' &&
1243 (char_idx + 2 >= chars.len() || !chars[char_idx + 2].is_alphabetic())) {
1244 let word_lower = current_word.to_lowercase();
1251 if let Some(expansion) =
1252 crate::lexicon::lookup_negative_contraction(&word_lower)
1253 {
1254 let words: Vec<&str> = expansion.split_whitespace().collect();
1255 let last_idx = words.len().saturating_sub(1);
1256 for (idx, part) in words.iter().enumerate() {
1257 items.push(WordItem {
1258 word: (*part).to_string(),
1259 trailing_punct: None,
1260 start: if idx == 0 { word_start } else { i },
1261 end: if idx == last_idx { i + 2 } else { i },
1262 punct_pos: None,
1263 });
1264 }
1265 current_word.clear();
1266 word_start = next_pos + 1;
1267 skip_count = 1;
1268 } else {
1269 if !current_word.is_empty() {
1271 items.push(WordItem {
1272 word: std::mem::take(&mut current_word),
1273 trailing_punct: Some('\''),
1274 start: word_start,
1275 end: i,
1276 punct_pos: Some(i),
1277 });
1278 }
1279 word_start = next_pos;
1280 }
1281 } else {
1282 if !current_word.is_empty() {
1284 items.push(WordItem {
1285 word: std::mem::take(&mut current_word),
1286 trailing_punct: Some('\''),
1287 start: word_start,
1288 end: i,
1289 punct_pos: Some(i),
1290 });
1291 }
1292 word_start = next_pos;
1293 }
1294 }
1295 c if Self::is_currency_symbol(c)
1300 && char_idx + 1 < chars.len()
1301 && chars[char_idx + 1].is_ascii_digit() => {
1302 if !current_word.is_empty() {
1303 items.push(WordItem {
1304 word: std::mem::take(&mut current_word),
1305 trailing_punct: None,
1306 start: word_start,
1307 end: i,
1308 punct_pos: None,
1309 });
1310 }
1311 word_start = i;
1312 current_word.push(c);
1313 }
1314 c if c.is_alphabetic() || c.is_ascii_digit() || (c == '.' && !current_word.is_empty() && current_word.chars().all(|ch| ch.is_ascii_digit())) || c == '_' => {
1315 if current_word.is_empty() {
1316 word_start = i;
1317 }
1318 current_word.push(c);
1319 }
1320 '&' => {
1321 if !current_word.is_empty() {
1328 items.push(WordItem {
1329 word: std::mem::take(&mut current_word),
1330 trailing_punct: None,
1331 start: word_start,
1332 end: i,
1333 punct_pos: None,
1334 });
1335 }
1336 let next_cap = chars[char_idx + 1..]
1337 .iter()
1338 .find(|ch| !ch.is_whitespace())
1339 .map_or(false, |ch| ch.is_ascii_uppercase());
1340 if !next_cap {
1341 items.push(WordItem {
1345 word: "\x00AMP".to_string(),
1346 trailing_punct: None,
1347 start: i,
1348 end: next_pos,
1349 punct_pos: None,
1350 });
1351 }
1352 word_start = next_pos;
1353 }
1354 _ => {
1355 word_start = next_pos;
1356 }
1357 }
1358 char_idx += 1;
1359 }
1360
1361 if !current_word.is_empty() {
1362 items.push(WordItem {
1363 word: current_word,
1364 trailing_punct: None,
1365 start: word_start,
1366 end: input.len(),
1367 punct_pos: None,
1368 });
1369 }
1370
1371 items
1372 }
1373
1374 fn peek_word(&self, offset: usize) -> Option<&str> {
1375 self.words.get(self.pos + offset).map(|w| w.word.as_str())
1376 }
1377
1378 fn prev_token_is_determiner(&self) -> bool {
1380 if self.pos == 0 { return false; }
1381 if let Some(prev) = self.words.get(self.pos - 1) {
1382 matches!(prev.word.to_lowercase().as_str(),
1383 "every" | "each" | "some" | "all" | "any" | "no" | "the" | "a" | "an")
1384 } else {
1385 false
1386 }
1387 }
1388
1389 fn prev_word_ends_sentence(&self) -> bool {
1394 if self.pos == 0 {
1395 return true;
1396 }
1397 self.words
1398 .get(self.pos - 1)
1399 .and_then(|w| w.word.chars().last())
1400 .map_or(true, |c| matches!(c, '.' | '!' | '?' | ':' | ';'))
1401 }
1402
1403 fn prev_word_is_numeric(&self) -> bool {
1407 if self.pos == 0 { return false; }
1408 self.words
1409 .get(self.pos - 1)
1410 .map(|p| {
1411 let w = p.word.trim_start_matches('$').replace(',', "");
1412 !w.is_empty() && w.chars().all(|c| c.is_ascii_digit() || c == '.')
1413 })
1414 .unwrap_or(false)
1415 }
1416
1417 fn next_token_is_copula(&self) -> bool {
1418 if let Some(next) = self.peek_word(1) {
1419 matches!(next.to_lowercase().as_str(), "is" | "are" | "was" | "were")
1420 } else {
1421 false
1422 }
1423 }
1424
1425 fn peek_sequence(&self, expected: &[&str]) -> bool {
1426 for (i, &exp) in expected.iter().enumerate() {
1427 match self.peek_word(i + 1) {
1428 Some(w) if w.to_lowercase() == exp => continue,
1429 _ => return false,
1430 }
1431 }
1432 true
1433 }
1434
1435 fn consume_words(&mut self, count: usize) {
1436 self.pos += count;
1437 }
1438
1439 pub fn tokenize(&mut self) -> Vec<Token> {
1450 let mut tokens = Vec::new();
1451
1452 while self.pos < self.words.len() {
1453 let item = &self.words[self.pos];
1454 let word = item.word.clone();
1455 let trailing_punct = item.trailing_punct;
1456 let word_start = item.start;
1457 let word_end = item.end;
1458 let punct_pos = item.punct_pos;
1459
1460 if word.is_empty() {
1461 if let Some(punct) = trailing_punct {
1462 let kind = match punct {
1463 '(' => TokenType::LParen,
1464 ')' => TokenType::RParen,
1465 '[' => TokenType::LBracket,
1466 ']' => TokenType::RBracket,
1467 '{' => TokenType::LBrace,
1468 '}' => TokenType::RBrace,
1469 '|' if matches!(self.mode, LexerMode::Imperative) => TokenType::VBar,
1472 '~' if matches!(self.mode, LexerMode::Imperative) => TokenType::Tilde,
1473 '^' if matches!(self.mode, LexerMode::Imperative) => TokenType::Caret,
1474 ',' => TokenType::Comma,
1475 ':' => TokenType::Colon,
1476 '.' | '?' => {
1477 self.in_let_context = false;
1478 TokenType::Period
1479 }
1480 '!' => TokenType::Exclamation,
1481 '+' => TokenType::Plus,
1482 '-' => TokenType::Minus,
1483 '*' => TokenType::Star,
1484 '/' => TokenType::Slash,
1485 '%' => TokenType::Percent,
1486 '<' => TokenType::Lt,
1487 '>' => TokenType::Gt,
1488 '=' => TokenType::Assign,
1489 _ => {
1490 self.pos += 1;
1491 continue;
1492 }
1493 };
1494 let lexeme = self.interner.intern(&punct.to_string());
1495 let span = Span::new(word_start, word_end);
1496 let dup_period = kind == TokenType::Period
1500 && tokens.last().map_or(false, |t: &Token| t.kind == TokenType::Period);
1501 if !dup_period {
1502 tokens.push(Token::new(kind, lexeme, span));
1503 }
1504 }
1505 self.pos += 1;
1506 continue;
1507 }
1508
1509 if word.starts_with("\x00STR:") {
1511 let content = &word[5..]; let span = Span::new(word_start, word_end);
1513 if Self::has_unescaped_brace(content) {
1514 let sym = self.interner.intern(content);
1515 tokens.push(Token::new(TokenType::InterpolatedString(sym), sym, span));
1516 } else {
1517 let normalized = content.replace("{{", "{").replace("}}", "}");
1519 let sym = self.interner.intern(&normalized);
1520 tokens.push(Token::new(TokenType::StringLiteral(sym), sym, span));
1521 }
1522 self.pos += 1;
1523 continue;
1524 }
1525
1526 if word.starts_with("\x00CHAR:") {
1528 let content = &word[6..]; let sym = self.interner.intern(content);
1530 let span = Span::new(word_start, word_end);
1531 tokens.push(Token::new(TokenType::CharLiteral(sym), sym, span));
1532 self.pos += 1;
1533 continue;
1534 }
1535
1536 if word.starts_with("\x00ESC:") {
1538 let content = &word[5..]; let sym = self.interner.intern(content);
1540 let span = Span::new(word_start, word_end);
1541 tokens.push(Token::new(TokenType::EscapeBlock(sym), sym, span));
1542 self.pos += 1;
1543 continue;
1544 }
1545
1546 if matches!(word.to_lowercase().as_str(), "exactly" | "precisely")
1553 && self.peek_word(1).map_or(false, |w| {
1554 let w = w.trim_start_matches('$');
1555 w.chars().next().map_or(false, |c| c.is_ascii_digit())
1556 || crate::lexicon::word_to_number(&w.to_lowercase()).is_some()
1557 })
1558 {
1559 self.pos += 1;
1560 continue;
1561 }
1562
1563 let kind = self.classify_with_lookahead(&word);
1564 let host_takes_copula_clitic = matches!(
1570 kind,
1571 TokenType::Pronoun { case: crate::lexicon::Case::Subject, .. }
1572 | TokenType::Who
1573 | TokenType::That
1574 | TokenType::What
1575 | TokenType::Where
1576 );
1577 let lexeme = self.interner.intern(&word);
1578 let span = Span::new(word_start, word_end);
1579 tokens.push(Token::new(kind, lexeme, span));
1580
1581 if let Some(punct) = trailing_punct {
1582 if punct == '\'' {
1583 if let Some(next_item) = self.words.get(self.pos + 1) {
1584 if next_item.word.to_lowercase() == "s" {
1585 let next_is_past_verb = self
1592 .words
1593 .get(self.pos + 2)
1594 .map(|a| a.word.to_lowercase())
1595 .and_then(|w| self.lexicon.lookup_verb(&w))
1596 .map_or(false, |v| v.aspect != crate::lexicon::Aspect::Progressive);
1597 let (poss_kind, poss_text) = if host_takes_copula_clitic && !next_is_past_verb {
1598 (TokenType::Is, "is")
1599 } else {
1600 (TokenType::Possessive, "'s")
1601 };
1602 let poss_lexeme = self.interner.intern(poss_text);
1603 let poss_start = punct_pos.unwrap_or(word_end);
1604 let poss_end = next_item.end;
1605 tokens.push(Token::new(poss_kind, poss_lexeme, Span::new(poss_start, poss_end)));
1606 self.pos += 1;
1607 if let Some(s_punct) = next_item.trailing_punct {
1608 let kind = match s_punct {
1609 '(' => TokenType::LParen,
1610 ')' => TokenType::RParen,
1611 '[' => TokenType::LBracket,
1612 ']' => TokenType::RBracket,
1613 '{' => TokenType::LBrace,
1614 '}' => TokenType::RBrace,
1615 '|' if matches!(self.mode, LexerMode::Imperative) => TokenType::VBar,
1616 '~' if matches!(self.mode, LexerMode::Imperative) => TokenType::Tilde,
1617 '^' if matches!(self.mode, LexerMode::Imperative) => TokenType::Caret,
1618 ',' => TokenType::Comma,
1619 ':' => TokenType::Colon,
1620 '.' | '?' => TokenType::Period,
1621 '!' => TokenType::Exclamation,
1622 '+' => TokenType::Plus,
1623 '-' => TokenType::Minus,
1624 '*' => TokenType::Star,
1625 '/' => TokenType::Slash,
1626 '%' => TokenType::Percent,
1627 '<' => TokenType::Lt,
1628 '>' => TokenType::Gt,
1629 '=' => TokenType::Assign,
1630 _ => {
1631 self.pos += 1;
1632 continue;
1633 }
1634 };
1635 let s_punct_pos = next_item.punct_pos.unwrap_or(next_item.end);
1636 let lexeme = self.interner.intern(&s_punct.to_string());
1637 tokens.push(Token::new(kind, lexeme, Span::new(s_punct_pos, s_punct_pos + 1)));
1638 }
1639 self.pos += 1;
1640 continue;
1641 }
1642 }
1643 self.pos += 1;
1644 continue;
1645 }
1646
1647 if punct == '.'
1653 && lexicon::is_abbreviation(&word.to_lowercase())
1654 && self.words.get(self.pos + 1).is_some()
1655 {
1656 self.pos += 1;
1657 continue;
1658 }
1659 let kind = match punct {
1660 '(' => TokenType::LParen,
1661 ')' => TokenType::RParen,
1662 '[' => TokenType::LBracket,
1663 ']' => TokenType::RBracket,
1664 '{' => TokenType::LBrace,
1665 '}' => TokenType::RBrace,
1666 '|' if matches!(self.mode, LexerMode::Imperative) => TokenType::VBar,
1667 '~' if matches!(self.mode, LexerMode::Imperative) => TokenType::Tilde,
1668 '^' if matches!(self.mode, LexerMode::Imperative) => TokenType::Caret,
1669 ',' => TokenType::Comma,
1670 ':' => TokenType::Colon,
1671 '.' | '?' => {
1672 self.in_let_context = false;
1673 TokenType::Period
1674 }
1675 '!' => TokenType::Exclamation,
1676 '+' => TokenType::Plus,
1677 '-' => TokenType::Minus,
1678 '*' => TokenType::Star,
1679 '/' => TokenType::Slash,
1680 '%' => TokenType::Percent,
1681 '<' => TokenType::Lt,
1682 '>' => TokenType::Gt,
1683 '=' => TokenType::Assign,
1684 _ => {
1685 self.pos += 1;
1686 continue;
1687 }
1688 };
1689 let p_start = punct_pos.unwrap_or(word_end);
1690 let lexeme = self.interner.intern(&punct.to_string());
1691 tokens.push(Token::new(kind, lexeme, Span::new(p_start, p_start + 1)));
1692 }
1693
1694 self.pos += 1;
1695 }
1696
1697 let eof_lexeme = self.interner.intern("");
1698 let eof_span = Span::new(self.input_len, self.input_len);
1699 tokens.push(Token::new(TokenType::EOF, eof_lexeme, eof_span));
1700
1701 self.insert_indentation_tokens(tokens)
1702 }
1703
1704 fn insert_indentation_tokens(&mut self, tokens: Vec<Token>) -> Vec<Token> {
1709 let mut result = Vec::new();
1710 let empty_sym = self.interner.intern("");
1711
1712 let line_lexer = LineLexer::new(&self.source);
1714 let line_tokens: Vec<LineToken> = line_lexer.collect();
1715
1716 let mut structural_events: Vec<(usize, bool)> = Vec::new(); let mut pending_indents = 0usize;
1720 let mut pending_dedents = 0usize;
1721
1722 for line_token in &line_tokens {
1723 match line_token {
1724 LineToken::Indent => {
1725 pending_indents += 1;
1726 }
1727 LineToken::Dedent => {
1728 pending_dedents += 1;
1729 }
1730 LineToken::Content { start, .. } => {
1731 for _ in 0..pending_dedents {
1733 structural_events.push((*start, false)); }
1735 pending_dedents = 0;
1736
1737 for _ in 0..pending_indents {
1739 structural_events.push((*start, true)); }
1741 pending_indents = 0;
1742 }
1743 LineToken::Newline => {}
1744 }
1745 }
1746
1747 for _ in 0..pending_dedents {
1749 structural_events.push((self.input_len, false));
1750 }
1751
1752 if !self.escape_body_ranges.is_empty() {
1757 let mut filtered = Vec::new();
1761 for &(pos, is_indent) in &structural_events {
1762 let is_inside_escape_body = self.escape_body_ranges.iter().any(|(start, end)| {
1763 pos > *start && pos < *end
1765 });
1766 if !is_inside_escape_body {
1767 filtered.push((pos, is_indent));
1768 }
1769 }
1770 structural_events = filtered;
1771 }
1772
1773 {
1777 let string_spans: Vec<(usize, usize)> = tokens.iter()
1778 .filter(|t| matches!(t.kind, TokenType::StringLiteral(_) | TokenType::InterpolatedString(_)))
1779 .filter(|t| t.span.end.saturating_sub(t.span.start) > 6) .map(|t| (t.span.start, t.span.end))
1781 .collect();
1782 if !string_spans.is_empty() {
1783 structural_events.retain(|&(pos, _)| {
1784 !string_spans.iter().any(|(start, end)| pos > *start && pos < *end)
1785 });
1786 }
1787 }
1788
1789 {
1796 let mut bracket_ranges: Vec<(usize, usize)> = Vec::new();
1797 let mut open_stack: Vec<usize> = Vec::new();
1798 for t in &tokens {
1799 match t.kind {
1800 TokenType::LParen | TokenType::LBracket | TokenType::LBrace => {
1801 open_stack.push(t.span.start);
1802 }
1803 TokenType::RParen | TokenType::RBracket | TokenType::RBrace => {
1804 if let Some(open) = open_stack.pop() {
1805 bracket_ranges.push((open, t.span.end));
1806 }
1807 }
1808 _ => {}
1809 }
1810 }
1811 if !bracket_ranges.is_empty() {
1812 structural_events.retain(|&(pos, _)| {
1813 !bracket_ranges.iter().any(|(start, end)| pos > *start && pos < *end)
1814 });
1815 }
1816 }
1817
1818 structural_events.sort_by(|a, b| {
1820 if a.0 != b.0 {
1821 a.0.cmp(&b.0)
1822 } else {
1823 a.1.cmp(&b.1)
1825 }
1826 });
1827
1828 let mut event_idx = 0;
1833 let mut last_colon_pos: Option<usize> = None;
1834
1835 for token in tokens.iter() {
1836 let token_start = token.span.start;
1837
1838 while event_idx < structural_events.len() {
1840 let (event_pos, is_indent) = structural_events[event_idx];
1841
1842 if event_pos <= token_start {
1844 let span = if is_indent {
1845 Span::new(last_colon_pos.unwrap_or(event_pos), last_colon_pos.unwrap_or(event_pos))
1847 } else {
1848 Span::new(event_pos, event_pos)
1849 };
1850 let kind = if is_indent { TokenType::Indent } else { TokenType::Dedent };
1851 result.push(Token::new(kind, empty_sym, span));
1852 event_idx += 1;
1853 } else {
1854 break;
1855 }
1856 }
1857
1858 result.push(token.clone());
1859
1860 if token.kind == TokenType::Colon && self.is_end_of_line(token.span.end) {
1862 last_colon_pos = Some(token.span.end);
1863 }
1864 }
1865
1866 while event_idx < structural_events.len() {
1868 let (event_pos, is_indent) = structural_events[event_idx];
1869 let span = Span::new(event_pos, event_pos);
1870 let kind = if is_indent { TokenType::Indent } else { TokenType::Dedent };
1871 result.push(Token::new(kind, empty_sym, span));
1872 event_idx += 1;
1873 }
1874
1875 let eof_pos = result.iter().position(|t| t.kind == TokenType::EOF);
1877 if let Some(pos) = eof_pos {
1878 let eof = result.remove(pos);
1879 result.push(eof);
1880 }
1881
1882 result
1883 }
1884
1885 fn is_end_of_line(&self, from_pos: usize) -> bool {
1887 let bytes = self.source.as_bytes();
1888 let mut pos = from_pos;
1889 while pos < bytes.len() {
1890 match bytes[pos] {
1891 b' ' | b'\t' => pos += 1,
1892 b'\n' => return true,
1893 _ => return false,
1894 }
1895 }
1896 true }
1898
1899 fn measure_next_line_indent(&self, from_pos: usize) -> Option<usize> {
1900 let bytes = self.source.as_bytes();
1901 let mut pos = from_pos;
1902
1903 while pos < bytes.len() && bytes[pos] != b'\n' {
1904 pos += 1;
1905 }
1906
1907 if pos >= bytes.len() {
1908 return None;
1909 }
1910
1911 pos += 1;
1912
1913 let mut indent = 0;
1914 while pos < bytes.len() {
1915 match bytes[pos] {
1916 b' ' => indent += 1,
1917 b'\t' => indent += 4,
1918 b'\n' => {
1919 indent = 0;
1920 }
1921 _ => break,
1922 }
1923 pos += 1;
1924 }
1925
1926 if pos >= bytes.len() {
1927 return None;
1928 }
1929
1930 Some(indent)
1931 }
1932
1933 fn word_to_number(word: &str) -> Option<u32> {
1934 lexicon::word_to_number(&word.to_lowercase())
1935 }
1936
1937 fn is_date_hyphen(current_word: &str, chars: &[char], char_idx: usize) -> bool {
1943 let word_chars: Vec<char> = current_word.chars().collect();
1945
1946 if word_chars.len() == 4 && word_chars.iter().all(|c| c.is_ascii_digit()) {
1948 if char_idx + 5 < chars.len()
1950 && chars[char_idx + 1].is_ascii_digit()
1951 && chars[char_idx + 2].is_ascii_digit()
1952 && chars[char_idx + 3] == '-'
1953 && chars[char_idx + 4].is_ascii_digit()
1954 && chars[char_idx + 5].is_ascii_digit()
1955 {
1956 return true;
1957 }
1958 }
1959
1960 if word_chars.len() == 7
1962 && word_chars[0..4].iter().all(|c| c.is_ascii_digit())
1963 && word_chars[4] == '-'
1964 && word_chars[5..7].iter().all(|c| c.is_ascii_digit())
1965 {
1966 if char_idx + 2 < chars.len()
1968 && chars[char_idx + 1].is_ascii_digit()
1969 && chars[char_idx + 2].is_ascii_digit()
1970 {
1971 let next_not_digit = char_idx + 3 >= chars.len()
1973 || !chars[char_idx + 3].is_ascii_digit();
1974 if next_not_digit {
1975 return true;
1976 }
1977 }
1978 }
1979
1980 false
1981 }
1982
1983 fn is_time_colon(current_word: &str, chars: &[char], char_idx: usize) -> bool {
1989 let word_chars: Vec<char> = current_word.chars().collect();
1991 if word_chars.is_empty() || word_chars.len() > 2 {
1992 return false;
1993 }
1994 if !word_chars.iter().all(|c| c.is_ascii_digit()) {
1995 return false;
1996 }
1997
1998 if char_idx + 3 < chars.len()
2001 && chars[char_idx + 1].is_ascii_digit()
2002 && chars[char_idx + 2].is_ascii_digit()
2003 {
2004 let suffix_start = if chars.get(char_idx + 3) == Some(&' ') {
2005 char_idx + 4
2006 } else {
2007 char_idx + 3
2008 };
2009 if suffix_start + 1 < chars.len() {
2010 let next_two: String = chars[suffix_start..suffix_start + 2].iter().collect();
2011 let lower = next_two.to_lowercase();
2012 if (lower == "am" || lower == "pm")
2013 && (suffix_start + 2 >= chars.len()
2014 || !chars[suffix_start + 2].is_alphabetic())
2015 {
2016 return true;
2017 }
2018 }
2019 }
2020
2021 false
2022 }
2023
2024 fn is_time_space_before_ampm(current_word: &str, chars: &[char], char_idx: usize) -> bool {
2028 let valid_time = match current_word.find(':') {
2029 Some(p) => {
2030 let hour = ¤t_word[..p];
2031 let min = ¤t_word[p + 1..];
2032 (1..=2).contains(&hour.len())
2033 && hour.chars().all(|c| c.is_ascii_digit())
2034 && min.len() == 2
2035 && min.chars().all(|c| c.is_ascii_digit())
2036 }
2037 None => false,
2038 };
2039 if !valid_time {
2040 return false;
2041 }
2042 let next_two: String = chars
2043 .get(char_idx + 1..char_idx + 3)
2044 .map(|s| s.iter().collect())
2045 .unwrap_or_default();
2046 let lower = next_two.to_lowercase();
2047 (lower == "am" || lower == "pm")
2048 && chars.get(char_idx + 3).map_or(true, |c| !c.is_alphabetic())
2049 }
2050
2051 fn has_unescaped_brace(content: &str) -> bool {
2054 let bytes = content.as_bytes();
2055 let mut i = 0;
2056 while i < bytes.len() {
2057 if bytes[i] == b'{' {
2058 if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
2059 i += 2;
2060 } else {
2061 return true;
2062 }
2063 } else {
2064 i += 1;
2065 }
2066 }
2067 false
2068 }
2069
2070 fn is_exponent_sign(current_word: &str, chars: &[char], char_idx: usize) -> bool {
2076 if !current_word.ends_with('e') && !current_word.ends_with('E') {
2078 return false;
2079 }
2080 let before_e = ¤t_word[..current_word.len() - 1];
2082 if before_e.is_empty() || !before_e.chars().next().unwrap().is_ascii_digit() {
2083 return false;
2084 }
2085 char_idx + 1 < chars.len() && chars[char_idx + 1].is_ascii_digit()
2087 }
2088
2089 fn dedent_triple_quote(raw: &str) -> String {
2092 let lines: Vec<&str> = raw.lines().collect();
2093 if lines.is_empty() {
2094 return String::new();
2095 }
2096 let min_indent = lines.iter()
2098 .filter(|l| !l.trim().is_empty())
2099 .map(|l| l.len() - l.trim_start().len())
2100 .min()
2101 .unwrap_or(0);
2102 lines.iter()
2104 .map(|l| {
2105 if l.len() >= min_indent {
2106 &l[min_indent..]
2107 } else {
2108 l.trim()
2109 }
2110 })
2111 .collect::<Vec<_>>()
2112 .join("\n")
2113 }
2114
2115 fn is_numeric_literal(word: &str) -> bool {
2116 if word.is_empty() {
2117 return false;
2118 }
2119 let chars: Vec<char> = word.chars().collect();
2120 let first = chars[0];
2121 if first.is_ascii_digit() {
2122 return true;
2124 }
2125 if let Some(underscore_pos) = word.rfind('_') {
2128 let before_underscore = &word[..underscore_pos];
2129 let after_underscore = &word[underscore_pos + 1..];
2130 let is_math_symbol = matches!(
2132 before_underscore.to_lowercase().as_str(),
2133 "aleph" | "omega" | "beth"
2134 );
2135 if is_math_symbol
2136 && !after_underscore.is_empty()
2137 && after_underscore.chars().all(|c| c.is_ascii_digit())
2138 {
2139 return true;
2140 }
2141 }
2142 false
2143 }
2144
2145 fn parse_duration_literal(word: &str) -> Option<(i64, &str)> {
2158 if word.is_empty() || !word.chars().next()?.is_ascii_digit() {
2159 return None;
2160 }
2161
2162 const SUFFIXES: &[(&str, i64)] = &[
2164 ("ns", 1),
2165 ("μs", 1_000),
2166 ("us", 1_000),
2167 ("ms", 1_000_000),
2168 ("sec", 1_000_000_000),
2169 ("s", 1_000_000_000),
2170 ("min", 60_000_000_000),
2171 ("hr", 3_600_000_000_000),
2172 ("h", 3_600_000_000_000),
2173 ];
2174
2175 for (suffix, multiplier) in SUFFIXES {
2177 if word.ends_with(suffix) {
2178 let num_part = &word[..word.len() - suffix.len()];
2179 let cleaned: String = num_part.chars().filter(|c| *c != '_').collect();
2181 if let Ok(n) = cleaned.parse::<i64>() {
2182 return Some((n.saturating_mul(*multiplier), *suffix));
2183 }
2184 }
2185 }
2186
2187 None
2188 }
2189
2190 fn parse_date_literal(word: &str) -> Option<i32> {
2195 if word.len() != 10 {
2197 return None;
2198 }
2199
2200 let bytes = word.as_bytes();
2201
2202 if bytes[4] != b'-' || bytes[7] != b'-' {
2204 return None;
2205 }
2206
2207 let year: i32 = word[0..4].parse().ok()?;
2209 let month: u32 = word[5..7].parse().ok()?;
2210 let day: u32 = word[8..10].parse().ok()?;
2211
2212 if month < 1 || month > 12 || day < 1 {
2216 return None;
2217 }
2218 let is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
2219 let max_day = match month {
2220 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2221 4 | 6 | 9 | 11 => 30,
2222 2 => if is_leap { 29 } else { 28 },
2223 _ => return None,
2224 };
2225 if day > max_day {
2226 return None;
2227 }
2228
2229 let y = if month <= 2 { year - 1 } else { year };
2232 let era = if y >= 0 { y / 400 } else { (y - 399) / 400 };
2233 let yoe = (y - era * 400) as u32;
2234 let m = month;
2235 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + day - 1;
2236 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
2237 let days = era * 146097 + doe as i32 - 719468;
2238
2239 Some(days)
2240 }
2241
2242 fn parse_time_literal(word: &str) -> Option<i64> {
2251 let lower = word.to_lowercase();
2252
2253 if lower == "noon" {
2255 return Some(12i64 * 3600 * 1_000_000_000);
2256 }
2257 if lower == "midnight" {
2258 return Some(0);
2259 }
2260
2261 let is_pm = lower.ends_with("pm");
2263 let is_am = lower.ends_with("am");
2264
2265 if !is_pm && !is_am {
2266 return None;
2267 }
2268
2269 let time_part = &lower[..lower.len() - 2];
2271
2272 let (hour, minute): (i64, i64) = if let Some(colon_idx) = time_part.find(':') {
2274 let hour_str = &time_part[..colon_idx];
2275 let min_str = &time_part[colon_idx + 1..];
2276 let h: i64 = hour_str.parse().ok()?;
2277 let m: i64 = min_str.parse().ok()?;
2278 (h, m)
2279 } else {
2280 let h: i64 = time_part.parse().ok()?;
2282 (h, 0)
2283 };
2284
2285 if hour < 1 || hour > 12 || minute < 0 || minute > 59 {
2287 return None;
2288 }
2289
2290 let hour_24 = if is_am {
2292 if hour == 12 { 0 } else { hour } } else {
2294 if hour == 12 { 12 } else { hour + 12 } };
2296
2297 let nanos = (hour_24 * 3600 + minute * 60) * 1_000_000_000;
2299 Some(nanos)
2300 }
2301
2302 fn is_currency_symbol(c: char) -> bool {
2304 matches!(c, '$' | '€' | '£' | '¥')
2305 }
2306
2307 fn currency_for_symbol(c: char) -> Option<&'static str> {
2310 Some(match c {
2311 '$' => "USD",
2312 '€' => "EUR",
2313 '£' => "GBP",
2314 '¥' => "JPY",
2315 _ => return None,
2316 })
2317 }
2318
2319 fn classify_with_lookahead(&mut self, word: &str) -> TokenType {
2320 if word == "\x00AMP" {
2324 return if matches!(self.mode, LexerMode::Imperative) {
2325 TokenType::Amp
2326 } else {
2327 TokenType::And
2328 };
2329 }
2330 if word == "\x00DOT" {
2331 return if matches!(self.mode, LexerMode::Imperative) {
2334 TokenType::Dot
2335 } else {
2336 TokenType::Period
2337 };
2338 }
2339 if word.starts_with("##") {
2341 let block_name = &word[2..];
2342 let block_type = match block_name.to_lowercase().as_str() {
2343 "theorem" => BlockType::Theorem,
2344 "main" => BlockType::Main,
2345 "definition" => BlockType::Definition,
2346 "define" => BlockType::Define, "axiom" => BlockType::Axiom, "theory" => BlockType::Theory, "proof" => BlockType::Proof,
2350 "example" => BlockType::Example,
2351 "logic" => BlockType::Logic,
2352 "note" => BlockType::Note,
2353 "to" => BlockType::Function, "a" | "an" => BlockType::TypeDef, "policy" => BlockType::Policy, "requires" => BlockType::Requires, "hardware" => BlockType::Hardware, "property" => BlockType::Property, "no" => BlockType::No, "tier" => BlockType::Tier, other => {
2362 const CONSEQUENTIAL: &[&str] = &[
2369 "main", "theorem", "definition", "define", "axiom",
2370 "theory", "proof", "policy", "requires", "hardware",
2371 "property", "tier",
2372 ];
2373 if let Some(similar) =
2374 crate::suggest::find_similar(other, CONSEQUENTIAL, 2)
2375 {
2376 let found = self.interner.intern(block_name);
2377 let suggestion = self.interner.intern(similar);
2378 BlockType::SuspectedTypo { found, suggestion }
2379 } else {
2380 BlockType::Note }
2382 }
2383 };
2384
2385 self.mode = match block_type {
2387 BlockType::Main | BlockType::Function => LexerMode::Imperative,
2388 _ => LexerMode::Declarative,
2389 };
2390
2391
2392 return TokenType::BlockHeader { block_type };
2393 }
2394
2395 let lower = word.to_lowercase();
2396
2397 if lower == "each" && self.peek_sequence(&["other"]) {
2398 self.consume_words(1);
2399 return TokenType::Reciprocal;
2400 }
2401
2402 if lower == "to" {
2403 if let Some(next) = self.peek_word(1) {
2404 if self.is_verb_like(next) {
2405 return TokenType::To;
2406 }
2407 }
2408 let sym = self.interner.intern("to");
2409 return TokenType::Preposition(sym);
2410 }
2411
2412 if lower == "at" {
2413 if let Some(next) = self.peek_word(1) {
2414 let next_lower = next.to_lowercase();
2415 if next_lower == "least" {
2416 if let Some(num_word) = self.peek_word(2) {
2417 if let Some(n) = Self::word_to_number(num_word) {
2418 self.consume_words(2);
2419 return TokenType::AtLeast(n);
2420 }
2421 }
2422 }
2423 if next_lower == "most" {
2424 if let Some(num_word) = self.peek_word(2) {
2425 if let Some(n) = Self::word_to_number(num_word) {
2426 self.consume_words(2);
2427 return TokenType::AtMost(n);
2428 }
2429 }
2430 }
2431 }
2432 }
2433
2434 if lower == "exactly" {
2436 if let Some(num_word) = self.peek_word(1) {
2437 if let Some(n) = Self::word_to_number(num_word) {
2438 self.consume_words(1);
2439 return TokenType::Cardinal(n);
2440 }
2441 }
2442 }
2443
2444 if let Some(n) = Self::word_to_number(&lower) {
2445 return TokenType::Cardinal(n);
2446 }
2447
2448 if let Some((nanos, unit)) = Self::parse_duration_literal(word) {
2450 let unit_sym = self.interner.intern(unit);
2451 return TokenType::DurationLiteral {
2452 nanos,
2453 original_unit: unit_sym,
2454 };
2455 }
2456
2457 if let Some(days) = Self::parse_date_literal(word) {
2459 return TokenType::DateLiteral { days };
2460 }
2461
2462 if let Some(nanos_from_midnight) = Self::parse_time_literal(word) {
2464 return TokenType::TimeLiteral { nanos_from_midnight };
2465 }
2466
2467 if self.mode == LexerMode::Imperative {
2474 if let Some(first) = word.chars().next() {
2475 if let Some(code) = Self::currency_for_symbol(first) {
2476 let cleaned: String =
2477 word.chars().skip(1).filter(|c| c.is_ascii_digit() || *c == '.').collect();
2478 if cleaned.starts_with(|c: char| c.is_ascii_digit()) {
2479 let amount = self.interner.intern(&cleaned);
2480 let currency = self.interner.intern(code);
2481 return TokenType::MoneyLiteral { amount, currency };
2482 }
2483 }
2484 }
2485 }
2486
2487 if word.starts_with(|c: char| Self::is_currency_symbol(c))
2491 || (word.starts_with(|c: char| c.is_ascii_digit()) && word.contains(','))
2492 {
2493 let cleaned: String = word
2494 .chars()
2495 .filter(|c| c.is_ascii_digit() || *c == '.')
2496 .collect();
2497 if cleaned.starts_with(|c: char| c.is_ascii_digit()) {
2498 let sym = self.interner.intern(&cleaned);
2499 return TokenType::Number(sym);
2500 }
2501 }
2502
2503 if Self::is_numeric_literal(word) {
2504 let sym = self.interner.intern(word);
2505 return TokenType::Number(sym);
2506 }
2507
2508 if lower == "if" && self.peek_sequence(&["and", "only", "if"]) {
2509 self.consume_words(3);
2510 return TokenType::Iff;
2511 }
2512
2513 if lower == "is" {
2514 if self.peek_sequence(&["equal", "to"]) {
2515 self.consume_words(2);
2516 return TokenType::Identity;
2517 }
2518 if self.peek_sequence(&["identical", "to"]) {
2519 self.consume_words(2);
2520 return TokenType::Identity;
2521 }
2522 }
2523
2524 if (lower == "a" || lower == "an") && word.chars().next().unwrap().is_uppercase() {
2525 if let Some(next) = self.peek_word(1) {
2528 let next_lower = next.to_lowercase();
2529 let next_starts_lowercase = next.chars().next().map(|c| c.is_lowercase()).unwrap_or(false);
2530
2531 if matches!(next_lower.as_str(), "if" | "and" | "or" | "implies" | "iff") {
2533 let sym = self.interner.intern(word);
2534 return TokenType::ProperName(sym);
2535 }
2536
2537 let is_verb = self.lexicon.lookup_verb(&next_lower).is_some()
2542 && !lexicon::is_disambiguation_not_verb(&next_lower);
2543 let is_gerund = next_lower.ends_with("ing");
2544 let is_also_noun_or_adj = self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower);
2545 if is_verb && !is_gerund && !is_also_noun_or_adj {
2546 let sym = self.interner.intern(word);
2547 return TokenType::ProperName(sym);
2548 }
2549
2550 if let Some(third) = self.peek_word(2) {
2553 let third_lower = third.to_lowercase();
2554 if third_lower == "is" || third_lower == "are" || third_lower == "has" {
2556 return TokenType::Article(Definiteness::Indefinite);
2557 }
2558 }
2559
2560 let is_content_word = self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower);
2564 if is_content_word || next_starts_lowercase {
2565 return TokenType::Article(Definiteness::Indefinite);
2566 }
2567 }
2568 let sym = self.interner.intern(word);
2569 return TokenType::ProperName(sym);
2570 }
2571
2572 self.classify_word(word)
2573 }
2574
2575 fn is_noun_like(&self, word: &str) -> bool {
2576 if lexicon::is_noun_pattern(word) || lexicon::is_common_noun(word) {
2577 return true;
2578 }
2579 if word.ends_with("er") || word.ends_with("ian") || word.ends_with("ist") {
2580 return true;
2581 }
2582 false
2583 }
2584
2585 fn is_adjective_like(&self, word: &str) -> bool {
2586 lexicon::is_adjective(word) || lexicon::is_non_intersective(word)
2587 }
2588
2589 fn classify_word(&mut self, word: &str) -> TokenType {
2590 let lower = word.to_lowercase();
2591 let first_char = word.chars().next().unwrap();
2592
2593 if lower == "that" {
2599 if let Some(next) = self.peek_word(1) {
2600 let next_lower = next.to_lowercase();
2601 let next_is_verb = self.lexicon.lookup_verb(&next_lower).is_some();
2605 if !next_is_verb
2606 && (self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower))
2607 {
2608 return TokenType::Article(Definiteness::Distal);
2609 }
2610 }
2611 }
2612
2613 if word == "->" {
2615 return TokenType::Arrow;
2616 }
2617
2618 if word == "<=" {
2620 return TokenType::LtEq;
2621 }
2622 if word == ">=" {
2623 return TokenType::GtEq;
2624 }
2625 match word {
2626 "+=" => return TokenType::PlusEq,
2627 "-=" => return TokenType::MinusEq,
2628 "*=" => return TokenType::StarEq,
2629 "/=" => return TokenType::SlashEq,
2630 "%=" => return TokenType::PercentEq,
2631 "**" => return TokenType::StarStar,
2632 "//" => return TokenType::SlashSlash,
2633 _ => {}
2634 }
2635 if word == "==" {
2636 return TokenType::EqEq;
2637 }
2638 if word == "!=" {
2639 return TokenType::NotEq;
2640 }
2641 if word == "<" {
2642 return TokenType::Lt;
2643 }
2644 if word == ">" {
2645 return TokenType::Gt;
2646 }
2647 if word == "=" {
2649 return TokenType::Assign;
2650 }
2651
2652 if let Some(kind) = lexicon::lookup_keyword(&lower) {
2653 let is_modal = matches!(
2659 kind,
2660 TokenType::Must
2661 | TokenType::Shall
2662 | TokenType::Should
2663 | TokenType::Can
2664 | TokenType::May
2665 | TokenType::Cannot
2666 | TokenType::Would
2667 | TokenType::Could
2668 | TokenType::Might
2669 );
2670 if is_modal && first_char.is_uppercase() && !self.prev_word_ends_sentence() {
2671 return TokenType::ProperName(self.interner.intern(word));
2672 }
2673 let kind = match (kind, self.mode) {
2677 (TokenType::From, LexerMode::Declarative) => {
2678 let prev_cap = self.pos > 0
2684 && self
2685 .words
2686 .get(self.pos - 1)
2687 .and_then(|w| w.word.chars().next())
2688 .map_or(false, |c| c.is_uppercase());
2689 let next_cap = self
2690 .words
2691 .get(self.pos + 1)
2692 .and_then(|w| w.word.chars().next())
2693 .map_or(false, |c| c.is_uppercase());
2694 if prev_cap && next_cap {
2695 TokenType::From
2696 } else {
2697 let sym = self.interner.intern("from");
2698 TokenType::Preposition(sym)
2699 }
2700 }
2701 (TokenType::For, LexerMode::Declarative) => {
2702 let sym = self.interner.intern("for");
2703 TokenType::Preposition(sym)
2704 }
2705 (other, _) => other,
2706 };
2707 return kind;
2708 }
2709
2710 if let Some(kind) = lexicon::lookup_pronoun(&lower) {
2711 return kind;
2712 }
2713
2714 if let Some(def) = lexicon::lookup_article(&lower) {
2715 return TokenType::Article(def);
2716 }
2717
2718 if let Some(time) = lexicon::lookup_auxiliary(&lower) {
2719 if first_char.is_uppercase() && !self.prev_word_ends_sentence() {
2722 return TokenType::ProperName(self.interner.intern(word));
2723 }
2724 return TokenType::Auxiliary(time);
2725 }
2726
2727 match lower.as_str() {
2729 "call" => return TokenType::Call,
2730 "in" if self.mode == LexerMode::Imperative => return TokenType::In,
2731 "inside" if self.mode == LexerMode::Imperative => return TokenType::Inside,
2733 "at" if self.mode == LexerMode::Imperative => return TokenType::At,
2735 "into" if self.mode == LexerMode::Imperative => return TokenType::Into,
2737 "before" => return TokenType::Before,
2739 _ => {}
2740 }
2741
2742 if lower == "per" {
2746 let sym = self.interner.intern("per");
2747 return TokenType::Preposition(sym);
2748 }
2749
2750 if lexicon::is_preposition(&lower)
2755 && (self.lexicon.lookup_verb(&lower).is_none()
2756 || lexicon::is_disambiguation_not_verb(&lower))
2757 {
2758 let sym = self.interner.intern(&lower);
2759 return TokenType::Preposition(sym);
2760 }
2761
2762 match lower.as_str() {
2763 "equals" => return TokenType::Equals,
2764 "item" | "items"
2773 if self.mode == LexerMode::Imperative
2774 || self.peek_word(1).map_or(false, |w| {
2775 w.chars().next().map_or(false, |c| c.is_ascii_digit())
2776 || crate::lexicon::word_to_number(&w.to_lowercase()).is_some()
2777 }) =>
2778 {
2779 return if lower == "item" {
2780 TokenType::Item
2781 } else {
2782 TokenType::Items
2783 };
2784 }
2785 "mut" if self.mode == LexerMode::Imperative => return TokenType::Mut,
2787 "let" => {
2788 self.in_let_context = true;
2789 return TokenType::Let;
2790 }
2791 "set" => {
2792 if self.peek_word(1).map_or(false, |w| w.to_lowercase() == "of") {
2795 } else if self.mode == LexerMode::Imperative {
2797 return TokenType::Set;
2799 } else {
2800 for offset in 2..=5 {
2803 if self.peek_word(offset).map_or(false, |w| w.to_lowercase() == "to") {
2804 return TokenType::Set;
2805 }
2806 }
2807 }
2808 }
2809 "return" => return TokenType::Return,
2810 "break" => return TokenType::Break,
2811 "xor" => return TokenType::Xor,
2812 "shifted" => return TokenType::Shifted,
2813 "be" if self.in_let_context => {
2814 self.in_let_context = false;
2815 return TokenType::Be;
2816 }
2817 "while" => return TokenType::While,
2818 "assert" => return TokenType::Assert,
2819 "trust" => return TokenType::Trust,
2820 "require" if self.mode == LexerMode::Imperative => return TokenType::Require,
2823 "requires" if self.mode == LexerMode::Imperative => return TokenType::Requires,
2824 "ensures" if self.mode == LexerMode::Imperative => return TokenType::Ensures,
2825 "check" => return TokenType::Check,
2826 "given" if self.mode == LexerMode::Declarative => return TokenType::Given,
2828 "prove" if self.mode == LexerMode::Declarative => return TokenType::Prove,
2829 "auto" if self.mode == LexerMode::Declarative => return TokenType::Auto,
2830 "listen" if self.mode == LexerMode::Imperative => return TokenType::Listen,
2832 "connect" if self.mode == LexerMode::Imperative => return TokenType::NetConnect,
2833 "sleep" if self.mode == LexerMode::Imperative => return TokenType::Sleep,
2834 "sync" if self.mode == LexerMode::Imperative => return TokenType::Sync,
2836 "mount" if self.mode == LexerMode::Imperative => return TokenType::Mount,
2838 "persistent" => return TokenType::Persistent, "combined" if self.mode == LexerMode::Imperative => return TokenType::Combined,
2840 "followed" if self.mode == LexerMode::Imperative => return TokenType::Followed,
2841 "launch" if self.mode == LexerMode::Imperative => return TokenType::Launch,
2845 "task" if self.mode == LexerMode::Imperative => return TokenType::Task,
2846 "pipe" if self.mode == LexerMode::Imperative => return TokenType::Pipe,
2847 "receive" if self.mode == LexerMode::Imperative => return TokenType::Receive,
2848 "stop" if self.mode == LexerMode::Imperative => return TokenType::Stop,
2849 "try" if self.mode == LexerMode::Imperative => return TokenType::Try,
2850 "into" if self.mode == LexerMode::Imperative => return TokenType::Into,
2851 "native" if self.mode == LexerMode::Imperative => return TokenType::Native,
2852 "escape" if self.mode == LexerMode::Imperative => return TokenType::Escape,
2853 "from" => return TokenType::From,
2854 "otherwise" => return TokenType::Otherwise,
2855 "else" => return TokenType::Else,
2857 "elif" => return TokenType::Elif,
2858 "either" if self.mode == LexerMode::Declarative => return TokenType::Either,
2860 "inspect" if self.mode == LexerMode::Imperative => return TokenType::Inspect,
2862 "new" if self.mode == LexerMode::Imperative => return TokenType::New,
2864 "give" if self.mode == LexerMode::Imperative => return TokenType::Give,
2867 "show" if self.mode == LexerMode::Imperative => return TokenType::Show,
2868 "push" if self.mode == LexerMode::Imperative => return TokenType::Push,
2870 "pop" if self.mode == LexerMode::Imperative => return TokenType::Pop,
2871 "copy" if self.mode == LexerMode::Imperative => return TokenType::Copy,
2872 "through" if self.mode == LexerMode::Imperative => return TokenType::Through,
2873 "length" if self.mode == LexerMode::Imperative => return TokenType::Length,
2874 "at" if self.mode == LexerMode::Imperative => return TokenType::At,
2875 "add" if self.mode == LexerMode::Imperative => return TokenType::Add,
2877 "remove" if self.mode == LexerMode::Imperative => return TokenType::Remove,
2878 "contains" if self.mode == LexerMode::Imperative => return TokenType::Contains,
2879 "union" if self.mode == LexerMode::Imperative => return TokenType::Union,
2880 "intersection" if self.mode == LexerMode::Imperative => return TokenType::Intersection,
2881 "inside" if self.mode == LexerMode::Imperative => return TokenType::Inside,
2883 "zone" if self.mode == LexerMode::Imperative => return TokenType::Zone,
2884 "called" if self.mode == LexerMode::Imperative => return TokenType::Called,
2885 "size" if self.mode == LexerMode::Imperative => return TokenType::Size,
2886 "mapped" if self.mode == LexerMode::Imperative => return TokenType::Mapped,
2887 "attempt" if self.mode == LexerMode::Imperative => return TokenType::Attempt,
2889 "following" if self.mode == LexerMode::Imperative => return TokenType::Following,
2890 "simultaneously" if self.mode == LexerMode::Imperative => return TokenType::Simultaneously,
2891 "read" if self.mode == LexerMode::Imperative => return TokenType::Read,
2893 "write" if self.mode == LexerMode::Imperative => return TokenType::Write,
2894 "console" if self.mode == LexerMode::Imperative => return TokenType::Console,
2895 "file" if self.mode == LexerMode::Imperative => return TokenType::File,
2896 "spawn" if self.mode == LexerMode::Imperative => return TokenType::Spawn,
2898 "send" if self.mode == LexerMode::Imperative => return TokenType::Send,
2899 "await" if self.mode == LexerMode::Imperative => return TokenType::Await,
2900 "portable" => return TokenType::Portable,
2902 "manifest" if self.mode == LexerMode::Imperative => return TokenType::Manifest,
2904 "chunk" if self.mode == LexerMode::Imperative => return TokenType::Chunk,
2905 "shared" => return TokenType::Shared, "merge" if self.mode == LexerMode::Imperative => return TokenType::Merge,
2908 "increase" if self.mode == LexerMode::Imperative => return TokenType::Increase,
2909 "decrease" if self.mode == LexerMode::Imperative => return TokenType::Decrease,
2911 "append" if self.mode == LexerMode::Imperative => return TokenType::Append,
2912 "resolve" if self.mode == LexerMode::Imperative => return TokenType::Resolve,
2913 "values" if self.mode == LexerMode::Imperative => return TokenType::Values,
2914 "tally" => return TokenType::Tally,
2916 "sharedset" => return TokenType::SharedSet,
2917 "sharedsequence" => return TokenType::SharedSequence,
2918 "collaborativesequence" => return TokenType::CollaborativeSequence,
2919 "sharedmap" => return TokenType::SharedMap,
2920 "divergent" => return TokenType::Divergent,
2921 "removewins" => return TokenType::RemoveWins,
2922 "addwins" => return TokenType::AddWins,
2923 "yata" => return TokenType::YATA,
2924 "seconds" => return TokenType::CalendarUnit(CalendarUnit::Second),
2929 "minutes" => return TokenType::CalendarUnit(CalendarUnit::Minute),
2930 "second" if self.prev_word_is_numeric() => {
2931 return TokenType::CalendarUnit(CalendarUnit::Second)
2932 }
2933 "minute" if self.prev_word_is_numeric() => {
2934 return TokenType::CalendarUnit(CalendarUnit::Minute)
2935 }
2936 "hour" | "hours" => return TokenType::CalendarUnit(CalendarUnit::Hour),
2937 "day" | "days" => return TokenType::CalendarUnit(CalendarUnit::Day),
2938 "week" | "weeks" => return TokenType::CalendarUnit(CalendarUnit::Week),
2939 "month" | "months" => return TokenType::CalendarUnit(CalendarUnit::Month),
2940 "year" | "years" => return TokenType::CalendarUnit(CalendarUnit::Year),
2941 "ago" => return TokenType::Ago,
2943 "hence" => return TokenType::Hence,
2944 "if" => return TokenType::If,
2945 "only" => return TokenType::Focus(FocusKind::Only),
2946 "even" => return TokenType::Focus(FocusKind::Even),
2947 "just" if self.peek_word(1).map_or(false, |w| {
2948 !self.is_verb_like(w) || w.to_lowercase() == "john" || w.chars().next().map_or(false, |c| c.is_uppercase())
2949 }) => return TokenType::Focus(FocusKind::Just),
2950 "much" => return TokenType::Measure(MeasureKind::Much),
2951 "little" => return TokenType::Measure(MeasureKind::Little),
2952 _ => {}
2953 }
2954
2955 if lexicon::is_scopal_adverb(&lower) {
2956 let sym = self.interner.intern(&Self::capitalize(&lower));
2957 return TokenType::ScopalAdverb(sym);
2958 }
2959
2960 if lexicon::is_temporal_adverb(&lower) && !self.prev_token_is_determiner() {
2961 let sym = self.interner.intern(&Self::capitalize(&lower));
2962 return TokenType::TemporalAdverb(sym);
2963 }
2964
2965 if lexicon::is_non_intersective(&lower) {
2966 let sym = self.interner.intern(&Self::capitalize(&lower));
2967 return TokenType::NonIntersectiveAdjective(sym);
2968 }
2969
2970 if lexicon::is_adverb(&lower) {
2971 let sym = self.interner.intern(&Self::capitalize(&lower));
2972 return TokenType::Adverb(sym);
2973 }
2974 let cap_ly_is_name = word.chars().next().map_or(false, |c| c.is_uppercase())
2978 && !self.peek_word(1).map_or(false, |w| {
2979 matches!(
2980 w.to_lowercase().as_str(),
2981 "he" | "she" | "it" | "they" | "we" | "i" | "you" | "who"
2982 )
2983 });
2984 if lower.ends_with("ly")
2985 && !lexicon::is_not_adverb(&lower)
2986 && !lexicon::is_common_noun(&lower)
2987 && lower.len() > 4
2988 && !cap_ly_is_name
2989 {
2990 let sym = self.interner.intern(&Self::capitalize(&lower));
2991 return TokenType::Adverb(sym);
2992 }
2993
2994 if let Some(base) = self.try_parse_superlative(&lower) {
2995 let sym = self.interner.intern(&base);
2996 return TokenType::Superlative(sym);
2997 }
2998
2999 let irregular_comparative = match lower.as_str() {
3005 "less" => Some("Little"),
3006 "fewer" => Some("Little"),
3007 "more" => Some("Much"),
3008 "better" => Some("Good"),
3009 "worse" => Some("Bad"),
3010 _ => None,
3011 };
3012 if let Some(base) = irregular_comparative {
3013 let sym = self.interner.intern(base);
3014 return TokenType::Comparative(sym);
3015 }
3016
3017 if let Some(base) = self.try_parse_comparative(&lower) {
3018 let next_is_than = self
3025 .peek_word(1)
3026 .map_or(false, |w| w.eq_ignore_ascii_case("than"));
3027 if !lexicon::is_common_noun(&lower) || next_is_than {
3028 let sym = self.interner.intern(&base);
3029 return TokenType::Comparative(sym);
3030 }
3031 }
3033
3034 if lexicon::is_performative(&lower) {
3035 let after_determiner = self.prev_token_is_determiner();
3041 let before_copula = self.next_token_is_copula();
3042 if !lexicon::is_common_noun(&lower) || (!after_determiner && !before_copula) {
3043 let sym = self.interner.intern(&Self::capitalize(&lower));
3044 return TokenType::Performative(sym);
3045 }
3046 }
3048
3049 if lexicon::is_base_verb_early(&lower) {
3050 let after_determiner = self.prev_token_is_determiner();
3055 let before_copula = self.next_token_is_copula();
3056 if !lexicon::is_common_noun(&lower) || (!after_determiner && !before_copula) {
3057 let sym = self.interner.intern(&Self::capitalize(&lower));
3058 let class = lexicon::lookup_verb_class(&lower);
3059 return TokenType::Verb {
3060 lemma: sym,
3061 time: Time::Present,
3062 aspect: Aspect::Simple,
3063 class,
3064 };
3065 }
3066 }
3068
3069 if lower.ends_with("ing") && lower.len() > 4 {
3072 let attributive = self.is_adjective_like(&lower)
3078 && self
3079 .peek_word(1)
3080 .map(|next| {
3081 let next_lower = next.to_lowercase();
3082 self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower)
3083 })
3084 .unwrap_or(false);
3085 if !attributive && !lexicon::is_preposition(&lower) {
3086 if let Some(entry) = self.lexicon.lookup_verb(&lower) {
3087 let sym = self.interner.intern(&entry.lemma);
3088 return TokenType::Verb {
3089 lemma: sym,
3090 time: entry.time,
3091 aspect: entry.aspect,
3092 class: entry.class,
3093 };
3094 }
3095 }
3096 }
3097
3098 if first_char.is_uppercase() {
3099 if let Some(next) = self.peek_word(1) {
3106 let next_lower = next.to_lowercase();
3107 let is_followed_by_verb = self.lexicon.lookup_verb(&next_lower).is_some()
3109 || matches!(next_lower.as_str(), "is" | "are" | "was" | "were" | "has" | "have" | "had");
3110
3111 if is_followed_by_verb {
3112 if let Some(analysis) = lexicon::analyze_word(&lower) {
3114 match analysis {
3115 lexicon::WordAnalysis::Noun(meta) if meta.number == lexicon::Number::Plural => {
3116 let sym = self.interner.intern(&lower);
3118 return TokenType::Noun(sym);
3119 }
3120 lexicon::WordAnalysis::DerivedNoun { number: lexicon::Number::Plural, .. } => {
3121 let sym = self.interner.intern(&lower);
3123 return TokenType::Noun(sym);
3124 }
3125 _ => {
3126 }
3129 }
3130 }
3131 }
3132 }
3133
3134 let sym = self.interner.intern(word);
3135 return TokenType::ProperName(sym);
3136 }
3137
3138 let verb_entry = self.lexicon.lookup_verb(&lower);
3139 let is_noun = lexicon::is_common_noun(&lower)
3143 || (verb_entry.is_none()
3144 && matches!(
3145 lexicon::analyze_word(&lower),
3146 Some(lexicon::WordAnalysis::Noun(_))
3147 ));
3148 let is_adj = self.is_adjective_like(&lower);
3149 let is_disambiguated = lexicon::is_disambiguation_not_verb(&lower);
3150
3151 let is_prep = lexicon::is_preposition(&lower);
3154 if verb_entry.is_some() && (is_noun || is_adj || is_prep) && !is_disambiguated {
3155 let entry = verb_entry.unwrap();
3156 let verb_token = TokenType::Verb {
3157 lemma: self.interner.intern(&entry.lemma),
3158 time: entry.time,
3159 aspect: entry.aspect,
3160 class: entry.class,
3161 };
3162
3163 let mut alternatives = Vec::new();
3164 if is_noun {
3165 alternatives.push(TokenType::Noun(self.interner.intern(word)));
3166 }
3167 if is_adj {
3168 alternatives.push(TokenType::Adjective(self.interner.intern(word)));
3169 }
3170 if is_prep {
3171 alternatives.push(TokenType::Preposition(self.interner.intern(&lower)));
3172 }
3173
3174 return TokenType::Ambiguous {
3175 primary: Box::new(verb_token),
3176 alternatives,
3177 };
3178 }
3179
3180 if let Some(entry) = &verb_entry {
3183 if is_disambiguated {
3184 let sym = self.interner.intern(word);
3185 if is_noun {
3186 return TokenType::Noun(sym);
3187 }
3188 if is_adj {
3189 return TokenType::Adjective(sym);
3190 }
3191 return TokenType::Verb {
3193 lemma: self.interner.intern(&entry.lemma),
3194 time: entry.time,
3195 aspect: entry.aspect,
3196 class: entry.class,
3197 };
3198 }
3199 }
3200
3201 if let Some(entry) = verb_entry {
3203 let sym = self.interner.intern(&entry.lemma);
3204 return TokenType::Verb {
3205 lemma: sym,
3206 time: entry.time,
3207 aspect: entry.aspect,
3208 class: entry.class,
3209 };
3210 }
3211
3212 if is_noun {
3214 let sym = self.interner.intern(word);
3215 return TokenType::Noun(sym);
3216 }
3217
3218 if is_adj {
3223 let sym = self.interner.intern(word);
3224 return TokenType::Adjective(sym);
3225 }
3226
3227 if lexicon::is_base_verb(&lower) {
3228 let sym = self.interner.intern(&Self::capitalize(&lower));
3229 let class = lexicon::lookup_verb_class(&lower);
3230 return TokenType::Verb {
3231 lemma: sym,
3232 time: Time::Present,
3233 aspect: Aspect::Simple,
3234 class,
3235 };
3236 }
3237
3238 if lower.ends_with("ian")
3239 || lower.ends_with("er")
3240 || lower == "logic"
3241 || lower == "time"
3242 || lower == "men"
3243 || lower == "book"
3244 || lower == "house"
3245 || lower == "code"
3246 || lower == "user"
3247 {
3248 let sym = self.interner.intern(word);
3249 return TokenType::Noun(sym);
3250 }
3251
3252 if lexicon::is_particle(&lower) {
3253 let sym = self.interner.intern(&lower);
3254 return TokenType::Particle(sym);
3255 }
3256
3257 if lower.len() >= 4 && lower.ends_with("ed") {
3263 let stem = if lower.ends_with("ied") {
3264 format!("{}y", &lower[..lower.len() - 3])
3265 } else {
3266 lower[..lower.len() - 2].to_string()
3267 };
3268 let verb_token = TokenType::Verb {
3269 lemma: self.interner.intern(&Self::capitalize(&stem)),
3270 time: Time::Past,
3271 aspect: Aspect::Simple,
3272 class: lexicon::lookup_verb_class(&stem),
3273 };
3274 return TokenType::Ambiguous {
3275 primary: Box::new(verb_token),
3276 alternatives: vec![TokenType::Noun(self.interner.intern(word))],
3277 };
3278 }
3279
3280 let sym = self.interner.intern(word);
3285 TokenType::Noun(sym)
3286 }
3287
3288 fn capitalize(s: &str) -> String {
3289 let mut chars = s.chars();
3290 match chars.next() {
3291 None => String::new(),
3292 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3293 }
3294 }
3295
3296 pub fn is_collective_verb(lemma: &str) -> bool {
3297 lexicon::is_collective_verb(&lemma.to_lowercase())
3298 }
3299
3300 pub fn is_mixed_verb(lemma: &str) -> bool {
3301 lexicon::is_mixed_verb(&lemma.to_lowercase())
3302 }
3303
3304 pub fn is_distributive_verb(lemma: &str) -> bool {
3305 lexicon::is_distributive_verb(&lemma.to_lowercase())
3306 }
3307
3308 pub fn is_intensional_predicate(lemma: &str) -> bool {
3309 lexicon::is_intensional_predicate(&lemma.to_lowercase())
3310 }
3311
3312 pub fn is_opaque_verb(lemma: &str) -> bool {
3313 lexicon::is_opaque_verb(&lemma.to_lowercase())
3314 }
3315
3316 pub fn is_ditransitive_verb(lemma: &str) -> bool {
3317 lexicon::is_ditransitive_verb(&lemma.to_lowercase())
3318 }
3319
3320 fn is_verb_like(&self, word: &str) -> bool {
3321 let lower = word.to_lowercase();
3322 if lexicon::is_infinitive_verb(&lower) {
3323 return true;
3324 }
3325 if let Some(entry) = self.lexicon.lookup_verb(&lower) {
3326 return entry.lemma.len() > 0;
3327 }
3328 false
3329 }
3330
3331 pub fn is_subject_control_verb(lemma: &str) -> bool {
3332 lexicon::is_subject_control_verb(&lemma.to_lowercase())
3333 }
3334
3335 pub fn is_raising_verb(lemma: &str) -> bool {
3336 lexicon::is_raising_verb(&lemma.to_lowercase())
3337 }
3338
3339 pub fn is_object_control_verb(lemma: &str) -> bool {
3340 lexicon::is_object_control_verb(&lemma.to_lowercase())
3341 }
3342
3343 pub fn is_weather_verb(lemma: &str) -> bool {
3344 matches!(
3345 lemma.to_lowercase().as_str(),
3346 "rain" | "snow" | "hail" | "thunder" | "pour"
3347 )
3348 }
3349
3350 fn try_parse_superlative(&self, word: &str) -> Option<String> {
3351 if !word.ends_with("est") || word.len() < 5 {
3352 return None;
3353 }
3354
3355 let base = &word[..word.len() - 3];
3356
3357 if base.len() >= 2 {
3358 let chars: Vec<char> = base.chars().collect();
3359 let last = chars[chars.len() - 1];
3360 let second_last = chars[chars.len() - 2];
3361 if last == second_last && !"aeiou".contains(last) {
3362 let stem = &base[..base.len() - 1];
3363 if lexicon::is_gradable_adjective(stem) {
3364 return Some(Self::capitalize(stem));
3365 }
3366 }
3367 }
3368
3369 if base.ends_with("i") {
3370 let stem = format!("{}y", &base[..base.len() - 1]);
3371 if lexicon::is_gradable_adjective(&stem) {
3372 return Some(Self::capitalize(&stem));
3373 }
3374 }
3375
3376 if lexicon::is_gradable_adjective(base) {
3377 return Some(Self::capitalize(base));
3378 }
3379
3380 None
3381 }
3382
3383 fn try_parse_comparative(&self, word: &str) -> Option<String> {
3384 if !word.ends_with("er") || word.len() < 4 {
3385 return None;
3386 }
3387
3388 let base = &word[..word.len() - 2];
3389
3390 if base.len() >= 2 {
3391 let chars: Vec<char> = base.chars().collect();
3392 let last = chars[chars.len() - 1];
3393 let second_last = chars[chars.len() - 2];
3394 if last == second_last && !"aeiou".contains(last) {
3395 let stem = &base[..base.len() - 1];
3396 if lexicon::is_gradable_adjective(stem) {
3397 return Some(Self::capitalize(stem));
3398 }
3399 }
3400 }
3401
3402 if base.ends_with("i") {
3403 let stem = format!("{}y", &base[..base.len() - 1]);
3404 if lexicon::is_gradable_adjective(&stem) {
3405 return Some(Self::capitalize(&stem));
3406 }
3407 }
3408
3409 if lexicon::is_gradable_adjective(base) {
3410 return Some(Self::capitalize(base));
3411 }
3412
3413 let with_e = format!("{}e", base);
3416 if lexicon::is_gradable_adjective(&with_e) {
3417 return Some(Self::capitalize(&with_e));
3418 }
3419
3420 None
3421 }
3422}
3423
3424#[cfg(test)]
3425mod tests {
3426 use super::*;
3427
3428 #[test]
3429 fn lexer_handles_apostrophe() {
3430 let mut interner = Interner::new();
3431 let mut lexer = Lexer::new("it's raining", &mut interner);
3432 let tokens = lexer.tokenize();
3433 assert!(!tokens.is_empty());
3434 }
3435
3436 #[test]
3437 fn lexer_handles_question_mark() {
3438 let mut interner = Interner::new();
3439 let mut lexer = Lexer::new("Is it raining?", &mut interner);
3440 let tokens = lexer.tokenize();
3441 assert!(!tokens.is_empty());
3442 }
3443
3444 #[test]
3445 fn ring_is_not_verb() {
3446 let mut interner = Interner::new();
3447 let mut lexer = Lexer::new("ring", &mut interner);
3448 let tokens = lexer.tokenize();
3449 assert!(matches!(tokens[0].kind, TokenType::Noun(_)));
3450 }
3451
3452 #[test]
3457 fn define_block_header_is_distinct_from_definition() {
3458 let mut interner = Interner::new();
3459 let mut lexer = Lexer::new("## Define\n", &mut interner);
3460 let tokens = lexer.tokenize();
3461 let header = tokens
3462 .iter()
3463 .find(|t| matches!(t.kind, TokenType::BlockHeader { .. }))
3464 .expect("## Define should produce a block header token");
3465 assert!(
3466 matches!(
3467 header.kind,
3468 TokenType::BlockHeader { block_type: BlockType::Define }
3469 ),
3470 "## Define must tokenize to BlockType::Define, got {:?}",
3471 header.kind
3472 );
3473
3474 let mut interner2 = Interner::new();
3476 let mut lexer2 = Lexer::new("## Definition\n", &mut interner2);
3477 let tokens2 = lexer2.tokenize();
3478 let header2 = tokens2
3479 .iter()
3480 .find(|t| matches!(t.kind, TokenType::BlockHeader { .. }))
3481 .expect("## Definition should produce a block header token");
3482 assert!(
3483 matches!(
3484 header2.kind,
3485 TokenType::BlockHeader { block_type: BlockType::Definition }
3486 ),
3487 "## Definition must still tokenize to BlockType::Definition, got {:?}",
3488 header2.kind
3489 );
3490 }
3491
3492 #[test]
3493 fn debug_that_token() {
3494 let mut interner = Interner::new();
3495 let mut lexer = Lexer::new("The cat that runs", &mut interner);
3496 let tokens = lexer.tokenize();
3497 for (i, t) in tokens.iter().enumerate() {
3498 let lex = interner.resolve(t.lexeme);
3499 eprintln!("Token[{}]: {:?} -> {:?}", i, lex, t.kind);
3500 }
3501 let that_token = tokens.iter().find(|t| interner.resolve(t.lexeme) == "that");
3502 if let Some(t) = that_token {
3503 let check = std::mem::discriminant(&t.kind) == std::mem::discriminant(&TokenType::That);
3505 eprintln!("Discriminant check for That: {}", check);
3506 assert!(matches!(t.kind, TokenType::That), "'that' should be TokenType::That, got {:?}", t.kind);
3507 } else {
3508 panic!("No 'that' token found");
3509 }
3510 }
3511
3512 #[test]
3513 fn bus_is_not_verb() {
3514 let mut interner = Interner::new();
3515 let mut lexer = Lexer::new("bus", &mut interner);
3516 let tokens = lexer.tokenize();
3517 assert!(matches!(tokens[0].kind, TokenType::Noun(_)));
3518 }
3519
3520 #[test]
3521 fn lowercase_a_is_article() {
3522 let mut interner = Interner::new();
3523 let mut lexer = Lexer::new("a car", &mut interner);
3524 let tokens = lexer.tokenize();
3525 for (i, t) in tokens.iter().enumerate() {
3526 let lex = interner.resolve(t.lexeme);
3527 eprintln!("Token[{}]: {:?} -> {:?}", i, lex, t.kind);
3528 }
3529 assert_eq!(tokens[0].kind, TokenType::Article(Definiteness::Indefinite));
3530 assert!(matches!(tokens[1].kind, TokenType::Noun(_)), "Expected Noun, got {:?}", tokens[1].kind);
3531 }
3532
3533 #[test]
3534 fn open_is_ambiguous() {
3535 let mut interner = Interner::new();
3536 let mut lexer = Lexer::new("open", &mut interner);
3537 let tokens = lexer.tokenize();
3538
3539 if let TokenType::Ambiguous { primary, alternatives } = &tokens[0].kind {
3540 assert!(matches!(**primary, TokenType::Verb { .. }), "Primary should be Verb");
3541 assert!(alternatives.iter().any(|t| matches!(t, TokenType::Adjective(_))),
3542 "Should have Adjective alternative");
3543 } else {
3544 panic!("Expected Ambiguous token for 'open', got {:?}", tokens[0].kind);
3545 }
3546 }
3547
3548 #[test]
3549 fn basic_tokenization() {
3550 let mut interner = Interner::new();
3551 let mut lexer = Lexer::new("All men are mortal.", &mut interner);
3552 let tokens = lexer.tokenize();
3553 assert_eq!(tokens[0].kind, TokenType::All);
3554 assert!(matches!(tokens[1].kind, TokenType::Noun(_)));
3555 assert_eq!(tokens[2].kind, TokenType::Are);
3556 }
3557
3558 #[test]
3559 fn iff_tokenizes_as_single_token() {
3560 let mut interner = Interner::new();
3561 let mut lexer = Lexer::new("A if and only if B", &mut interner);
3562 let tokens = lexer.tokenize();
3563 assert!(
3564 tokens.iter().any(|t| t.kind == TokenType::Iff),
3565 "should contain Iff token: got {:?}",
3566 tokens
3567 );
3568 }
3569
3570 #[test]
3571 fn is_equal_to_tokenizes_as_identity() {
3572 let mut interner = Interner::new();
3573 let mut lexer = Lexer::new("Socrates is equal to Socrates", &mut interner);
3574 let tokens = lexer.tokenize();
3575 assert!(
3576 tokens.iter().any(|t| t.kind == TokenType::Identity),
3577 "should contain Identity token: got {:?}",
3578 tokens
3579 );
3580 }
3581
3582 #[test]
3583 fn is_identical_to_tokenizes_as_identity() {
3584 let mut interner = Interner::new();
3585 let mut lexer = Lexer::new("Clark is identical to Superman", &mut interner);
3586 let tokens = lexer.tokenize();
3587 assert!(
3588 tokens.iter().any(|t| t.kind == TokenType::Identity),
3589 "should contain Identity token: got {:?}",
3590 tokens
3591 );
3592 }
3593
3594 #[test]
3595 fn itself_tokenizes_as_reflexive() {
3596 let mut interner = Interner::new();
3597 let mut lexer = Lexer::new("John loves itself", &mut interner);
3598 let tokens = lexer.tokenize();
3599 assert!(
3600 tokens.iter().any(|t| t.kind == TokenType::Reflexive),
3601 "should contain Reflexive token: got {:?}",
3602 tokens
3603 );
3604 }
3605
3606 #[test]
3607 fn himself_tokenizes_as_reflexive() {
3608 let mut interner = Interner::new();
3609 let mut lexer = Lexer::new("John sees himself", &mut interner);
3610 let tokens = lexer.tokenize();
3611 assert!(
3612 tokens.iter().any(|t| t.kind == TokenType::Reflexive),
3613 "should contain Reflexive token: got {:?}",
3614 tokens
3615 );
3616 }
3617
3618 #[test]
3619 fn to_stay_tokenizes_correctly() {
3620 let mut interner = Interner::new();
3621 let mut lexer = Lexer::new("to stay", &mut interner);
3622 let tokens = lexer.tokenize();
3623 assert!(
3624 tokens.iter().any(|t| t.kind == TokenType::To),
3625 "should contain To token: got {:?}",
3626 tokens
3627 );
3628 assert!(
3629 tokens.iter().any(|t| matches!(t.kind, TokenType::Verb { .. })),
3630 "should contain Verb token for stay: got {:?}",
3631 tokens
3632 );
3633 }
3634
3635 #[test]
3636 fn possessive_apostrophe_s() {
3637 let mut interner = Interner::new();
3638 let mut lexer = Lexer::new("John's dog", &mut interner);
3639 let tokens = lexer.tokenize();
3640 assert!(
3641 tokens.iter().any(|t| t.kind == TokenType::Possessive),
3642 "should contain Possessive token: got {:?}",
3643 tokens
3644 );
3645 assert!(
3646 tokens.iter().any(|t| matches!(&t.kind, TokenType::ProperName(_))),
3647 "should have John as proper name: got {:?}",
3648 tokens
3649 );
3650 }
3651
3652 #[test]
3653 fn lexer_produces_valid_spans() {
3654 let input = "All men are mortal.";
3655 let mut interner = Interner::new();
3656 let mut lexer = Lexer::new(input, &mut interner);
3657 let tokens = lexer.tokenize();
3658
3659 assert_eq!(tokens[0].span.start, 0);
3661 assert_eq!(tokens[0].span.end, 3);
3662 assert_eq!(&input[tokens[0].span.start..tokens[0].span.end], "All");
3663
3664 assert_eq!(tokens[1].span.start, 4);
3666 assert_eq!(tokens[1].span.end, 7);
3667 assert_eq!(&input[tokens[1].span.start..tokens[1].span.end], "men");
3668
3669 assert_eq!(tokens[2].span.start, 8);
3671 assert_eq!(tokens[2].span.end, 11);
3672 assert_eq!(&input[tokens[2].span.start..tokens[2].span.end], "are");
3673
3674 assert_eq!(tokens[3].span.start, 12);
3676 assert_eq!(tokens[3].span.end, 18);
3677 assert_eq!(&input[tokens[3].span.start..tokens[3].span.end], "mortal");
3678
3679 assert_eq!(tokens[4].span.start, 18);
3681 assert_eq!(tokens[4].span.end, 19);
3682
3683 assert_eq!(tokens[5].span.start, input.len());
3685 assert_eq!(tokens[5].kind, TokenType::EOF);
3686 }
3687
3688 #[test]
3689 fn triple_quote_produces_string_token() {
3690 let mut interner = Interner::new();
3691 let source = "## Main\nLet msg be \"\"\"\n Hello\n World\n\"\"\".\nShow msg.";
3692 let mut lexer = Lexer::new(source, &mut interner);
3693 let tokens = lexer.tokenize();
3694 for (i, t) in tokens.iter().enumerate() {
3696 let lex = interner.resolve(t.lexeme);
3697 eprintln!("Token[{}]: {:?} lex={:?} span={}..{}", i, t.kind, lex, t.span.start, t.span.end);
3698 }
3699 let str_token = tokens.iter().find(|t| matches!(t.kind, TokenType::StringLiteral(_) | TokenType::InterpolatedString(_)));
3701 assert!(str_token.is_some(), "Should have a string token. Tokens: {:?}", tokens.iter().map(|t| format!("{:?}", t.kind)).collect::<Vec<_>>());
3702 if let Some(tok) = str_token {
3703 let content = interner.resolve(tok.lexeme);
3704 eprintln!("Triple-quote content: {:?}", content);
3705 assert!(content.contains("Hello"), "Should contain Hello, got: {:?}", content);
3706 }
3707 }
3708
3709 #[test]
3713 fn string_literal_span_stays_byte_indexed_after_leading_multibyte_text() {
3714 let src = "\u{3b1}\u{3b1}\u{3b1}\u{3b1}\u{3b1}\u{3b1}\u{3b1} \"x\"";
3716 let mut interner = Interner::new();
3717 let mut lexer = Lexer::new(src, &mut interner);
3718 let tokens = lexer.tokenize(); let s = tokens
3721 .iter()
3722 .find(|t| matches!(t.kind, TokenType::StringLiteral(_) | TokenType::InterpolatedString(_)))
3723 .expect("should produce a string literal token");
3724
3725 assert!(s.span.end >= s.span.start, "span end {} must be >= start {}", s.span.end, s.span.start);
3726 assert!(
3727 src.is_char_boundary(s.span.start) && src.is_char_boundary(s.span.end),
3728 "span [{}, {}) must lie on char boundaries",
3729 s.span.start, s.span.end
3730 );
3731 assert_eq!(&src[s.span.start..s.span.end], "\"x\"");
3732 }
3733
3734 #[test]
3738 fn date_literal_rejects_impossible_day_of_month() {
3739 let impossible = [
3740 "2026-02-30", "2026-02-31", "2026-02-29", "2026-04-31", "2026-06-31", "2026-09-31", "2026-11-31",
3742 ];
3743 for input in impossible {
3744 let mut interner = Interner::new();
3745 let mut lexer = Lexer::new(input, &mut interner);
3746 let tokens = lexer.tokenize();
3747 assert!(
3748 !tokens.iter().any(|t| matches!(t.kind, TokenType::DateLiteral { .. })),
3749 "{input} is not a real calendar date and must not become a DateLiteral; got {tokens:?}"
3750 );
3751 }
3752 let mut interner = Interner::new();
3754 let mut lexer = Lexer::new("2024-02-29", &mut interner);
3755 let tokens = lexer.tokenize();
3756 assert!(
3757 tokens.iter().any(|t| matches!(t.kind, TokenType::DateLiteral { .. })),
3758 "2024-02-29 is a valid leap-day and must still tokenize; got {tokens:?}"
3759 );
3760 }
3761}