Skip to main content

llvmkit_asmparser/
ll_lexer.rs

1//! `.ll` lexer — direct port of LLVM's textual-IR lexer.
2//!
3//! Mirrors the union of
4//! `orig_cpp/llvm-project-llvmorg-22.1.4/llvm/include/llvm/AsmParser/LLLexer.h`
5//! and `orig_cpp/llvm-project-llvmorg-22.1.4/llvm/lib/AsmParser/LLLexer.cpp`.
6//! Token kinds live alongside in [`crate::ll_token`].
7//!
8//! The lexer borrows from a pre-loaded byte slice and yields
9//! [`Spanned<Token>`] values. Numeric semantics (APInt/APFloat decoding) and
10//! identifier-table lookups belong to the parser layer.
11
12use llvmkit_support::{Span, Spanned};
13
14use super::ll_token::{
15    FpLit, HexFpKind, IntLit, Keyword, NumBase, Opcode, PrimitiveTy, QuotedNameKind, Sign, Token,
16};
17
18// ─── Implementation-detail submodules ─────────────────────────────────────────
19//
20// These mirror static helpers defined inside `LLLexer.cpp`; they live in
21// `src/ll_lexer/` so this file stays focused on the state machine while still
22// presenting as a single conceptual module to outside crates.
23
24mod escape;
25mod keywords;
26
27// ─── Public constants ─────────────────────────────────────────────────────────
28
29/// Inclusive minimum width of an `iN` integer type. Mirrors
30/// `llvm::IntegerType::MIN_INT_BITS`.
31pub const INT_TY_MIN_BITS: u64 = 1;
32
33/// Inclusive maximum width of an `iN` integer type. Mirrors
34/// `llvm::IntegerType::MAX_INT_BITS`.
35pub const INT_TY_MAX_BITS: u64 = (1u64 << 24) - 1; // 16_777_215
36
37// ─── LexError ─────────────────────────────────────────────────────────────────
38//
39// The lexer does no I/O — it walks a pre-loaded slice — so I/O failures live
40// one layer up. Every variant here is a structural lexing failure carrying the
41// source [`Span`] of the offending lexeme.
42
43/// All lexer-level failures.
44#[derive(Clone, PartialEq, Eq, Hash, Debug, thiserror::Error)]
45pub enum LexError {
46    #[error("unterminated /* */ block comment")]
47    UnterminatedBlockComment { span: Span },
48
49    #[error("end of file in string constant")]
50    UnterminatedString { span: Span },
51
52    #[error("end of file in {kind:?} name")]
53    UnterminatedQuotedName { kind: QuotedNameKind, span: Span },
54
55    #[error("NUL character is not allowed in names")]
56    NulInName { span: Span },
57
58    #[error("invalid value number (does not fit in u32)")]
59    IdOverflow { span: Span },
60
61    #[error("constant bigger than 64 bits detected")]
62    IntegerOverflow64 { span: Span },
63
64    #[error("constant bigger than 128 bits detected")]
65    IntegerOverflow128 { span: Span },
66
67    #[error("bitwidth for integer type out of range (got {width}, must be 1..={max})")]
68    IntegerWidthOutOfRange { width: u64, max: u32, span: Span },
69
70    #[error("hexadecimal constant too large for {target:?} (16-bit)")]
71    HexFpTooLarge { target: HexFpKind, span: Span },
72
73    #[error("invalid token")]
74    UnknownToken { span: Span },
75
76    #[error("expected '*' after '/' to start block comment")]
77    StraySlash { span: Span },
78}
79
80impl LexError {
81    /// The span the diagnostic should highlight. Useful for callers that want
82    /// to render `(line, column)` without case-matching every variant.
83    pub fn span(&self) -> Span {
84        match self {
85            LexError::UnterminatedBlockComment { span }
86            | LexError::UnterminatedString { span }
87            | LexError::UnterminatedQuotedName { span, .. }
88            | LexError::NulInName { span }
89            | LexError::IdOverflow { span }
90            | LexError::IntegerOverflow64 { span }
91            | LexError::IntegerOverflow128 { span }
92            | LexError::IntegerWidthOutOfRange { span, .. }
93            | LexError::HexFpTooLarge { span, .. }
94            | LexError::UnknownToken { span }
95            | LexError::StraySlash { span } => *span,
96        }
97    }
98}
99
100// ─── Lexer ────────────────────────────────────────────────────────────────────
101
102/// A `.ll` lexer over a borrowed byte buffer.
103///
104/// `'src` is the lifetime of the underlying bytes. Tokens carrying string
105/// payloads (`LabelStr`, `GlobalVar`, …) borrow from `'src` whenever escape
106/// decoding is unnecessary; quoted forms with `\xx` escapes allocate.
107///
108/// `Clone` is provided to support cheap snapshot/lookahead patterns: a clone
109/// re-uses the same backing slice and copies just the cursor state.
110#[derive(Clone, Debug)]
111pub struct Lexer<'src> {
112    src: &'src [u8],
113    pos: usize,
114    tok_start: usize,
115    prev_tok_end: usize,
116    /// When `true`, a trailing `:` after an identifier is **not** consumed as
117    /// a label terminator. Mirrors `LLLexer::IgnoreColonInIdentifiers`.
118    pub ignore_colon_in_idents: bool,
119}
120
121impl<'src> Lexer<'src> {
122    /// Construct a lexer over a byte slice.
123    #[inline]
124    pub fn new(src: &'src [u8]) -> Self {
125        Self {
126            src,
127            pos: 0,
128            tok_start: 0,
129            prev_tok_end: 0,
130            ignore_colon_in_idents: false,
131        }
132    }
133
134    /// The full source buffer.
135    #[inline]
136    pub fn source(&self) -> &'src [u8] {
137        self.src
138    }
139
140    /// Current byte offset.
141    #[inline]
142    pub fn position(&self) -> u32 {
143        self.pos as u32
144    }
145
146    /// Exclusive end of the previous token. Mirrors `LLLexer::PrevTokEnd`.
147    #[inline]
148    pub fn prev_token_end(&self) -> u32 {
149        self.prev_tok_end as u32
150    }
151
152    // ── Cursor primitives ────────────────────────────────────────────────────
153
154    #[inline]
155    fn peek(&self) -> Option<u8> {
156        self.src.get(self.pos).copied()
157    }
158
159    #[inline]
160    fn peek_at(&self, n: usize) -> Option<u8> {
161        self.src.get(self.pos + n).copied()
162    }
163
164    #[inline]
165    fn bump(&mut self) -> Option<u8> {
166        let b = self.peek()?;
167        self.pos += 1;
168        Some(b)
169    }
170
171    #[inline]
172    fn span_from(&self, start: usize) -> Span {
173        Span::new(start as u32, self.pos as u32)
174    }
175
176    #[inline]
177    fn current_span(&self) -> Span {
178        self.span_from(self.tok_start)
179    }
180
181    #[inline]
182    fn spanned<T>(&self, value: T, span: Span) -> Spanned<T> {
183        Spanned::new(value, span)
184    }
185
186    // ── Public driver ────────────────────────────────────────────────────────
187
188    /// Produce the next token. EOF is returned as a real `Token::Eof`; calling
189    /// `next_token` after EOF returns more `Eof` tokens (mirroring `LLLexer`'s
190    /// "another call to lex will return EOF again" behavior at LLLexer.cpp:188).
191    pub fn next_token(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
192        loop {
193            // Per LLLexer::LexToken (LLLexer.cpp:194), PrevTokEnd is set at the
194            // start of each *attempt* to lex a token, before whitespace/comment
195            // skipping advances the cursor.
196            self.prev_tok_end = self.pos;
197            self.tok_start = self.pos;
198
199            let Some(c) = self.peek() else {
200                let span = Span::empty_at(self.pos as u32);
201                return Ok(self.spanned(Token::Eof, span));
202            };
203
204            match c {
205                // Whitespace + mid-buffer NUL (LLLexer.cpp:182).
206                b' ' | b'\t' | b'\n' | b'\r' | 0 => {
207                    self.bump();
208                    continue;
209                }
210                // Line comment.
211                b';' => {
212                    self.skip_line_comment();
213                    continue;
214                }
215                // Block comment.
216                b'/' => {
217                    self.bump();
218                    self.skip_block_comment()?;
219                    continue;
220                }
221                // Single-byte punctuation.
222                b'=' => {
223                    self.bump();
224                    return Ok(self.spanned(Token::Equal, self.current_span()));
225                }
226                b',' => {
227                    self.bump();
228                    return Ok(self.spanned(Token::Comma, self.current_span()));
229                }
230                b'*' => {
231                    self.bump();
232                    return Ok(self.spanned(Token::Star, self.current_span()));
233                }
234                b'[' => {
235                    self.bump();
236                    return Ok(self.spanned(Token::LSquare, self.current_span()));
237                }
238                b']' => {
239                    self.bump();
240                    return Ok(self.spanned(Token::RSquare, self.current_span()));
241                }
242                b'{' => {
243                    self.bump();
244                    return Ok(self.spanned(Token::LBrace, self.current_span()));
245                }
246                b'}' => {
247                    self.bump();
248                    return Ok(self.spanned(Token::RBrace, self.current_span()));
249                }
250                b'<' => {
251                    self.bump();
252                    return Ok(self.spanned(Token::Less, self.current_span()));
253                }
254                b'>' => {
255                    self.bump();
256                    return Ok(self.spanned(Token::Greater, self.current_span()));
257                }
258                b'(' => {
259                    self.bump();
260                    return Ok(self.spanned(Token::LParen, self.current_span()));
261                }
262                b')' => {
263                    self.bump();
264                    return Ok(self.spanned(Token::RParen, self.current_span()));
265                }
266                b'|' => {
267                    self.bump();
268                    return Ok(self.spanned(Token::Bar, self.current_span()));
269                }
270                b':' => {
271                    self.bump();
272                    return Ok(self.spanned(Token::Colon, self.current_span()));
273                }
274
275                // Prefix tokens.
276                b'+' => return self.lex_positive(),
277                b'-' | b'0'..=b'9' => return self.lex_digit_or_negative(),
278                b'@' => return self.lex_var(QuotedNameKind::Global),
279                b'%' => return self.lex_var(QuotedNameKind::Local),
280                b'$' => return self.lex_dollar(),
281                b'!' => return self.lex_exclaim(),
282                b'^' => return self.lex_caret(),
283                b'#' => return self.lex_hash(),
284                b'"' => return self.lex_quote(),
285                b'.' => return self.lex_dot(),
286
287                c if c.is_ascii_alphabetic() || c == b'_' => return self.lex_identifier(),
288
289                _ => {
290                    self.bump();
291                    return Err(LexError::UnknownToken {
292                        span: self.current_span(),
293                    });
294                }
295            }
296        }
297    }
298
299    // ── Comments ─────────────────────────────────────────────────────────────
300
301    /// Skip from the current `;` through the end of the line. Mirrors
302    /// `LLLexer::SkipLineComment` (LLLexer.cpp:265).
303    fn skip_line_comment(&mut self) {
304        while let Some(b) = self.peek() {
305            if b == b'\n' || b == b'\r' {
306                return;
307            }
308            self.bump();
309        }
310    }
311
312    /// Skip the body of a `/* … */` comment (the leading `/` was already
313    /// consumed). Mirrors `LLLexer::SkipCComment` (LLLexer.cpp:274).
314    fn skip_block_comment(&mut self) -> Result<(), LexError> {
315        // Expect '*' immediately after the '/'.
316        match self.bump() {
317            Some(b'*') => {}
318            _ => {
319                return Err(LexError::StraySlash {
320                    span: self.span_from(self.tok_start),
321                });
322            }
323        }
324        loop {
325            match self.bump() {
326                None => {
327                    return Err(LexError::UnterminatedBlockComment {
328                        span: self.span_from(self.tok_start),
329                    });
330                }
331                Some(b'*') => match self.peek() {
332                    Some(b'/') => {
333                        self.bump();
334                        return Ok(());
335                    }
336                    Some(_) => continue,
337                    None => {
338                        return Err(LexError::UnterminatedBlockComment {
339                            span: self.span_from(self.tok_start),
340                        });
341                    }
342                },
343                Some(_) => {}
344            }
345        }
346    }
347
348    // ── Identifier-prefix lexers ─────────────────────────────────────────────
349
350    /// Lex `@…` (global) or `%…` (local) tokens.
351    /// Mirrors `LLLexer::LexVar` (LLLexer.cpp:391).
352    fn lex_var(&mut self, kind: QuotedNameKind) -> Result<Spanned<Token<'src>>, LexError> {
353        self.bump(); // consume sigil
354
355        // Quoted form: @"…"
356        if self.peek() == Some(b'"') {
357            return self.lex_quoted_name(kind);
358        }
359
360        // Unquoted name: @[-a-zA-Z$._][-a-zA-Z$._0-9]*
361        let name_start = self.pos;
362        if let Some(b) = self.peek()
363            && is_unquoted_name_start(b)
364        {
365            self.bump();
366            while let Some(b) = self.peek() {
367                if is_unquoted_name_cont(b) {
368                    self.bump();
369                } else {
370                    break;
371                }
372            }
373            let name = &self.src[name_start..self.pos];
374            let token = match kind {
375                QuotedNameKind::Global => Token::GlobalVar(std::borrow::Cow::Borrowed(name)),
376                QuotedNameKind::Local => Token::LocalVar(std::borrow::Cow::Borrowed(name)),
377                QuotedNameKind::Comdat => Token::ComdatVar(std::borrow::Cow::Borrowed(name)),
378                QuotedNameKind::String | QuotedNameKind::Metadata => {
379                    unreachable!("lex_var handles only Global/Local/Comdat")
380                }
381            };
382            return Ok(self.spanned(token, self.current_span()));
383        }
384
385        // Numeric form: @[0-9]+
386        if matches!(self.peek(), Some(b'0'..=b'9')) {
387            let id = self.lex_uint()?;
388            let span = self.current_span();
389            let token = match kind {
390                QuotedNameKind::Global => Token::GlobalId(id),
391                QuotedNameKind::Local => Token::LocalVarId(id),
392                _ => unreachable!("only @42 / %42 reach this branch"),
393            };
394            return Ok(self.spanned(token, span));
395        }
396
397        Err(LexError::UnknownToken {
398            span: self.current_span(),
399        })
400    }
401
402    /// Lex `$…` — quoted comdat or label-tail starting with `$`.
403    /// Mirrors `LLLexer::LexDollar` (LLLexer.cpp:302).
404    fn lex_dollar(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
405        // Try label-tail rooted at TokStart (which is already at the `$`).
406        if let Some(end) = is_label_tail(self.src, self.pos) {
407            // end is the position *after* the trailing colon.
408            // Label content excludes the colon: [tok_start, end-1).
409            let content = &self.src[self.tok_start..end - 1];
410            self.pos = end;
411            return Ok(self.spanned(
412                Token::LabelStr(std::borrow::Cow::Borrowed(content)),
413                self.current_span(),
414            ));
415        }
416
417        // Consume the '$'.
418        self.bump();
419
420        // Quoted comdat: $"…"
421        if self.peek() == Some(b'"') {
422            return self.lex_quoted_name(QuotedNameKind::Comdat);
423        }
424
425        // Unquoted comdat: $[-a-zA-Z$._][-a-zA-Z$._0-9]*
426        let name_start = self.pos;
427        if let Some(b) = self.peek()
428            && is_unquoted_name_start(b)
429        {
430            self.bump();
431            while let Some(b) = self.peek() {
432                if is_unquoted_name_cont(b) {
433                    self.bump();
434                } else {
435                    break;
436                }
437            }
438            let name = &self.src[name_start..self.pos];
439            return Ok(self.spanned(
440                Token::ComdatVar(std::borrow::Cow::Borrowed(name)),
441                self.current_span(),
442            ));
443        }
444
445        Err(LexError::UnknownToken {
446            span: self.current_span(),
447        })
448    }
449
450    /// Lex `!…`. Either `!` alone (`Exclaim`) or `!name` (`MetadataVar`).
451    /// Mirrors `LLLexer::LexExclaim` (LLLexer.cpp:455).
452    fn lex_exclaim(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
453        self.bump(); // consume '!'
454
455        let name_start = self.pos;
456        let Some(first) = self.peek() else {
457            return Ok(self.spanned(Token::Exclaim, self.current_span()));
458        };
459        if !is_metadata_name_start(first) {
460            return Ok(self.spanned(Token::Exclaim, self.current_span()));
461        }
462        self.bump();
463        while let Some(b) = self.peek() {
464            if is_metadata_name_cont(b) {
465                self.bump();
466            } else {
467                break;
468            }
469        }
470        let raw = &self.src[name_start..self.pos];
471        let decoded = escape::unescape(raw);
472        Ok(self.spanned(Token::MetadataVar(decoded), self.current_span()))
473    }
474
475    /// Lex `^[0-9]+` summary id. Mirrors `LLLexer::LexCaret` (LLLexer.cpp:475).
476    fn lex_caret(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
477        self.bump(); // '^'
478        if !matches!(self.peek(), Some(b'0'..=b'9')) {
479            return Err(LexError::UnknownToken {
480                span: self.current_span(),
481            });
482        }
483        let id = self.lex_uint()?;
484        Ok(self.spanned(Token::SummaryId(id), self.current_span()))
485    }
486
487    /// Lex `#…` — either `#42` (AttrGrpId) or `#` alone (Hash).
488    /// Mirrors `LLLexer::LexHash` (LLLexer.cpp:483).
489    fn lex_hash(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
490        self.bump(); // '#'
491        if matches!(self.peek(), Some(b'0'..=b'9')) {
492            let id = self.lex_uint()?;
493            return Ok(self.spanned(Token::AttrGrpId(id), self.current_span()));
494        }
495        Ok(self.spanned(Token::Hash, self.current_span()))
496    }
497
498    /// Lex a `"…"` token. Either a string constant or, if followed by `:`,
499    /// a quoted label.  Mirrors `LLLexer::LexQuote` (LLLexer.cpp:434).
500    fn lex_quote(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
501        let raw = self.read_quoted_payload(QuotedNameKind::String)?;
502        let decoded = escape::unescape(raw);
503
504        // Check trailing ':' — that turns this into a label.
505        if self.peek() == Some(b':') {
506            self.bump();
507            if decoded.contains(&0) {
508                return Err(LexError::NulInName {
509                    span: self.current_span(),
510                });
511            }
512            return Ok(self.spanned(Token::LabelStr(decoded), self.current_span()));
513        }
514
515        // Plain string constant: NUL bytes are *allowed*.
516        Ok(self.spanned(Token::StringConstant(decoded), self.current_span()))
517    }
518
519    /// Lex a quoted name (@"x", %"x", $"x"). The leading sigil is already
520    /// consumed; the cursor is currently on the opening `"`.
521    fn lex_quoted_name(&mut self, kind: QuotedNameKind) -> Result<Spanned<Token<'src>>, LexError> {
522        let raw = self.read_quoted_payload(kind)?;
523        let decoded = escape::unescape(raw);
524        if decoded.contains(&0) {
525            return Err(LexError::NulInName {
526                span: self.current_span(),
527            });
528        }
529        let token = match kind {
530            QuotedNameKind::Global => Token::GlobalVar(decoded),
531            QuotedNameKind::Local => Token::LocalVar(decoded),
532            QuotedNameKind::Comdat => Token::ComdatVar(decoded),
533            QuotedNameKind::Metadata | QuotedNameKind::String => {
534                unreachable!("metadata and string use direct paths")
535            }
536        };
537        Ok(self.spanned(token, self.current_span()))
538    }
539
540    /// Consume `"…"` and return the **raw** byte slice of the payload (no
541    /// escape decoding). Cursor ends just past the closing `"`. The opening
542    /// `"` must be at `self.pos`.
543    fn read_quoted_payload(&mut self, kind: QuotedNameKind) -> Result<&'src [u8], LexError> {
544        debug_assert_eq!(self.peek(), Some(b'"'));
545        self.bump(); // opening '"'
546        let start = self.pos;
547        loop {
548            match self.bump() {
549                None => {
550                    let span = self.current_span();
551                    return Err(match kind {
552                        QuotedNameKind::String => LexError::UnterminatedString { span },
553                        _ => LexError::UnterminatedQuotedName { kind, span },
554                    });
555                }
556                Some(b'"') => {
557                    // self.pos is now one past the closing '"'.
558                    return Ok(&self.src[start..self.pos - 1]);
559                }
560                _ => {}
561            }
562        }
563    }
564
565    // ── Numbers / labels ─────────────────────────────────────────────────────
566
567    /// Consume `[0-9]+` and return as `u32`, erroring if the value overflows.
568    /// Used for `@42`, `%42`, `^42`, `#42`, and numeric labels. The cursor
569    /// must already be on the first digit (i.e. caller didn't consume it).
570    fn lex_uint(&mut self) -> Result<u32, LexError> {
571        let digits_start = self.pos;
572        while matches!(self.peek(), Some(b'0'..=b'9')) {
573            self.bump();
574        }
575        // The lexer guarantees a non-empty ASCII digit run, so `parse` only
576        // fails on overflow (`PosOverflow`). Collapse to one user-facing error.
577        let digits = ascii_str(&self.src[digits_start..self.pos]);
578        digits.parse::<u32>().map_err(|_| LexError::IdOverflow {
579            span: self.span_from(self.tok_start),
580        })
581    }
582
583    /// Lex `.` — either `...` punctuation or a `.` -prefixed label.
584    /// Mirrors LLLexer.cpp:219.
585    fn lex_dot(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
586        // Look-ahead label tail starting at the '.'.
587        if let Some(end) = is_label_tail(self.src, self.pos + 1) {
588            let content = &self.src[self.pos..end - 1];
589            self.pos = end;
590            return Ok(self.spanned(
591                Token::LabelStr(std::borrow::Cow::Borrowed(content)),
592                self.current_span(),
593            ));
594        }
595        // ...
596        if self.peek_at(0) == Some(b'.')
597            && self.peek_at(1) == Some(b'.')
598            && self.peek_at(2) == Some(b'.')
599        {
600            self.pos += 3;
601            return Ok(self.spanned(Token::DotDotDot, self.current_span()));
602        }
603        // Bare '.' isn't a token in LLVM IR.
604        self.bump();
605        Err(LexError::UnknownToken {
606            span: self.current_span(),
607        })
608    }
609
610    /// `[0-9]+` or `-[0-9…]` or numeric label. Mirrors
611    /// `LLLexer::LexDigitOrNegative` (LLLexer.cpp:1163).
612    fn lex_digit_or_negative(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
613        let first = self.peek().expect("caller verified non-empty");
614
615        // If '-' is followed by a non-digit, this is a label-tail.
616        if first == b'-' && !matches!(self.peek_at(1), Some(b'0'..=b'9')) {
617            self.bump(); // '-'
618            // isLabelTail rooted at the byte right after '-'.
619            if let Some(end) = is_label_tail(self.src, self.pos) {
620                let content = &self.src[self.tok_start..end - 1];
621                self.pos = end;
622                return Ok(self.spanned(
623                    Token::LabelStr(std::borrow::Cow::Borrowed(content)),
624                    self.current_span(),
625                ));
626            }
627            return Err(LexError::UnknownToken {
628                span: self.current_span(),
629            });
630        }
631
632        // Either '-' followed by digits, or already a digit.
633        let sign = if first == b'-' {
634            self.bump();
635            Sign::Neg
636        } else {
637            Sign::Pos
638        };
639        let digits_start = self.pos;
640        while matches!(self.peek(), Some(b'0'..=b'9')) {
641            self.bump();
642        }
643
644        // Numeric label: digits then ':'. (Only allowed if we started with a digit.)
645        if matches!(sign, Sign::Pos) && self.peek() == Some(b':') {
646            // We started with a digit (since sign == Pos). The leading char is
647            // included in the label slice.
648            let content = &self.src[self.tok_start..self.pos];
649            self.bump(); // ':'
650            return Ok(self.spanned(
651                Token::LabelStr(std::borrow::Cow::Borrowed(content)),
652                self.current_span(),
653            ));
654        }
655
656        // String label, e.g. `-1.foo:` or `1bb:`.
657        if matches!(self.peek(), Some(c) if is_label_char(c) || c == b':')
658            && let Some(end) = is_label_tail(self.src, self.pos)
659        {
660            let content = &self.src[self.tok_start..end - 1];
661            self.pos = end;
662            return Ok(self.spanned(
663                Token::LabelStr(std::borrow::Cow::Borrowed(content)),
664                self.current_span(),
665            ));
666        }
667
668        // FP if next char is '.', else int. The 0x… prefix branches into Lex0x.
669        if self.peek() != Some(b'.') {
670            // Hex FP `0x…`: only valid for digit-start (sign==Pos).
671            if matches!(sign, Sign::Pos)
672                && self.src.get(self.tok_start) == Some(&b'0')
673                && self.src.get(self.tok_start + 1) == Some(&b'x')
674            {
675                return self.lex_hex_fp();
676            }
677
678            let digits = ascii_str(&self.src[digits_start..self.pos]);
679            return Ok(self.spanned(
680                Token::IntegerLit(IntLit {
681                    sign,
682                    base: NumBase::Dec,
683                    digits,
684                }),
685                self.current_span(),
686            ));
687        }
688
689        self.consume_fp_tail()?;
690        let lex = ascii_str(&self.src[self.tok_start..self.pos]);
691        Ok(self.spanned(Token::FloatLit(FpLit::Decimal(lex)), self.current_span()))
692    }
693
694    /// Lex `+1.5` style positive FP literal. Mirrors LLLexer.cpp:1232.
695    fn lex_positive(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
696        self.bump(); // '+'
697        if !matches!(self.peek(), Some(b'0'..=b'9')) {
698            return Err(LexError::UnknownToken {
699                span: self.current_span(),
700            });
701        }
702        // Skip integer part.
703        while matches!(self.peek(), Some(b'0'..=b'9')) {
704            self.bump();
705        }
706        // Must have a '.'.
707        if self.peek() != Some(b'.') {
708            return Err(LexError::UnknownToken {
709                span: self.current_span(),
710            });
711        }
712        self.consume_fp_tail()?;
713        let lex = ascii_str(&self.src[self.tok_start..self.pos]);
714        Ok(self.spanned(Token::FloatLit(FpLit::Decimal(lex)), self.current_span()))
715    }
716
717    /// Consume `\.[0-9]*([eE][-+]?[0-9]+)?`. Cursor is on the `.`. Returns Ok
718    /// regardless of whether the exponent is present.
719    fn consume_fp_tail(&mut self) -> Result<(), LexError> {
720        debug_assert_eq!(self.peek(), Some(b'.'));
721        self.bump(); // '.'
722        while matches!(self.peek(), Some(b'0'..=b'9')) {
723            self.bump();
724        }
725        // Optional exponent.
726        if matches!(self.peek(), Some(b'e' | b'E')) {
727            // Lookahead: digit, or (+/-) then digit.
728            let after_e = self.peek_at(1);
729            let after_sign = self.peek_at(2);
730            let has_exp = matches!(after_e, Some(b'0'..=b'9'))
731                || (matches!(after_e, Some(b'+' | b'-'))
732                    && matches!(after_sign, Some(b'0'..=b'9')));
733            if has_exp {
734                self.bump(); // 'e' or 'E'
735                if matches!(self.peek(), Some(b'+' | b'-')) {
736                    self.bump();
737                }
738                while matches!(self.peek(), Some(b'0'..=b'9')) {
739                    self.bump();
740                }
741            }
742        }
743        Ok(())
744    }
745
746    /// Lex `0x[KLMHR]?[0-9A-Fa-f]+` (the `0x` is already at `self.tok_start`).
747    /// Mirrors `LLLexer::Lex0x` (LLLexer.cpp:1085).
748    fn lex_hex_fp(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
749        // Cursor sits just past the digit run we already consumed in
750        // `lex_digit_or_negative`. Re-park it after the `0x` prefix and reparse.
751        self.pos = self.tok_start + 2;
752
753        let kind = match self.peek() {
754            Some(b @ (b'K' | b'L' | b'M' | b'H' | b'R')) => {
755                self.bump();
756                b
757            }
758            _ => b'J', // plain hex double
759        };
760
761        let digits_start = self.pos;
762        if !matches!(self.peek(), Some(c) if c.is_ascii_hexdigit()) {
763            // Bad token; LLLexer.cpp:1097-1100 rewinds to TokStart+1 and
764            // returns an error. Mirror it.
765            self.pos = self.tok_start + 1;
766            return Err(LexError::UnknownToken {
767                span: self.current_span(),
768            });
769        }
770        while matches!(self.peek(), Some(c) if c.is_ascii_hexdigit()) {
771            self.bump();
772        }
773        let digits = ascii_str(&self.src[digits_start..self.pos]);
774
775        let token = match kind {
776            b'J' => Token::FloatLit(FpLit::HexDouble(digits)),
777            b'K' => Token::FloatLit(FpLit::HexX87(digits)),
778            b'L' => Token::FloatLit(FpLit::HexQuad(digits)),
779            b'M' => Token::FloatLit(FpLit::HexPpc128(digits)),
780            b'H' => {
781                self.check_hex16_fits(digits, HexFpKind::Half)?;
782                Token::FloatLit(FpLit::HexHalf(digits))
783            }
784            b'R' => {
785                self.check_hex16_fits(digits, HexFpKind::BFloat)?;
786                Token::FloatLit(FpLit::HexBFloat(digits))
787            }
788            _ => unreachable!(),
789        };
790        Ok(self.spanned(token, self.current_span()))
791    }
792
793    /// Reject `0xH` / `0xR` constants whose payload doesn't fit in 16 bits.
794    /// Mirrors `llvm::isUInt<16>(Val)` (LLLexer.cpp:1134, :1144).
795    fn check_hex16_fits(&self, digits: &str, target: HexFpKind) -> Result<(), LexError> {
796        u16::from_str_radix(digits, 16)
797            .map(|_| ())
798            .map_err(|_| LexError::HexFpTooLarge {
799                target,
800                span: self.current_span(),
801            })
802    }
803
804    // ── Identifier / keyword path ────────────────────────────────────────────
805
806    /// Lex a label, integer type, or keyword starting with `[a-zA-Z_]`.
807    /// Mirrors `LLLexer::LexIdentifier` (LLLexer.cpp:495).
808    fn lex_identifier(&mut self) -> Result<Spanned<Token<'src>>, LexError> {
809        let first = self.bump().expect("caller verified non-empty");
810
811        // Walk the label-char run, recording the first non-keyword-char position.
812        // (Keyword chars are alnum + `_`; label chars also include `-`, `$`, `.`.)
813        // Mirrors LLLexer.cpp:500-507.
814        let mut keyword_end: Option<usize> = None;
815        while let Some(b) = self.peek() {
816            if !is_label_char(b) {
817                break;
818            }
819            if keyword_end.is_none() && !b.is_ascii_alphanumeric() && b != b'_' {
820                keyword_end = Some(self.pos);
821            }
822            self.bump();
823        }
824
825        // Label terminator unless we've been told to ignore it (LLLexer.cpp:511).
826        if !self.ignore_colon_in_idents && self.peek() == Some(b':') {
827            let content = &self.src[self.tok_start..self.pos];
828            self.bump(); // ':'
829            return Ok(self.spanned(
830                Token::LabelStr(std::borrow::Cow::Borrowed(content)),
831                self.current_span(),
832            ));
833        }
834
835        // Integer type `iN` — only meaningful when the leading byte is `i`
836        // followed by at least one digit (LLLexer.cpp:518-528).
837        if first == b'i' {
838            let mut int_end = self.tok_start + 1;
839            while int_end < self.pos && self.src[int_end].is_ascii_digit() {
840                int_end += 1;
841            }
842            if int_end > self.tok_start + 1 {
843                self.pos = int_end;
844                let digits = ascii_str(&self.src[self.tok_start + 1..self.pos]);
845                // `digits` is a non-empty ASCII digit run, so `parse` only
846                // fails with `PosOverflow`. Treat that as "way out of range";
847                // `u64::MAX` makes the range check below fail uniformly.
848                let width = digits.parse::<u64>().unwrap_or(u64::MAX);
849                if !(INT_TY_MIN_BITS..=INT_TY_MAX_BITS).contains(&width) {
850                    return Err(LexError::IntegerWidthOutOfRange {
851                        width,
852                        max: INT_TY_MAX_BITS as u32,
853                        span: self.current_span(),
854                    });
855                }
856                // `width` is now in `1..=16_777_215`, so both the `u32` cast
857                // and `NonZeroU32::new` are guaranteed to succeed.
858                let nz = std::num::NonZeroU32::new(width as u32)
859                    .expect("width validated within 1..=INT_TY_MAX_BITS");
860                return Ok(self.spanned(
861                    Token::PrimitiveType(PrimitiveTy::Integer(nz)),
862                    self.current_span(),
863                ));
864            }
865        }
866
867        // Truncate to keyword_end (the first non-keyword char) so the run we
868        // look up only contains alphanumerics and underscores. Cursor is
869        // rewound — any `-`/`$`/`.` tail becomes the next token.
870        let kw_end = keyword_end.unwrap_or(self.pos);
871        self.pos = kw_end;
872        let word = &self.src[self.tok_start..self.pos];
873        let span = self.current_span();
874
875        // DWARF / DI / CSK / dbg / EmissionKind / NameTableKind / FixedPointKind.
876        if let Some(tok) = self.classify_prefixed(word) {
877            return Ok(self.spanned(tok, span));
878        }
879
880        // Plain keywords.
881        if let Some(tok) = keywords::classify_word(word) {
882            return Ok(self.spanned(tok, span));
883        }
884
885        // [us]0x[hex]+ APSInt.
886        if let Some(tok) = self.classify_hex_apsint(word)? {
887            return Ok(self.spanned(tok, span));
888        }
889
890        // cc<digits> rewind: emit `kw_cc` and rewind cursor to just past `cc`.
891        if word.len() > 2 && word.starts_with(b"cc") && word[2..].iter().all(|c| c.is_ascii_digit())
892        {
893            self.pos = self.tok_start + 2;
894            return Ok(self.spanned(
895                Token::Kw(Keyword::Cc),
896                Span::new(self.tok_start as u32, self.pos as u32),
897            ));
898        }
899
900        // Truly unknown — rewind to a single byte and emit error (LLLexer.cpp:1073).
901        self.pos = self.tok_start + 1;
902        Err(LexError::UnknownToken {
903            span: self.current_span(),
904        })
905    }
906
907    /// Classify words with structured prefixes: `DW_..._`, `DIFlag`, `DISPFlag`,
908    /// `CSK_`, `dbg_`, plus the small fixed-set tokens for emission kind, etc.
909    fn classify_prefixed(&self, word: &'src [u8]) -> Option<Token<'src>> {
910        // Interpret word as ASCII (lexer regex guarantees this).
911        let s = ascii_str(word);
912
913        // `dbg_<suffix>` — store only the suffix.
914        if let Some(suffix) = s.strip_prefix("dbg_") {
915            return match suffix {
916                "value" | "declare" | "assign" | "label" | "declare_value" => {
917                    let suffix_str = ascii_str(&word[4..]);
918                    Some(Token::DbgRecordType(suffix_str))
919                }
920                _ => None,
921            };
922        }
923
924        // DW_<TYPE>_… — payload is the full keyword.
925        if let Some(rest) = s.strip_prefix("DW_") {
926            let token = match rest.split_once('_').map(|(t, _)| t) {
927                Some("TAG") => Token::DwarfTag(s),
928                Some("ATE") => Token::DwarfAttEncoding(s),
929                Some("VIRTUALITY") => Token::DwarfVirtuality(s),
930                Some("LANG") => Token::DwarfLang(s),
931                Some("LNAME") => Token::DwarfSourceLangName(s),
932                Some("CC") => Token::DwarfCC(s),
933                Some("OP") => Token::DwarfOp(s),
934                Some("MACINFO") => Token::DwarfMacinfo(s),
935                Some("APPLE") if rest.starts_with("APPLE_ENUM_KIND_") => Token::DwarfEnumKind(s),
936                _ => return None,
937            };
938            return Some(token);
939        }
940
941        if matches!(
942            s,
943            "DIFile"
944                | "DICompileUnit"
945                | "DISubprogram"
946                | "DILocation"
947                | "DILocalVariable"
948                | "DIBasicType"
949                | "DIDerivedType"
950                | "DICompositeType"
951                | "DISubrange"
952                | "DINamespace"
953                | "DIExpression"
954                | "DIGlobalVariable"
955                | "DIGlobalVariableExpression"
956                | "DISubroutineType"
957                | "DIEnumerator"
958                | "DIModule"
959                | "DITemplateTypeParameter"
960                | "DITemplateValueParameter"
961        ) {
962            return Some(Token::SpecializedMetadata(s));
963        }
964
965        if s.starts_with("DIFlag") {
966            return Some(Token::DiFlag(s));
967        }
968        if s.starts_with("DISPFlag") {
969            return Some(Token::DiSpFlag(s));
970        }
971        if s.starts_with("CSK_") {
972            return Some(Token::ChecksumKind(s));
973        }
974
975        match s {
976            "NoDebug" | "FullDebug" | "LineTablesOnly" | "DebugDirectivesOnly" => {
977                Some(Token::EmissionKind(s))
978            }
979            "GNU" | "Apple" | "None" | "Default" => Some(Token::NameTableKind(s)),
980            "Binary" | "Decimal" | "Rational" => Some(Token::FixedPointKind(s)),
981            _ => None,
982        }
983    }
984
985    /// `[us]0x[0-9A-Fa-f]+` — emit an `IntegerLit` flagged as
986    /// hex-signed/unsigned. Returns Ok(None) when the word doesn't fit the
987    /// pattern; returns Ok(Some) on a match.
988    fn classify_hex_apsint(&self, word: &'src [u8]) -> Result<Option<Token<'src>>, LexError> {
989        if word.len() < 4 {
990            return Ok(None);
991        }
992        let base = match word[0] {
993            b's' => NumBase::HexSigned,
994            b'u' => NumBase::HexUnsigned,
995            _ => return Ok(None),
996        };
997        if word[1] != b'0' || word[2] != b'x' {
998            return Ok(None);
999        }
1000        if !word[3..].iter().all(|c| c.is_ascii_hexdigit()) {
1001            return Ok(None);
1002        }
1003        let digits = ascii_str(&word[3..]);
1004        Ok(Some(Token::IntegerLit(IntLit {
1005            sign: Sign::Pos,
1006            base,
1007            digits,
1008        })))
1009    }
1010}
1011
1012// ─── Iterator ─────────────────────────────────────────────────────────────────
1013
1014// ─── Conversions ──────────────────────────────────────────────────────────────
1015
1016impl<'src> From<&'src [u8]> for Lexer<'src> {
1017    #[inline]
1018    fn from(src: &'src [u8]) -> Self {
1019        Self::new(src)
1020    }
1021}
1022
1023impl<'src> From<&'src str> for Lexer<'src> {
1024    #[inline]
1025    fn from(src: &'src str) -> Self {
1026        Self::new(src.as_bytes())
1027    }
1028}
1029
1030impl<'src> Iterator for Lexer<'src> {
1031    type Item = Result<Spanned<Token<'src>>, LexError>;
1032
1033    fn next(&mut self) -> Option<Self::Item> {
1034        match self.next_token() {
1035            Ok(spanned) if matches!(spanned.value, Token::Eof) => None,
1036            other => Some(other),
1037        }
1038    }
1039}
1040
1041// ─── Free helpers (LLLexer.cpp top-level statics) ─────────────────────────────
1042
1043/// `[-a-zA-Z$._0-9]`. Mirrors `isLabelChar` (LLLexer.cpp:153).
1044#[inline]
1045fn is_label_char(b: u8) -> bool {
1046    b.is_ascii_alphanumeric() || b == b'-' || b == b'$' || b == b'.' || b == b'_'
1047}
1048
1049/// `[-a-zA-Z$._]` — first character of an unquoted name.
1050#[inline]
1051fn is_unquoted_name_start(b: u8) -> bool {
1052    b.is_ascii_alphabetic() || b == b'-' || b == b'$' || b == b'.' || b == b'_'
1053}
1054
1055/// `[-a-zA-Z$._0-9]` — continuation of an unquoted name.
1056#[inline]
1057fn is_unquoted_name_cont(b: u8) -> bool {
1058    is_label_char(b)
1059}
1060
1061/// `[-a-zA-Z$._\\]` — first character of a metadata name (escape allowed).
1062#[inline]
1063fn is_metadata_name_start(b: u8) -> bool {
1064    is_unquoted_name_start(b) || b == b'\\'
1065}
1066
1067/// `[-a-zA-Z$._0-9\\]` — continuation of a metadata name.
1068#[inline]
1069fn is_metadata_name_cont(b: u8) -> bool {
1070    is_label_char(b) || b == b'\\'
1071}
1072
1073/// Treat `bytes` as ASCII text. The lexer's regex guarantees this; the
1074/// `expect` is intentionally retained as a tripwire (not unsafe).
1075#[inline]
1076fn ascii_str(bytes: &[u8]) -> &str {
1077    std::str::from_utf8(bytes).expect("lexer regex guarantees ASCII")
1078}
1079
1080/// Mirrors `isLabelTail` (LLLexer.cpp:158): scan forward from `pos`, return
1081/// `Some(end)` where `end` is the offset just *past* the colon if the bytes
1082/// from `pos` form `[-a-zA-Z$._0-9]*:`. Returns `None` otherwise.
1083#[inline]
1084fn is_label_tail(src: &[u8], mut pos: usize) -> Option<usize> {
1085    while let Some(&b) = src.get(pos) {
1086        if b == b':' {
1087            return Some(pos + 1);
1088        }
1089        if !is_label_char(b) {
1090            return None;
1091        }
1092        pos += 1;
1093    }
1094    None
1095}
1096
1097#[cfg(test)]
1098#[path = "ll_lexer_tests.rs"]
1099mod tests;