Skip to main content

logicaffeine_language/
lexer.rs

1//! Two-stage lexer for LOGOS natural language input.
2//!
3//! The lexer transforms natural language text into a token stream suitable
4//! for parsing. It operates in two stages:
5//!
6//! ## Stage 1: Line Lexer
7//!
8//! The [`LineLexer`] handles structural concerns:
9//!
10//! - **Indentation**: Tracks indent levels, emits `Indent`/`Dedent` tokens
11//! - **Block boundaries**: Identifies significant whitespace
12//! - **Content extraction**: Passes line content to Stage 2
13//!
14//! ## Stage 2: Word Lexer
15//!
16//! The [`Lexer`] performs word-level tokenization:
17//!
18//! - **Vocabulary lookup**: Identifies words via the lexicon database
19//! - **Morphological analysis**: Handles inflection (verb tenses, plurals)
20//! - **Ambiguity resolution**: Uses priority rules for ambiguous words
21//!
22//! ## Ambiguity Rules
23//!
24//! When a word matches multiple lexicon entries, priority determines the token:
25//!
26//! 1. **Quantifiers** over nouns ("some" → Quantifier, not Noun)
27//! 2. **Determiners** over adjectives ("the" → Determiner, not Adjective)
28//! 3. **Verbs** over nouns for -ing/-ed forms ("running" → Verb)
29//!
30//! ## Example
31//!
32//! ```text
33//! Input:  "Every cat sleeps."
34//! Output: [Quantifier("every"), Noun("cat"), Verb("sleeps"), Period]
35//! ```
36
37use logicaffeine_base::Interner;
38use crate::lexicon::{self, Aspect, Definiteness, Lexicon, Time};
39use crate::token::{BlockType, CalendarUnit, FocusKind, MeasureKind, Span, Token, TokenType};
40
41// ============================================================================
42// Stage 1: Line Lexer (Spec §2.5.2)
43// ============================================================================
44
45/// Tokens emitted by the LineLexer (Stage 1).
46/// Handles structural tokens (Indent, Dedent, Newline) while treating
47/// all other content as opaque for Stage 2 word classification.
48#[derive(Debug, Clone, PartialEq)]
49pub enum LineToken {
50    /// Block increased indentation
51    Indent,
52    /// Block decreased indentation
53    Dedent,
54    /// Logical newline (statement boundary) - reserved for future use
55    Newline,
56    /// Content to be further tokenized (line content, trimmed)
57    Content { text: String, start: usize, end: usize },
58}
59
60/// Stage 1 Lexer: Handles only lines, indentation, and structural tokens.
61/// Treats all other text as opaque `Content` for the Stage 2 WordLexer.
62pub struct LineLexer<'a> {
63    source: &'a str,
64    bytes: &'a [u8],
65    indent_stack: Vec<usize>,
66    pending_dedents: usize,
67    position: usize,
68    /// True if we need to emit Content for current line
69    has_pending_content: bool,
70    pending_content_start: usize,
71    pending_content_end: usize,
72    pending_content_text: String,
73    /// True after we've finished processing all lines
74    finished_lines: bool,
75    /// True if we've emitted at least one Indent (need to emit Dedents at EOF)
76    emitted_indent: bool,
77    /// Escape block body byte ranges to skip (start_byte, end_byte)
78    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    /// Check if a byte position falls within an escape body range.
117    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    /// Calculate indentation level at current position (at start of line).
122    /// Returns (indent_level, content_start_pos).
123    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; // Tab = 4 spaces
135                    pos += 1;
136                }
137                _ => break,
138            }
139        }
140
141        (indent, pos)
142    }
143
144    /// Read content from current position until end of line or EOF.
145    /// Returns (content_text, content_start, content_end, next_line_start).
146    fn read_line_content(&self, content_start: usize) -> (String, usize, usize, usize) {
147        let mut pos = content_start;
148
149        // Find end of line
150        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        // Move past newline if present
158        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    /// Check if the line starting at `pos` is blank (only whitespace).
168    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 // EOF counts as blank
178    }
179
180    /// Process the next line and update internal state.
181    /// Returns true if we have tokens to emit, false if we're done.
182    fn process_next_line(&mut self) -> bool {
183        // Skip blank lines
184        while self.position < self.bytes.len() && self.is_blank_line(self.position) {
185            // Skip to next line
186            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; // Skip the newline
191            }
192        }
193
194        // Check if we've reached EOF
195        if self.position >= self.bytes.len() {
196            self.finished_lines = true;
197            // Emit remaining dedents at EOF
198            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        // Measure indentation of current line
206        let (line_indent, content_start) = self.measure_indent(self.position);
207
208        // Read line content
209        let (text, start, end, next_pos) = self.read_line_content(content_start);
210
211        // Skip if content is empty (shouldn't happen after blank line skip, but be safe)
212        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        // Handle indentation changes
220        if line_indent > current_indent {
221            // Indent: push new level
222            self.indent_stack.push(line_indent);
223            self.emitted_indent = true;
224            // Store content to emit after Indent
225            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            // We'll emit Indent first, then Content
231            return true;
232        } else if line_indent < current_indent {
233            // Dedent: pop until we match
234            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            // Store content to emit after Dedents
244            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            // Same indentation level
252            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        // 1. Emit pending dedents first
267        if self.pending_dedents > 0 {
268            self.pending_dedents -= 1;
269            return Some(LineToken::Dedent);
270        }
271
272        // 2. Emit pending content
273        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        // 3. Check if we need to emit Indent (after pushing to stack)
282        // This happens when we detected an indent but haven't emitted the token yet
283        // We need to check if indent_stack was just modified
284
285        // 4. Process next line
286        if !self.finished_lines {
287            let had_indent = self.indent_stack.len();
288            if self.process_next_line() {
289                // Check if we added an indent level
290                if self.indent_stack.len() > had_indent {
291                    return Some(LineToken::Indent);
292                }
293                // Check if we have pending dedents
294                if self.pending_dedents > 0 {
295                    self.pending_dedents -= 1;
296                    return Some(LineToken::Dedent);
297                }
298                // Otherwise emit content
299                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                // EOF with pending dedents
308                self.pending_dedents -= 1;
309                return Some(LineToken::Dedent);
310            }
311        }
312
313        // 5. Emit any remaining dedents at EOF
314        if self.pending_dedents > 0 {
315            self.pending_dedents -= 1;
316            return Some(LineToken::Dedent);
317        }
318
319        None
320    }
321}
322
323// ============================================================================
324// Stage 2: Word Lexer (existing Lexer)
325// ============================================================================
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
328pub enum LexerMode {
329    #[default]
330    Declarative, // Logic, Theorems, Definitions
331    Imperative,  // Main, Functions, Code
332}
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 block body byte ranges: (skip_start, skip_end) for filtering LineLexer events
344    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    /// Creates a new lexer for the given input text.
357    ///
358    /// The lexer will tokenize natural language text according to the
359    /// lexicon database, performing morphological analysis and ambiguity
360    /// resolution.
361    ///
362    /// # Arguments
363    ///
364    /// * `input` - The natural language text to tokenize
365    /// * `interner` - String interner for efficient symbol handling
366    ///
367    /// # Example
368    ///
369    /// ```
370    /// use logicaffeine_language::lexer::Lexer;
371    /// use logicaffeine_base::Interner;
372    ///
373    /// let mut interner = Interner::new();
374    /// let mut lexer = Lexer::new("Every cat sleeps.", &mut interner);
375    /// let tokens = lexer.tokenize();
376    ///
377    /// assert_eq!(tokens.len(), 5); // Quantifier, Noun, Verb, Period, EOI
378    /// ```
379    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    /// Pre-scan source text for escape block bodies.
401    /// Returns (skip_start_byte, skip_end_byte, content_start_byte, raw_code) tuples.
402    /// `skip_start` is the line start (for byte skipping in split_into_words).
403    /// `content_start` is after leading whitespace (for token span alignment with Indent events).
404    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; // +1 for the newline
412        }
413
414        let mut i = 0;
415        while i < lines.len() {
416            let trimmed = lines[i].trim();
417            // Check if this line contains an escape header: "Escape to Rust:"
418            // Matches both statement position (whole line) and expression position
419            // (e.g., "Let x: Int be Escape to Rust:")
420            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                // Find the body: subsequent lines with deeper indentation
426                let header_indent = Self::measure_indent_static(lines[i]);
427                i += 1;
428
429                // Skip blank lines to find the first body line
430                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                    // No body found
437                    continue;
438                }
439
440                let base_indent = Self::measure_indent_static(lines[body_start_line]);
441                if base_indent <= header_indent {
442                    // No indented body
443                    continue;
444                }
445
446                // Capture all lines at base_indent or deeper
447                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                        // Blank lines are preserved
456                        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                    // Strip base indentation
466                    let stripped = Self::strip_indent(line, base_indent);
467                    code_lines.push(stripped);
468                    body_end_line = j;
469                    j += 1;
470                }
471
472                // Trim trailing empty lines from code
473                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                    // Compute content start (after leading whitespace of first body line)
484                    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    /// Count leading whitespace bytes in a line.
499    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    /// Measure indent of a line (static helper for pre-scan).
511    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    /// Strip `count` leading spaces/tabs from a line.
524    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        // Track byte offset for escape range matching
552        let mut skip_to_byte: Option<usize> = None;
553
554        for (i, c) in input.char_indices() {
555            if skip_count > 0 {
556                skip_count -= 1;
557                char_idx += 1;
558                continue;
559            }
560            // Skip bytes inside escape block bodies
561            if let Some(end) = skip_to_byte {
562                if i < end {
563                    char_idx += 1;
564                    continue;
565                }
566                skip_to_byte = None;
567                word_start = i;
568            }
569            // Check if this byte position starts an escape block body
570            if let Some((_, end, content_start, raw_code)) = escape_ranges.iter().find(|(s, _, _, _)| i == *s) {
571                // Flush any pending word
572                if !current_word.is_empty() {
573                    items.push(WordItem {
574                        word: std::mem::take(&mut current_word),
575                        trailing_punct: None,
576                        start: word_start,
577                        end: i,
578                        punct_pos: None,
579                    });
580                }
581                // Emit the entire block as a single \x00ESC: marker
582                // Use content_start (after whitespace) for span alignment with Indent events
583                items.push(WordItem {
584                    word: format!("\x00ESC:{}", raw_code),
585                    trailing_punct: None,
586                    start: *content_start,
587                    end: *end,
588                    punct_pos: None,
589                });
590                skip_to_byte = Some(*end);
591                word_start = *end;
592                char_idx += 1;
593                continue;
594            }
595            let next_pos = i + c.len_utf8();
596            match c {
597                ' ' | '\t' | '\n' | '\r' => {
598                    if !current_word.is_empty() {
599                        items.push(WordItem {
600                            word: std::mem::take(&mut current_word),
601                            trailing_punct: None,
602                            start: word_start,
603                            end: i,
604                            punct_pos: None,
605                        });
606                    }
607                    word_start = next_pos;
608                }
609                '.' => {
610                    // Check if this is a decimal point (digit before and after)
611                    let prev_is_digit = !current_word.is_empty()
612                        && current_word.chars().last().map_or(false, |ch| ch.is_ascii_digit());
613                    let next_is_digit = char_idx + 1 < chars.len()
614                        && chars[char_idx + 1].is_ascii_digit();
615
616                    if prev_is_digit && next_is_digit {
617                        // This is a decimal point, include it in the current word
618                        current_word.push(c);
619                    } else {
620                        // This is a sentence period
621                        if !current_word.is_empty() {
622                            items.push(WordItem {
623                                word: std::mem::take(&mut current_word),
624                                trailing_punct: Some(c),
625                                start: word_start,
626                                end: i,
627                                punct_pos: Some(i),
628                            });
629                        } else {
630                            items.push(WordItem {
631                                word: String::new(),
632                                trailing_punct: Some(c),
633                                start: i,
634                                end: next_pos,
635                                punct_pos: Some(i),
636                            });
637                        }
638                        word_start = next_pos;
639                    }
640                }
641                '#' => {
642                    // Check for ## block header (markdown-style)
643                    if char_idx + 1 < chars.len() && chars[char_idx + 1] == '#' {
644                        // This is a ## block header
645                        // Skip the second # and capture the next word as a block header
646                        if !current_word.is_empty() {
647                            items.push(WordItem {
648                                word: std::mem::take(&mut current_word),
649                                trailing_punct: None,
650                                start: word_start,
651                                end: i,
652                                punct_pos: None,
653                            });
654                        }
655                        // Skip whitespace after ##
656                        let header_start = i;
657                        let mut j = char_idx + 2;
658                        while j < chars.len() && (chars[j] == ' ' || chars[j] == '\t') {
659                            j += 1;
660                        }
661                        // Capture the block type word
662                        let mut block_word = String::from("##");
663                        while j < chars.len() && chars[j].is_alphabetic() {
664                            block_word.push(chars[j]);
665                            j += 1;
666                        }
667                        if block_word.len() > 2 {
668                            items.push(WordItem {
669                                word: block_word,
670                                trailing_punct: None,
671                                start: header_start,
672                                end: header_start + (j - char_idx),
673                                punct_pos: None,
674                            });
675                        }
676                        skip_count = j - char_idx - 1;
677                        word_start = header_start + (j - char_idx);
678                    } else {
679                        // Single # - treat as comment, skip to end of line
680                        // Count how many chars to skip (without modifying char_idx here -
681                        // the main loop's skip handler will increment it)
682                        let mut look_ahead = char_idx + 1;
683                        while look_ahead < chars.len() && chars[look_ahead] != '\n' {
684                            skip_count += 1;
685                            look_ahead += 1;
686                        }
687                        if !current_word.is_empty() {
688                            items.push(WordItem {
689                                word: std::mem::take(&mut current_word),
690                                trailing_punct: None,
691                                start: word_start,
692                                end: i,
693                                punct_pos: None,
694                            });
695                        }
696                        word_start = look_ahead + 1; // Start after the newline
697                    }
698                }
699                // String literals: "hello world" or """multi-line"""
700                '"' => {
701                    // Push any pending word
702                    if !current_word.is_empty() {
703                        items.push(WordItem {
704                            word: std::mem::take(&mut current_word),
705                            trailing_punct: None,
706                            start: word_start,
707                            end: i,
708                            punct_pos: None,
709                        });
710                    }
711
712                    // Check for triple-quote: """
713                    if char_idx + 2 < chars.len() && chars[char_idx + 1] == '"' && chars[char_idx + 2] == '"' {
714                        let string_start = i;
715                        let mut j = char_idx + 3; // skip opening """
716                        // Skip optional newline after opening """
717                        if j < chars.len() && chars[j] == '\n' {
718                            j += 1;
719                        }
720                        let mut raw_content = String::new();
721                        // Scan until closing """
722                        while j < chars.len() {
723                            if j + 2 < chars.len() && chars[j] == '"' && chars[j + 1] == '"' && chars[j + 2] == '"' {
724                                break;
725                            }
726                            raw_content.push(chars[j]);
727                            j += 1;
728                        }
729                        // Strip trailing newline before closing """
730                        if raw_content.ends_with('\n') {
731                            raw_content.pop();
732                        }
733                        // Dedent: find minimum common indentation and strip it
734                        let dedented = Self::dedent_triple_quote(&raw_content);
735                        let end_pos = if j + 2 < chars.len() { j + 3 } else { chars.len() };
736                        items.push(WordItem {
737                            word: format!("\x00STR:{}", dedented),
738                            trailing_punct: None,
739                            start: string_start,
740                            end: end_pos,
741                            punct_pos: None,
742                        });
743                        // Skip past the closing """
744                        if j + 2 < chars.len() {
745                            skip_count = (j + 2) - char_idx;
746                        } else {
747                            skip_count = chars.len() - 1 - char_idx;
748                        }
749                        word_start = end_pos;
750                    } else {
751                        // Single-quoted string: scan until closing quote
752                        let string_start = i;
753                        let mut j = char_idx + 1;
754                        let mut string_content = String::new();
755                        while j < chars.len() && chars[j] != '"' {
756                            if chars[j] == '\\' && j + 1 < chars.len() {
757                                // Escape sequence - skip backslash, include next char
758                                j += 1;
759                                if j < chars.len() {
760                                    string_content.push(chars[j]);
761                                }
762                            } else {
763                                string_content.push(chars[j]);
764                            }
765                            j += 1;
766                        }
767
768                        // Create a special marker for string literals
769                        // We prefix with a special character to identify in tokenize()
770                        items.push(WordItem {
771                            word: format!("\x00STR:{}", string_content),
772                            trailing_punct: None,
773                            start: string_start,
774                            end: if j < chars.len() { j + 1 } else { j },
775                            punct_pos: None,
776                        });
777
778                        // Skip past the closing quote
779                        if j < chars.len() {
780                            skip_count = j - char_idx;
781                        } else {
782                            skip_count = j - char_idx - 1;
783                        }
784                        word_start = if j < chars.len() { j + 1 } else { j };
785                    }
786                }
787                // Character literals with backticks: `x`
788                '`' => {
789                    // Push any pending word
790                    if !current_word.is_empty() {
791                        items.push(WordItem {
792                            word: std::mem::take(&mut current_word),
793                            trailing_punct: None,
794                            start: word_start,
795                            end: i,
796                            punct_pos: None,
797                        });
798                    }
799
800                    // Scan for character content and closing backtick
801                    let char_start = i;
802                    let mut j = char_idx + 1;
803                    let mut char_content = String::new();
804
805                    if j < chars.len() {
806                        if chars[j] == '\\' && j + 1 < chars.len() {
807                            // Escape sequence
808                            j += 1;
809                            let escaped_char = match chars[j] {
810                                'n' => '\n',
811                                't' => '\t',
812                                'r' => '\r',
813                                '\\' => '\\',
814                                '`' => '`',
815                                '0' => '\0',
816                                c => c,
817                            };
818                            char_content.push(escaped_char);
819                            j += 1;
820                        } else if chars[j] != '`' {
821                            // Regular character
822                            char_content.push(chars[j]);
823                            j += 1;
824                        }
825                    }
826
827                    // Expect closing backtick
828                    if j < chars.len() && chars[j] == '`' {
829                        j += 1; // skip closing backtick
830                    }
831
832                    // Create a special marker for char literals
833                    items.push(WordItem {
834                        word: format!("\x00CHAR:{}", char_content),
835                        trailing_punct: None,
836                        start: char_start,
837                        end: if j <= chars.len() { char_start + (j - char_idx) } else { char_start + 1 },
838                        punct_pos: None,
839                    });
840
841                    if j > char_idx + 1 {
842                        skip_count = j - char_idx - 1;
843                    }
844                    word_start = char_start + (j - char_idx);
845                }
846                // Handle -> as a single token for return type syntax
847                '-' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '>' => {
848                    // Push any pending word first
849                    if !current_word.is_empty() {
850                        items.push(WordItem {
851                            word: std::mem::take(&mut current_word),
852                            trailing_punct: None,
853                            start: word_start,
854                            end: i,
855                            punct_pos: None,
856                        });
857                    }
858                    // Push -> as its own word
859                    items.push(WordItem {
860                        word: "->".to_string(),
861                        trailing_punct: None,
862                        start: i,
863                        end: i + 2,
864                        punct_pos: None,
865                    });
866                    skip_count = 1; // Skip the '>' character
867                    word_start = i + 2;
868                }
869                // Grand Challenge: Handle <= as a single token
870                '<' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
871                    if !current_word.is_empty() {
872                        items.push(WordItem {
873                            word: std::mem::take(&mut current_word),
874                            trailing_punct: None,
875                            start: word_start,
876                            end: i,
877                            punct_pos: None,
878                        });
879                    }
880                    items.push(WordItem {
881                        word: "<=".to_string(),
882                        trailing_punct: None,
883                        start: i,
884                        end: i + 2,
885                        punct_pos: None,
886                    });
887                    skip_count = 1;
888                    word_start = i + 2;
889                }
890                // Grand Challenge: Handle >= as a single token
891                '>' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
892                    if !current_word.is_empty() {
893                        items.push(WordItem {
894                            word: std::mem::take(&mut current_word),
895                            trailing_punct: None,
896                            start: word_start,
897                            end: i,
898                            punct_pos: None,
899                        });
900                    }
901                    items.push(WordItem {
902                        word: ">=".to_string(),
903                        trailing_punct: None,
904                        start: i,
905                        end: i + 2,
906                        punct_pos: None,
907                    });
908                    skip_count = 1;
909                    word_start = i + 2;
910                }
911                // Handle == as a single token
912                '=' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
913                    if !current_word.is_empty() {
914                        items.push(WordItem {
915                            word: std::mem::take(&mut current_word),
916                            trailing_punct: None,
917                            start: word_start,
918                            end: i,
919                            punct_pos: None,
920                        });
921                    }
922                    items.push(WordItem {
923                        word: "==".to_string(),
924                        trailing_punct: None,
925                        start: i,
926                        end: i + 2,
927                        punct_pos: None,
928                    });
929                    skip_count = 1;
930                    word_start = i + 2;
931                }
932                // Handle != as a single token
933                '!' if char_idx + 1 < chars.len() && chars[char_idx + 1] == '=' => {
934                    if !current_word.is_empty() {
935                        items.push(WordItem {
936                            word: std::mem::take(&mut current_word),
937                            trailing_punct: None,
938                            start: word_start,
939                            end: i,
940                            punct_pos: None,
941                        });
942                    }
943                    items.push(WordItem {
944                        word: "!=".to_string(),
945                        trailing_punct: None,
946                        start: i,
947                        end: i + 2,
948                        punct_pos: None,
949                    });
950                    skip_count = 1;
951                    word_start = i + 2;
952                }
953                // Special handling for '-' in ISO-8601 dates (YYYY-MM-DD)
954                '-' if Self::is_date_hyphen(&current_word, &chars, char_idx) => {
955                    // This hyphen is part of a date, include it in the word
956                    current_word.push(c);
957                }
958                // Special handling for ':' in time literals (9:30am, 11:45pm)
959                ':' if Self::is_time_colon(&current_word, &chars, char_idx) => {
960                    // This colon is part of a time, include it in the word
961                    current_word.push(c);
962                }
963                // Scientific notation: 4.84e+00, 1.66E-03, 2.5e-2
964                '+' | '-' if Self::is_exponent_sign(&current_word, &chars, char_idx) => {
965                    current_word.push(c);
966                }
967                '(' | ')' | '[' | ']' | ',' | '?' | '!' | ':' | '+' | '-' | '*' | '/' | '%' | '<' | '>' | '=' => {
968                    if !current_word.is_empty() {
969                        items.push(WordItem {
970                            word: std::mem::take(&mut current_word),
971                            trailing_punct: Some(c),
972                            start: word_start,
973                            end: i,
974                            punct_pos: Some(i),
975                        });
976                    } else {
977                        items.push(WordItem {
978                            word: String::new(),
979                            trailing_punct: Some(c),
980                            start: i,
981                            end: next_pos,
982                            punct_pos: Some(i),
983                        });
984                    }
985                    word_start = next_pos;
986                }
987                '\'' => {
988                    // Handle contractions: expand "don't" → "do" + "not", etc.
989                    let remaining: String = chars[char_idx + 1..].iter().collect();
990                    let remaining_lower = remaining.to_lowercase();
991
992                    if remaining_lower.starts_with("t ") || remaining_lower.starts_with("t.") ||
993                       remaining_lower.starts_with("t,") || remaining_lower == "t" ||
994                       (char_idx + 1 < chars.len() && chars[char_idx + 1] == 't' &&
995                        (char_idx + 2 >= chars.len() || !chars[char_idx + 2].is_alphabetic())) {
996                        // This is a contraction ending in 't (don't, doesn't, won't, can't, etc.)
997                        let word_lower = current_word.to_lowercase();
998                        if word_lower == "don" || word_lower == "doesn" || word_lower == "didn" {
999                            // do/does/did + not
1000                            let base = if word_lower == "don" { "do" }
1001                                      else if word_lower == "doesn" { "does" }
1002                                      else { "did" };
1003                            items.push(WordItem {
1004                                word: base.to_string(),
1005                                trailing_punct: None,
1006                                start: word_start,
1007                                end: i,
1008                                punct_pos: None,
1009                            });
1010                            items.push(WordItem {
1011                                word: "not".to_string(),
1012                                trailing_punct: None,
1013                                start: i,
1014                                end: i + 2,
1015                                punct_pos: None,
1016                            });
1017                            current_word.clear();
1018                            word_start = next_pos + 1;
1019                            skip_count = 1;
1020                        } else if word_lower == "won" {
1021                            // will + not
1022                            items.push(WordItem {
1023                                word: "will".to_string(),
1024                                trailing_punct: None,
1025                                start: word_start,
1026                                end: i,
1027                                punct_pos: None,
1028                            });
1029                            items.push(WordItem {
1030                                word: "not".to_string(),
1031                                trailing_punct: None,
1032                                start: i,
1033                                end: i + 2,
1034                                punct_pos: None,
1035                            });
1036                            current_word.clear();
1037                            word_start = next_pos + 1;
1038                            skip_count = 1;
1039                        } else if word_lower == "can" {
1040                            // cannot
1041                            items.push(WordItem {
1042                                word: "cannot".to_string(),
1043                                trailing_punct: None,
1044                                start: word_start,
1045                                end: i + 2,
1046                                punct_pos: None,
1047                            });
1048                            current_word.clear();
1049                            word_start = next_pos + 1;
1050                            skip_count = 1;
1051                        } else {
1052                            // Unknown contraction, split normally
1053                            if !current_word.is_empty() {
1054                                items.push(WordItem {
1055                                    word: std::mem::take(&mut current_word),
1056                                    trailing_punct: Some('\''),
1057                                    start: word_start,
1058                                    end: i,
1059                                    punct_pos: Some(i),
1060                                });
1061                            }
1062                            word_start = next_pos;
1063                        }
1064                    } else {
1065                        // Not a 't contraction, handle normally
1066                        if !current_word.is_empty() {
1067                            items.push(WordItem {
1068                                word: std::mem::take(&mut current_word),
1069                                trailing_punct: Some('\''),
1070                                start: word_start,
1071                                end: i,
1072                                punct_pos: Some(i),
1073                            });
1074                        }
1075                        word_start = next_pos;
1076                    }
1077                }
1078                c if c.is_alphabetic() || c.is_ascii_digit() || (c == '.' && !current_word.is_empty() && current_word.chars().all(|ch| ch.is_ascii_digit())) || c == '_' => {
1079                    if current_word.is_empty() {
1080                        word_start = i;
1081                    }
1082                    current_word.push(c);
1083                }
1084                _ => {
1085                    word_start = next_pos;
1086                }
1087            }
1088            char_idx += 1;
1089        }
1090
1091        if !current_word.is_empty() {
1092            items.push(WordItem {
1093                word: current_word,
1094                trailing_punct: None,
1095                start: word_start,
1096                end: input.len(),
1097                punct_pos: None,
1098            });
1099        }
1100
1101        items
1102    }
1103
1104    fn peek_word(&self, offset: usize) -> Option<&str> {
1105        self.words.get(self.pos + offset).map(|w| w.word.as_str())
1106    }
1107
1108    /// Check if the previous word is a determiner (every, each, some, all, any, no, the, a, an).
1109    fn prev_token_is_determiner(&self) -> bool {
1110        if self.pos == 0 { return false; }
1111        if let Some(prev) = self.words.get(self.pos - 1) {
1112            matches!(prev.word.to_lowercase().as_str(),
1113                "every" | "each" | "some" | "all" | "any" | "no" | "the" | "a" | "an")
1114        } else {
1115            false
1116        }
1117    }
1118
1119    fn peek_sequence(&self, expected: &[&str]) -> bool {
1120        for (i, &exp) in expected.iter().enumerate() {
1121            match self.peek_word(i + 1) {
1122                Some(w) if w.to_lowercase() == exp => continue,
1123                _ => return false,
1124            }
1125        }
1126        true
1127    }
1128
1129    fn consume_words(&mut self, count: usize) {
1130        self.pos += count;
1131    }
1132
1133    /// Tokenizes the input text and returns a vector of [`Token`]s.
1134    ///
1135    /// Each token includes its type, the interned lexeme, and the source
1136    /// span for error reporting. Words are classified according to the
1137    /// lexicon database with priority-based ambiguity resolution.
1138    ///
1139    /// # Returns
1140    ///
1141    /// A vector of tokens representing the input. The final token is
1142    /// typically `TokenType::Eof`.
1143    pub fn tokenize(&mut self) -> Vec<Token> {
1144        let mut tokens = Vec::new();
1145
1146        while self.pos < self.words.len() {
1147            let item = &self.words[self.pos];
1148            let word = item.word.clone();
1149            let trailing_punct = item.trailing_punct;
1150            let word_start = item.start;
1151            let word_end = item.end;
1152            let punct_pos = item.punct_pos;
1153
1154            if word.is_empty() {
1155                if let Some(punct) = trailing_punct {
1156                    let kind = match punct {
1157                        '(' => TokenType::LParen,
1158                        ')' => TokenType::RParen,
1159                        '[' => TokenType::LBracket,
1160                        ']' => TokenType::RBracket,
1161                        ',' => TokenType::Comma,
1162                        ':' => TokenType::Colon,
1163                        '.' | '?' => {
1164                            self.in_let_context = false;
1165                            TokenType::Period
1166                        }
1167                        '!' => TokenType::Exclamation,
1168                        '+' => TokenType::Plus,
1169                        '-' => TokenType::Minus,
1170                        '*' => TokenType::Star,
1171                        '/' => TokenType::Slash,
1172                        '%' => TokenType::Percent,
1173                        '<' => TokenType::Lt,
1174                        '>' => TokenType::Gt,
1175                        '=' => TokenType::Assign,
1176                        _ => {
1177                            self.pos += 1;
1178                            continue;
1179                        }
1180                    };
1181                    let lexeme = self.interner.intern(&punct.to_string());
1182                    let span = Span::new(word_start, word_end);
1183                    tokens.push(Token::new(kind, lexeme, span));
1184                }
1185                self.pos += 1;
1186                continue;
1187            }
1188
1189            // Check for string literal marker (pre-tokenized in Stage 1)
1190            if word.starts_with("\x00STR:") {
1191                let content = &word[5..]; // Skip the marker prefix
1192                let span = Span::new(word_start, word_end);
1193                if Self::has_unescaped_brace(content) {
1194                    let sym = self.interner.intern(content);
1195                    tokens.push(Token::new(TokenType::InterpolatedString(sym), sym, span));
1196                } else {
1197                    // Collapse {{ → { and }} → } for plain strings
1198                    let normalized = content.replace("{{", "{").replace("}}", "}");
1199                    let sym = self.interner.intern(&normalized);
1200                    tokens.push(Token::new(TokenType::StringLiteral(sym), sym, span));
1201                }
1202                self.pos += 1;
1203                continue;
1204            }
1205
1206            // Check for character literal marker
1207            if word.starts_with("\x00CHAR:") {
1208                let content = &word[6..]; // Skip the marker prefix
1209                let sym = self.interner.intern(content);
1210                let span = Span::new(word_start, word_end);
1211                tokens.push(Token::new(TokenType::CharLiteral(sym), sym, span));
1212                self.pos += 1;
1213                continue;
1214            }
1215
1216            // Check for escape block marker (pre-captured raw foreign code)
1217            if word.starts_with("\x00ESC:") {
1218                let content = &word[5..]; // Skip the "\x00ESC:" prefix
1219                let sym = self.interner.intern(content);
1220                let span = Span::new(word_start, word_end);
1221                tokens.push(Token::new(TokenType::EscapeBlock(sym), sym, span));
1222                self.pos += 1;
1223                continue;
1224            }
1225
1226            let kind = self.classify_with_lookahead(&word);
1227            let lexeme = self.interner.intern(&word);
1228            let span = Span::new(word_start, word_end);
1229            tokens.push(Token::new(kind, lexeme, span));
1230
1231            if let Some(punct) = trailing_punct {
1232                if punct == '\'' {
1233                    if let Some(next_item) = self.words.get(self.pos + 1) {
1234                        if next_item.word.to_lowercase() == "s" {
1235                            let poss_lexeme = self.interner.intern("'s");
1236                            let poss_start = punct_pos.unwrap_or(word_end);
1237                            let poss_end = next_item.end;
1238                            tokens.push(Token::new(TokenType::Possessive, poss_lexeme, Span::new(poss_start, poss_end)));
1239                            self.pos += 1;
1240                            if let Some(s_punct) = next_item.trailing_punct {
1241                                let kind = match s_punct {
1242                                    '(' => TokenType::LParen,
1243                                    ')' => TokenType::RParen,
1244                                    '[' => TokenType::LBracket,
1245                                    ']' => TokenType::RBracket,
1246                                    ',' => TokenType::Comma,
1247                                    ':' => TokenType::Colon,
1248                                    '.' | '?' => TokenType::Period,
1249                                    '!' => TokenType::Exclamation,
1250                                    '+' => TokenType::Plus,
1251                                    '-' => TokenType::Minus,
1252                                    '*' => TokenType::Star,
1253                                    '/' => TokenType::Slash,
1254                                    '%' => TokenType::Percent,
1255                                    '<' => TokenType::Lt,
1256                                    '>' => TokenType::Gt,
1257                                    '=' => TokenType::Assign,
1258                                    _ => {
1259                                        self.pos += 1;
1260                                        continue;
1261                                    }
1262                                };
1263                                let s_punct_pos = next_item.punct_pos.unwrap_or(next_item.end);
1264                                let lexeme = self.interner.intern(&s_punct.to_string());
1265                                tokens.push(Token::new(kind, lexeme, Span::new(s_punct_pos, s_punct_pos + 1)));
1266                            }
1267                            self.pos += 1;
1268                            continue;
1269                        }
1270                    }
1271                    self.pos += 1;
1272                    continue;
1273                }
1274
1275                let kind = match punct {
1276                    '(' => TokenType::LParen,
1277                    ')' => TokenType::RParen,
1278                    '[' => TokenType::LBracket,
1279                    ']' => TokenType::RBracket,
1280                    ',' => TokenType::Comma,
1281                    ':' => TokenType::Colon,
1282                    '.' | '?' => {
1283                        self.in_let_context = false;
1284                        TokenType::Period
1285                    }
1286                    '!' => TokenType::Exclamation,
1287                    '+' => TokenType::Plus,
1288                    '-' => TokenType::Minus,
1289                    '*' => TokenType::Star,
1290                    '/' => TokenType::Slash,
1291                    '%' => TokenType::Percent,
1292                    '<' => TokenType::Lt,
1293                    '>' => TokenType::Gt,
1294                    '=' => TokenType::Assign,
1295                    _ => {
1296                        self.pos += 1;
1297                        continue;
1298                    }
1299                };
1300                let p_start = punct_pos.unwrap_or(word_end);
1301                let lexeme = self.interner.intern(&punct.to_string());
1302                tokens.push(Token::new(kind, lexeme, Span::new(p_start, p_start + 1)));
1303            }
1304
1305            self.pos += 1;
1306        }
1307
1308        let eof_lexeme = self.interner.intern("");
1309        let eof_span = Span::new(self.input_len, self.input_len);
1310        tokens.push(Token::new(TokenType::EOF, eof_lexeme, eof_span));
1311
1312        self.insert_indentation_tokens(tokens)
1313    }
1314
1315    /// Insert Indent/Dedent tokens using LineLexer's two-pass architecture (Spec §2.5.2).
1316    ///
1317    /// Phase 1: LineLexer determines the structural layout (where indents/dedents occur)
1318    /// Phase 2: We correlate these with word token positions
1319    fn insert_indentation_tokens(&mut self, tokens: Vec<Token>) -> Vec<Token> {
1320        let mut result = Vec::new();
1321        let empty_sym = self.interner.intern("");
1322
1323        // Phase 1: Run LineLexer to determine structural positions
1324        let line_lexer = LineLexer::new(&self.source);
1325        let line_tokens: Vec<LineToken> = line_lexer.collect();
1326
1327        // Build a list of (byte_position, is_indent) for structural tokens
1328        // Position is where the NEXT Content starts after the Indent/Dedent
1329        let mut structural_events: Vec<(usize, bool)> = Vec::new(); // (byte_pos, true=Indent, false=Dedent)
1330        let mut pending_indents = 0usize;
1331        let mut pending_dedents = 0usize;
1332
1333        for line_token in &line_tokens {
1334            match line_token {
1335                LineToken::Indent => {
1336                    pending_indents += 1;
1337                }
1338                LineToken::Dedent => {
1339                    pending_dedents += 1;
1340                }
1341                LineToken::Content { start, .. } => {
1342                    // Emit pending dedents first (they come BEFORE the content)
1343                    for _ in 0..pending_dedents {
1344                        structural_events.push((*start, false)); // false = Dedent
1345                    }
1346                    pending_dedents = 0;
1347
1348                    // Emit pending indents (they also come BEFORE the content)
1349                    for _ in 0..pending_indents {
1350                        structural_events.push((*start, true)); // true = Indent
1351                    }
1352                    pending_indents = 0;
1353                }
1354                LineToken::Newline => {}
1355            }
1356        }
1357
1358        // Handle any remaining dedents at EOF
1359        for _ in 0..pending_dedents {
1360            structural_events.push((self.input_len, false));
1361        }
1362
1363        // Filter out structural events from within escape block bodies.
1364        // The LineLexer sees raw Rust code lines and generates spurious Indent/Dedent
1365        // events for their indentation changes. We keep exactly the boundary events
1366        // (Indent at body start, Dedent at body end) but remove internal ones.
1367        if !self.escape_body_ranges.is_empty() {
1368            // For each escape body range, find the first Indent at the body start and
1369            // track that we're inside the range. Filter out all events strictly inside
1370            // the range except for the first Indent and events at/after the end.
1371            let mut filtered = Vec::new();
1372            for &(pos, is_indent) in &structural_events {
1373                let is_inside_escape_body = self.escape_body_ranges.iter().any(|(start, end)| {
1374                    // Strictly inside the body (not at start boundary and not at/after end)
1375                    pos > *start && pos < *end
1376                });
1377                if !is_inside_escape_body {
1378                    filtered.push((pos, is_indent));
1379                }
1380            }
1381            structural_events = filtered;
1382        }
1383
1384        // Filter out structural events from within multi-line string literals.
1385        // Triple-quote strings span multiple lines; their internal indentation
1386        // must not generate Indent/Dedent tokens.
1387        {
1388            let string_spans: Vec<(usize, usize)> = tokens.iter()
1389                .filter(|t| matches!(t.kind, TokenType::StringLiteral(_) | TokenType::InterpolatedString(_)))
1390                .filter(|t| t.span.end - t.span.start > 6) // only multi-line strings (""" adds >=6 chars)
1391                .map(|t| (t.span.start, t.span.end))
1392                .collect();
1393            if !string_spans.is_empty() {
1394                structural_events.retain(|&(pos, _)| {
1395                    !string_spans.iter().any(|(start, end)| pos > *start && pos < *end)
1396                });
1397            }
1398        }
1399
1400        // Sort events by position, with dedents before indents at same position
1401        structural_events.sort_by(|a, b| {
1402            if a.0 != b.0 {
1403                a.0.cmp(&b.0)
1404            } else {
1405                // Dedents (false) before Indents (true) at same position
1406                a.1.cmp(&b.1)
1407            }
1408        });
1409
1410        // Phase 2: Insert structural tokens at the right positions
1411        // Strategy: For each word token, check if any structural events should be inserted
1412        // before it (based on byte position)
1413
1414        let mut event_idx = 0;
1415        let mut last_colon_pos: Option<usize> = None;
1416
1417        for token in tokens.iter() {
1418            let token_start = token.span.start;
1419
1420            // Insert any structural tokens that should come BEFORE this token
1421            while event_idx < structural_events.len() {
1422                let (event_pos, is_indent) = structural_events[event_idx];
1423
1424                // Insert structural tokens before this token if the event position <= token start
1425                if event_pos <= token_start {
1426                    let span = if is_indent {
1427                        // Indent is inserted after the preceding Colon
1428                        Span::new(last_colon_pos.unwrap_or(event_pos), last_colon_pos.unwrap_or(event_pos))
1429                    } else {
1430                        Span::new(event_pos, event_pos)
1431                    };
1432                    let kind = if is_indent { TokenType::Indent } else { TokenType::Dedent };
1433                    result.push(Token::new(kind, empty_sym, span));
1434                    event_idx += 1;
1435                } else {
1436                    break;
1437                }
1438            }
1439
1440            result.push(token.clone());
1441
1442            // Track colon positions for Indent span calculation
1443            if token.kind == TokenType::Colon && self.is_end_of_line(token.span.end) {
1444                last_colon_pos = Some(token.span.end);
1445            }
1446        }
1447
1448        // Insert any remaining structural tokens (typically Dedents at EOF)
1449        while event_idx < structural_events.len() {
1450            let (event_pos, is_indent) = structural_events[event_idx];
1451            let span = Span::new(event_pos, event_pos);
1452            let kind = if is_indent { TokenType::Indent } else { TokenType::Dedent };
1453            result.push(Token::new(kind, empty_sym, span));
1454            event_idx += 1;
1455        }
1456
1457        // Ensure EOF is at the end
1458        let eof_pos = result.iter().position(|t| t.kind == TokenType::EOF);
1459        if let Some(pos) = eof_pos {
1460            let eof = result.remove(pos);
1461            result.push(eof);
1462        }
1463
1464        result
1465    }
1466
1467    /// Check if position is at end of line (only whitespace until newline)
1468    fn is_end_of_line(&self, from_pos: usize) -> bool {
1469        let bytes = self.source.as_bytes();
1470        let mut pos = from_pos;
1471        while pos < bytes.len() {
1472            match bytes[pos] {
1473                b' ' | b'\t' => pos += 1,
1474                b'\n' => return true,
1475                _ => return false,
1476            }
1477        }
1478        true // End of input is also end of line
1479    }
1480
1481    fn measure_next_line_indent(&self, from_pos: usize) -> Option<usize> {
1482        let bytes = self.source.as_bytes();
1483        let mut pos = from_pos;
1484
1485        while pos < bytes.len() && bytes[pos] != b'\n' {
1486            pos += 1;
1487        }
1488
1489        if pos >= bytes.len() {
1490            return None;
1491        }
1492
1493        pos += 1;
1494
1495        let mut indent = 0;
1496        while pos < bytes.len() {
1497            match bytes[pos] {
1498                b' ' => indent += 1,
1499                b'\t' => indent += 4,
1500                b'\n' => {
1501                    indent = 0;
1502                }
1503                _ => break,
1504            }
1505            pos += 1;
1506        }
1507
1508        if pos >= bytes.len() {
1509            return None;
1510        }
1511
1512        Some(indent)
1513    }
1514
1515    fn word_to_number(word: &str) -> Option<u32> {
1516        lexicon::word_to_number(&word.to_lowercase())
1517    }
1518
1519    /// Check if a hyphen at the current position is part of an ISO-8601 date.
1520    ///
1521    /// Detects patterns like:
1522    /// - "2026-" followed by "05-20" → first hyphen of date
1523    /// - "2026-05-" followed by "20" → second hyphen of date
1524    fn is_date_hyphen(current_word: &str, chars: &[char], char_idx: usize) -> bool {
1525        // Current word must be all digits (year or year-month)
1526        let word_chars: Vec<char> = current_word.chars().collect();
1527
1528        // Check for first hyphen pattern: YYYY- followed by MM-DD
1529        if word_chars.len() == 4 && word_chars.iter().all(|c| c.is_ascii_digit()) {
1530            // Check if followed by exactly 2 digits, hyphen, 2 digits
1531            if char_idx + 5 < chars.len()
1532                && chars[char_idx + 1].is_ascii_digit()
1533                && chars[char_idx + 2].is_ascii_digit()
1534                && chars[char_idx + 3] == '-'
1535                && chars[char_idx + 4].is_ascii_digit()
1536                && chars[char_idx + 5].is_ascii_digit()
1537            {
1538                return true;
1539            }
1540        }
1541
1542        // Check for second hyphen pattern: YYYY-MM- followed by DD
1543        if word_chars.len() == 7
1544            && word_chars[0..4].iter().all(|c| c.is_ascii_digit())
1545            && word_chars[4] == '-'
1546            && word_chars[5..7].iter().all(|c| c.is_ascii_digit())
1547        {
1548            // Check if followed by exactly 2 digits
1549            if char_idx + 2 < chars.len()
1550                && chars[char_idx + 1].is_ascii_digit()
1551                && chars[char_idx + 2].is_ascii_digit()
1552            {
1553                // Make sure we're not followed by more digits (would be a longer number)
1554                let next_not_digit = char_idx + 3 >= chars.len()
1555                    || !chars[char_idx + 3].is_ascii_digit();
1556                if next_not_digit {
1557                    return true;
1558                }
1559            }
1560        }
1561
1562        false
1563    }
1564
1565    /// Check if a colon is part of a time literal (e.g., 9:30am, 11:45pm).
1566    ///
1567    /// Detects patterns like:
1568    /// - "9:" followed by "30am" or "30pm"
1569    /// - "11:" followed by "45pm"
1570    fn is_time_colon(current_word: &str, chars: &[char], char_idx: usize) -> bool {
1571        // Current word must be 1-2 digits (hour)
1572        let word_chars: Vec<char> = current_word.chars().collect();
1573        if word_chars.is_empty() || word_chars.len() > 2 {
1574            return false;
1575        }
1576        if !word_chars.iter().all(|c| c.is_ascii_digit()) {
1577            return false;
1578        }
1579
1580        // Check if followed by exactly 2 digits and then "am" or "pm"
1581        if char_idx + 4 < chars.len()
1582            && chars[char_idx + 1].is_ascii_digit()
1583            && chars[char_idx + 2].is_ascii_digit()
1584        {
1585            // Check for "am" or "pm" suffix
1586            let next_two: String = chars[char_idx + 3..char_idx + 5].iter().collect();
1587            let lower = next_two.to_lowercase();
1588            if lower == "am" || lower == "pm" {
1589                // Make sure we're not followed by more alphabetic chars
1590                let after_suffix = char_idx + 5 >= chars.len()
1591                    || !chars[char_idx + 5].is_alphabetic();
1592                if after_suffix {
1593                    return true;
1594                }
1595            }
1596        }
1597
1598        false
1599    }
1600
1601    /// Check if a string contains an unescaped `{` (i.e., not part of `{{`).
1602    /// Used to distinguish `InterpolatedString` from `StringLiteral`.
1603    fn has_unescaped_brace(content: &str) -> bool {
1604        let bytes = content.as_bytes();
1605        let mut i = 0;
1606        while i < bytes.len() {
1607            if bytes[i] == b'{' {
1608                if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1609                    i += 2;
1610                } else {
1611                    return true;
1612                }
1613            } else {
1614                i += 1;
1615            }
1616        }
1617        false
1618    }
1619
1620    /// Check if a `+` or `-` at the current position is the sign of a scientific notation exponent.
1621    ///
1622    /// Detects patterns like:
1623    /// - "4.84e+" followed by "00" → exponent sign in `4.84e+00`
1624    /// - "2.5e-" followed by "2"  → exponent sign in `2.5e-2`
1625    fn is_exponent_sign(current_word: &str, chars: &[char], char_idx: usize) -> bool {
1626        // Word must end with e/E
1627        if !current_word.ends_with('e') && !current_word.ends_with('E') {
1628            return false;
1629        }
1630        // Before e/E must contain a digit (ensures it's a number, not a bare "e")
1631        let before_e = &current_word[..current_word.len() - 1];
1632        if before_e.is_empty() || !before_e.chars().next().unwrap().is_ascii_digit() {
1633            return false;
1634        }
1635        // Next char must be a digit (the exponent value)
1636        char_idx + 1 < chars.len() && chars[char_idx + 1].is_ascii_digit()
1637    }
1638
1639    /// Dedent a triple-quoted string: strip the common leading whitespace from each line.
1640    /// Joins lines with literal newline characters (not escape sequences).
1641    fn dedent_triple_quote(raw: &str) -> String {
1642        let lines: Vec<&str> = raw.lines().collect();
1643        if lines.is_empty() {
1644            return String::new();
1645        }
1646        // Find minimum indentation of non-empty lines
1647        let min_indent = lines.iter()
1648            .filter(|l| !l.trim().is_empty())
1649            .map(|l| l.len() - l.trim_start().len())
1650            .min()
1651            .unwrap_or(0);
1652        // Strip that indentation and join with actual newlines
1653        lines.iter()
1654            .map(|l| {
1655                if l.len() >= min_indent {
1656                    &l[min_indent..]
1657                } else {
1658                    l.trim()
1659                }
1660            })
1661            .collect::<Vec<_>>()
1662            .join("\n")
1663    }
1664
1665    fn is_numeric_literal(word: &str) -> bool {
1666        if word.is_empty() {
1667            return false;
1668        }
1669        let chars: Vec<char> = word.chars().collect();
1670        let first = chars[0];
1671        if first.is_ascii_digit() {
1672            // Numeric literal: starts with digit (may have underscore separators like 1_000)
1673            return true;
1674        }
1675        // Symbolic numbers: only recognize known mathematical symbols
1676        // (aleph, omega, beth) followed by underscore and digits
1677        if let Some(underscore_pos) = word.rfind('_') {
1678            let before_underscore = &word[..underscore_pos];
1679            let after_underscore = &word[underscore_pos + 1..];
1680            // Must be a known mathematical symbol prefix AND digits after underscore
1681            let is_math_symbol = matches!(
1682                before_underscore.to_lowercase().as_str(),
1683                "aleph" | "omega" | "beth"
1684            );
1685            if is_math_symbol
1686                && !after_underscore.is_empty()
1687                && after_underscore.chars().all(|c| c.is_ascii_digit())
1688            {
1689                return true;
1690            }
1691        }
1692        false
1693    }
1694
1695    /// Parse a duration literal with SI suffix.
1696    ///
1697    /// Returns Some((nanoseconds, unit_str)) if the word is a valid duration literal,
1698    /// None otherwise.
1699    ///
1700    /// Supported suffixes:
1701    /// - ns: nanoseconds
1702    /// - us, μs: microseconds
1703    /// - ms: milliseconds
1704    /// - s, sec: seconds
1705    /// - min: minutes
1706    /// - h, hr: hours
1707    fn parse_duration_literal(word: &str) -> Option<(i64, &str)> {
1708        if word.is_empty() || !word.chars().next()?.is_ascii_digit() {
1709            return None;
1710        }
1711
1712        // SI suffix table with multipliers to nanoseconds
1713        const SUFFIXES: &[(&str, i64)] = &[
1714            ("ns", 1),
1715            ("μs", 1_000),
1716            ("us", 1_000),
1717            ("ms", 1_000_000),
1718            ("sec", 1_000_000_000),
1719            ("s", 1_000_000_000),
1720            ("min", 60_000_000_000),
1721            ("hr", 3_600_000_000_000),
1722            ("h", 3_600_000_000_000),
1723        ];
1724
1725        // Try each suffix (longer suffixes first to avoid partial matches)
1726        for (suffix, multiplier) in SUFFIXES {
1727            if word.ends_with(suffix) {
1728                let num_part = &word[..word.len() - suffix.len()];
1729                // Parse the numeric part (may have underscore separators)
1730                let cleaned: String = num_part.chars().filter(|c| *c != '_').collect();
1731                if let Ok(n) = cleaned.parse::<i64>() {
1732                    return Some((n.saturating_mul(*multiplier), *suffix));
1733                }
1734            }
1735        }
1736
1737        None
1738    }
1739
1740    /// Parse an ISO-8601 date literal (YYYY-MM-DD).
1741    ///
1742    /// Returns Some(days_since_epoch) if the word is a valid date literal,
1743    /// None otherwise.
1744    fn parse_date_literal(word: &str) -> Option<i32> {
1745        // Must match pattern: YYYY-MM-DD
1746        if word.len() != 10 {
1747            return None;
1748        }
1749
1750        let bytes = word.as_bytes();
1751
1752        // Check format: 4 digits, hyphen, 2 digits, hyphen, 2 digits
1753        if bytes[4] != b'-' || bytes[7] != b'-' {
1754            return None;
1755        }
1756
1757        // Parse year, month, day
1758        let year: i32 = word[0..4].parse().ok()?;
1759        let month: u32 = word[5..7].parse().ok()?;
1760        let day: u32 = word[8..10].parse().ok()?;
1761
1762        // Basic validation
1763        if month < 1 || month > 12 || day < 1 || day > 31 {
1764            return None;
1765        }
1766
1767        // Convert to days since Unix epoch using Howard Hinnant's algorithm
1768        // https://howardhinnant.github.io/date_algorithms.html
1769        let y = if month <= 2 { year - 1 } else { year };
1770        let era = if y >= 0 { y / 400 } else { (y - 399) / 400 };
1771        let yoe = (y - era * 400) as u32;
1772        let m = month;
1773        let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + day - 1;
1774        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
1775        let days = era * 146097 + doe as i32 - 719468;
1776
1777        Some(days)
1778    }
1779
1780    /// Parse a time-of-day literal.
1781    ///
1782    /// Supported formats:
1783    /// - 12-hour with am/pm: "4pm", "9am", "12pm"
1784    /// - 12-hour with minutes: "9:30am", "11:45pm"
1785    /// - Special words: "noon" (12:00), "midnight" (00:00)
1786    ///
1787    /// Returns Some(nanos_from_midnight) if valid, None otherwise.
1788    fn parse_time_literal(word: &str) -> Option<i64> {
1789        let lower = word.to_lowercase();
1790
1791        // Handle special time words
1792        if lower == "noon" {
1793            return Some(12i64 * 3600 * 1_000_000_000);
1794        }
1795        if lower == "midnight" {
1796            return Some(0);
1797        }
1798
1799        // Handle 12-hour formats: "4pm", "9am", "9:30am", "11:45pm"
1800        let is_pm = lower.ends_with("pm");
1801        let is_am = lower.ends_with("am");
1802
1803        if !is_pm && !is_am {
1804            return None;
1805        }
1806
1807        // Strip the am/pm suffix
1808        let time_part = &lower[..lower.len() - 2];
1809
1810        // Check for hour:minute format
1811        let (hour, minute): (i64, i64) = if let Some(colon_idx) = time_part.find(':') {
1812            let hour_str = &time_part[..colon_idx];
1813            let min_str = &time_part[colon_idx + 1..];
1814            let h: i64 = hour_str.parse().ok()?;
1815            let m: i64 = min_str.parse().ok()?;
1816            (h, m)
1817        } else {
1818            // Just hour: "4pm", "9am"
1819            let h: i64 = time_part.parse().ok()?;
1820            (h, 0)
1821        };
1822
1823        // Validate ranges
1824        if hour < 1 || hour > 12 || minute < 0 || minute > 59 {
1825            return None;
1826        }
1827
1828        // Convert to 24-hour format
1829        let hour_24 = if is_am {
1830            if hour == 12 { 0 } else { hour }  // 12am = midnight = 0
1831        } else {
1832            if hour == 12 { 12 } else { hour + 12 }  // 12pm = noon = 12, 4pm = 16
1833        };
1834
1835        // Convert to nanoseconds from midnight
1836        let nanos = (hour_24 * 3600 + minute * 60) * 1_000_000_000;
1837        Some(nanos)
1838    }
1839
1840    fn classify_with_lookahead(&mut self, word: &str) -> TokenType {
1841        // Handle block headers (##Theorem, ##Main, etc.)
1842        if word.starts_with("##") {
1843            let block_name = &word[2..];
1844            let block_type = match block_name.to_lowercase().as_str() {
1845                "theorem" => BlockType::Theorem,
1846                "main" => BlockType::Main,
1847                "definition" => BlockType::Definition,
1848                "proof" => BlockType::Proof,
1849                "example" => BlockType::Example,
1850                "logic" => BlockType::Logic,
1851                "note" => BlockType::Note,
1852                "to" => BlockType::Function,  // Function definition block
1853                "a" | "an" => BlockType::TypeDef,  // Inline type definitions: ## A Point has:
1854                "policy" => BlockType::Policy,  // Security policy definitions
1855                "requires" => BlockType::Requires,  // External crate dependencies
1856                "hardware" => BlockType::Hardware,  // Signal declarations
1857                "property" => BlockType::Property,  // Temporal assertions
1858                "no" => BlockType::No,  // Optimization annotation: ## No Memo, ## No TCO, etc.
1859                _ => BlockType::Note, // Default unknown block types to Note
1860            };
1861
1862            // Update lexer mode based on block type
1863            self.mode = match block_type {
1864                BlockType::Main | BlockType::Function => LexerMode::Imperative,
1865                _ => LexerMode::Declarative,
1866            };
1867
1868            return TokenType::BlockHeader { block_type };
1869        }
1870
1871        let lower = word.to_lowercase();
1872
1873        if lower == "each" && self.peek_sequence(&["other"]) {
1874            self.consume_words(1);
1875            return TokenType::Reciprocal;
1876        }
1877
1878        if lower == "to" {
1879            if let Some(next) = self.peek_word(1) {
1880                if self.is_verb_like(next) {
1881                    return TokenType::To;
1882                }
1883            }
1884            let sym = self.interner.intern("to");
1885            return TokenType::Preposition(sym);
1886        }
1887
1888        if lower == "at" {
1889            if let Some(next) = self.peek_word(1) {
1890                let next_lower = next.to_lowercase();
1891                if next_lower == "least" {
1892                    if let Some(num_word) = self.peek_word(2) {
1893                        if let Some(n) = Self::word_to_number(num_word) {
1894                            self.consume_words(2);
1895                            return TokenType::AtLeast(n);
1896                        }
1897                    }
1898                }
1899                if next_lower == "most" {
1900                    if let Some(num_word) = self.peek_word(2) {
1901                        if let Some(n) = Self::word_to_number(num_word) {
1902                            self.consume_words(2);
1903                            return TokenType::AtMost(n);
1904                        }
1905                    }
1906                }
1907            }
1908        }
1909
1910        if let Some(n) = Self::word_to_number(&lower) {
1911            return TokenType::Cardinal(n);
1912        }
1913
1914        // Check for duration literal first (e.g., "500ms", "2s", "50ns")
1915        if let Some((nanos, unit)) = Self::parse_duration_literal(word) {
1916            let unit_sym = self.interner.intern(unit);
1917            return TokenType::DurationLiteral {
1918                nanos,
1919                original_unit: unit_sym,
1920            };
1921        }
1922
1923        // Check for ISO-8601 date literal (e.g., "2026-05-20")
1924        if let Some(days) = Self::parse_date_literal(word) {
1925            return TokenType::DateLiteral { days };
1926        }
1927
1928        // Check for time-of-day literal (e.g., "4pm", "9:30am", "noon", "midnight")
1929        if let Some(nanos_from_midnight) = Self::parse_time_literal(word) {
1930            return TokenType::TimeLiteral { nanos_from_midnight };
1931        }
1932
1933        if Self::is_numeric_literal(word) {
1934            let sym = self.interner.intern(word);
1935            return TokenType::Number(sym);
1936        }
1937
1938        if lower == "if" && self.peek_sequence(&["and", "only", "if"]) {
1939            self.consume_words(3);
1940            return TokenType::Iff;
1941        }
1942
1943        if lower == "is" {
1944            if self.peek_sequence(&["equal", "to"]) {
1945                self.consume_words(2);
1946                return TokenType::Identity;
1947            }
1948            if self.peek_sequence(&["identical", "to"]) {
1949                self.consume_words(2);
1950                return TokenType::Identity;
1951            }
1952        }
1953
1954        if (lower == "a" || lower == "an") && word.chars().next().unwrap().is_uppercase() {
1955            // Capitalized "A" or "An" - disambiguate article vs proper name
1956            // Heuristic: articles are followed by nouns/adjectives, not verbs or keywords
1957            if let Some(next) = self.peek_word(1) {
1958                let next_lower = next.to_lowercase();
1959                let next_starts_lowercase = next.chars().next().map(|c| c.is_lowercase()).unwrap_or(false);
1960
1961                // If followed by logical keyword, treat as proper name (propositional variable)
1962                if matches!(next_lower.as_str(), "if" | "and" | "or" | "implies" | "iff") {
1963                    let sym = self.interner.intern(word);
1964                    return TokenType::ProperName(sym);
1965                }
1966
1967                // If next word is ONLY a verb (like "has", "is", "ran"), A is likely a name
1968                // Exception: gerunds (like "running") can follow articles
1969                // Exception: words in disambiguation_not_verbs (like "red") are not verbs
1970                // Exception: words that are also nouns/adjectives (like "fire") can follow articles
1971                let is_verb = self.lexicon.lookup_verb(&next_lower).is_some()
1972                    && !lexicon::is_disambiguation_not_verb(&next_lower);
1973                let is_gerund = next_lower.ends_with("ing");
1974                let is_also_noun_or_adj = self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower);
1975                if is_verb && !is_gerund && !is_also_noun_or_adj {
1976                    let sym = self.interner.intern(word);
1977                    return TokenType::ProperName(sym);
1978                }
1979
1980                // Definition pattern: "A [TypeName] is a..." or "A [TypeName] has:" - treat A as article
1981                // even when TypeName is capitalized and unknown
1982                if let Some(third) = self.peek_word(2) {
1983                    let third_lower = third.to_lowercase();
1984                    // "has" for struct definitions: "A Point has:"
1985                    if third_lower == "is" || third_lower == "are" || third_lower == "has" {
1986                        return TokenType::Article(Definiteness::Indefinite);
1987                    }
1988                }
1989
1990                // It's an article if next word is:
1991                // - A known noun or adjective, or
1992                // - Lowercase (likely a common word we don't recognize)
1993                let is_content_word = self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower);
1994                if is_content_word || next_starts_lowercase {
1995                    return TokenType::Article(Definiteness::Indefinite);
1996                }
1997            }
1998            let sym = self.interner.intern(word);
1999            return TokenType::ProperName(sym);
2000        }
2001
2002        self.classify_word(word)
2003    }
2004
2005    fn is_noun_like(&self, word: &str) -> bool {
2006        if lexicon::is_noun_pattern(word) || lexicon::is_common_noun(word) {
2007            return true;
2008        }
2009        if word.ends_with("er") || word.ends_with("ian") || word.ends_with("ist") {
2010            return true;
2011        }
2012        false
2013    }
2014
2015    fn is_adjective_like(&self, word: &str) -> bool {
2016        lexicon::is_adjective(word) || lexicon::is_non_intersective(word)
2017    }
2018
2019    fn classify_word(&mut self, word: &str) -> TokenType {
2020        let lower = word.to_lowercase();
2021        let first_char = word.chars().next().unwrap();
2022
2023        // Disambiguate "that" as determiner vs complementizer
2024        // "that dog" → Article(Distal), "I know that he ran" → That (complementizer)
2025        if lower == "that" {
2026            if let Some(next) = self.peek_word(1) {
2027                let next_lower = next.to_lowercase();
2028                if self.is_noun_like(&next_lower) || self.is_adjective_like(&next_lower) {
2029                    return TokenType::Article(Definiteness::Distal);
2030                }
2031            }
2032        }
2033
2034        // Arrow token for return type syntax
2035        if word == "->" {
2036            return TokenType::Arrow;
2037        }
2038
2039        // Grand Challenge: Comparison operator tokens
2040        if word == "<=" {
2041            return TokenType::LtEq;
2042        }
2043        if word == ">=" {
2044            return TokenType::GtEq;
2045        }
2046        if word == "==" {
2047            return TokenType::EqEq;
2048        }
2049        if word == "!=" {
2050            return TokenType::NotEq;
2051        }
2052        if word == "<" {
2053            return TokenType::Lt;
2054        }
2055        if word == ">" {
2056            return TokenType::Gt;
2057        }
2058        // Single = for assignment (must come after == check)
2059        if word == "=" {
2060            return TokenType::Assign;
2061        }
2062
2063        if let Some(kind) = lexicon::lookup_keyword(&lower) {
2064            return kind;
2065        }
2066
2067        if let Some(kind) = lexicon::lookup_pronoun(&lower) {
2068            return kind;
2069        }
2070
2071        if let Some(def) = lexicon::lookup_article(&lower) {
2072            return TokenType::Article(def);
2073        }
2074
2075        if let Some(time) = lexicon::lookup_auxiliary(&lower) {
2076            return TokenType::Auxiliary(time);
2077        }
2078
2079        // Handle imperative keywords that might conflict with prepositions
2080        match lower.as_str() {
2081            "call" => return TokenType::Call,
2082            "in" if self.mode == LexerMode::Imperative => return TokenType::In,
2083            // Zone keywords (must come before is_preposition check)
2084            "inside" if self.mode == LexerMode::Imperative => return TokenType::Inside,
2085            // "at" for chunk access (must come before is_preposition check)
2086            "at" if self.mode == LexerMode::Imperative => return TokenType::At,
2087            // "into" for pipe send (must come before is_preposition check)
2088            "into" if self.mode == LexerMode::Imperative => return TokenType::Into,
2089            // Temporal span operator (must come before is_preposition check)
2090            "before" => return TokenType::Before,
2091            _ => {}
2092        }
2093
2094        if lexicon::is_preposition(&lower) {
2095            let sym = self.interner.intern(&lower);
2096            return TokenType::Preposition(sym);
2097        }
2098
2099        match lower.as_str() {
2100            "equals" => return TokenType::Equals,
2101            "item" => return TokenType::Item,
2102            "items" => return TokenType::Items,
2103            // Mutability keyword for `mut x = 5` syntax
2104            "mut" if self.mode == LexerMode::Imperative => return TokenType::Mut,
2105            "let" => {
2106                self.in_let_context = true;
2107                return TokenType::Let;
2108            }
2109            "set" => {
2110                // Check if "set" is used as a type (followed by "of") - "Set of Int"
2111                // This takes priority over the assignment keyword
2112                if self.peek_word(1).map_or(false, |w| w.to_lowercase() == "of") {
2113                    // It's a type like "Set of Int" - don't return keyword, let it be a noun
2114                } else if self.mode == LexerMode::Imperative {
2115                    // In Imperative mode, treat "set" as the assignment keyword
2116                    return TokenType::Set;
2117                } else {
2118                    // In Declarative mode, check positions 2-5 for "to"
2119                    // (handles field access like "set p's x to")
2120                    for offset in 2..=5 {
2121                        if self.peek_word(offset).map_or(false, |w| w.to_lowercase() == "to") {
2122                            return TokenType::Set;
2123                        }
2124                    }
2125                }
2126            }
2127            "return" => return TokenType::Return,
2128            "break" => return TokenType::Break,
2129            "xor" => return TokenType::Xor,
2130            "shifted" => return TokenType::Shifted,
2131            "be" if self.in_let_context => {
2132                self.in_let_context = false;
2133                return TokenType::Be;
2134            }
2135            "while" => return TokenType::While,
2136            "assert" => return TokenType::Assert,
2137            "trust" => return TokenType::Trust,
2138            "check" => return TokenType::Check,
2139            // Theorem keywords (Declarative mode - for theorem blocks)
2140            "given" if self.mode == LexerMode::Declarative => return TokenType::Given,
2141            "prove" if self.mode == LexerMode::Declarative => return TokenType::Prove,
2142            "auto" if self.mode == LexerMode::Declarative => return TokenType::Auto,
2143            // P2P Networking keywords (Imperative mode only)
2144            "listen" if self.mode == LexerMode::Imperative => return TokenType::Listen,
2145            "connect" if self.mode == LexerMode::Imperative => return TokenType::NetConnect,
2146            "sleep" if self.mode == LexerMode::Imperative => return TokenType::Sleep,
2147            // GossipSub keywords (Imperative mode only)
2148            "sync" if self.mode == LexerMode::Imperative => return TokenType::Sync,
2149            // Persistence keywords
2150            "mount" if self.mode == LexerMode::Imperative => return TokenType::Mount,
2151            "persistent" => return TokenType::Persistent,  // Works in type expressions
2152            "combined" if self.mode == LexerMode::Imperative => return TokenType::Combined,
2153            // Go-like Concurrency keywords (Imperative mode only)
2154            // Note: "first" and "after" are NOT keywords - they're checked via lookahead in parser
2155            // to avoid conflicting with their use as variable names
2156            "launch" if self.mode == LexerMode::Imperative => return TokenType::Launch,
2157            "task" if self.mode == LexerMode::Imperative => return TokenType::Task,
2158            "pipe" if self.mode == LexerMode::Imperative => return TokenType::Pipe,
2159            "receive" if self.mode == LexerMode::Imperative => return TokenType::Receive,
2160            "stop" if self.mode == LexerMode::Imperative => return TokenType::Stop,
2161            "try" if self.mode == LexerMode::Imperative => return TokenType::Try,
2162            "into" if self.mode == LexerMode::Imperative => return TokenType::Into,
2163            "native" => return TokenType::Native,
2164            "escape" if self.mode == LexerMode::Imperative => return TokenType::Escape,
2165            "from" => return TokenType::From,
2166            "otherwise" => return TokenType::Otherwise,
2167            // Phase 30c: Else/elif as aliases for Otherwise/Otherwise If
2168            "else" => return TokenType::Else,
2169            "elif" => return TokenType::Elif,
2170            // Sum type definition (Declarative mode only - for enum "either...or...")
2171            "either" if self.mode == LexerMode::Declarative => return TokenType::Either,
2172            // Pattern matching statement
2173            "inspect" if self.mode == LexerMode::Imperative => return TokenType::Inspect,
2174            // Constructor keyword (Imperative mode only)
2175            "new" if self.mode == LexerMode::Imperative => return TokenType::New,
2176            // Only emit Give/Show as keywords in Imperative mode
2177            // In Declarative mode, they fall through to lexicon lookup as verbs
2178            "give" if self.mode == LexerMode::Imperative => return TokenType::Give,
2179            "show" if self.mode == LexerMode::Imperative => return TokenType::Show,
2180            // Collection operation keywords (Imperative mode only)
2181            "push" if self.mode == LexerMode::Imperative => return TokenType::Push,
2182            "pop" if self.mode == LexerMode::Imperative => return TokenType::Pop,
2183            "copy" if self.mode == LexerMode::Imperative => return TokenType::Copy,
2184            "through" if self.mode == LexerMode::Imperative => return TokenType::Through,
2185            "length" if self.mode == LexerMode::Imperative => return TokenType::Length,
2186            "at" if self.mode == LexerMode::Imperative => return TokenType::At,
2187            // Set operation keywords (Imperative mode only)
2188            "add" if self.mode == LexerMode::Imperative => return TokenType::Add,
2189            "remove" if self.mode == LexerMode::Imperative => return TokenType::Remove,
2190            "contains" if self.mode == LexerMode::Imperative => return TokenType::Contains,
2191            "union" if self.mode == LexerMode::Imperative => return TokenType::Union,
2192            "intersection" if self.mode == LexerMode::Imperative => return TokenType::Intersection,
2193            // Zone keywords (Imperative mode only)
2194            "inside" if self.mode == LexerMode::Imperative => return TokenType::Inside,
2195            "zone" if self.mode == LexerMode::Imperative => return TokenType::Zone,
2196            "called" if self.mode == LexerMode::Imperative => return TokenType::Called,
2197            "size" if self.mode == LexerMode::Imperative => return TokenType::Size,
2198            "mapped" if self.mode == LexerMode::Imperative => return TokenType::Mapped,
2199            // Structured Concurrency keywords (Imperative mode only)
2200            "attempt" if self.mode == LexerMode::Imperative => return TokenType::Attempt,
2201            "following" if self.mode == LexerMode::Imperative => return TokenType::Following,
2202            "simultaneously" if self.mode == LexerMode::Imperative => return TokenType::Simultaneously,
2203            // IO keywords (Imperative mode only)
2204            "read" if self.mode == LexerMode::Imperative => return TokenType::Read,
2205            "write" if self.mode == LexerMode::Imperative => return TokenType::Write,
2206            "console" if self.mode == LexerMode::Imperative => return TokenType::Console,
2207            "file" if self.mode == LexerMode::Imperative => return TokenType::File,
2208            // Agent System keywords (Imperative mode only)
2209            "spawn" if self.mode == LexerMode::Imperative => return TokenType::Spawn,
2210            "send" if self.mode == LexerMode::Imperative => return TokenType::Send,
2211            "await" if self.mode == LexerMode::Imperative => return TokenType::Await,
2212            // Serialization keyword (works in Definition blocks too)
2213            "portable" => return TokenType::Portable,
2214            // Sipping Protocol keywords (Imperative mode only)
2215            "manifest" if self.mode == LexerMode::Imperative => return TokenType::Manifest,
2216            "chunk" if self.mode == LexerMode::Imperative => return TokenType::Chunk,
2217            // CRDT keywords
2218            "shared" => return TokenType::Shared,  // Works in Definition blocks like Portable
2219            "merge" if self.mode == LexerMode::Imperative => return TokenType::Merge,
2220            "increase" if self.mode == LexerMode::Imperative => return TokenType::Increase,
2221            // Extended CRDT keywords
2222            "decrease" if self.mode == LexerMode::Imperative => return TokenType::Decrease,
2223            "append" if self.mode == LexerMode::Imperative => return TokenType::Append,
2224            "resolve" if self.mode == LexerMode::Imperative => return TokenType::Resolve,
2225            "values" if self.mode == LexerMode::Imperative => return TokenType::Values,
2226            // Type keywords (work in both modes like "Shared"):
2227            "tally" => return TokenType::Tally,
2228            "sharedset" => return TokenType::SharedSet,
2229            "sharedsequence" => return TokenType::SharedSequence,
2230            "collaborativesequence" => return TokenType::CollaborativeSequence,
2231            "sharedmap" => return TokenType::SharedMap,
2232            "divergent" => return TokenType::Divergent,
2233            "removewins" => return TokenType::RemoveWins,
2234            "addwins" => return TokenType::AddWins,
2235            "yata" => return TokenType::YATA,
2236            // Calendar time unit words (Span expressions)
2237            "day" | "days" => return TokenType::CalendarUnit(CalendarUnit::Day),
2238            "week" | "weeks" => return TokenType::CalendarUnit(CalendarUnit::Week),
2239            "month" | "months" => return TokenType::CalendarUnit(CalendarUnit::Month),
2240            "year" | "years" => return TokenType::CalendarUnit(CalendarUnit::Year),
2241            // Span-related keywords (note: "before" is handled earlier to avoid preposition conflict)
2242            "ago" => return TokenType::Ago,
2243            "hence" => return TokenType::Hence,
2244            "if" => return TokenType::If,
2245            "only" => return TokenType::Focus(FocusKind::Only),
2246            "even" => return TokenType::Focus(FocusKind::Even),
2247            "just" if self.peek_word(1).map_or(false, |w| {
2248                !self.is_verb_like(w) || w.to_lowercase() == "john" || w.chars().next().map_or(false, |c| c.is_uppercase())
2249            }) => return TokenType::Focus(FocusKind::Just),
2250            "much" => return TokenType::Measure(MeasureKind::Much),
2251            "little" => return TokenType::Measure(MeasureKind::Little),
2252            _ => {}
2253        }
2254
2255        if lexicon::is_scopal_adverb(&lower) {
2256            let sym = self.interner.intern(&Self::capitalize(&lower));
2257            return TokenType::ScopalAdverb(sym);
2258        }
2259
2260        if lexicon::is_temporal_adverb(&lower) {
2261            let sym = self.interner.intern(&Self::capitalize(&lower));
2262            return TokenType::TemporalAdverb(sym);
2263        }
2264
2265        if lexicon::is_non_intersective(&lower) {
2266            let sym = self.interner.intern(&Self::capitalize(&lower));
2267            return TokenType::NonIntersectiveAdjective(sym);
2268        }
2269
2270        if lexicon::is_adverb(&lower) {
2271            let sym = self.interner.intern(&Self::capitalize(&lower));
2272            return TokenType::Adverb(sym);
2273        }
2274        if lower.ends_with("ly") && !lexicon::is_not_adverb(&lower) && lower.len() > 4 {
2275            let sym = self.interner.intern(&Self::capitalize(&lower));
2276            return TokenType::Adverb(sym);
2277        }
2278
2279        if let Some(base) = self.try_parse_superlative(&lower) {
2280            let sym = self.interner.intern(&base);
2281            return TokenType::Superlative(sym);
2282        }
2283
2284        // Handle irregular comparatives (less, more, better, worse)
2285        let irregular_comparative = match lower.as_str() {
2286            "less" => Some("Little"),
2287            "more" => Some("Much"),
2288            "better" => Some("Good"),
2289            "worse" => Some("Bad"),
2290            _ => None,
2291        };
2292        if let Some(base) = irregular_comparative {
2293            let sym = self.interner.intern(base);
2294            return TokenType::Comparative(sym);
2295        }
2296
2297        if let Some(base) = self.try_parse_comparative(&lower) {
2298            let sym = self.interner.intern(&base);
2299            return TokenType::Comparative(sym);
2300        }
2301
2302        if lexicon::is_performative(&lower) {
2303            // If the word is also a common noun AND follows a determiner,
2304            // don't force performative reading.
2305            // "every request holds" → request is a noun, not a performative verb.
2306            // "I promise to come" → promise IS a performative verb.
2307            let after_determiner = self.prev_token_is_determiner();
2308            if !lexicon::is_common_noun(&lower) || !after_determiner {
2309                let sym = self.interner.intern(&Self::capitalize(&lower));
2310                return TokenType::Performative(sym);
2311            }
2312            // Fall through to noun/verb disambiguation below
2313        }
2314
2315        if lexicon::is_base_verb_early(&lower) {
2316            // If the word is also a common noun AND follows a determiner,
2317            // don't force verb reading.
2318            // "every grant holds" → grant is a noun, not a verb.
2319            let after_determiner = self.prev_token_is_determiner();
2320            if !lexicon::is_common_noun(&lower) || !after_determiner {
2321                let sym = self.interner.intern(&Self::capitalize(&lower));
2322                let class = lexicon::lookup_verb_class(&lower);
2323                return TokenType::Verb {
2324                    lemma: sym,
2325                    time: Time::Present,
2326                    aspect: Aspect::Simple,
2327                    class,
2328                };
2329            }
2330            // Fall through to noun/verb disambiguation below
2331        }
2332
2333        // Check for gerunds/progressive verbs BEFORE ProperName check
2334        // "Running" at start of sentence should be Verb, not ProperName
2335        if lower.ends_with("ing") && lower.len() > 4 {
2336            if let Some(entry) = self.lexicon.lookup_verb(&lower) {
2337                let sym = self.interner.intern(&entry.lemma);
2338                return TokenType::Verb {
2339                    lemma: sym,
2340                    time: entry.time,
2341                    aspect: entry.aspect,
2342                    class: entry.class,
2343                };
2344            }
2345        }
2346
2347        if first_char.is_uppercase() {
2348            // Smart Lexicon: Check if this capitalized word is actually a common noun
2349            // Only apply for sentence-initial words (followed by verb) to avoid
2350            // breaking type definitions like "A Point has:"
2351            //
2352            // Pattern: "Farmers walk." → Farmers is plural of Farmer (common noun)
2353            // Pattern: "A Point has:" → Point is a type name (proper name)
2354            if let Some(next) = self.peek_word(1) {
2355                let next_lower = next.to_lowercase();
2356                // If next word is a verb, this capitalized word is likely a subject noun
2357                let is_followed_by_verb = self.lexicon.lookup_verb(&next_lower).is_some()
2358                    || matches!(next_lower.as_str(), "is" | "are" | "was" | "were" | "has" | "have" | "had");
2359
2360                if is_followed_by_verb {
2361                    // Check if lowercase version is a derivable common noun
2362                    if let Some(analysis) = lexicon::analyze_word(&lower) {
2363                        match analysis {
2364                            lexicon::WordAnalysis::Noun(meta) if meta.number == lexicon::Number::Plural => {
2365                                // It's a plural noun - definitely a common noun
2366                                let sym = self.interner.intern(&lower);
2367                                return TokenType::Noun(sym);
2368                            }
2369                            lexicon::WordAnalysis::DerivedNoun { number: lexicon::Number::Plural, .. } => {
2370                                // Derived plural agentive noun (e.g., "Bloggers")
2371                                let sym = self.interner.intern(&lower);
2372                                return TokenType::Noun(sym);
2373                            }
2374                            _ => {
2375                                // Singular nouns at sentence start could still be proper names
2376                                // e.g., "John walks." vs "Farmer walks."
2377                            }
2378                        }
2379                    }
2380                }
2381            }
2382
2383            let sym = self.interner.intern(word);
2384            return TokenType::ProperName(sym);
2385        }
2386
2387        let verb_entry = self.lexicon.lookup_verb(&lower);
2388        let is_noun = lexicon::is_common_noun(&lower);
2389        let is_adj = self.is_adjective_like(&lower);
2390        let is_disambiguated = lexicon::is_disambiguation_not_verb(&lower);
2391
2392        // Ambiguous: word is Verb AND (Noun OR Adjective), not disambiguated
2393        if verb_entry.is_some() && (is_noun || is_adj) && !is_disambiguated {
2394            let entry = verb_entry.unwrap();
2395            let verb_token = TokenType::Verb {
2396                lemma: self.interner.intern(&entry.lemma),
2397                time: entry.time,
2398                aspect: entry.aspect,
2399                class: entry.class,
2400            };
2401
2402            let mut alternatives = Vec::new();
2403            if is_noun {
2404                alternatives.push(TokenType::Noun(self.interner.intern(word)));
2405            }
2406            if is_adj {
2407                alternatives.push(TokenType::Adjective(self.interner.intern(word)));
2408            }
2409
2410            return TokenType::Ambiguous {
2411                primary: Box::new(verb_token),
2412                alternatives,
2413            };
2414        }
2415
2416        // Disambiguated to noun/adjective (not verb)
2417        if let Some(_) = &verb_entry {
2418            if is_disambiguated {
2419                let sym = self.interner.intern(word);
2420                if is_noun {
2421                    return TokenType::Noun(sym);
2422                }
2423                return TokenType::Adjective(sym);
2424            }
2425        }
2426
2427        // Pure verb
2428        if let Some(entry) = verb_entry {
2429            let sym = self.interner.intern(&entry.lemma);
2430            return TokenType::Verb {
2431                lemma: sym,
2432                time: entry.time,
2433                aspect: entry.aspect,
2434                class: entry.class,
2435            };
2436        }
2437
2438        // Pure noun
2439        if is_noun {
2440            let sym = self.interner.intern(word);
2441            return TokenType::Noun(sym);
2442        }
2443
2444        if lexicon::is_base_verb(&lower) {
2445            let sym = self.interner.intern(&Self::capitalize(&lower));
2446            let class = lexicon::lookup_verb_class(&lower);
2447            return TokenType::Verb {
2448                lemma: sym,
2449                time: Time::Present,
2450                aspect: Aspect::Simple,
2451                class,
2452            };
2453        }
2454
2455        if lower.ends_with("ian")
2456            || lower.ends_with("er")
2457            || lower == "logic"
2458            || lower == "time"
2459            || lower == "men"
2460            || lower == "book"
2461            || lower == "house"
2462            || lower == "code"
2463            || lower == "user"
2464        {
2465            let sym = self.interner.intern(word);
2466            return TokenType::Noun(sym);
2467        }
2468
2469        if lexicon::is_particle(&lower) {
2470            let sym = self.interner.intern(&lower);
2471            return TokenType::Particle(sym);
2472        }
2473
2474        let sym = self.interner.intern(word);
2475        TokenType::Adjective(sym)
2476    }
2477
2478    fn capitalize(s: &str) -> String {
2479        let mut chars = s.chars();
2480        match chars.next() {
2481            None => String::new(),
2482            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2483        }
2484    }
2485
2486    pub fn is_collective_verb(lemma: &str) -> bool {
2487        lexicon::is_collective_verb(&lemma.to_lowercase())
2488    }
2489
2490    pub fn is_mixed_verb(lemma: &str) -> bool {
2491        lexicon::is_mixed_verb(&lemma.to_lowercase())
2492    }
2493
2494    pub fn is_distributive_verb(lemma: &str) -> bool {
2495        lexicon::is_distributive_verb(&lemma.to_lowercase())
2496    }
2497
2498    pub fn is_intensional_predicate(lemma: &str) -> bool {
2499        lexicon::is_intensional_predicate(&lemma.to_lowercase())
2500    }
2501
2502    pub fn is_opaque_verb(lemma: &str) -> bool {
2503        lexicon::is_opaque_verb(&lemma.to_lowercase())
2504    }
2505
2506    pub fn is_ditransitive_verb(lemma: &str) -> bool {
2507        lexicon::is_ditransitive_verb(&lemma.to_lowercase())
2508    }
2509
2510    fn is_verb_like(&self, word: &str) -> bool {
2511        let lower = word.to_lowercase();
2512        if lexicon::is_infinitive_verb(&lower) {
2513            return true;
2514        }
2515        if let Some(entry) = self.lexicon.lookup_verb(&lower) {
2516            return entry.lemma.len() > 0;
2517        }
2518        false
2519    }
2520
2521    pub fn is_subject_control_verb(lemma: &str) -> bool {
2522        lexicon::is_subject_control_verb(&lemma.to_lowercase())
2523    }
2524
2525    pub fn is_raising_verb(lemma: &str) -> bool {
2526        lexicon::is_raising_verb(&lemma.to_lowercase())
2527    }
2528
2529    pub fn is_object_control_verb(lemma: &str) -> bool {
2530        lexicon::is_object_control_verb(&lemma.to_lowercase())
2531    }
2532
2533    pub fn is_weather_verb(lemma: &str) -> bool {
2534        matches!(
2535            lemma.to_lowercase().as_str(),
2536            "rain" | "snow" | "hail" | "thunder" | "pour"
2537        )
2538    }
2539
2540    fn try_parse_superlative(&self, word: &str) -> Option<String> {
2541        if !word.ends_with("est") || word.len() < 5 {
2542            return None;
2543        }
2544
2545        let base = &word[..word.len() - 3];
2546
2547        if base.len() >= 2 {
2548            let chars: Vec<char> = base.chars().collect();
2549            let last = chars[chars.len() - 1];
2550            let second_last = chars[chars.len() - 2];
2551            if last == second_last && !"aeiou".contains(last) {
2552                let stem = &base[..base.len() - 1];
2553                if lexicon::is_gradable_adjective(stem) {
2554                    return Some(Self::capitalize(stem));
2555                }
2556            }
2557        }
2558
2559        if base.ends_with("i") {
2560            let stem = format!("{}y", &base[..base.len() - 1]);
2561            if lexicon::is_gradable_adjective(&stem) {
2562                return Some(Self::capitalize(&stem));
2563            }
2564        }
2565
2566        if lexicon::is_gradable_adjective(base) {
2567            return Some(Self::capitalize(base));
2568        }
2569
2570        None
2571    }
2572
2573    fn try_parse_comparative(&self, word: &str) -> Option<String> {
2574        if !word.ends_with("er") || word.len() < 4 {
2575            return None;
2576        }
2577
2578        let base = &word[..word.len() - 2];
2579
2580        if base.len() >= 2 {
2581            let chars: Vec<char> = base.chars().collect();
2582            let last = chars[chars.len() - 1];
2583            let second_last = chars[chars.len() - 2];
2584            if last == second_last && !"aeiou".contains(last) {
2585                let stem = &base[..base.len() - 1];
2586                if lexicon::is_gradable_adjective(stem) {
2587                    return Some(Self::capitalize(stem));
2588                }
2589            }
2590        }
2591
2592        if base.ends_with("i") {
2593            let stem = format!("{}y", &base[..base.len() - 1]);
2594            if lexicon::is_gradable_adjective(&stem) {
2595                return Some(Self::capitalize(&stem));
2596            }
2597        }
2598
2599        if lexicon::is_gradable_adjective(base) {
2600            return Some(Self::capitalize(base));
2601        }
2602
2603        None
2604    }
2605}
2606
2607#[cfg(test)]
2608mod tests {
2609    use super::*;
2610
2611    #[test]
2612    fn lexer_handles_apostrophe() {
2613        let mut interner = Interner::new();
2614        let mut lexer = Lexer::new("it's raining", &mut interner);
2615        let tokens = lexer.tokenize();
2616        assert!(!tokens.is_empty());
2617    }
2618
2619    #[test]
2620    fn lexer_handles_question_mark() {
2621        let mut interner = Interner::new();
2622        let mut lexer = Lexer::new("Is it raining?", &mut interner);
2623        let tokens = lexer.tokenize();
2624        assert!(!tokens.is_empty());
2625    }
2626
2627    #[test]
2628    fn ring_is_not_verb() {
2629        let mut interner = Interner::new();
2630        let mut lexer = Lexer::new("ring", &mut interner);
2631        let tokens = lexer.tokenize();
2632        assert!(matches!(tokens[0].kind, TokenType::Noun(_)));
2633    }
2634
2635    #[test]
2636    fn debug_that_token() {
2637        let mut interner = Interner::new();
2638        let mut lexer = Lexer::new("The cat that runs", &mut interner);
2639        let tokens = lexer.tokenize();
2640        for (i, t) in tokens.iter().enumerate() {
2641            let lex = interner.resolve(t.lexeme);
2642            eprintln!("Token[{}]: {:?} -> {:?}", i, lex, t.kind);
2643        }
2644        let that_token = tokens.iter().find(|t| interner.resolve(t.lexeme) == "that");
2645        if let Some(t) = that_token {
2646            // Verify discriminant comparison works
2647            let check = std::mem::discriminant(&t.kind) == std::mem::discriminant(&TokenType::That);
2648            eprintln!("Discriminant check for That: {}", check);
2649            assert!(matches!(t.kind, TokenType::That), "'that' should be TokenType::That, got {:?}", t.kind);
2650        } else {
2651            panic!("No 'that' token found");
2652        }
2653    }
2654
2655    #[test]
2656    fn bus_is_not_verb() {
2657        let mut interner = Interner::new();
2658        let mut lexer = Lexer::new("bus", &mut interner);
2659        let tokens = lexer.tokenize();
2660        assert!(matches!(tokens[0].kind, TokenType::Noun(_)));
2661    }
2662
2663    #[test]
2664    fn lowercase_a_is_article() {
2665        let mut interner = Interner::new();
2666        let mut lexer = Lexer::new("a car", &mut interner);
2667        let tokens = lexer.tokenize();
2668        for (i, t) in tokens.iter().enumerate() {
2669            let lex = interner.resolve(t.lexeme);
2670            eprintln!("Token[{}]: {:?} -> {:?}", i, lex, t.kind);
2671        }
2672        assert_eq!(tokens[0].kind, TokenType::Article(Definiteness::Indefinite));
2673        assert!(matches!(tokens[1].kind, TokenType::Noun(_)), "Expected Noun, got {:?}", tokens[1].kind);
2674    }
2675
2676    #[test]
2677    fn open_is_ambiguous() {
2678        let mut interner = Interner::new();
2679        let mut lexer = Lexer::new("open", &mut interner);
2680        let tokens = lexer.tokenize();
2681
2682        if let TokenType::Ambiguous { primary, alternatives } = &tokens[0].kind {
2683            assert!(matches!(**primary, TokenType::Verb { .. }), "Primary should be Verb");
2684            assert!(alternatives.iter().any(|t| matches!(t, TokenType::Adjective(_))),
2685                "Should have Adjective alternative");
2686        } else {
2687            panic!("Expected Ambiguous token for 'open', got {:?}", tokens[0].kind);
2688        }
2689    }
2690
2691    #[test]
2692    fn basic_tokenization() {
2693        let mut interner = Interner::new();
2694        let mut lexer = Lexer::new("All men are mortal.", &mut interner);
2695        let tokens = lexer.tokenize();
2696        assert_eq!(tokens[0].kind, TokenType::All);
2697        assert!(matches!(tokens[1].kind, TokenType::Noun(_)));
2698        assert_eq!(tokens[2].kind, TokenType::Are);
2699    }
2700
2701    #[test]
2702    fn iff_tokenizes_as_single_token() {
2703        let mut interner = Interner::new();
2704        let mut lexer = Lexer::new("A if and only if B", &mut interner);
2705        let tokens = lexer.tokenize();
2706        assert!(
2707            tokens.iter().any(|t| t.kind == TokenType::Iff),
2708            "should contain Iff token: got {:?}",
2709            tokens
2710        );
2711    }
2712
2713    #[test]
2714    fn is_equal_to_tokenizes_as_identity() {
2715        let mut interner = Interner::new();
2716        let mut lexer = Lexer::new("Socrates is equal to Socrates", &mut interner);
2717        let tokens = lexer.tokenize();
2718        assert!(
2719            tokens.iter().any(|t| t.kind == TokenType::Identity),
2720            "should contain Identity token: got {:?}",
2721            tokens
2722        );
2723    }
2724
2725    #[test]
2726    fn is_identical_to_tokenizes_as_identity() {
2727        let mut interner = Interner::new();
2728        let mut lexer = Lexer::new("Clark is identical to Superman", &mut interner);
2729        let tokens = lexer.tokenize();
2730        assert!(
2731            tokens.iter().any(|t| t.kind == TokenType::Identity),
2732            "should contain Identity token: got {:?}",
2733            tokens
2734        );
2735    }
2736
2737    #[test]
2738    fn itself_tokenizes_as_reflexive() {
2739        let mut interner = Interner::new();
2740        let mut lexer = Lexer::new("John loves itself", &mut interner);
2741        let tokens = lexer.tokenize();
2742        assert!(
2743            tokens.iter().any(|t| t.kind == TokenType::Reflexive),
2744            "should contain Reflexive token: got {:?}",
2745            tokens
2746        );
2747    }
2748
2749    #[test]
2750    fn himself_tokenizes_as_reflexive() {
2751        let mut interner = Interner::new();
2752        let mut lexer = Lexer::new("John sees himself", &mut interner);
2753        let tokens = lexer.tokenize();
2754        assert!(
2755            tokens.iter().any(|t| t.kind == TokenType::Reflexive),
2756            "should contain Reflexive token: got {:?}",
2757            tokens
2758        );
2759    }
2760
2761    #[test]
2762    fn to_stay_tokenizes_correctly() {
2763        let mut interner = Interner::new();
2764        let mut lexer = Lexer::new("to stay", &mut interner);
2765        let tokens = lexer.tokenize();
2766        assert!(
2767            tokens.iter().any(|t| t.kind == TokenType::To),
2768            "should contain To token: got {:?}",
2769            tokens
2770        );
2771        assert!(
2772            tokens.iter().any(|t| matches!(t.kind, TokenType::Verb { .. })),
2773            "should contain Verb token for stay: got {:?}",
2774            tokens
2775        );
2776    }
2777
2778    #[test]
2779    fn possessive_apostrophe_s() {
2780        let mut interner = Interner::new();
2781        let mut lexer = Lexer::new("John's dog", &mut interner);
2782        let tokens = lexer.tokenize();
2783        assert!(
2784            tokens.iter().any(|t| t.kind == TokenType::Possessive),
2785            "should contain Possessive token: got {:?}",
2786            tokens
2787        );
2788        assert!(
2789            tokens.iter().any(|t| matches!(&t.kind, TokenType::ProperName(_))),
2790            "should have John as proper name: got {:?}",
2791            tokens
2792        );
2793    }
2794
2795    #[test]
2796    fn lexer_produces_valid_spans() {
2797        let input = "All men are mortal.";
2798        let mut interner = Interner::new();
2799        let mut lexer = Lexer::new(input, &mut interner);
2800        let tokens = lexer.tokenize();
2801
2802        // "All" at 0..3
2803        assert_eq!(tokens[0].span.start, 0);
2804        assert_eq!(tokens[0].span.end, 3);
2805        assert_eq!(&input[tokens[0].span.start..tokens[0].span.end], "All");
2806
2807        // "men" at 4..7
2808        assert_eq!(tokens[1].span.start, 4);
2809        assert_eq!(tokens[1].span.end, 7);
2810        assert_eq!(&input[tokens[1].span.start..tokens[1].span.end], "men");
2811
2812        // "are" at 8..11
2813        assert_eq!(tokens[2].span.start, 8);
2814        assert_eq!(tokens[2].span.end, 11);
2815        assert_eq!(&input[tokens[2].span.start..tokens[2].span.end], "are");
2816
2817        // "mortal" at 12..18
2818        assert_eq!(tokens[3].span.start, 12);
2819        assert_eq!(tokens[3].span.end, 18);
2820        assert_eq!(&input[tokens[3].span.start..tokens[3].span.end], "mortal");
2821
2822        // "." at 18..19
2823        assert_eq!(tokens[4].span.start, 18);
2824        assert_eq!(tokens[4].span.end, 19);
2825
2826        // EOF at end
2827        assert_eq!(tokens[5].span.start, input.len());
2828        assert_eq!(tokens[5].kind, TokenType::EOF);
2829    }
2830
2831    #[test]
2832    fn triple_quote_produces_string_token() {
2833        let mut interner = Interner::new();
2834        let source = "## Main\nLet msg be \"\"\"\n    Hello\n    World\n\"\"\".\nShow msg.";
2835        let mut lexer = Lexer::new(source, &mut interner);
2836        let tokens = lexer.tokenize();
2837        // Dump all tokens for debugging
2838        for (i, t) in tokens.iter().enumerate() {
2839            let lex = interner.resolve(t.lexeme);
2840            eprintln!("Token[{}]: {:?} lex={:?} span={}..{}", i, t.kind, lex, t.span.start, t.span.end);
2841        }
2842        // Find the string token
2843        let str_token = tokens.iter().find(|t| matches!(t.kind, TokenType::StringLiteral(_) | TokenType::InterpolatedString(_)));
2844        assert!(str_token.is_some(), "Should have a string token. Tokens: {:?}", tokens.iter().map(|t| format!("{:?}", t.kind)).collect::<Vec<_>>());
2845        if let Some(tok) = str_token {
2846            let content = interner.resolve(tok.lexeme);
2847            eprintln!("Triple-quote content: {:?}", content);
2848            assert!(content.contains("Hello"), "Should contain Hello, got: {:?}", content);
2849        }
2850    }
2851}