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}