Skip to main content

asm_rs/
lexer.rs

1//! Lexer for assembly source text.
2//!
3//! The lexer tokenizes assembly source into a stream of [`Token`](crate::lexer::Token)s, each
4//! carrying its [`Span`](crate::error::Span) (source position) so that error messages can
5//! point back to the exact location in the original input.
6
7use alloc::borrow::Cow;
8use alloc::string::String;
9#[allow(unused_imports)]
10use alloc::vec;
11use alloc::vec::Vec;
12use core::str;
13
14use crate::error::{AsmError, Span};
15use crate::ir::Syntax;
16
17/// A token produced by the lexer.
18///
19/// Token text is borrowed from the source string (`Cow::Borrowed`) in the
20/// common case, avoiding per-token heap allocation.  String literals with
21/// escape sequences are the only tokens that own their text on the heap.
22#[derive(Debug, Clone, PartialEq)]
23pub struct Token<'src> {
24    /// Token classification.
25    pub kind: TokenKind,
26    /// Source text of the token — borrowed from input in the common case.
27    pub text: Cow<'src, str>,
28    /// Source location.
29    pub span: Span,
30}
31
32impl<'src> Token<'src> {
33    /// Returns the token text as a `&str`.
34    #[inline]
35    pub fn text(&self) -> &str {
36        &self.text
37    }
38}
39
40/// The type of a token.
41#[derive(Debug, Clone, PartialEq)]
42pub enum TokenKind {
43    /// An identifier: mnemonic, register name, or label reference.
44    Ident,
45    /// A numeric literal (integer).
46    Number(i128),
47    /// A string literal (content without quotes).
48    StringLit,
49    /// A character literal (e.g., 'A').
50    CharLit(u8),
51    /// A directive (starts with `.`).
52    Directive,
53    /// Label definition (`name:`).
54    LabelDef,
55    /// Numeric label definition (`1:`).
56    NumericLabelDef(u32),
57    /// Numeric label forward reference (`1f`).
58    NumericLabelFwd(u32),
59    /// Numeric label backward reference (`1b`).
60    NumericLabelBwd(u32),
61    /// Comma separator.
62    Comma,
63    /// Open bracket `[`.
64    OpenBracket,
65    /// Close bracket `]`.
66    CloseBracket,
67    /// Plus `+`.
68    Plus,
69    /// Minus `-`.
70    Minus,
71    /// Asterisk `*` (for scale in memory operands).
72    Star,
73    /// Colon `:` (segment override: `fs:`).
74    Colon,
75    /// Equals `=` (constant assignment: `name = value`).
76    Equals,
77    /// Open brace `{` (ARM register list).
78    OpenBrace,
79    /// Close brace `}` (ARM register list).
80    CloseBrace,
81    /// Open parenthesis `(` (RISC-V memory operand).
82    OpenParen,
83    /// Close parenthesis `)` (RISC-V memory operand).
84    CloseParen,
85    /// Exclamation mark `!` (ARM writeback).
86    Bang,
87    /// Percent sign `%` (AT&T register prefix).
88    Percent,
89    /// Dollar sign `$` (AT&T immediate prefix).
90    Dollar,
91    /// Forward slash `/` (SVE predicate qualifier: p0/m, p0/z).
92    Slash,
93    /// Ampersand `&` (bitwise AND in constant expressions).
94    Ampersand,
95    /// Pipe `|` (bitwise OR in constant expressions).
96    Pipe,
97    /// Caret `^` (bitwise XOR in constant expressions).
98    Caret,
99    /// Tilde `~` (bitwise NOT in constant expressions).
100    Tilde,
101    /// Left shift `<<`.
102    LShift,
103    /// Right shift `>>`.
104    RShift,
105    /// A newline (statement separator).
106    Newline,
107    /// End of input.
108    Eof,
109}
110
111/// Tokenize assembly source text into a vector of tokens, using the default
112/// (Intel / GNU-as) comment conventions.
113///
114/// Equivalent to [`tokenize_with_syntax`] with [`Syntax::Intel`]. Use
115/// [`tokenize_with_syntax`] when assembling ARM, Thumb or AArch64 so that `#`
116/// is read as the UAL immediate prefix rather than a comment.
117///
118/// # Errors
119///
120/// Returns `Err(AsmError::Syntax)` if the input contains an unrecognised
121/// character or a malformed token (e.g. an unterminated string literal).
122pub fn tokenize(source: &str) -> Result<Vec<Token<'_>>, AsmError> {
123    tokenize_with_syntax(source, Syntax::Intel)
124}
125
126/// Tokenize assembly source text into a vector of tokens.
127///
128/// The lexer recognizes:
129/// - Identifiers (mnemonics, registers, label references)
130/// - Numeric literals (decimal, hex `0x`, binary `0b`, octal `0o`)
131/// - String literals (`"..."`)
132/// - Character literals (`'A'`)
133/// - Directives (`.byte`, `.equ`, etc.)
134/// - Label definitions (`name:`)
135/// - Numeric labels (`1:`, `1b`, `1f`)
136/// - Punctuation: `,`, `[`, `]`, `+`, `-`, `*`, `:`
137/// - Comments: `//` to end of line, plus a dialect-dependent character
138/// - Newlines and semicolons as statement separators
139///
140/// # The `#` character
141///
142/// `#` is genuinely ambiguous across assembly dialects, so its meaning follows
143/// `syntax`, exactly as it does in GNU `as`:
144///
145/// - [`Syntax::Ual`] (ARM, Thumb, AArch64) — `#` prefixes an immediate, as in
146///   `mov r0, #1`. Line comments are introduced by `@` or `//`.
147/// - Every other dialect — `#` starts a line comment, as in `# note`.
148///
149/// # Errors
150///
151/// Returns `Err(AsmError::Syntax)` if the input contains an unrecognised
152/// character or a malformed token (e.g. an unterminated string literal).
153pub fn tokenize_with_syntax(source: &str, syntax: Syntax) -> Result<Vec<Token<'_>>, AsmError> {
154    let ual = syntax == Syntax::Ual;
155    // Heuristic: ~3 bytes of source per token for dense assembly. Capped so a
156    // large source does not reserve hundreds of megabytes of `Token` up front
157    // on the strength of a guess; `Vec` growth handles the rest.
158    const MAX_PREALLOC_TOKENS: usize = 64 * 1024;
159    let mut tokens = Vec::with_capacity(core::cmp::min(source.len() / 3 + 1, MAX_PREALLOC_TOKENS));
160    let bytes = source.as_bytes();
161    let len = bytes.len();
162    let mut pos = 0;
163    let mut line: u32 = 1;
164    let mut col: u32 = 1;
165    let mut line_start = 0usize;
166
167    while pos < len {
168        let ch = bytes[pos];
169
170        // Skip whitespace (but not newlines)
171        if ch == b' ' || ch == b'\t' || ch == b'\r' {
172            pos += 1;
173            col += 1;
174            continue;
175        }
176
177        // Newline
178        if ch == b'\n' {
179            tokens.push(Token {
180                kind: TokenKind::Newline,
181                text: Cow::Borrowed("\n"),
182                span: Span::new(line, col, pos, 1),
183            });
184            pos += 1;
185            line += 1;
186            col = 1;
187            line_start = pos;
188            continue;
189        }
190
191        // Semicolon as statement separator
192        if ch == b';' {
193            let start = pos;
194            tokens.push(Token {
195                kind: TokenKind::Newline,
196                text: Cow::Borrowed(";"),
197                span: Span::new(line, col, start, 1),
198            });
199            pos += 1;
200            col += 1;
201            continue;
202        }
203
204        // Line comments: `//` in every dialect, plus `@` in UAL (where `#` is
205        // taken by the immediate prefix) and `#` everywhere else.
206        let starts_comment = match ch {
207            b'/' => pos + 1 < len && bytes[pos + 1] == b'/',
208            b'@' => ual,
209            b'#' => !ual,
210            _ => false,
211        };
212        if starts_comment {
213            while pos < len && bytes[pos] != b'\n' {
214                pos += 1;
215            }
216            col = (pos - line_start) as u32 + 1;
217            continue;
218        }
219
220        // UAL immediate prefix: `mov r0, #1`, `add r0, r1, r2, lsl #3`.
221        //
222        // The prefix carries no information beyond "an immediate follows", and
223        // it can appear anywhere an immediate can — as a bare operand, as a
224        // memory displacement, as a shift amount. Rather than teach every one
225        // of those parse sites about it, validate it here and drop it, so the
226        // parser sees the same token stream either way. Requiring an
227        // immediate-like token after it keeps a stray `#` an error instead of
228        // silently vanishing.
229        if ch == b'#' {
230            let next = bytes.get(pos + 1).copied();
231            let immediate_follows = next.is_some_and(|c| {
232                c.is_ascii_digit()
233                    || c.is_ascii_alphabetic()
234                    || matches!(c, b'_' | b'-' | b'+' | b'\'' | b'(' | b'~')
235            });
236            if !immediate_follows {
237                return Err(AsmError::Syntax {
238                    msg: String::from("'#' must be followed by an immediate value"),
239                    span: Span::new(line, col, pos, 1),
240                });
241            }
242            pos += 1;
243            col += 1;
244            continue;
245        }
246
247        // Comma
248        if ch == b',' {
249            tokens.push(Token {
250                kind: TokenKind::Comma,
251                text: Cow::Borrowed(","),
252                span: Span::new(line, col, pos, 1),
253            });
254            pos += 1;
255            col += 1;
256            continue;
257        }
258
259        // Brackets
260        if ch == b'[' {
261            tokens.push(Token {
262                kind: TokenKind::OpenBracket,
263                text: Cow::Borrowed("["),
264                span: Span::new(line, col, pos, 1),
265            });
266            pos += 1;
267            col += 1;
268            continue;
269        }
270        if ch == b']' {
271            tokens.push(Token {
272                kind: TokenKind::CloseBracket,
273                text: Cow::Borrowed("]"),
274                span: Span::new(line, col, pos, 1),
275            });
276            pos += 1;
277            col += 1;
278            continue;
279        }
280
281        // Plus
282        if ch == b'+' {
283            tokens.push(Token {
284                kind: TokenKind::Plus,
285                text: Cow::Borrowed("+"),
286                span: Span::new(line, col, pos, 1),
287            });
288            pos += 1;
289            col += 1;
290            continue;
291        }
292
293        // Minus (standalone, not part of negative number if preceded by identifier or number)
294        if ch == b'-' {
295            // Check if this is a negative sign for a number
296            let is_unary = tokens.is_empty()
297                || matches!(
298                    tokens.last().map(|t| &t.kind),
299                    Some(
300                        TokenKind::Comma
301                            | TokenKind::OpenBracket
302                            | TokenKind::OpenBrace
303                            | TokenKind::Plus
304                            | TokenKind::Minus
305                            | TokenKind::Star
306                            | TokenKind::Newline
307                            | TokenKind::Equals
308                    )
309                );
310
311            if is_unary && pos + 1 < len && bytes[pos + 1].is_ascii_digit() {
312                // Parse as negative number
313                let start = pos;
314                let start_col = col;
315                pos += 1; // skip '-'
316                let value = parse_number_at(bytes, &mut pos, line, start_col)?;
317                let token_len = pos - start;
318                let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
319                tokens.push(Token {
320                    kind: TokenKind::Number(-value),
321                    text,
322                    span: Span::new(line, start_col, start, token_len),
323                });
324                col = (pos - line_start) as u32 + 1;
325                continue;
326            }
327
328            tokens.push(Token {
329                kind: TokenKind::Minus,
330                text: Cow::Borrowed("-"),
331                span: Span::new(line, col, pos, 1),
332            });
333            pos += 1;
334            col += 1;
335            continue;
336        }
337
338        // Star
339        if ch == b'*' {
340            tokens.push(Token {
341                kind: TokenKind::Star,
342                text: Cow::Borrowed("*"),
343                span: Span::new(line, col, pos, 1),
344            });
345            pos += 1;
346            col += 1;
347            continue;
348        }
349
350        // Colon (standalone, used for segment overrides)
351        if ch == b':' {
352            tokens.push(Token {
353                kind: TokenKind::Colon,
354                text: Cow::Borrowed(":"),
355                span: Span::new(line, col, pos, 1),
356            });
357            pos += 1;
358            col += 1;
359            continue;
360        }
361
362        // Equals sign (constant assignment)
363        if ch == b'=' {
364            tokens.push(Token {
365                kind: TokenKind::Equals,
366                text: Cow::Borrowed("="),
367                span: Span::new(line, col, pos, 1),
368            });
369            pos += 1;
370            col += 1;
371            continue;
372        }
373
374        // String literal
375        if ch == b'"' {
376            let start = pos;
377            let start_col = col;
378            pos += 1;
379            col += 1;
380            let mut content = Vec::new();
381            while pos < len && bytes[pos] != b'"' {
382                if bytes[pos] == b'\\' && pos + 1 < len {
383                    pos += 1;
384                    col += 1;
385                    match bytes[pos] {
386                        b'n' => content.push(b'\n'),
387                        b't' => content.push(b'\t'),
388                        b'\\' => content.push(b'\\'),
389                        b'"' => content.push(b'"'),
390                        b'0' => content.push(0),
391                        b'x' => {
392                            // \xHH
393                            if pos + 2 < len {
394                                let hi = hex_digit(bytes[pos + 1]);
395                                let lo = hex_digit(bytes[pos + 2]);
396                                if let (Some(h), Some(l)) = (hi, lo) {
397                                    content.push(h * 16 + l);
398                                    pos += 2;
399                                    col += 2;
400                                } else {
401                                    return Err(AsmError::Syntax {
402                                        msg: String::from("invalid \\xHH escape sequence"),
403                                        span: Span::new(line, col, pos, 3),
404                                    });
405                                }
406                            }
407                        }
408                        _ => {
409                            return Err(AsmError::Syntax {
410                                msg: alloc::format!(
411                                    "unknown escape sequence '\\{}'",
412                                    bytes[pos] as char
413                                ),
414                                span: Span::new(line, col, pos - 1, 2),
415                            });
416                        }
417                    }
418                } else if bytes[pos] == b'\n' {
419                    return Err(AsmError::Syntax {
420                        msg: String::from("unterminated string literal"),
421                        span: Span::new(line, start_col, start, pos - start),
422                    });
423                } else {
424                    content.push(bytes[pos]);
425                }
426                pos += 1;
427                col += 1;
428            }
429            if pos >= len {
430                return Err(AsmError::Syntax {
431                    msg: String::from("unterminated string literal"),
432                    span: Span::new(line, start_col, start, pos - start),
433                });
434            }
435            pos += 1; // skip closing quote
436            col += 1;
437            let text_str = Cow::Owned(String::from_utf8(content).unwrap_or_default());
438            tokens.push(Token {
439                kind: TokenKind::StringLit,
440                text: text_str,
441                span: Span::new(line, start_col, start, pos - start),
442            });
443            continue;
444        }
445
446        // Character literal
447        if ch == b'\'' {
448            let start = pos;
449            let start_col = col;
450            pos += 1;
451            col += 1;
452            if pos >= len {
453                return Err(AsmError::Syntax {
454                    msg: String::from("unterminated character literal"),
455                    span: Span::new(line, start_col, start, 1),
456                });
457            }
458            let ch_val = if bytes[pos] == b'\\' && pos + 1 < len {
459                pos += 1;
460                col += 1;
461                match bytes[pos] {
462                    b'n' => b'\n',
463                    b't' => b'\t',
464                    b'\\' => b'\\',
465                    b'\'' => b'\'',
466                    b'0' => 0,
467                    _ => {
468                        return Err(AsmError::Syntax {
469                            msg: "unknown escape in character literal".into(),
470                            span: Span::new(line, col, pos - 1, 2),
471                        });
472                    }
473                }
474            } else {
475                bytes[pos]
476            };
477            pos += 1;
478            col += 1;
479            if pos >= len || bytes[pos] != b'\'' {
480                return Err(AsmError::Syntax {
481                    msg: String::from("unterminated character literal"),
482                    span: Span::new(line, start_col, start, pos - start),
483                });
484            }
485            pos += 1;
486            col += 1;
487            tokens.push(Token {
488                kind: TokenKind::CharLit(ch_val),
489                text: Cow::Owned(alloc::format!("'{}'", ch_val as char)),
490                span: Span::new(line, start_col, start, pos - start),
491            });
492            continue;
493        }
494
495        // Directive (starts with '.')
496        if ch == b'.' {
497            let start = pos;
498            let start_col = col;
499            pos += 1;
500            col += 1;
501            while pos < len && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_') {
502                pos += 1;
503                col += 1;
504            }
505            let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
506            tokens.push(Token {
507                kind: TokenKind::Directive,
508                text,
509                span: Span::new(line, start_col, start, pos - start),
510            });
511            continue;
512        }
513
514        // Number
515        if ch.is_ascii_digit() {
516            let start = pos;
517            let start_col = col;
518
519            // Check for numeric label: digit(s) followed by `:`, `b`, or `f`
520            // but NOT hex prefix 0x, 0b (binary), 0o
521            let mut temp = pos;
522            while temp < len && bytes[temp].is_ascii_digit() {
523                temp += 1;
524            }
525            // Check for numeric label def: `1:`
526            // Only single-digit labels (0-9) are valid, matching GAS convention.
527            // Multi-digit numbers followed by `:` are rejected to avoid
528            // silent mismatch with references (which must be single-digit).
529            if temp < len && bytes[temp] == b':' && (temp + 1 >= len || bytes[temp + 1] != b':') {
530                // Must be all digits
531                let num_str = str::from_utf8(&bytes[start..temp]).unwrap_or("0");
532                if let Ok(n) = num_str.parse::<u32>() {
533                    if temp != start + 1 {
534                        return Err(AsmError::Syntax {
535                            msg: alloc::format!(
536                                "numeric labels must be a single digit (0-9), got `{}`",
537                                n
538                            ),
539                            span: Span::new(line, start_col, start, temp - start + 1),
540                        });
541                    }
542                    pos = temp + 1; // past the ':'
543                    col = (pos - line_start) as u32 + 1;
544                    tokens.push(Token {
545                        kind: TokenKind::NumericLabelDef(n),
546                        text: Cow::Owned(alloc::format!("{}:", n)),
547                        span: Span::new(line, start_col, start, pos - start),
548                    });
549                    continue;
550                }
551            }
552            // Check for numeric label ref: `1b` or `1f` (only single digit before b/f)
553            if temp < len && temp == start + 1 && (bytes[temp] == b'b' || bytes[temp] == b'f') {
554                // Make sure it's not '0b' (binary prefix) — '0b' followed by 0/1 is binary
555                let digit = bytes[start] - b'0';
556                let suffix = bytes[temp];
557                if !(digit == 0
558                    && suffix == b'b'
559                    && temp + 1 < len
560                    && (bytes[temp + 1] == b'0' || bytes[temp + 1] == b'1'))
561                {
562                    pos = temp + 1;
563                    col = (pos - line_start) as u32 + 1;
564                    let kind = if suffix == b'b' {
565                        TokenKind::NumericLabelBwd(digit as u32)
566                    } else {
567                        TokenKind::NumericLabelFwd(digit as u32)
568                    };
569                    tokens.push(Token {
570                        kind,
571                        text: Cow::Owned(alloc::format!("{}{}", digit, suffix as char)),
572                        span: Span::new(line, start_col, start, pos - start),
573                    });
574                    continue;
575                }
576            }
577
578            let value = parse_number_at(bytes, &mut pos, line, start_col)?;
579            let token_len = pos - start;
580            let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
581            tokens.push(Token {
582                kind: TokenKind::Number(value),
583                text,
584                span: Span::new(line, start_col, start, token_len),
585            });
586            col = (pos - line_start) as u32 + 1;
587            continue;
588        }
589
590        // Identifier or keyword (including register names)
591        if ch.is_ascii_alphabetic() || ch == b'_' {
592            let start = pos;
593            let start_col = col;
594            while pos < len
595                && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_' || bytes[pos] == b'.')
596            {
597                pos += 1;
598            }
599            let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
600            let token_len = pos - start;
601
602            // Check if followed by ':' → label definition
603            // But NOT if it's a segment register (cs, ds, es, fs, gs, ss)
604            if pos < len && bytes[pos] == b':' {
605                let is_segment_reg = text.eq_ignore_ascii_case("cs")
606                    || text.eq_ignore_ascii_case("ds")
607                    || text.eq_ignore_ascii_case("es")
608                    || text.eq_ignore_ascii_case("fs")
609                    || text.eq_ignore_ascii_case("gs")
610                    || text.eq_ignore_ascii_case("ss");
611                if is_segment_reg {
612                    // Emit as Ident; the ':' will be consumed next iteration
613                    tokens.push(Token {
614                        kind: TokenKind::Ident,
615                        text,
616                        span: Span::new(line, start_col, start, token_len),
617                    });
618                    col = (pos - line_start) as u32 + 1;
619                    continue;
620                }
621                pos += 1; // consume ':'
622                tokens.push(Token {
623                    kind: TokenKind::LabelDef,
624                    text,
625                    span: Span::new(line, start_col, start, pos - start),
626                });
627                col = (pos - line_start) as u32 + 1;
628                continue;
629            }
630
631            tokens.push(Token {
632                kind: TokenKind::Ident,
633                text,
634                span: Span::new(line, start_col, start, token_len),
635            });
636            col = (pos - line_start) as u32 + 1;
637            continue;
638        }
639
640        // Open brace (ARM register lists)
641        if ch == b'{' {
642            tokens.push(Token {
643                kind: TokenKind::OpenBrace,
644                text: Cow::Borrowed("{"),
645                span: Span::new(line, col, pos, 1),
646            });
647            pos += 1;
648            col += 1;
649            continue;
650        }
651
652        // Close brace (ARM register lists)
653        if ch == b'}' {
654            tokens.push(Token {
655                kind: TokenKind::CloseBrace,
656                text: Cow::Borrowed("}"),
657                span: Span::new(line, col, pos, 1),
658            });
659            pos += 1;
660            col += 1;
661            continue;
662        }
663
664        // Open parenthesis (RISC-V memory operands)
665        if ch == b'(' {
666            tokens.push(Token {
667                kind: TokenKind::OpenParen,
668                text: Cow::Borrowed("("),
669                span: Span::new(line, col, pos, 1),
670            });
671            pos += 1;
672            col += 1;
673            continue;
674        }
675
676        // Close parenthesis (RISC-V memory operands)
677        if ch == b')' {
678            tokens.push(Token {
679                kind: TokenKind::CloseParen,
680                text: Cow::Borrowed(")"),
681                span: Span::new(line, col, pos, 1),
682            });
683            pos += 1;
684            col += 1;
685            continue;
686        }
687
688        // Bang (ARM writeback)
689        if ch == b'!' {
690            tokens.push(Token {
691                kind: TokenKind::Bang,
692                text: Cow::Borrowed("!"),
693                span: Span::new(line, col, pos, 1),
694            });
695            pos += 1;
696            col += 1;
697            continue;
698        }
699
700        // Percent (AT&T register prefix)
701        if ch == b'%' {
702            tokens.push(Token {
703                kind: TokenKind::Percent,
704                text: Cow::Borrowed("%"),
705                span: Span::new(line, col, pos, 1),
706            });
707            pos += 1;
708            col += 1;
709            continue;
710        }
711
712        // Dollar (AT&T immediate prefix)
713        if ch == b'$' {
714            tokens.push(Token {
715                kind: TokenKind::Dollar,
716                text: Cow::Borrowed("$"),
717                span: Span::new(line, col, pos, 1),
718            });
719            pos += 1;
720            col += 1;
721            continue;
722        }
723
724        // Slash (SVE predicate qualifier)
725        if ch == b'/' {
726            // Check for C-style comments first
727            if pos + 1 < len && bytes[pos + 1] == b'/' {
728                // Line comment: skip to end of line
729                pos += 2;
730                while pos < len && bytes[pos] != b'\n' {
731                    pos += 1;
732                }
733                col = (pos - line_start) as u32 + 1;
734                continue;
735            }
736            if pos + 1 < len && bytes[pos + 1] == b'*' {
737                // Block comment: skip to matching */
738                let comment_start_line = line;
739                let comment_start_col = col;
740                let comment_start_pos = pos;
741                pos += 2;
742                col += 2;
743                while pos + 1 < len && !(bytes[pos] == b'*' && bytes[pos + 1] == b'/') {
744                    if bytes[pos] == b'\n' {
745                        line += 1;
746                        col = 1;
747                        line_start = pos + 1;
748                    } else {
749                        col += 1;
750                    }
751                    pos += 1;
752                }
753                if pos + 1 < len {
754                    pos += 2; // skip */
755                    col += 2;
756                } else {
757                    // Reached EOF without finding */
758                    return Err(AsmError::Syntax {
759                        msg: String::from("unterminated block comment"),
760                        span: Span::new(
761                            comment_start_line,
762                            comment_start_col,
763                            comment_start_pos,
764                            2,
765                        ),
766                    });
767                }
768                continue;
769            }
770            tokens.push(Token {
771                kind: TokenKind::Slash,
772                text: Cow::Borrowed("/"),
773                span: Span::new(line, col, pos, 1),
774            });
775            pos += 1;
776            col += 1;
777            continue;
778        }
779
780        // Ampersand (bitwise AND)
781        if ch == b'&' {
782            tokens.push(Token {
783                kind: TokenKind::Ampersand,
784                text: Cow::Borrowed("&"),
785                span: Span::new(line, col, pos, 1),
786            });
787            pos += 1;
788            col += 1;
789            continue;
790        }
791
792        // Pipe (bitwise OR)
793        if ch == b'|' {
794            tokens.push(Token {
795                kind: TokenKind::Pipe,
796                text: Cow::Borrowed("|"),
797                span: Span::new(line, col, pos, 1),
798            });
799            pos += 1;
800            col += 1;
801            continue;
802        }
803
804        // Caret (bitwise XOR)
805        if ch == b'^' {
806            tokens.push(Token {
807                kind: TokenKind::Caret,
808                text: Cow::Borrowed("^"),
809                span: Span::new(line, col, pos, 1),
810            });
811            pos += 1;
812            col += 1;
813            continue;
814        }
815
816        // Tilde (bitwise NOT)
817        if ch == b'~' {
818            tokens.push(Token {
819                kind: TokenKind::Tilde,
820                text: Cow::Borrowed("~"),
821                span: Span::new(line, col, pos, 1),
822            });
823            pos += 1;
824            col += 1;
825            continue;
826        }
827
828        // Shift operators << >>
829        if ch == b'<' && pos + 1 < len && bytes[pos + 1] == b'<' {
830            tokens.push(Token {
831                kind: TokenKind::LShift,
832                text: Cow::Borrowed("<<"),
833                span: Span::new(line, col, pos, 2),
834            });
835            pos += 2;
836            col += 2;
837            continue;
838        }
839        if ch == b'>' && pos + 1 < len && bytes[pos + 1] == b'>' {
840            tokens.push(Token {
841                kind: TokenKind::RShift,
842                text: Cow::Borrowed(">>"),
843                span: Span::new(line, col, pos, 2),
844            });
845            pos += 2;
846            col += 2;
847            continue;
848        }
849
850        // Unknown character
851        return Err(AsmError::Syntax {
852            msg: alloc::format!("unexpected character '{}'", ch as char),
853            span: Span::new(line, col, pos, 1),
854        });
855    }
856
857    tokens.push(Token {
858        kind: TokenKind::Eof,
859        text: Cow::Borrowed(""),
860        span: Span::new(line, col, pos, 0),
861    });
862
863    Ok(tokens)
864}
865
866/// Parse a number starting at `pos` in `bytes`. Advances `pos` past the number.
867#[inline]
868fn parse_number_at(
869    bytes: &[u8],
870    pos: &mut usize,
871    span_line: u32,
872    span_col: u32,
873) -> Result<i128, AsmError> {
874    let start = *pos;
875    let len = bytes.len();
876
877    if *pos >= len {
878        return Err(AsmError::Syntax {
879            msg: String::from("expected number"),
880            span: Span::new(span_line, span_col, start, 0),
881        });
882    }
883
884    // Check for hex, binary, octal prefix
885    if bytes[*pos] == b'0' && *pos + 1 < len {
886        match bytes[*pos + 1] {
887            b'x' | b'X' => {
888                *pos += 2;
889                let num_start = *pos;
890                while *pos < len && bytes[*pos].is_ascii_hexdigit() {
891                    *pos += 1;
892                }
893                if *pos == num_start {
894                    return Err(AsmError::Syntax {
895                        msg: String::from("expected hex digits after '0x'"),
896                        span: Span::new(span_line, span_col, start, *pos - start),
897                    });
898                }
899                let s = str::from_utf8(&bytes[num_start..*pos]).unwrap_or("0");
900                return i128::from_str_radix(s, 16).map_err(|_| AsmError::Syntax {
901                    msg: alloc::format!("invalid hex number '0x{}'", s),
902                    span: Span::new(span_line, span_col, start, *pos - start),
903                });
904            }
905            b'b' | b'B' => {
906                // Could be binary 0b prefix — check if next chars are 0 or 1
907                if *pos + 2 < len && (bytes[*pos + 2] == b'0' || bytes[*pos + 2] == b'1') {
908                    *pos += 2;
909                    let num_start = *pos;
910                    while *pos < len && (bytes[*pos] == b'0' || bytes[*pos] == b'1') {
911                        *pos += 1;
912                    }
913                    let s = str::from_utf8(&bytes[num_start..*pos]).unwrap_or("0");
914                    return i128::from_str_radix(s, 2).map_err(|_| AsmError::Syntax {
915                        msg: alloc::format!("invalid binary number '0b{}'", s),
916                        span: Span::new(span_line, span_col, start, *pos - start),
917                    });
918                }
919                // Otherwise, just '0' followed by 'b' which is not a binary prefix
920            }
921            b'o' | b'O' => {
922                *pos += 2;
923                let num_start = *pos;
924                while *pos < len && bytes[*pos] >= b'0' && bytes[*pos] <= b'7' {
925                    *pos += 1;
926                }
927                if *pos == num_start {
928                    return Err(AsmError::Syntax {
929                        msg: String::from("expected octal digits after '0o'"),
930                        span: Span::new(span_line, span_col, start, *pos - start),
931                    });
932                }
933                let s = str::from_utf8(&bytes[num_start..*pos]).unwrap_or("0");
934                return i128::from_str_radix(s, 8).map_err(|_| AsmError::Syntax {
935                    msg: alloc::format!("invalid octal number '0o{}'", s),
936                    span: Span::new(span_line, span_col, start, *pos - start),
937                });
938            }
939            _ => {}
940        }
941    }
942
943    // Decimal
944    while *pos < len && bytes[*pos].is_ascii_digit() {
945        *pos += 1;
946    }
947    // Check for hex suffix (e.g., 0FFh) — common in NASM/MASM
948    if *pos < len && (bytes[*pos] == b'h' || bytes[*pos] == b'H') {
949        let s = str::from_utf8(&bytes[start..*pos]).unwrap_or("0");
950        *pos += 1; // consume 'h'
951        return i128::from_str_radix(s, 16).map_err(|_| AsmError::Syntax {
952            msg: alloc::format!("invalid hex number '{}h'", s),
953            span: Span::new(span_line, span_col, start, *pos - start),
954        });
955    }
956    let s = str::from_utf8(&bytes[start..*pos]).unwrap_or("0");
957    s.parse::<i128>().map_err(|_| AsmError::Syntax {
958        msg: alloc::format!("invalid number '{}'", s),
959        span: Span::new(span_line, span_col, start, *pos - start),
960    })
961}
962
963#[inline]
964fn hex_digit(b: u8) -> Option<u8> {
965    match b {
966        b'0'..=b'9' => Some(b - b'0'),
967        b'a'..=b'f' => Some(b - b'a' + 10),
968        b'A'..=b'F' => Some(b - b'A' + 10),
969        _ => None,
970    }
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976
977    fn tok_kinds(src: &str) -> Vec<TokenKind> {
978        tokenize(src).unwrap().into_iter().map(|t| t.kind).collect()
979    }
980
981    #[allow(dead_code)]
982    fn tok_texts(src: &str) -> Vec<String> {
983        tokenize(src)
984            .unwrap()
985            .into_iter()
986            .map(|t| t.text.into_owned())
987            .collect()
988    }
989
990    #[test]
991    fn empty_input() {
992        let tokens = tokenize("").unwrap();
993        assert_eq!(tokens.len(), 1);
994        assert_eq!(tokens[0].kind, TokenKind::Eof);
995    }
996
997    #[test]
998    fn only_whitespace() {
999        let tokens = tokenize("   \t  ").unwrap();
1000        assert_eq!(tokens.len(), 1);
1001        assert_eq!(tokens[0].kind, TokenKind::Eof);
1002    }
1003
1004    #[test]
1005    fn only_comment() {
1006        // Hash is the comment marker; semicolons are statement separators
1007        let tokens = tokenize("# this is a comment").unwrap();
1008        assert_eq!(tokens.len(), 1);
1009        assert_eq!(tokens[0].kind, TokenKind::Eof);
1010    }
1011
1012    #[test]
1013    fn hash_comment() {
1014        let tokens = tokenize("# comment").unwrap();
1015        assert_eq!(tokens.len(), 1);
1016        assert_eq!(tokens[0].kind, TokenKind::Eof);
1017    }
1018
1019    #[test]
1020    fn simple_instruction() {
1021        let kinds = tok_kinds("mov rax, rbx");
1022        assert_eq!(
1023            kinds,
1024            vec![
1025                TokenKind::Ident, // mov
1026                TokenKind::Ident, // rax
1027                TokenKind::Comma,
1028                TokenKind::Ident, // rbx
1029                TokenKind::Eof,
1030            ]
1031        );
1032    }
1033
1034    #[test]
1035    fn instruction_with_immediate() {
1036        let tokens = tokenize("mov rax, 42").unwrap();
1037        assert_eq!(tokens[3].kind, TokenKind::Number(42));
1038    }
1039
1040    #[test]
1041    fn hex_immediate() {
1042        let tokens = tokenize("mov rax, 0xFF").unwrap();
1043        assert_eq!(tokens[3].kind, TokenKind::Number(255));
1044    }
1045
1046    #[test]
1047    fn hex_uppercase() {
1048        let tokens = tokenize("mov rax, 0XAB").unwrap();
1049        assert_eq!(tokens[3].kind, TokenKind::Number(0xAB));
1050    }
1051
1052    #[test]
1053    fn binary_immediate() {
1054        let tokens = tokenize("mov rax, 0b1010").unwrap();
1055        assert_eq!(tokens[3].kind, TokenKind::Number(10));
1056    }
1057
1058    #[test]
1059    fn octal_immediate() {
1060        let tokens = tokenize("mov rax, 0o77").unwrap();
1061        assert_eq!(tokens[3].kind, TokenKind::Number(63));
1062    }
1063
1064    #[test]
1065    fn negative_immediate() {
1066        let tokens = tokenize("mov rax, -1").unwrap();
1067        assert_eq!(tokens[3].kind, TokenKind::Number(-1));
1068    }
1069
1070    #[test]
1071    fn negative_hex() {
1072        let tokens = tokenize("add rsp, -0x10").unwrap();
1073        assert_eq!(tokens[3].kind, TokenKind::Number(-16));
1074    }
1075
1076    #[test]
1077    fn label_definition() {
1078        let tokens = tokenize("entry_point:").unwrap();
1079        assert_eq!(tokens[0].kind, TokenKind::LabelDef);
1080        assert_eq!(tokens[0].text, "entry_point");
1081    }
1082
1083    #[test]
1084    fn label_definition_with_instruction() {
1085        let kinds = tok_kinds("loop: dec rcx");
1086        assert_eq!(kinds[0], TokenKind::LabelDef);
1087        assert_eq!(kinds[1], TokenKind::Ident); // dec
1088        assert_eq!(kinds[2], TokenKind::Ident); // rcx
1089    }
1090
1091    #[test]
1092    fn numeric_label_def() {
1093        let tokens = tokenize("1:").unwrap();
1094        assert_eq!(tokens[0].kind, TokenKind::NumericLabelDef(1));
1095    }
1096
1097    #[test]
1098    fn numeric_label_backward_ref() {
1099        let tokens = tokenize("jnz 1b").unwrap();
1100        assert_eq!(tokens[1].kind, TokenKind::NumericLabelBwd(1));
1101    }
1102
1103    #[test]
1104    fn numeric_label_forward_ref() {
1105        let tokens = tokenize("jmp 2f").unwrap();
1106        assert_eq!(tokens[1].kind, TokenKind::NumericLabelFwd(2));
1107    }
1108
1109    #[test]
1110    fn directive() {
1111        let tokens = tokenize(".byte 0x90").unwrap();
1112        assert_eq!(tokens[0].kind, TokenKind::Directive);
1113        assert_eq!(tokens[0].text, ".byte");
1114        assert_eq!(tokens[1].kind, TokenKind::Number(0x90));
1115    }
1116
1117    #[test]
1118    fn equ_directive() {
1119        let tokens = tokenize(".equ SYS_WRITE, 1").unwrap();
1120        assert_eq!(tokens[0].kind, TokenKind::Directive);
1121        assert_eq!(tokens[0].text, ".equ");
1122        assert_eq!(tokens[1].kind, TokenKind::Ident);
1123        assert_eq!(tokens[1].text, "SYS_WRITE");
1124    }
1125
1126    #[test]
1127    fn memory_operand_tokens() {
1128        let kinds = tok_kinds("[rax + rbx*4 + 8]");
1129        assert_eq!(
1130            kinds,
1131            vec![
1132                TokenKind::OpenBracket,
1133                TokenKind::Ident, // rax
1134                TokenKind::Plus,
1135                TokenKind::Ident, // rbx
1136                TokenKind::Star,
1137                TokenKind::Number(4),
1138                TokenKind::Plus,
1139                TokenKind::Number(8),
1140                TokenKind::CloseBracket,
1141                TokenKind::Eof,
1142            ]
1143        );
1144    }
1145
1146    #[test]
1147    fn string_literal() {
1148        let tokens = tokenize(".asciz \"hello\"").unwrap();
1149        assert_eq!(tokens[1].kind, TokenKind::StringLit);
1150        assert_eq!(tokens[1].text, "hello");
1151    }
1152
1153    #[test]
1154    fn string_escape_sequences() {
1155        let tokens = tokenize(".ascii \"a\\nb\\t\\\\c\\0\\x41\"").unwrap();
1156        assert_eq!(tokens[1].kind, TokenKind::StringLit);
1157        assert_eq!(tokens[1].text, "a\nb\t\\c\0A");
1158    }
1159
1160    #[test]
1161    fn character_literal() {
1162        let tokens = tokenize("mov al, 'A'").unwrap();
1163        assert_eq!(tokens[3].kind, TokenKind::CharLit(b'A'));
1164    }
1165
1166    #[test]
1167    fn semicolon_separator() {
1168        let kinds = tok_kinds("nop; ret");
1169        assert_eq!(
1170            kinds,
1171            vec![
1172                TokenKind::Ident,   // nop
1173                TokenKind::Newline, // ;
1174                TokenKind::Ident,   // ret
1175                TokenKind::Eof,
1176            ]
1177        );
1178    }
1179
1180    #[test]
1181    fn newline_separator() {
1182        let kinds = tok_kinds("nop\nret");
1183        assert_eq!(
1184            kinds,
1185            vec![
1186                TokenKind::Ident, // nop
1187                TokenKind::Newline,
1188                TokenKind::Ident, // ret
1189                TokenKind::Eof,
1190            ]
1191        );
1192    }
1193
1194    #[test]
1195    fn segment_override_tokens() {
1196        let kinds = tok_kinds("fs:[rax]");
1197        assert_eq!(kinds[0], TokenKind::Ident); // fs
1198        assert_eq!(kinds[1], TokenKind::Colon);
1199        assert_eq!(kinds[2], TokenKind::OpenBracket);
1200        assert_eq!(kinds[3], TokenKind::Ident); // rax
1201        assert_eq!(kinds[4], TokenKind::CloseBracket);
1202    }
1203
1204    #[test]
1205    fn size_hint_tokens() {
1206        let kinds = tok_kinds("byte ptr [rax]");
1207        assert_eq!(kinds[0], TokenKind::Ident); // byte
1208        assert_eq!(kinds[1], TokenKind::Ident); // ptr
1209        assert_eq!(kinds[2], TokenKind::OpenBracket);
1210    }
1211
1212    #[test]
1213    fn prefix_and_instruction() {
1214        let kinds = tok_kinds("lock add [rax], 1");
1215        assert_eq!(kinds[0], TokenKind::Ident); // lock
1216        assert_eq!(kinds[1], TokenKind::Ident); // add
1217    }
1218
1219    #[test]
1220    fn span_tracking() {
1221        let tokens = tokenize("mov rax, 1").unwrap();
1222        assert_eq!(tokens[0].span, Span::new(1, 1, 0, 3)); // "mov"
1223        assert_eq!(tokens[1].span, Span::new(1, 5, 4, 3)); // "rax"
1224        assert_eq!(tokens[2].span, Span::new(1, 8, 7, 1)); // ","
1225    }
1226
1227    #[test]
1228    fn multiline_span_tracking() {
1229        let tokens = tokenize("nop\nmov rax, 1").unwrap();
1230        assert_eq!(tokens[0].span.line, 1); // nop
1231        assert_eq!(tokens[2].span.line, 2); // mov (after newline)
1232    }
1233
1234    #[test]
1235    fn unknown_character_error() {
1236        let err = tokenize("mov rax, @").unwrap_err();
1237        match err {
1238            AsmError::Syntax { msg, .. } => {
1239                assert!(msg.contains("unexpected character '@'"));
1240            }
1241            _ => panic!("expected Syntax error"),
1242        }
1243    }
1244
1245    #[test]
1246    fn unterminated_string() {
1247        let err = tokenize(".ascii \"hello").unwrap_err();
1248        match err {
1249            AsmError::Syntax { msg, .. } => {
1250                assert!(msg.contains("unterminated string"));
1251            }
1252            _ => panic!("expected Syntax error"),
1253        }
1254    }
1255
1256    #[test]
1257    fn unterminated_block_comment() {
1258        let err = tokenize("nop /* this is never closed").unwrap_err();
1259        match err {
1260            AsmError::Syntax { msg, span } => {
1261                assert!(
1262                    msg.contains("unterminated block comment"),
1263                    "expected 'unterminated block comment', got: {msg}"
1264                );
1265                // Span should point to the start of the comment, not (0,0)
1266                assert!(span.line > 0 || span.col > 0, "span should not be (0,0)");
1267            }
1268            _ => panic!("expected Syntax error"),
1269        }
1270    }
1271
1272    #[test]
1273    fn complex_instruction() {
1274        let tokens = tokenize("mov qword ptr [rbp - 0x10], rax").unwrap();
1275        let texts: Vec<_> = tokens.iter().map(|t| &*t.text).collect();
1276        assert_eq!(
1277            texts,
1278            vec!["mov", "qword", "ptr", "[", "rbp", "-", "0x10", "]", ",", "rax", ""]
1279        );
1280    }
1281
1282    #[test]
1283    fn all_punctuation() {
1284        let kinds = tok_kinds(", [ ] + - * :");
1285        assert_eq!(
1286            kinds,
1287            vec![
1288                TokenKind::Comma,
1289                TokenKind::OpenBracket,
1290                TokenKind::CloseBracket,
1291                TokenKind::Plus,
1292                TokenKind::Minus,
1293                TokenKind::Star,
1294                TokenKind::Colon,
1295                TokenKind::Eof,
1296            ]
1297        );
1298    }
1299
1300    #[test]
1301    fn trailing_whitespace() {
1302        let tokens = tokenize("nop   ").unwrap();
1303        assert_eq!(tokens.len(), 2); // nop + Eof
1304    }
1305
1306    #[test]
1307    fn zero_immediate() {
1308        let tokens = tokenize("xor eax, 0").unwrap();
1309        assert_eq!(tokens[3].kind, TokenKind::Number(0));
1310    }
1311
1312    #[test]
1313    fn large_hex_immediate() {
1314        let tokens = tokenize("mov rdi, 0x68732f2f6e69622f").unwrap();
1315        assert_eq!(tokens[3].kind, TokenKind::Number(0x68732f2f6e69622f));
1316    }
1317
1318    #[test]
1319    fn minus_in_memory_operand_is_not_unary() {
1320        // After identifier, '-' should be an operator, not unary
1321        let kinds = tok_kinds("[rbp - 0x10]");
1322        assert_eq!(
1323            kinds,
1324            vec![
1325                TokenKind::OpenBracket,
1326                TokenKind::Ident, // rbp
1327                TokenKind::Minus,
1328                TokenKind::Number(0x10),
1329                TokenKind::CloseBracket,
1330                TokenKind::Eof,
1331            ]
1332        );
1333    }
1334
1335    #[test]
1336    fn equals_token() {
1337        let kinds = tok_kinds("EXIT = 60");
1338        assert_eq!(
1339            kinds,
1340            vec![
1341                TokenKind::Ident, // EXIT
1342                TokenKind::Equals,
1343                TokenKind::Number(60),
1344                TokenKind::Eof,
1345            ]
1346        );
1347    }
1348
1349    #[test]
1350    fn equals_with_negative() {
1351        let kinds = tok_kinds("NEG = -1");
1352        assert_eq!(
1353            kinds,
1354            vec![
1355                TokenKind::Ident,
1356                TokenKind::Equals,
1357                TokenKind::Number(-1),
1358                TokenKind::Eof,
1359            ]
1360        );
1361    }
1362}