Skip to main content

uxn_tal/
lexer.rs

1//! Lexer for TAL assembly language
2
3use crate::{
4    error::{AssemblerError, Result},
5    runes::Rune,
6};
7
8/// Token types in TAL assembly
9#[derive(Debug, Clone, PartialEq)]
10pub enum Token {
11    Word(String), // could be a macro name before syntax error.
12    /// Hexadecimal literal (e.g., #1234, #ab)
13    HexLiteral(String),
14    /// Raw hexadecimal byte (e.g., 20, ff)
15    RawHex(String),
16    /// Decimal literal (e.g., 42)
17    DecLiteral(String),
18    /// Binary literal (e.g., #b10101010)
19    BinLiteral(String),
20    /// Character literal (e.g., 'A')
21    CharLiteral(char),
22    /// Instruction/opcode (e.g., ADD, LDA2k)
23    Instruction(String),
24    /// Label definition (e.g., @main)
25    LabelDef(Rune, String),
26    /// Label reference (e.g., ;main)
27    LabelRef(Rune, String),
28    /// Sublabel definition (e.g., &loop)
29    SublabelDef(String),
30    /// Sublabel reference (e.g., ,loop)
31    SublabelRef(String),
32    /// Relative address reference (e.g., /loop)
33    RelativeRef(String),
34    /// Conditional jump reference (e.g., ?loop)
35    ConditionalRef(String),
36    /// Conditional operator (e.g., ?)
37    ConditionalOperator,
38    /// Conditional block start (e.g., ?{)
39    ConditionalBlockStart,
40    /// Raw address reference (e.g., =label)
41    RawAddressRef(String),
42    /// JSR call reference (e.g., !label)
43    JSRRef(String),
44    /// Hyphen address reference (e.g., -Screen/auto)
45    HyphenRef(String),
46    /// Padding directive (absolute) |ADDR
47    Padding(u16),
48    PaddingLabel(String),
49    /// Relative padding (was Skip): $HEX means advance current address by HEX bytes
50    RelativePadding(u16),
51    /// Relative padding to label: $label sets ptr = current + label.addr
52    RelativePaddingLabel(String),
53    /// Device access (e.g., .Screen/width)
54    DeviceAccess(String, String), // device, field
55
56    /// Macro definition (e.g., MACRO)
57    MacroDef(String),
58
59    /// Dot reference - generates LIT + 8-bit address (like uxnasm's '.' rune)
60    DotRef(String),
61    /// Semicolon reference - generates LIT2 + 16-bit address (like uxnasm's ';' rune)  
62    SemicolonRef(String),
63    /// Equals reference - generates 16-bit address directly (like uxnasm's '=' rune)
64    EqualsRef(String),
65    /// Comma reference - generates LIT + relative 8-bit address (like uxnasm's ',' rune)
66    CommaRef(String),
67    /// Underscore reference - generates relative 8-bit address (like uxnasm's '_' rune)
68    UnderscoreRef(String),
69    /// Question reference - generates conditional jump (like uxnasm's '?' rune)
70    QuestionRef(String),
71    /// Exclamation reference - generates JSR call (like uxnasm's '!' rune)
72    ExclamationRef(String),
73    /// Brace open
74    BraceOpen,
75    /// Brace close
76    BraceClose,
77    /// Bracket open
78    BracketOpen,
79    /// Bracket close
80    BracketClose,
81    /// Include directive (e.g., ~filename.tal)
82    Include(String),
83    /// Raw string literal
84    RawString(String),
85    /// Comment
86    Comment(String),
87    /// Newline
88    Newline,
89    /// End of file
90    Eof,
91    Ignored,
92}
93
94/// Token with position information and optional scope
95#[derive(Debug, Clone, PartialEq)]
96pub struct TokenWithPos {
97    pub token: Token,
98    pub line: usize,
99    pub start_pos: usize,
100    pub end_pos: usize,
101    pub scope: Option<String>, // <-- Add scope field
102}
103
104/// Lexer for TAL assembly language
105pub struct Lexer {
106    input: String,
107    position: usize,      // byte position
108    char_position: usize, // character position
109    line: usize,
110    path: Option<String>,
111    position_on_line: usize,
112    next_conditional_sublabel: Option<String>,
113    current_scope: Option<String>, // <-- Track current scope
114}
115
116impl Lexer {
117    // get_column removed; use position_on_line directly
118    pub fn new(input: String, path: Option<String>) -> Self {
119        Self {
120            input,
121            position: 0,
122            char_position: 0,
123            line: 1,
124            path,
125            position_on_line: 1,
126            next_conditional_sublabel: None,
127            current_scope: None, // <-- Initialize
128        }
129    }
130
131    /// Get the current line content for error reporting
132    fn get_current_line(&self) -> String {
133        let lines: Vec<&str> = self.input.lines().collect();
134        if self.line > 0 && self.line <= lines.len() {
135            lines[self.line - 1].to_string()
136        } else {
137            String::new()
138        }
139    }
140
141    /// Create a syntax error with current line information
142    fn syntax_error(&self, message: String) -> AssemblerError {
143        AssemblerError::SyntaxError {
144            path: self.path.clone().unwrap_or_default(),
145            line: self.line,
146            position: self.position_on_line,
147            message,
148            source_line: self.get_current_line(),
149        }
150    }
151
152    pub fn line(&self) -> usize {
153        self.line
154    }
155
156    /// Tokenize the entire input
157    pub fn tokenize(&mut self) -> Result<Vec<TokenWithPos>> {
158        let mut tokens = Vec::new();
159
160        while self.position < self.input.len() {
161            let start_line = self.line;
162            let start_pos = self.position_on_line;
163            let token_start_position = self.position;
164
165            // Handle newlines and whitespace
166            let ch = self.current_char();
167            if ch == '\n' {
168                tokens.push(TokenWithPos {
169                    token: Token::Newline,
170                    line: start_line,
171                    start_pos,
172                    end_pos: start_pos,
173                    scope: self.current_scope.clone(),
174                });
175                self.advance();
176                continue;
177            }
178            if ch.is_whitespace() && ch != '\n' {
179                self.advance();
180                continue;
181            }
182
183            // Handle comments
184            if ch == '(' {
185                let comment_start_line = self.line;
186                let comment_start_pos = self.position_on_line;
187                self.advance();
188                let comment = self.read_comment()?;
189                let comment_end_pos = self.position_on_line;
190                tokens.push(TokenWithPos {
191                    token: Token::Comment(comment.clone()),
192                    line: comment_start_line,
193                    start_pos: comment_start_pos,
194                    end_pos: comment_end_pos,
195                    scope: self.current_scope.clone(),
196                });
197                continue;
198            }
199
200            // Get next token
201            match self.next_token()? {
202                Token::Eof => break,
203                token => {
204                    let token_end_position = self.position;
205                    let token_end_pos =
206                        start_pos + (token_end_position - token_start_position).max(1) - 1;
207
208                    // --- SCOPE TRACKING ---
209                    // Update current_scope for label/sublabel definitions
210                    if let Token::LabelDef(_rune, label) = &token {
211                        self.current_scope = Some(label.clone());
212                    }
213
214                    // For sublabel definition, set scope to parent label (not full sublabel path)
215                    let token_scope = match &token {
216                        Token::RelativeRef(_) => {
217                            // For /down, use parent label scope (not full sublabel scope)
218                            if let Some(ref scope) = self.current_scope {
219                                if let Some(pos) = scope.find('/') {
220                                    Some(scope[..pos].to_string())
221                                } else {
222                                    Some(scope.clone())
223                                }
224                            } else {
225                                None
226                            }
227                        }
228                        Token::SublabelDef(_)
229                        | Token::SublabelRef(_)
230                        | Token::CommaRef(_)
231                        | Token::UnderscoreRef(_) => {
232                            // Use parent label as scope
233                            if let Some(ref scope) = self.current_scope {
234                                if let Some(pos) = scope.rfind('/') {
235                                    Some(scope[..pos].to_string())
236                                } else {
237                                    Some(scope.clone())
238                                }
239                            } else {
240                                None
241                            }
242                        }
243                        _ => self.current_scope.clone(),
244                    };
245
246                    tokens.push(TokenWithPos {
247                        token: token.clone(),
248                        line: start_line,
249                        start_pos,
250                        end_pos: token_end_pos,
251                        scope: token_scope,
252                    });
253                }
254            }
255        }
256        Ok(tokens)
257    }
258
259    /// Move to the next character
260    fn advance(&mut self) {
261        if self.position < self.input.len() {
262            let mut chars = self.input[self.position..].chars();
263            if let Some(ch) = chars.next() {
264                let ch_len = ch.len_utf8();
265                if ch == '\n' {
266                    self.line += 1;
267                    self.position_on_line = 1;
268                } else {
269                    self.position_on_line += 1;
270                }
271                self.position += ch_len;
272                self.char_position += 1;
273            }
274        }
275    }
276
277    /// Skip whitespace characters except newlines
278    fn skip_whitespace(&mut self) {
279        while self.position < self.input.len() {
280            let ch = self.current_char();
281            if ch.is_whitespace() && ch != '\n' {
282                self.advance();
283            } else {
284                break;
285            }
286        }
287    }
288
289    /// Read characters until the specified delimiter is found
290    #[allow(dead_code)]
291    fn read_until(&mut self, delimiter: char) -> Result<String> {
292        let mut result = String::new();
293        while self.position < self.input.len() && self.current_char() != delimiter {
294            result.push(self.current_char());
295            self.advance();
296        }
297        Ok(result)
298    }
299
300    /// Read comment with proper nested parentheses handling
301    fn read_comment(&mut self) -> Result<String> {
302        // Match uxnasm's walkcomment: only consider '(' and ')' that begin a token
303        // (i.e., occur right after whitespace) for nesting and termination.
304        let mut result = String::new();
305        let mut depth = 1;
306        let mut last: char = '\0'; // last token-head character or 0 if in whitespace
307
308        while self.position < self.input.len() && depth > 0 {
309            let ch = self.current_char();
310
311            if ch.is_ascii() && ch <= ' ' {
312                // Whitespace: process the last token-head seen, then reset
313                result.push(ch);
314                self.advance();
315                if last == '(' {
316                    depth += 1;
317                } else if last == ')' {
318                    depth -= 1;
319                    if depth < 1 {
320                        break; // end of comment
321                    }
322                }
323                last = '\0';
324            } else if last <= ' ' {
325                // Start of a new token: remember its first character
326                last = ch;
327                result.push(ch);
328                self.advance();
329            } else {
330                // Inside a token: ignore nested parens here
331                last = '~';
332                result.push(ch);
333                self.advance();
334            }
335        }
336
337        Ok(result)
338    }
339
340    /// Read hexadecimal digits for #hex literals
341    fn read_hex(&mut self) -> Result<String> {
342        let mut result = String::new();
343        while self.position < self.input.len() {
344            let ch = self.current_char();
345            if ch.is_ascii_hexdigit() && (ch.is_ascii_lowercase() || ch.is_ascii_digit()) {
346                result.push(ch);
347                self.advance();
348            } else {
349                break;
350            }
351        }
352        if result.is_empty() {
353            return Err(AssemblerError::SyntaxError {
354                path: self.path.clone().unwrap_or_default(),
355                line: self.line,
356                position: self.position_on_line,
357                message: "Expected hexadecimal digits".to_string(),
358                source_line: self.get_current_line(),
359            });
360        }
361        Ok(result)
362    }
363
364    // Read binary digits for #b binary literals
365    // fn read_binary(&mut self) -> Result<String> {
366    //     let mut result = String::new();
367    //     while self.position < self.input.len() {
368    //         let ch = self.current_char();
369    //         if ch == '0' || ch == '1' {
370    //             result.push(ch);
371    //             self.advance();
372    //         } else {
373    //             break;
374    //         }
375    //     }
376    //     if result.is_empty() {
377    //         return Err(AssemblerError::SyntaxError {
378    //             path: self.path.clone().unwrap_or_default(),
379    //             line: self.line,
380    //             position: self.position_on_line,
381    //             message: "Expected binary digits".to_string(),
382    //             source_line: self.get_current_line(),
383    //         });
384    //     }
385    //     Ok(result)
386    // }
387
388    /// Read a decimal number (digits only)
389    #[allow(dead_code)]
390    fn read_number(&mut self) -> Result<String> {
391        let mut result = String::new();
392        while self.position < self.input.len() {
393            let ch = self.current_char();
394            if ch.is_ascii_digit() {
395                result.push(ch);
396                self.advance();
397            } else {
398                break;
399            }
400        }
401        Ok(result)
402    }
403
404    /// Read a hexadecimal number (hex digits only)
405    /// For asset data, reads up to 4 hex digits for RawHex tokens
406    fn read_hex_number(&mut self) -> Result<String> {
407        let mut result = String::new();
408        let mut count = 0;
409        while self.position < self.input.len() && count < 4 {
410            let ch = self.current_char();
411            if ch.is_ascii_hexdigit() {
412                result.push(ch);
413                self.advance();
414                count += 1;
415            } else {
416                break;
417            }
418        }
419        // Fix position_on_line drift: always update position_on_line based on chars consumed
420        // This is already handled by advance(), so do NOT manually update position_on_line here.
421        Ok(result)
422    }
423
424    /// Read an identifier (labels, instructions, etc.)
425    /// Allows alphanumeric characters, underscores, hyphens, forward slashes, asterisks, etc.
426    fn read_identifier(&mut self) -> Result<String> {
427        let mut result = String::new();
428        let mut first = true;
429        let mut macro_mode = false;
430        if self.char_position > 0 {
431            let prev_byte = self
432                .input
433                .char_indices()
434                .nth(self.char_position - 1)
435                .map(|(i, _)| i);
436            if let Some(prev_byte) = prev_byte {
437                if self.input[prev_byte..].starts_with('%') {
438                    macro_mode = true;
439                }
440            }
441        }
442        while self.position < self.input.len() {
443            let ch = self.current_char();
444            if (macro_mode && ch.is_whitespace())
445                || (!macro_mode && (ch.is_whitespace() || ch == '(' || ch == ')'))
446            {
447                break;
448            }
449            if first && ch == '"' {
450                let mut chars = self.input[self.position..].chars();
451                chars.next();
452                if chars.next() == Some('"') {
453                    self.advance();
454                    self.advance();
455                    return Ok(String::from("\""));
456                }
457            }
458            result.push(ch);
459            self.advance();
460            first = false;
461        }
462        if result.is_empty() {
463            if self.char_position > 1 {
464                let prev_chars: Vec<char> = self.input.chars().take(self.char_position).collect();
465                if prev_chars[self.char_position - 2] == '"'
466                    && prev_chars[self.char_position - 1] == '"'
467                {
468                    return Ok(String::from("\""));
469                }
470            }
471            if self.position >= self.input.len() - 1 && self.current_char() == '\n' {
472                return Ok(String::new());
473            }
474            return Err(AssemblerError::SyntaxError {
475                path: self.path.clone().unwrap_or_default(),
476                line: self.line,
477                position: self.position_on_line,
478                message: format!(
479                    "Expected identifier @ position {} (found '{}')",
480                    self.position_on_line,
481                    if self.position < self.input.len() {
482                        self.input[self.position..].chars().next().unwrap_or('␀')
483                    } else {
484                        '␀'
485                    }
486                ),
487                source_line: self.get_current_line(),
488            });
489        }
490        Ok(result)
491    }
492
493    /// Read include path after ~ token
494    fn read_include_path(&mut self) -> Result<String> {
495        let mut path = String::new();
496
497        // Skip any whitespace after ~
498        self.skip_whitespace();
499
500        while self.position < self.input.len() {
501            let ch = self.current_char();
502
503            // Include path ends at whitespace or newline
504            if ch.is_whitespace() {
505                break;
506            }
507
508            path.push(ch);
509            self.advance();
510        }
511
512        if path.is_empty() {
513            return Err(AssemblerError::SyntaxError {
514                path: self.path.clone().unwrap_or_default(),
515                line: self.line,
516                position: self.position_on_line,
517                message: "Empty include path".to_string(),
518                source_line: self.get_current_line(),
519            });
520        }
521
522        Ok(path)
523    }
524
525    /// Get the current character at the position
526    fn current_char(&self) -> char {
527        if self.position < self.input.len() {
528            self.input[self.position..].chars().next().unwrap_or('\0')
529        } else {
530            '\0'
531        }
532    }
533
534    /// Get the next token from the input
535    pub fn next_token(&mut self) -> Result<Token> {
536        // Check for pending sublabel/label from conditional operator
537        if let Some(label) = self.next_conditional_sublabel.take() {
538            if label.starts_with('&') {
539                return Ok(Token::SublabelRef(
540                    label.trim_start_matches('&').to_string(),
541                ));
542            }
543            if label.starts_with('<') {
544                let rune = Rune::from('<');
545                return Ok(Token::LabelRef(
546                    rune,
547                    label.trim_start_matches('<').to_string(),
548                ));
549            }
550            // Use Rune based on the first character of the label
551            // let rune = Rune::from(label.chars().next().unwrap_or('\0'));
552            return Ok(Token::LabelRef(
553                Rune::from(label.chars().next().unwrap_or('\0')),
554                label,
555            ));
556        }
557
558        // Robustly skip all whitespace except newlines before tokenizing
559        while self.position < self.input.len() {
560            let ch = self.current_char();
561            if ch.is_whitespace() && ch != '\n' {
562                self.advance();
563            } else {
564                break;
565            }
566        }
567
568        // Skip comments before tokenizing
569        while self.position < self.input.len() {
570            let ch = self.current_char();
571            if ch.is_whitespace() && ch != '\n' {
572                self.advance();
573            } else if ch == '(' {
574                self.advance();
575                let mut depth = 1;
576                while self.position < self.input.len() && depth > 0 {
577                    let ch = self.current_char();
578                    if ch == '(' {
579                        depth += 1;
580                    } else if ch == ')' {
581                        depth -= 1;
582                    }
583                    self.advance();
584                }
585                continue;
586            } else {
587                break;
588            }
589        }
590
591        // if self.position >= self.input.len() {
592        //     return Ok(Token::Eof);
593        // }
594
595        let ch = self.current_char();
596
597        match ch {
598            '\n' => {
599                self.advance();
600                Ok(Token::Newline)
601            }
602            '(' => {
603                self.advance();
604                let comment = self.read_comment()?;
605                Ok(Token::Comment(comment))
606            }
607            '"' => {
608                self.advance(); // consume opening quote
609                let mut string_content = String::new();
610                while self.position < self.input.len() {
611                    let ch = self.current_char();
612                    // Double quote: emit a quote and skip both
613                    if ch == '"'
614                        && self.position + 1 < self.input.len()
615                        && self.input.as_bytes()[self.position + 1] as char == '"'
616                    {
617                        string_content.push('"');
618                        self.advance(); // skip first quote
619                        self.advance(); // skip second quote
620                        continue;
621                    }
622                    // End string at whitespace characters (space, tab, newline, etc.)
623                    if ch.is_whitespace() {
624                        break;
625                    }
626                    string_content.push(ch);
627                    self.advance();
628                }
629                Ok(Token::RawString(string_content))
630            }
631            '\'' => {
632                self.advance();
633                let ch = self.current_char();
634                if ch == '\0' {
635                    return Err(AssemblerError::SyntaxError {
636                        path: self.path.clone().unwrap_or_default(),
637                        line: self.line,
638                        position: self.position_on_line,
639                        message: "Unexpected end of file in character literal".to_string(),
640                        source_line: self.get_current_line(),
641                    });
642                }
643                self.advance();
644                Ok(Token::CharLiteral(ch))
645            }
646            '#' => {
647                self.advance();
648                loop {
649                    let ch = self.current_char();
650                    if ch.is_whitespace() && ch != '\n' {
651                        self.advance();
652                    } else if ch == '(' {
653                        self.advance();
654                        let comment = self.read_comment()?;
655                        let comment_newlines = comment.chars().filter(|c| *c == '\n').count();
656                        self.line += comment_newlines;
657                    } else {
658                        break;
659                    }
660                }
661                if self.current_char() == '"' {
662                    self.advance();
663                    let ch = self.current_char();
664                    if ch != '\0' {
665                        self.advance();
666                        return Ok(Token::CharLiteral(ch));
667                    }
668                }
669                if self.current_char() == '\'' {
670                    self.advance();
671                    let ch = self.current_char();
672                    if ch != '\0' {
673                        self.advance();
674                        return Ok(Token::CharLiteral(ch));
675                    }
676                }
677                if self.current_char() == '[' {
678                    self.advance();
679                    let ch = self.current_char();
680                    if ch != '\0'
681                        && self.position + 1 < self.input.len()
682                        && self.input.as_bytes()[self.position + 1] as char == ']'
683                    {
684                        self.advance();
685                        self.advance();
686                        return Ok(Token::CharLiteral(ch));
687                    }
688                }
689                if self.current_char().is_ascii_alphabetic()
690                    && self
691                        .input
692                        .chars()
693                        .nth(self.position + 1)
694                        .map(|c| c.is_whitespace() || c == ']')
695                        == Some(true)
696                {
697                    let ch = self.current_char();
698                    self.advance();
699                    return Ok(Token::CharLiteral(ch));
700                }
701                if !self.current_char().is_ascii_hexdigit() {
702                    return Err(AssemblerError::SyntaxError {
703                        path: self.path.clone().unwrap_or_default(),
704                        line: self.line,
705                        position: self.position_on_line,
706                        message: "Expected hexadecimal digits after '#'".to_string(),
707                        source_line: self.get_current_line(),
708                    });
709                }
710                let hex = self.read_hex()?;
711                Ok(Token::HexLiteral(hex))
712            }
713            '@' => {
714                self.advance();
715                let label = self.read_identifier()?;
716                // println!("LEXER DEBUG: Parsed label definition: @{}", label);
717                Ok(Token::LabelDef('@'.into(), label))
718            }
719            ';' => {
720                self.advance();
721                let label = self.read_identifier()?;
722                Ok(Token::SemicolonRef(label))
723            }
724            '.' => {
725                self.advance();
726                let ident = self.read_identifier()?;
727                Ok(Token::DotRef(ident))
728            }
729            '=' => {
730                self.advance();
731                let label = self.read_identifier()?;
732                Ok(Token::EqualsRef(label))
733            }
734            ',' => {
735                self.advance();
736                if self.current_char() == '&' {
737                    self.advance();
738                }
739                let label = self.read_identifier()?;
740                Ok(Token::CommaRef(label))
741            }
742            '_' => {
743                self.advance();
744                if self.current_char() == '&' {
745                    self.advance();
746                }
747                let label = self.read_identifier()?;
748                // println!("DEBUG: Read underscore label: '{}'", label);
749                // if label == "_" {
750                //     // If the label is just "_", skip and try to read the next identifier
751                //     let label = self.read_identifier()?;
752                //     // println!("DEBUG: Read underscore label (after skip): '{}'", label);
753                //     return Ok(Token::UnderscoreRef(label));
754                // }
755                Ok(Token::UnderscoreRef(label))
756            }
757            '-' => {
758                // // Lookahead: treat 8+ consecutive '-' followed by whitespace/EOF as a separator comment.
759                // let mut look = self.position;
760                // let mut count = 0;
761                // while look < self.input.len()
762                //     && self.input.chars().nth(look) == Some('-')
763                // {
764                //     count += 1;
765                //     look += 1;
766                // }
767                // let next_ch = self.input.chars().nth(look).unwrap_or('\0');
768                // if count >= 8 && (next_ch == '\0' || next_ch.is_whitespace()) {
769                //     // Consume all the dashes.
770                //     for _ in 0..count {
771                //         self.advance();
772                //     }
773                //     return Ok(Token::Comment("-".repeat(count)));
774                // }
775                // Normal hyphen reference
776                self.advance();
777                let identifier = self.read_identifier()?;
778                Ok(Token::HyphenRef(identifier))
779            }
780            '/' => {
781                self.advance();
782                let label = self.read_identifier()?;
783                // Don't include the leading slash in the label name - it's just syntax
784                Ok(Token::RelativeRef(label))
785            }
786            '?' => {
787                self.advance();
788                if self.current_char() == '{' {
789                    self.advance(); // consume '{'
790                    Ok(Token::ConditionalBlockStart)
791                } else {
792                    let name = self.read_identifier()?;
793                    Ok(Token::ConditionalRef(name))
794                }
795            }
796            '!' => {
797                self.advance();
798                let label = self.read_identifier()?;
799                Ok(Token::ExclamationRef(label))
800            }
801            '{' => {
802                self.advance();
803                Ok(Token::BraceOpen)
804            }
805            '}' => {
806                self.advance();
807                Ok(Token::BraceClose)
808            }
809            '[' => {
810                self.advance();
811                if self.current_char() == '"'
812                    && self.position + 1 < self.input.len()
813                    && self.input.as_bytes()[self.position + 1] as char == '"'
814                {
815                    self.advance();
816                    self.advance();
817                    return Ok(Token::CharLiteral('"'));
818                }
819                // Improved EOF handling: only call read_identifier if next char is not whitespace, not ']', and not EOF
820                let next_ch = self.current_char();
821                if !next_ch.is_whitespace() && next_ch != ']' && next_ch != '\0' {
822                    let _word = self.read_identifier()?;
823                    return Ok(Token::BracketOpen); // we eat the word for now.  Token::BracketOpenWithWord(word));
824                }
825                Ok(Token::BracketOpen)
826            }
827            ']' => {
828                self.advance();
829                // Only call read_identifier if next char is not whitespace and not EOF
830                let next_ch = self.current_char();
831                if !next_ch.is_whitespace() && next_ch != '\0' {
832                    let _word = self.read_identifier()?;
833                    return Ok(Token::BracketClose); // we eat the word for now.  Token::BracketCloseWithWord(word));
834                }
835                Ok(Token::BracketClose)
836            }
837            '~' => {
838                self.advance();
839                let filename = self.read_include_path()?;
840                println!("LEXER DEBUG: Include path: {:?}", self.path);
841                println!("LEXER DEBUG: Parsed include filename: ~{}", filename);
842                Ok(Token::Include(filename))
843            }
844            // '<' => {
845            //     self.advance();
846            //     let name = self.read_identifier()?;
847            //     // NEW: allow immediate "/sublabel" after closing '>' (e.g., <phex>/b)
848            //     let mut full = format!("<{}>", name);
849            //     if self.current_char() == '/' {
850            //         self.advance();
851            //         // read sublabel segment (stop at whitespace or rune delimiters)
852            //         let mut sub = String::new();
853            //         while self.position < self.input.len() {
854            //             let ch = self.current_char();
855            //             if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
856            //                 sub.push(ch);
857            //                 self.advance();
858            //             } else {
859            //                 break;
860            //             }
861            //         }
862            //         if !sub.is_empty() {
863            //             full.push('/');
864            //             full.push_str(&sub);
865            //         }
866            //     }
867            //     let rune = Rune::from(' ');
868            //     Ok(Token::LabelRef(rune, full))
869            //     // Ok(Token::LabelRef(full)) // Always treat <name> (and optional /sub) as LabelRef
870            // }
871            _ if ch.is_ascii_digit() => {
872                // PATCH: If the digit is followed by +, -, or any valid identifier char, read as identifier
873                let mut lookahead = self.position + 1;
874                let mut is_complex = false;
875                while lookahead < self.input.len() {
876                    let next = if lookahead < self.input.len() {
877                        self.input.as_bytes()[lookahead] as char
878                    } else {
879                        '\0'
880                    };
881                    if !next.is_whitespace() && next != '(' && next != ')' {
882                        is_complex = true;
883                        break;
884                    } else if next.is_whitespace() || next == '\0' || next == '(' || next == ')' {
885                        break;
886                    }
887                    lookahead += 1;
888                }
889                if is_complex {
890                    let ident = self.read_identifier()?;
891                    // Only treat as hex if it's a simple pattern like "ff", "1234", etc.
892                    if ident.len() >= 2
893                        && ident.len() <= 4
894                        && ident.len() % 2 == 0
895                        && ident.chars().all(|c| {
896                            c.is_ascii_hexdigit() && (c.is_ascii_lowercase() || c.is_ascii_digit())
897                        })
898                        && !ident.contains('-')
899                        && !ident.contains('/')
900                        && !ident.contains('_')
901                        && !is_instruction_name(&ident)
902                    {
903                        Ok(Token::RawHex(ident))
904                    } else if is_instruction_name(&ident) {
905                        Ok(Token::Instruction(ident))
906                    } else {
907                        Ok(Token::LabelRef(Rune::from(' '), ident))
908                    }
909                } else {
910                    let number = self.read_hex_number()?;
911                    if (number.len() == 2 || number.len() == 4)
912                        && number.chars().all(|c| {
913                            c.is_ascii_hexdigit() && (c.is_ascii_lowercase() || c.is_ascii_digit())
914                        })
915                    {
916                        Ok(Token::RawHex(number))
917                    } else {
918                        Ok(Token::LabelRef(Rune::from(' '), number))
919                    }
920                }
921            }
922            _ if ch.is_ascii_alphabetic() || ch == '_' => {
923                let identifier = self.read_identifier()?;
924                // Only treat as hex if it's a simple pattern like "ff", "1234", etc.
925                // Don't treat words with hyphens or complex patterns as hex
926                // Also don't treat known instruction names as hex
927                // Don't treat instruction names as hex
928                if identifier.len() >= 2
929                    && identifier.len() <= 4
930                    && identifier.len() % 2 == 0
931                    && identifier.chars().all(|c| c.is_ascii_hexdigit() && (c.is_ascii_lowercase() || c.is_ascii_digit()))
932                    && !identifier.contains('-')  // No hyphens in hex values
933                    && !identifier.contains('/')  // No slashes in hex values
934                    && !identifier.contains('_')  // No underscores in hex values
935                    && !is_instruction_name(&identifier)
936                {
937                    Ok(Token::RawHex(identifier))
938                } else if is_instruction_name(&identifier) {
939                    Ok(Token::Instruction(identifier))
940                } else {
941                    // If not a known instruction, treat as a label reference (bare word)
942                    Ok(Token::LabelRef(Rune::from(' '), identifier))
943                }
944            }
945            '|' => {
946                self.advance();
947                let padding_value = self.read_identifier()?;
948
949                // Check if it's a hex value or a label
950                if padding_value.chars().all(|c| c.is_ascii_hexdigit()) {
951                    // Parse as hex address
952                    let addr = u16::from_str_radix(&padding_value, 16)
953                        .map_err(|_| self.syntax_error("Invalid padding address".to_string()))?;
954                    Ok(Token::Padding(addr))
955                } else {
956                    // It's a label reference for padding
957                    Ok(Token::PaddingLabel(padding_value))
958                }
959            }
960            '$' => {
961                self.advance();
962                if self.current_char() == '&' {
963                    self.advance();
964                }
965                let val = self.read_identifier()?;
966                if val.chars().all(|c| c.is_ascii_hexdigit()) {
967                    // relative hex padding
968                    let count = u16::from_str_radix(&val, 16).map_err(|_| {
969                        self.syntax_error("Invalid relative padding value".to_string())
970                    })?;
971                    Ok(Token::RelativePadding(count))
972                } else {
973                    // relative padding to label
974                    Ok(Token::RelativePaddingLabel(val))
975                }
976            }
977            '%' => {
978                self.advance();
979                let macro_name = self.read_identifier()?;
980                // println!("Macro name: {}", macro_name);
981                Ok(Token::MacroDef(macro_name))
982            }
983            '&' => {
984                self.advance();
985                // NEW: Support bare '&' (no identifier) to define an empty sublabel (parent/)
986                let next = self.current_char();
987                if next == '\n'
988                    || next == '\0'
989                    || next.is_whitespace()
990                    || next == ')'
991                    || next == ']'
992                    || next == '}'
993                {
994                    // Treat as empty sublabel definition
995                    return Ok(Token::SublabelDef(String::new()));
996                }
997                let sublabel = self.read_identifier()?;
998                // In uxnasm.c, '&' always creates a sublabel definition (makelabel)
999                Ok(Token::SublabelDef(sublabel))
1000            }
1001            // Try to parse as macro call if it looks like a macro invocation (e.g., MACRO_NAME!)
1002            _ => {
1003                // If at EOF or at a newline, return Eof (don't error on trailing empty lines)
1004                if self.position >= self.input.len() || ch == '\0' || ch == '\n' {
1005                    return Ok(Token::Eof);
1006                }
1007                let name = self.read_identifier().ok();
1008                if let Some(name) = name {
1009                    if name.is_empty()
1010                        && (self.position >= self.input.len() || self.current_char() == '\0')
1011                    {
1012                        return Ok(Token::Eof);
1013                    }
1014                    let rune = Rune::from(' ');
1015                    return Ok(Token::LabelRef(rune, name));
1016                    //return Ok(Token::Word(name));
1017                }
1018                Err(self.syntax_error(format!("Unexpected character: '{}'", ch)))
1019            }
1020        }
1021    }
1022}
1023
1024/// Check if an identifier is a known instruction name
1025fn is_instruction_name(identifier: &str) -> bool {
1026    // List of UXN instruction names that could be confused with hex
1027    const INSTRUCTIONS: &[&str] = &[
1028        "ADD", "ADD2", "SUB", "SUB2", "MUL", "MUL2", "DIV", "DIV2", "AND", "AND2", "ORA", "ORA2",
1029        "EOR", "EOR2", "SFT", "SFT2", "LDZ", "LDZ2", "STZ", "STZ2", "LDR", "LDR2", "STR", "STR2",
1030        "LDA", "LDA2", "STA", "STA2", "DEI", "DEI2", "DEO", "DEO2", "INC", "INC2", "POP", "POP2",
1031        "NIP", "NIP2", "SWP", "SWP2", "ROT", "ROT2", "DUP", "DUP2", "OVR", "OVR2", "EQU", "EQU2",
1032        "NEQ", "NEQ2", "GTH", "GTH2", "LTH", "LTH2", "JMP", "JMP2", "JCN", "JCN2", "JSR", "JSR2",
1033        "STH", "STH2", "BRK", "LIT", "LIT2",
1034    ];
1035
1036    // Check the base instruction name (without mode flags)
1037    let base_name = if identifier.len() > 3 {
1038        let mut base = identifier.to_string();
1039        // Remove mode flags (k, r, 2)
1040        while base.ends_with('k') || base.ends_with('r') || base.ends_with('2') {
1041            base.pop();
1042        }
1043        base
1044    } else {
1045        identifier.to_string()
1046    };
1047
1048    INSTRUCTIONS.iter().any(|&inst| {
1049        inst == identifier
1050            || inst == base_name
1051            || (inst.ends_with('2') && inst[..inst.len() - 1] == base_name)
1052    })
1053}
1054
1055/// Extract include filenames from TAL source using the lexer
1056pub fn extract_includes_from_lexer(input: &str, path: Option<String>) -> Vec<String> {
1057    let mut lexer = Lexer::new(input.to_string(), path);
1058    let mut includes = Vec::new();
1059    while let Ok(token) = lexer.next_token() {
1060        match token {
1061            Token::Include(filename) => includes.push(filename),
1062            Token::Eof => break,
1063            _ => {}
1064        }
1065    }
1066    includes
1067}