Skip to main content

mago_syntax/lexer/
mod.rs

1#![allow(clippy::unreachable)]
2
3use core::hint::unreachable_unchecked;
4
5use std::collections::VecDeque;
6use std::fmt::Debug;
7
8use memchr::memchr2;
9use memchr::memmem;
10
11/// Lookup table for single-character tokens that are ALWAYS single-char
12/// (i.e., they can never be part of a multi-character token).
13/// Maps byte -> Option<TokenKind>
14const SIMPLE_TOKEN_TABLE: [Option<TokenKind>; 256] = {
15    let mut table: [Option<TokenKind>; 256] = [None; 256];
16    table[b';' as usize] = Some(TokenKind::Semicolon);
17    table[b',' as usize] = Some(TokenKind::Comma);
18    table[b')' as usize] = Some(TokenKind::RightParenthesis);
19    table[b'[' as usize] = Some(TokenKind::LeftBracket);
20    table[b']' as usize] = Some(TokenKind::RightBracket);
21    table[b'{' as usize] = Some(TokenKind::LeftBrace);
22    table[b'}' as usize] = Some(TokenKind::RightBrace);
23    table[b'~' as usize] = Some(TokenKind::Tilde);
24    table[b'@' as usize] = Some(TokenKind::At);
25    table
26};
27
28/// Lookup table for identifier start characters (a-z, A-Z, _, 0x80-0xFF)
29const IDENT_START_TABLE: [bool; 256] = {
30    let mut table = [false; 256];
31    let mut i = 0usize;
32    while i < 256 {
33        table[i] = matches!(i as u8, b'a'..=b'z' | b'A'..=b'Z' | b'_' | 0x80..=0xFF);
34        i += 1;
35    }
36
37    table
38};
39
40use mago_database::file::FileId;
41use mago_database::file::HasFileId;
42use mago_span::Position;
43use mago_syntax_core::float_exponent;
44use mago_syntax_core::float_separator;
45use mago_syntax_core::input::Input;
46use mago_syntax_core::number_sign;
47use mago_syntax_core::start_of_binary_number;
48use mago_syntax_core::start_of_float_number;
49use mago_syntax_core::start_of_hexadecimal_number;
50use mago_syntax_core::start_of_identifier;
51use mago_syntax_core::start_of_number;
52use mago_syntax_core::start_of_octal_number;
53use mago_syntax_core::start_of_octal_or_float_number;
54use mago_syntax_core::utils::is_part_of_identifier;
55use mago_syntax_core::utils::is_start_of_identifier;
56use mago_syntax_core::utils::read_digits_of_base;
57
58use crate::error::SyntaxError;
59use crate::lexer::internal::mode::HaltStage;
60use crate::lexer::internal::mode::Interpolation;
61use crate::lexer::internal::mode::LexerMode;
62use crate::lexer::internal::utils::NumberKind;
63use crate::settings::LexerSettings;
64use crate::token::DocumentKind;
65use crate::token::Token;
66use crate::token::TokenKind;
67
68mod internal;
69
70/// The `Lexer` struct is responsible for tokenizing input source code into discrete tokens
71/// based on PHP language syntax. It is designed to work with PHP code from version 7.0 up to 8.4.
72///
73/// The lexer reads through the provided input and processes it accordingly.
74///
75/// It identifies PHP-specific tokens, including operators, keywords, comments, strings, and other syntax elements,
76/// and produces a sequence of [`Token`] objects that are used in further stages of compilation or interpretation.
77///
78/// The lexer is designed to be used in a streaming fashion, where it reads the input source code in chunks
79/// and produces tokens incrementally. This allows for efficient processing of large source files and
80/// minimizes memory usage.
81#[derive(Debug)]
82pub struct Lexer<'input> {
83    input: Input<'input>,
84    settings: LexerSettings,
85    mode: LexerMode<'input>,
86    interpolating: bool,
87    brace_interpolating: bool,
88    var_offset_depth: u32,
89    expect_string_varname: bool,
90    interpolation_depth: u16,
91    /// Buffer for tokens during string interpolation.
92    buffer: VecDeque<Token<'input>>,
93}
94
95impl<'input> Lexer<'input> {
96    /// Nested interpolations recurse through `advance`; keep enough stack headroom for debug builds.
97    const MAX_INTERPOLATION_DEPTH: u16 = 64;
98
99    /// Creates a new `Lexer` instance.
100    ///
101    /// # Parameters
102    ///
103    /// - `input`: The input source code to tokenize.
104    /// - `settings`: The lexer settings.
105    ///
106    /// # Returns
107    ///
108    /// A new `Lexer` instance that reads from the provided byte slice.
109    #[must_use]
110    pub fn new(input: Input<'input>, settings: LexerSettings) -> Lexer<'input> {
111        Lexer {
112            input,
113            settings,
114            mode: LexerMode::Inline,
115            interpolating: false,
116            brace_interpolating: false,
117            var_offset_depth: 0,
118            expect_string_varname: false,
119            interpolation_depth: 0,
120            buffer: VecDeque::new(),
121        }
122    }
123
124    /// Creates a new `Lexer` instance for parsing a script block.
125    ///
126    /// # Parameters
127    ///
128    /// - `input`: The input source code to tokenize.
129    /// - `settings`: The lexer settings.
130    ///
131    /// # Returns
132    ///
133    /// A new `Lexer` instance that reads from the provided byte slice.
134    #[must_use]
135    pub fn scripting(input: Input<'input>, settings: LexerSettings) -> Lexer<'input> {
136        Lexer {
137            input,
138            settings,
139            mode: LexerMode::Script,
140            interpolating: false,
141            brace_interpolating: false,
142            var_offset_depth: 0,
143            expect_string_varname: false,
144            interpolation_depth: 0,
145            buffer: VecDeque::new(),
146        }
147    }
148
149    /// Check if the lexer has reached the end of the input.
150    ///
151    /// If this method returns `true`, the lexer will not produce any more tokens.
152    #[must_use]
153    pub fn has_reached_eof(&self) -> bool {
154        self.input.has_reached_eof()
155    }
156
157    /// Get the current position of the lexer in the input source code.
158    #[inline]
159    #[must_use]
160    pub const fn current_position(&self) -> Position {
161        self.input.current_position()
162    }
163
164    /// Tokenizes the next input from the source code.
165    ///
166    /// This method reads from the input and produces the next [`Token`] based on the current [`LexerMode`].
167    /// It handles various lexical elements such as inline text, script code, strings with interpolation,
168    /// comments, and different PHP-specific constructs.
169    ///
170    /// # Returns
171    ///
172    /// - `Some(Ok(Token))` if a token was successfully parsed.
173    /// - `Some(Err(SyntaxError))` if a syntax error occurred while parsing the next token.
174    /// - `None` if the end of the input has been reached.
175    ///
176    /// # Notes
177    ///
178    /// - It efficiently handles tokenization by consuming input based on patterns specific to PHP syntax.
179    /// - The lexer supports complex features like string interpolation and different numeric formats.
180    ///
181    /// # Errors
182    ///
183    /// Returns `Some(Err(SyntaxError))` in cases such as:
184    ///
185    /// - Unrecognized tokens that do not match any known PHP syntax.
186    /// - Unexpected tokens in a given context, such as an unexpected end of string.
187    ///
188    /// # Panics
189    ///
190    /// This method should not panic under normal operation. If it does, it indicates a bug in the lexer implementation.
191    ///
192    /// # See Also
193    ///
194    /// - [`Token`]: Represents a lexical token with its kind, value, and span.
195    /// - [`SyntaxError`]: Represents errors that can occur during lexing.
196    #[inline]
197    pub fn advance(&mut self) -> Option<Result<Token<'input>, SyntaxError>> {
198        // Check if there are buffered tokens from string interpolation.
199        if !self.interpolating
200            && let Some(token) = self.buffer.pop_front()
201        {
202            return Some(Ok(token));
203        }
204
205        if self.input.has_reached_eof() {
206            return None;
207        }
208
209        match self.mode {
210            LexerMode::Inline => {
211                let start = self.input.current_position();
212                let offset = self.input.current_offset();
213
214                // Shebang is only valid at the absolute start of the file (offset 0).
215                if offset == 0
216                    && self.input.len() >= 2
217                    // SAFETY: `self.input.len() >= 2` was just checked, so indices 0 and 1 are in bounds.
218                    && unsafe { *self.input.read_at_unchecked(0) } == b'#'
219                    // SAFETY: same as above.
220                    && unsafe { *self.input.read_at_unchecked(1) } == b'!'
221                {
222                    let buffer = self.input.consume_through(b'\n');
223                    let end = self.input.current_position();
224
225                    return Some(Ok(self.token(TokenKind::InlineShebang, buffer, start, end)));
226                }
227
228                // Get the remaining bytes to scan.
229                let bytes = self.input.read_remaining();
230
231                if self.settings.enable_short_tags {
232                    if let Some(pos) = memchr::memmem::find(bytes, b"<?") {
233                        if pos > 0 {
234                            let buffer = self.input.consume(pos);
235                            let end = self.input.current_position();
236
237                            return Some(Ok(self.token(TokenKind::InlineText, buffer, start, end)));
238                        }
239
240                        if self.input.is_at(b"<?php", true) {
241                            let buffer = self.input.consume(5);
242                            self.mode = LexerMode::Script;
243                            return Some(Ok(self.token(
244                                TokenKind::OpenTag,
245                                buffer,
246                                start,
247                                self.input.current_position(),
248                            )));
249                        }
250
251                        if self.input.is_at(b"<?=", false) {
252                            let buffer = self.input.consume(3);
253                            self.mode = LexerMode::Script;
254                            return Some(Ok(self.token(
255                                TokenKind::EchoTag,
256                                buffer,
257                                start,
258                                self.input.current_position(),
259                            )));
260                        }
261
262                        let buffer = self.input.consume(2);
263                        self.mode = LexerMode::Script;
264                        return Some(Ok(self.token(
265                            TokenKind::ShortOpenTag,
266                            buffer,
267                            start,
268                            self.input.current_position(),
269                        )));
270                    }
271                } else {
272                    let iter = memchr::memmem::find_iter(bytes, b"<?");
273
274                    for pos in iter {
275                        // SAFETY: `pos` is guaranteed to be within `bytes` by `find_iter`.
276                        let candidate = unsafe { bytes.get_unchecked(pos..) };
277
278                        if candidate.len() >= 5
279                            // SAFETY: `candidate.len() >= 5` was just checked, so indices 2, 3, 4 are in bounds.
280                            && (unsafe { *candidate.get_unchecked(2) } | 0x20) == b'p'
281                            // SAFETY: same as above.
282                            && (unsafe { *candidate.get_unchecked(3) } | 0x20) == b'h'
283                            // SAFETY: same as above.
284                            && (unsafe { *candidate.get_unchecked(4) } | 0x20) == b'p'
285                        {
286                            if pos > 0 {
287                                let buffer = self.input.consume(pos);
288                                let end = self.input.current_position();
289                                return Some(Ok(self.token(TokenKind::InlineText, buffer, start, end)));
290                            }
291
292                            let buffer = self.input.consume(5);
293                            self.mode = LexerMode::Script;
294                            return Some(Ok(self.token(
295                                TokenKind::OpenTag,
296                                buffer,
297                                start,
298                                self.input.current_position(),
299                            )));
300                        }
301
302                        // SAFETY: index 2 is in bounds because the right-hand side is only evaluated when
303                        // `candidate.len() >= 3` holds.
304                        if candidate.len() >= 3 && unsafe { *candidate.get_unchecked(2) } == b'=' {
305                            if pos > 0 {
306                                let buffer = self.input.consume(pos);
307                                let end = self.input.current_position();
308                                return Some(Ok(self.token(TokenKind::InlineText, buffer, start, end)));
309                            }
310
311                            let buffer = self.input.consume(3);
312                            self.mode = LexerMode::Script;
313                            return Some(Ok(self.token(
314                                TokenKind::EchoTag,
315                                buffer,
316                                start,
317                                self.input.current_position(),
318                            )));
319                        }
320                    }
321                }
322
323                if self.input.has_reached_eof() {
324                    return None;
325                }
326
327                let buffer = self.input.consume_remaining();
328                let end = self.input.current_position();
329                Some(Ok(self.token(TokenKind::InlineText, buffer, start, end)))
330            }
331            LexerMode::Script => {
332                let start = self.input.current_position();
333
334                if self.expect_string_varname {
335                    self.expect_string_varname = false;
336                    let (length, _) = self.input.scan_identifier(0);
337                    let buffer = self.input.consume(length);
338                    let end = self.input.current_position();
339
340                    return Some(Ok(self.token(TokenKind::StringVariableName, buffer, start, end)));
341                }
342
343                let whitespaces = self.input.consume_whitespaces();
344                if !whitespaces.is_empty() {
345                    return Some(Ok(self.token(
346                        TokenKind::Whitespace,
347                        whitespaces,
348                        start,
349                        self.input.current_position(),
350                    )));
351                }
352
353                let Some(&first_byte) = self.input.read(1).first() else {
354                    // SAFETY: we check for EOF before entering scripting section,
355                    unsafe { unreachable_unchecked() }
356                };
357
358                if let Some(kind) = SIMPLE_TOKEN_TABLE[first_byte as usize] {
359                    let buffer = self.input.consume(1);
360                    let end = self.input.current_position();
361                    return Some(Ok(self.token(kind, buffer, start, end)));
362                }
363
364                if self.var_offset_depth > 0 && IDENT_START_TABLE[first_byte as usize] {
365                    let (length, _) = self.input.scan_identifier(0);
366                    let buffer = self.input.consume(length);
367                    let end = self.input.current_position();
368                    return Some(Ok(self.token(TokenKind::OffsetString, buffer, start, end)));
369                }
370
371                if IDENT_START_TABLE[first_byte as usize] {
372                    let is_binary_string_prefix = !self.interpolating
373                        && matches!(first_byte, b'b' | b'B')
374                        && matches!(self.input.read(4), [_, b'\'' | b'"', ..] | [_, b'<', b'<', b'<']);
375
376                    if !is_binary_string_prefix {
377                        let (token_kind, len) = self.scan_identifier_or_keyword_info();
378
379                        if token_kind == TokenKind::HaltCompiler {
380                            self.mode = LexerMode::Halt(HaltStage::LookingForLeftParenthesis);
381                        }
382
383                        let buffer = self.input.consume(len);
384                        let end = self.input.current_position();
385                        return Some(Ok(self.token(token_kind, buffer, start, end)));
386                    }
387
388                    // Fall through to handle b-prefix strings in the match block below
389                }
390
391                if first_byte == b'$'
392                    && let Some(&next) = self.input.read(2).get(1)
393                    && IDENT_START_TABLE[next as usize]
394                {
395                    let (ident_len, _) = self.input.scan_identifier(1);
396                    let buffer = self.input.consume(1 + ident_len);
397                    let end = self.input.current_position();
398                    return Some(Ok(self.token(TokenKind::Variable, buffer, start, end)));
399                }
400
401                let mut document_label: &[u8] = &[];
402
403                let (token_kind, len) = match self.input.read(3) {
404                    [b'!', b'=', b'='] => (TokenKind::BangEqualEqual, 3),
405                    [b'?', b'?', b'='] => (TokenKind::QuestionQuestionEqual, 3),
406                    [b'?', b'-', b'>'] => (TokenKind::QuestionMinusGreaterThan, 3),
407                    [b'=', b'=', b'='] => (TokenKind::EqualEqualEqual, 3),
408                    [b'.', b'.', b'.'] => (TokenKind::DotDotDot, 3),
409                    [b'<', b'=', b'>'] => (TokenKind::LessThanEqualGreaterThan, 3),
410                    [b'<', b'<', b'='] => (TokenKind::LeftShiftEqual, 3),
411                    [b'>', b'>', b'='] => (TokenKind::RightShiftEqual, 3),
412                    [b'*', b'*', b'='] => (TokenKind::AsteriskAsteriskEqual, 3),
413                    [b'<', b'<', b'<'] if matches_start_of_heredoc_document(&self.input, 0) => {
414                        let (length, whitespaces, label_length) = read_start_of_heredoc_document(&self.input, false, 0);
415
416                        document_label = self.input.peek(3 + whitespaces, label_length);
417
418                        (TokenKind::DocumentStart(DocumentKind::Heredoc), length)
419                    }
420                    [b'<', b'<', b'<'] if matches_start_of_double_quote_heredoc_document(&self.input, 0) => {
421                        let (length, whitespaces, label_length) = read_start_of_heredoc_document(&self.input, true, 0);
422
423                        document_label = self.input.peek(4 + whitespaces, label_length);
424
425                        (TokenKind::DocumentStart(DocumentKind::Heredoc), length)
426                    }
427                    [b'<', b'<', b'<'] if matches_start_of_nowdoc_document(&self.input, 0) => {
428                        let (length, whitespaces, label_length) = read_start_of_nowdoc_document(&self.input, 0);
429
430                        document_label = self.input.peek(4 + whitespaces, label_length);
431
432                        (TokenKind::DocumentStart(DocumentKind::Nowdoc), length)
433                    }
434                    [b'!', b'=', ..] => (TokenKind::BangEqual, 2),
435                    [b'&', b'&', ..] => (TokenKind::AmpersandAmpersand, 2),
436                    [b'&', b'=', ..] => (TokenKind::AmpersandEqual, 2),
437                    [b'.', b'=', ..] => (TokenKind::DotEqual, 2),
438                    [b'?', b'?', ..] => (TokenKind::QuestionQuestion, 2),
439                    [b'?', b'>', ..] => (TokenKind::CloseTag, 2),
440                    [b'=', b'>', ..] => (TokenKind::EqualGreaterThan, 2),
441                    [b'=', b'=', ..] => (TokenKind::EqualEqual, 2),
442                    [b'+', b'+', ..] => (TokenKind::PlusPlus, 2),
443                    [b'+', b'=', ..] => (TokenKind::PlusEqual, 2),
444                    [b'%', b'=', ..] => (TokenKind::PercentEqual, 2),
445                    [b'-', b'-', ..] => (TokenKind::MinusMinus, 2),
446                    [b'-', b'>', ..] => (TokenKind::MinusGreaterThan, 2),
447                    [b'-', b'=', ..] => (TokenKind::MinusEqual, 2),
448                    [b'<', b'<', ..] => (TokenKind::LeftShift, 2),
449                    [b'<', b'=', ..] => (TokenKind::LessThanEqual, 2),
450                    [b'<', b'>', ..] => (TokenKind::LessThanGreaterThan, 2),
451                    [b'>', b'>', ..] => (TokenKind::RightShift, 2),
452                    [b'>', b'=', ..] => (TokenKind::GreaterThanEqual, 2),
453                    [b':', b':', ..] => (TokenKind::ColonColon, 2),
454                    [b'#', b'[', ..] => (TokenKind::HashLeftBracket, 2),
455                    [b'|', b'=', ..] => (TokenKind::PipeEqual, 2),
456                    [b'|', b'|', ..] => (TokenKind::PipePipe, 2),
457                    [b'/', b'=', ..] => (TokenKind::SlashEqual, 2),
458                    [b'^', b'=', ..] => (TokenKind::CaretEqual, 2),
459                    [b'*', b'*', ..] => (TokenKind::AsteriskAsterisk, 2),
460                    [b'*', b'=', ..] => (TokenKind::AsteriskEqual, 2),
461                    [b'|', b'>', ..] => (TokenKind::PipeGreaterThan, 2),
462                    [b'/', b'/', ..] => {
463                        let remaining = self.input.peek(2, self.input.len() - self.input.current_offset());
464                        let comment_len = scan_single_line_comment(remaining);
465                        (TokenKind::SingleLineComment, 2 + comment_len)
466                    }
467                    [b'/', b'*', asterisk] => {
468                        let remaining = self.input.peek(2, self.input.len() - self.input.current_offset());
469                        match scan_multi_line_comment(remaining) {
470                            Some(len) => {
471                                let is_docblock = asterisk == &b'*' && len > 2;
472                                if is_docblock {
473                                    (TokenKind::DocBlockComment, len + 2)
474                                } else {
475                                    (TokenKind::MultiLineComment, len + 2)
476                                }
477                            }
478                            None => {
479                                self.input.consume(remaining.len() + 2);
480                                return Some(Err(SyntaxError::UnexpectedEndOfFile(
481                                    self.file_id(),
482                                    self.input.current_position(),
483                                )));
484                            }
485                        }
486                    }
487                    [b'\\', start_of_identifier!(), ..] => {
488                        let mut length = 1;
489                        loop {
490                            let (ident_len, ends_with_ns) = self.input.scan_identifier(length);
491                            length += ident_len;
492                            if ends_with_ns {
493                                length += 1; // Include the backslash
494                            } else {
495                                break;
496                            }
497                        }
498
499                        (TokenKind::FullyQualifiedIdentifier, length)
500                    }
501                    [b'$', b'{', ..] => (TokenKind::DollarLeftBrace, 2),
502                    [b'$', ..] => (TokenKind::Dollar, 1),
503                    [b'!', ..] => (TokenKind::Bang, 1),
504                    [b'&', ..] => (TokenKind::Ampersand, 1),
505                    [b'?', ..] => (TokenKind::Question, 1),
506                    [b'=', ..] => (TokenKind::Equal, 1),
507                    [b'`', ..] => (TokenKind::Backtick, 1),
508                    [b'+', ..] => (TokenKind::Plus, 1),
509                    [b'%', ..] => (TokenKind::Percent, 1),
510                    [b'-', ..] => (TokenKind::Minus, 1),
511                    [b'<', ..] => (TokenKind::LessThan, 1),
512                    [b'>', ..] => (TokenKind::GreaterThan, 1),
513                    [b':', ..] => (TokenKind::Colon, 1),
514                    [b'|', ..] => (TokenKind::Pipe, 1),
515                    [b'^', ..] => (TokenKind::Caret, 1),
516                    [b'*', ..] => (TokenKind::Asterisk, 1),
517                    [b'/', ..] => (TokenKind::Slash, 1),
518                    [b'b' | b'B', b'\'', ..] => read_literal_string(&self.input, b'\'', 1),
519                    [b'b' | b'B', b'"', ..] if matches_literal_double_quote_string(&self.input, 1) => {
520                        read_literal_string(&self.input, b'"', 1)
521                    }
522                    [b'b' | b'B', b'"', ..] => (TokenKind::DoubleQuote, 2),
523                    [b'b' | b'B', b'<', b'<']
524                        if self.input.read(4).len() == 4
525                            && self.input.read(4)[3] == b'<'
526                            && matches_start_of_heredoc_document(&self.input, 1) =>
527                    {
528                        let (length, whitespaces, label_length) = read_start_of_heredoc_document(&self.input, false, 1);
529
530                        document_label = self.input.peek(4 + whitespaces, label_length);
531
532                        (TokenKind::DocumentStart(DocumentKind::Heredoc), length)
533                    }
534                    [b'b' | b'B', b'<', b'<']
535                        if self.input.read(4).len() == 4
536                            && self.input.read(4)[3] == b'<'
537                            && matches_start_of_double_quote_heredoc_document(&self.input, 1) =>
538                    {
539                        let (length, whitespaces, label_length) = read_start_of_heredoc_document(&self.input, true, 1);
540
541                        document_label = self.input.peek(5 + whitespaces, label_length);
542
543                        (TokenKind::DocumentStart(DocumentKind::Heredoc), length)
544                    }
545                    [b'b' | b'B', b'<', b'<']
546                        if self.input.read(4).len() == 4
547                            && self.input.read(4)[3] == b'<'
548                            && matches_start_of_nowdoc_document(&self.input, 1) =>
549                    {
550                        let (length, whitespaces, label_length) = read_start_of_nowdoc_document(&self.input, 1);
551
552                        document_label = self.input.peek(5 + whitespaces, label_length);
553
554                        (TokenKind::DocumentStart(DocumentKind::Nowdoc), length)
555                    }
556                    // Regular string literals
557                    [quote @ b'\'', ..] => read_literal_string(&self.input, *quote, 0),
558                    [quote @ b'"', ..] if matches_literal_double_quote_string(&self.input, 0) => {
559                        read_literal_string(&self.input, *quote, 0)
560                    }
561                    [b'"', ..] => (TokenKind::DoubleQuote, 1),
562                    [b'(', ..] => 'parenthesis: {
563                        let mut peek_offset = 1;
564                        while let Some(&b) = self.input.read(peek_offset + 1).get(peek_offset) {
565                            if b.is_ascii_whitespace() {
566                                peek_offset += 1;
567                            } else {
568                                // Check if this byte could start a cast type (case-insensitive)
569                                let lower = b | 0x20; // ASCII lowercase
570                                if !matches!(lower, b'i' | b'b' | b'f' | b'd' | b'r' | b's' | b'a' | b'o' | b'u' | b'v')
571                                {
572                                    break 'parenthesis (TokenKind::LeftParenthesis, 1);
573                                }
574                                break;
575                            }
576                        }
577
578                        for (value, kind) in internal::consts::CAST_TYPES {
579                            if let Some(length) = self.input.match_sequence_ignore_whitespace(value, true) {
580                                break 'parenthesis (kind, length);
581                            }
582                        }
583
584                        (TokenKind::LeftParenthesis, 1)
585                    }
586                    [b'#', ..] => {
587                        let remaining = self.input.peek(1, self.input.len() - self.input.current_offset());
588                        let comment_len = scan_single_line_comment(remaining);
589                        (TokenKind::HashComment, 1 + comment_len)
590                    }
591                    [b'\\', ..] => (TokenKind::NamespaceSeparator, 1),
592                    [start_of_number!(), ..] if self.var_offset_depth > 0 => {
593                        let mut length = 1;
594                        let base = match self.input.read(2) {
595                            [b'0', b'x' | b'X'] => {
596                                length = 2;
597                                16
598                            }
599                            [b'0', b'b' | b'B'] => {
600                                length = 2;
601                                2
602                            }
603                            [b'0', b'o' | b'O'] => {
604                                length = 2;
605                                8
606                            }
607                            _ => 10,
608                        };
609
610                        (TokenKind::OffsetNumber, read_digits_of_base(&self.input, length, base))
611                    }
612                    [b'.', start_of_number!(), ..] if self.var_offset_depth == 0 => {
613                        let mut length = read_digits_of_base(&self.input, 2, 10);
614                        if let float_exponent!() = self.input.peek(length, 1) {
615                            let mut exp_length = length + 1;
616                            if let number_sign!() = self.input.peek(exp_length, 1) {
617                                exp_length += 1;
618                            }
619
620                            let after_exp = read_digits_of_base(&self.input, exp_length, 10);
621                            if after_exp > exp_length {
622                                length = after_exp;
623                            }
624                        }
625
626                        (TokenKind::LiteralFloat, length)
627                    }
628                    [start_of_number!(), ..] => 'number: {
629                        let mut length = 1;
630
631                        let (base, kind): (u8, NumberKind) = match self.input.read(3) {
632                            start_of_binary_number!() => {
633                                length += 1;
634
635                                (2, NumberKind::Integer)
636                            }
637                            start_of_octal_number!() => {
638                                length += 1;
639
640                                (8, NumberKind::Integer)
641                            }
642                            start_of_hexadecimal_number!() => {
643                                length += 1;
644
645                                (16, NumberKind::Integer)
646                            }
647                            start_of_octal_or_float_number!() => (10, NumberKind::OctalOrFloat),
648                            start_of_float_number!() => (10, NumberKind::Float),
649                            _ => (10, NumberKind::IntegerOrFloat),
650                        };
651
652                        if kind != NumberKind::Float {
653                            length = read_digits_of_base(&self.input, length, base);
654
655                            if kind == NumberKind::Integer {
656                                break 'number (TokenKind::LiteralInteger, length);
657                            }
658                        }
659
660                        let is_float = matches!(self.input.peek(length, 3), float_separator!());
661
662                        if !is_float {
663                            if kind == NumberKind::OctalOrFloat
664                                && let Some(invalid_idx) =
665                                    (1..length).find(|&i| matches!(self.input.peek(i, 1), [b'8' | b'9']))
666                            {
667                                let invalid_byte = self.input.peek(invalid_idx, 1)[0];
668                                let start = self.input.current_position();
669                                let invalid_position = Position { offset: start.offset + invalid_idx as u32 };
670                                self.input.consume(length);
671                                return Some(Err(SyntaxError::UnexpectedToken(
672                                    self.file_id(),
673                                    invalid_byte,
674                                    invalid_position,
675                                )));
676                            }
677                            break 'number (TokenKind::LiteralInteger, length);
678                        }
679
680                        if let [b'.'] = self.input.peek(length, 1) {
681                            length += 1;
682                            length = read_digits_of_base(&self.input, length, 10);
683                        }
684
685                        if let float_exponent!() = self.input.peek(length, 1) {
686                            // Only include exponent if there are digits after it
687                            let mut exp_length = length + 1;
688                            if let number_sign!() = self.input.peek(exp_length, 1) {
689                                exp_length += 1;
690                            }
691                            let after_exp = read_digits_of_base(&self.input, exp_length, 10);
692                            if after_exp > exp_length {
693                                // There are digits after the exponent marker
694                                length = after_exp;
695                            }
696                        }
697
698                        (TokenKind::LiteralFloat, length)
699                    }
700                    [b'.', ..] => (TokenKind::Dot, 1),
701                    [unknown_byte, ..] => {
702                        let position = self.input.current_position();
703                        self.input.consume(1);
704
705                        return Some(Err(SyntaxError::UnrecognizedToken(self.file_id(), *unknown_byte, position)));
706                    }
707                    [] => {
708                        // We check for EOF before entering scripting section, so this should be
709                        // unreachable. If we ever land here it means an upstream invariant broke,
710                        // so signal EOF gracefully rather than panicking inside the lexer.
711                        return None;
712                    }
713                };
714
715                self.mode = match token_kind {
716                    TokenKind::DoubleQuote => LexerMode::DoubleQuoteString(Interpolation::None),
717                    TokenKind::Backtick => LexerMode::ShellExecuteString(Interpolation::None),
718                    TokenKind::CloseTag => LexerMode::Inline,
719                    TokenKind::HaltCompiler => LexerMode::Halt(HaltStage::LookingForLeftParenthesis),
720                    TokenKind::DocumentStart(document_kind) => {
721                        let body_offset = self.input.current_offset() + len;
722                        let interpolated = matches!(document_kind, DocumentKind::Heredoc);
723                        let indent = read_document_indentation(&self.input, body_offset, document_label, interpolated);
724
725                        LexerMode::DocumentString(document_kind, document_label, indent, Interpolation::None)
726                    }
727                    _ => LexerMode::Script,
728                };
729
730                let buffer = self.input.consume(len);
731                let end = self.input.current_position();
732
733                Some(Ok(self.token(token_kind, buffer, start, end)))
734            }
735            LexerMode::DoubleQuoteString(interpolation) => match &interpolation {
736                Interpolation::None => {
737                    let start = self.input.current_position();
738
739                    let mut length = 0;
740                    let mut last_was_slash = false;
741                    let mut token_kind = TokenKind::StringPart;
742                    loop {
743                        match self.input.peek(length, 2) {
744                            [b'$', start_of_identifier!(), ..] if !last_was_slash => {
745                                let until_offset = read_until_end_of_variable_interpolation(&self.input, length + 2);
746
747                                self.mode =
748                                    LexerMode::DoubleQuoteString(Interpolation::Until(start.offset + until_offset));
749
750                                break;
751                            }
752                            [b'{', b'$', ..] | [b'$', b'{', ..] if !last_was_slash => {
753                                let until_offset = read_until_end_of_brace_interpolation(&self.input, length + 2);
754
755                                self.mode = LexerMode::DoubleQuoteString(Interpolation::BraceUntil(
756                                    start.offset + until_offset,
757                                ));
758
759                                break;
760                            }
761                            [b'\\', ..] => {
762                                length += 1;
763
764                                last_was_slash = !last_was_slash;
765                            }
766                            [b'"', ..] if !last_was_slash => {
767                                if length == 0 {
768                                    length += 1;
769                                    token_kind = TokenKind::DoubleQuote;
770
771                                    break;
772                                }
773
774                                break;
775                            }
776                            [_, ..] => {
777                                length += 1;
778                                last_was_slash = false;
779                            }
780                            [] => {
781                                break;
782                            }
783                        }
784                    }
785
786                    let buffer = self.input.consume(length);
787                    let end = self.input.current_position();
788
789                    if TokenKind::DoubleQuote == token_kind {
790                        self.mode = LexerMode::Script;
791                    }
792
793                    Some(Ok(self.token(token_kind, buffer, start, end)))
794                }
795                Interpolation::Until(offset) => {
796                    self.interpolation(*offset, LexerMode::DoubleQuoteString(Interpolation::None), false)
797                }
798                Interpolation::BraceUntil(offset) => {
799                    self.interpolation(*offset, LexerMode::DoubleQuoteString(Interpolation::None), true)
800                }
801            },
802            LexerMode::ShellExecuteString(interpolation) => match &interpolation {
803                Interpolation::None => {
804                    let start = self.input.current_position();
805
806                    let mut length = 0;
807                    let mut last_was_slash = false;
808                    let mut token_kind = TokenKind::StringPart;
809                    loop {
810                        match self.input.peek(length, 2) {
811                            [b'$', start_of_identifier!(), ..] if !last_was_slash => {
812                                let until_offset = read_until_end_of_variable_interpolation(&self.input, length + 2);
813
814                                self.mode =
815                                    LexerMode::ShellExecuteString(Interpolation::Until(start.offset + until_offset));
816
817                                break;
818                            }
819                            [b'{', b'$', ..] | [b'$', b'{', ..] if !last_was_slash => {
820                                let until_offset = read_until_end_of_brace_interpolation(&self.input, length + 2);
821
822                                self.mode = LexerMode::ShellExecuteString(Interpolation::BraceUntil(
823                                    start.offset + until_offset,
824                                ));
825
826                                break;
827                            }
828                            [b'\\', ..] => {
829                                length += 1;
830                                last_was_slash = !last_was_slash;
831                            }
832                            [b'`', ..] if !last_was_slash => {
833                                if length == 0 {
834                                    length += 1;
835                                    token_kind = TokenKind::Backtick;
836
837                                    break;
838                                }
839
840                                break;
841                            }
842                            [_, ..] => {
843                                length += 1;
844                                last_was_slash = false;
845                            }
846                            [] => {
847                                break;
848                            }
849                        }
850                    }
851
852                    let buffer = self.input.consume(length);
853                    let end = self.input.current_position();
854
855                    if TokenKind::Backtick == token_kind {
856                        self.mode = LexerMode::Script;
857                    }
858
859                    Some(Ok(self.token(token_kind, buffer, start, end)))
860                }
861                Interpolation::Until(offset) => {
862                    self.interpolation(*offset, LexerMode::ShellExecuteString(Interpolation::None), false)
863                }
864                Interpolation::BraceUntil(offset) => {
865                    self.interpolation(*offset, LexerMode::ShellExecuteString(Interpolation::None), true)
866                }
867            },
868            LexerMode::DocumentString(kind, label, indent, interpolation) => match &kind {
869                DocumentKind::Heredoc => match &interpolation {
870                    Interpolation::None => {
871                        let start = self.input.current_position();
872
873                        if indent > 0 && document_body_at_line_start(&self.input) {
874                            let width = document_leading_indent_width(&self.input, indent);
875                            if width > 0 && !document_line_is_closing_marker(&self.input, width, label) {
876                                let buffer = self.input.consume(width);
877                                let end = self.input.current_position();
878
879                                return Some(Ok(self.token(TokenKind::Whitespace, buffer, start, end)));
880                            }
881                        }
882
883                        let mut length = 0;
884                        let mut last_was_slash = false;
885                        let mut only_whitespaces = true;
886                        let mut token_kind = TokenKind::StringPart;
887                        loop {
888                            match self.input.peek(length, 2) {
889                                [b'\r', b'\n'] => {
890                                    if document_next_line_is_closing_marker(&self.input, length + 2, label) {
891                                        return self.document_segment_before_closing_marker(length, 2, start);
892                                    }
893
894                                    length += 2;
895
896                                    break;
897                                }
898                                [b'\n' | b'\r', ..] => {
899                                    if document_next_line_is_closing_marker(&self.input, length + 1, label) {
900                                        return self.document_segment_before_closing_marker(length, 1, start);
901                                    }
902
903                                    length += 1;
904
905                                    break;
906                                }
907                                [byte, ..] if byte.is_ascii_whitespace() => {
908                                    length += 1;
909                                }
910                                [b'$', start_of_identifier!(), ..] if !last_was_slash => {
911                                    let until_offset =
912                                        read_until_end_of_variable_interpolation(&self.input, length + 2);
913
914                                    self.mode = LexerMode::DocumentString(
915                                        kind,
916                                        label,
917                                        indent,
918                                        Interpolation::Until(start.offset + until_offset),
919                                    );
920
921                                    break;
922                                }
923                                [b'{', b'$', ..] | [b'$', b'{', ..] if !last_was_slash => {
924                                    let until_offset = read_until_end_of_brace_interpolation(&self.input, length + 2);
925
926                                    self.mode = LexerMode::DocumentString(
927                                        kind,
928                                        label,
929                                        indent,
930                                        Interpolation::BraceUntil(start.offset + until_offset),
931                                    );
932
933                                    break;
934                                }
935                                [b'\\', ..] => {
936                                    length += 1;
937                                    last_was_slash = !last_was_slash;
938                                    only_whitespaces = false;
939                                }
940                                [_, ..] => {
941                                    if only_whitespaces
942                                        && self.input.peek(length, label.len()) == label
943                                        && self
944                                            .input
945                                            .peek(length + label.len(), 1)
946                                            .first()
947                                            .is_none_or(|c| !is_part_of_identifier(c))
948                                    {
949                                        length += label.len();
950                                        token_kind = TokenKind::DocumentEnd;
951
952                                        break;
953                                    }
954
955                                    length += 1;
956                                    last_was_slash = false;
957                                    only_whitespaces = false;
958                                }
959                                [] => {
960                                    break;
961                                }
962                            }
963                        }
964
965                        let buffer = self.input.consume(length);
966                        let end = self.input.current_position();
967
968                        if TokenKind::DocumentEnd == token_kind {
969                            self.mode = LexerMode::Script;
970                        }
971
972                        Some(Ok(self.token(token_kind, buffer, start, end)))
973                    }
974                    Interpolation::Until(offset) => self.interpolation(
975                        *offset,
976                        LexerMode::DocumentString(kind, label, indent, Interpolation::None),
977                        false,
978                    ),
979                    Interpolation::BraceUntil(offset) => self.interpolation(
980                        *offset,
981                        LexerMode::DocumentString(kind, label, indent, Interpolation::None),
982                        true,
983                    ),
984                },
985                DocumentKind::Nowdoc => {
986                    let start = self.input.current_position();
987
988                    if indent > 0 && document_body_at_line_start(&self.input) {
989                        let width = document_leading_indent_width(&self.input, indent);
990                        if width > 0 && !document_line_is_closing_marker(&self.input, width, label) {
991                            let buffer = self.input.consume(width);
992                            let end = self.input.current_position();
993
994                            return Some(Ok(self.token(TokenKind::Whitespace, buffer, start, end)));
995                        }
996                    }
997
998                    let mut length = 0;
999                    let mut terminated = false;
1000                    let mut only_whitespaces = true;
1001
1002                    loop {
1003                        match self.input.peek(length, 2) {
1004                            [b'\r', b'\n'] => {
1005                                if document_next_line_is_closing_marker(&self.input, length + 2, label) {
1006                                    return self.document_segment_before_closing_marker(length, 2, start);
1007                                }
1008
1009                                length += 2;
1010
1011                                break;
1012                            }
1013                            [b'\n' | b'\r', ..] => {
1014                                if document_next_line_is_closing_marker(&self.input, length + 1, label) {
1015                                    return self.document_segment_before_closing_marker(length, 1, start);
1016                                }
1017
1018                                length += 1;
1019
1020                                break;
1021                            }
1022                            [byte, ..] if byte.is_ascii_whitespace() => {
1023                                length += 1;
1024                            }
1025                            [_, ..] => {
1026                                if only_whitespaces
1027                                    && self.input.peek(length, label.len()) == label
1028                                    && self
1029                                        .input
1030                                        .peek(length + label.len(), 1)
1031                                        .first()
1032                                        .is_none_or(|c| !is_part_of_identifier(c))
1033                                {
1034                                    length += label.len();
1035                                    terminated = true;
1036
1037                                    break;
1038                                }
1039
1040                                only_whitespaces = false;
1041                                length += 1;
1042                            }
1043                            [] => {
1044                                break;
1045                            }
1046                        }
1047                    }
1048
1049                    let buffer = self.input.consume(length);
1050                    let end = self.input.current_position();
1051
1052                    if terminated {
1053                        self.mode = LexerMode::Script;
1054
1055                        return Some(Ok(self.token(TokenKind::DocumentEnd, buffer, start, end)));
1056                    }
1057
1058                    Some(Ok(self.token(TokenKind::StringPart, buffer, start, end)))
1059                }
1060            },
1061            LexerMode::Halt(stage) => 'halt: {
1062                let start = self.input.current_position();
1063                if let HaltStage::End = stage {
1064                    let buffer = self.input.consume_remaining();
1065                    let end = self.input.current_position();
1066
1067                    break 'halt Some(Ok(self.token(TokenKind::InlineText, buffer, start, end)));
1068                }
1069
1070                let whitespaces = self.input.consume_whitespaces();
1071                if !whitespaces.is_empty() {
1072                    let end = self.input.current_position();
1073
1074                    break 'halt Some(Ok(self.token(TokenKind::Whitespace, whitespaces, start, end)));
1075                }
1076
1077                match &stage {
1078                    HaltStage::LookingForLeftParenthesis => {
1079                        if self.input.is_at(b"(", false) {
1080                            let buffer = self.input.consume(1);
1081                            let end = self.input.current_position();
1082
1083                            self.mode = LexerMode::Halt(HaltStage::LookingForRightParenthesis);
1084
1085                            Some(Ok(self.token(TokenKind::LeftParenthesis, buffer, start, end)))
1086                        } else {
1087                            let byte = self.input.read(1)[0];
1088                            let position = self.input.current_position();
1089                            // Consume the unexpected byte to avoid infinite loops
1090                            self.input.consume(1);
1091                            Some(Err(SyntaxError::UnexpectedToken(self.file_id(), byte, position)))
1092                        }
1093                    }
1094                    HaltStage::LookingForRightParenthesis => {
1095                        if self.input.is_at(b")", false) {
1096                            let buffer = self.input.consume(1);
1097                            let end = self.input.current_position();
1098
1099                            self.mode = LexerMode::Halt(HaltStage::LookingForTerminator);
1100
1101                            Some(Ok(self.token(TokenKind::RightParenthesis, buffer, start, end)))
1102                        } else {
1103                            let byte = self.input.read(1)[0];
1104                            let position = self.input.current_position();
1105                            self.input.consume(1);
1106                            Some(Err(SyntaxError::UnexpectedToken(self.file_id(), byte, position)))
1107                        }
1108                    }
1109                    HaltStage::LookingForTerminator => {
1110                        if self.input.is_at(b";", false) {
1111                            let buffer = self.input.consume(1);
1112                            let end = self.input.current_position();
1113
1114                            self.mode = LexerMode::Halt(HaltStage::End);
1115
1116                            Some(Ok(self.token(TokenKind::Semicolon, buffer, start, end)))
1117                        } else if self.input.is_at(b"?>", false) {
1118                            let buffer = self.input.consume(2);
1119                            let end = self.input.current_position();
1120
1121                            self.mode = LexerMode::Halt(HaltStage::End);
1122
1123                            Some(Ok(self.token(TokenKind::CloseTag, buffer, start, end)))
1124                        } else {
1125                            let byte = self.input.read(1)[0];
1126                            let position = self.input.current_position();
1127                            self.input.consume(1);
1128                            Some(Err(SyntaxError::UnexpectedToken(self.file_id(), byte, position)))
1129                        }
1130                    }
1131                    HaltStage::End => {
1132                        // The `HaltStage::End` arm is reached only after the early-return at the top of
1133                        // this branch consumed the terminating `?>` or EOF; surfacing `None` keeps the
1134                        // lexer total instead of relying on `unreachable!`.
1135                        None
1136                    }
1137                }
1138            }
1139        }
1140    }
1141
1142    /// Fast path for scanning identifiers and keywords.
1143    /// Called when we know the first byte is an identifier start character.
1144    /// Returns (TokenKind, length) to allow proper mode switching.
1145    #[inline]
1146    fn scan_identifier_or_keyword_info(&self) -> (TokenKind, usize) {
1147        let (length, ended_with_slash) = self.input.scan_identifier(0);
1148
1149        if !ended_with_slash && matches!(self.input.peek(length, 1), [b'(']) {
1150            match length {
1151                6 if self.input.is_at(b"public(set)", true) => {
1152                    return (TokenKind::PublicSet, 11);
1153                }
1154                7 if self.input.is_at(b"private(set)", true) => {
1155                    return (TokenKind::PrivateSet, 12);
1156                }
1157                9 if self.input.is_at(b"protected(set)", true) => {
1158                    return (TokenKind::ProtectedSet, 14);
1159                }
1160                _ => {}
1161            }
1162        }
1163
1164        if !ended_with_slash {
1165            if let Some(kind) = internal::keyword::lookup_keyword(self.input.read(length)) {
1166                return (kind, length);
1167            }
1168
1169            return (TokenKind::Identifier, length);
1170        }
1171
1172        self.scan_qualified_identifier_info(length)
1173    }
1174
1175    // Keep qualified-name scanning out of the common identifier/keyword path.
1176    #[inline(never)]
1177    fn scan_qualified_identifier_info(&self, mut length: usize) -> (TokenKind, usize) {
1178        let mut slashes = 0;
1179        let mut last_was_slash = false;
1180        loop {
1181            match self.input.peek(length, 1) {
1182                [b'a'..=b'z' | b'A'..=b'Z' | b'_' | 0x80..=0xFF] if last_was_slash => {
1183                    length += 1;
1184                    last_was_slash = false;
1185                }
1186                [b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | 0x80..=0xFF] if !last_was_slash => {
1187                    length += 1;
1188                }
1189                [b'\\'] if !self.interpolating || self.brace_interpolating => {
1190                    if last_was_slash {
1191                        length -= 1;
1192                        slashes -= 1;
1193                        last_was_slash = false;
1194                        break;
1195                    }
1196
1197                    length += 1;
1198                    slashes += 1;
1199                    last_was_slash = true;
1200                }
1201                _ => {
1202                    break;
1203                }
1204            }
1205        }
1206
1207        if last_was_slash {
1208            length -= 1;
1209            slashes -= 1;
1210        }
1211
1212        let kind = if slashes > 0 { TokenKind::QualifiedIdentifier } else { TokenKind::Identifier };
1213
1214        (kind, length)
1215    }
1216
1217    #[inline]
1218    fn token(&self, kind: TokenKind, value: &'input [u8], start: Position, _end: Position) -> Token<'input> {
1219        Token { kind, start, value }
1220    }
1221
1222    /// Emits the last body segment before a heredoc/nowdoc closing marker. The
1223    /// line terminator immediately preceding the marker is not part of the
1224    /// string value, so it is surfaced as `Whitespace` trivia: when there is
1225    /// preceding content the terminator is queued to follow the `StringPart`,
1226    /// otherwise the terminator is emitted on its own.
1227    fn document_segment_before_closing_marker(
1228        &mut self,
1229        content_length: usize,
1230        terminator_length: usize,
1231        start: Position,
1232    ) -> Option<Result<Token<'input>, SyntaxError>> {
1233        if content_length == 0 {
1234            let buffer = self.input.consume(terminator_length);
1235            let end = self.input.current_position();
1236
1237            return Some(Ok(self.token(TokenKind::Whitespace, buffer, start, end)));
1238        }
1239
1240        let content = self.input.consume(content_length);
1241        let content_end = self.input.current_position();
1242        let terminator = self.input.consume(terminator_length);
1243        let terminator_end = self.input.current_position();
1244
1245        if self.interpolating {
1246            self.buffer.push_back(self.token(TokenKind::StringPart, content, start, content_end));
1247
1248            return Some(Ok(self.token(TokenKind::Whitespace, terminator, content_end, terminator_end)));
1249        }
1250
1251        self.buffer.push_back(self.token(TokenKind::Whitespace, terminator, content_end, terminator_end));
1252
1253        Some(Ok(self.token(TokenKind::StringPart, content, start, content_end)))
1254    }
1255
1256    fn peek_string_varname_label(&self) -> bool {
1257        let Some(&first) = self.input.read(1).first() else {
1258            return false;
1259        };
1260
1261        if !IDENT_START_TABLE[first as usize] {
1262            return false;
1263        }
1264
1265        let (label_length, _) = self.input.scan_identifier(0);
1266
1267        matches!(self.input.read(label_length + 1).get(label_length), Some(b'[' | b'}'))
1268    }
1269
1270    #[inline]
1271    fn interpolation(
1272        &mut self,
1273        end_offset: u32,
1274        post_interpolation_mode: LexerMode<'input>,
1275        brace: bool,
1276    ) -> Option<Result<Token<'input>, SyntaxError>> {
1277        self.interpolation_depth += 1;
1278        if self.interpolation_depth > Self::MAX_INTERPOLATION_DEPTH {
1279            self.interpolation_depth -= 1;
1280
1281            return Some(Err(SyntaxError::RecursionLimitExceeded(self.file_id(), self.input.current_position())));
1282        }
1283
1284        self.mode = LexerMode::Script;
1285
1286        let was_interpolating = self.interpolating;
1287        self.interpolating = true;
1288        let was_brace_interpolating = self.brace_interpolating;
1289        // For brace interpolation ({$...}), allow qualified identifiers with backslashes.
1290        self.brace_interpolating = brace;
1291        let was_var_offset_depth = self.var_offset_depth;
1292        self.var_offset_depth = 0;
1293        let was_expect_string_varname = self.expect_string_varname;
1294        self.expect_string_varname = false;
1295
1296        let pending_error = loop {
1297            match self.advance() {
1298                Some(Ok(token)) => {
1299                    let token_start = token.start.offset;
1300                    let token_end = token_start + token.value.len() as u32;
1301                    let is_final_token = token_start <= end_offset && end_offset <= token_end;
1302
1303                    if brace {
1304                        if token.kind == TokenKind::DollarLeftBrace && self.peek_string_varname_label() {
1305                            self.expect_string_varname = true;
1306                        }
1307                    } else {
1308                        match token.kind {
1309                            TokenKind::LeftBracket => self.var_offset_depth += 1,
1310                            TokenKind::RightBracket => {
1311                                self.var_offset_depth = self.var_offset_depth.saturating_sub(1);
1312                            }
1313                            _ => {}
1314                        }
1315                    }
1316
1317                    self.buffer.push_back(token);
1318
1319                    if is_final_token {
1320                        break None;
1321                    }
1322                }
1323                Some(Err(error)) => break Some(error),
1324                None => break None,
1325            }
1326        };
1327
1328        self.mode = post_interpolation_mode;
1329        self.interpolating = was_interpolating;
1330        self.brace_interpolating = was_brace_interpolating;
1331        self.var_offset_depth = was_var_offset_depth;
1332        self.expect_string_varname = was_expect_string_varname;
1333        self.interpolation_depth -= 1;
1334
1335        if let Some(error) = pending_error {
1336            return Some(Err(error));
1337        }
1338
1339        self.advance()
1340    }
1341}
1342
1343impl HasFileId for Lexer<'_> {
1344    #[inline]
1345    fn file_id(&self) -> FileId {
1346        self.input.file_id()
1347    }
1348}
1349
1350#[inline]
1351fn matches_start_of_heredoc_document(input: &Input, prefix_len: usize) -> bool {
1352    let total = input.len();
1353    let base = input.current_offset();
1354
1355    // Start after the prefix (if any) and the fixed opener (3 bytes).
1356    let mut length = 3 + prefix_len;
1357    // Consume any following whitespace.
1358    while base + length < total && input.read_at(base + length).is_ascii_whitespace() {
1359        length += 1;
1360    }
1361
1362    // The next byte must be a valid start-of-identifier.
1363    if base + length >= total || !is_start_of_identifier(input.read_at(base + length)) {
1364        return false;
1365    }
1366    length += 1; // Include that identifier start.
1367
1368    // Now continue reading identifier characters until a newline is found.
1369    loop {
1370        let pos = base + length;
1371        if pos >= total {
1372            return false; // Unexpected EOF
1373        }
1374
1375        let byte = *input.read_at(pos);
1376        if byte == b'\n' {
1377            return true; // Newline found: valid heredoc opener.
1378        } else if byte == b'\r' {
1379            // Handle CRLF: treat '\r' followed by '\n' as a newline as well.
1380            return pos + 1 < total && *input.read_at(pos + 1) == b'\n';
1381        } else if is_part_of_identifier(input.read_at(pos)) {
1382            length += 1;
1383        } else {
1384            return false; // Unexpected character.
1385        }
1386    }
1387}
1388
1389#[inline]
1390fn matches_start_of_double_quote_heredoc_document(input: &Input, prefix_len: usize) -> bool {
1391    let total = input.len();
1392    let base = input.current_offset();
1393
1394    // Start after the prefix (if any) and the fixed opener (3 bytes), then skip any whitespace.
1395    let mut length = 3 + prefix_len;
1396    while base + length < total && input.read_at(base + length).is_ascii_whitespace() {
1397        length += 1;
1398    }
1399
1400    // Next, expect an opening double quote.
1401    if base + length >= total || *input.read_at(base + length) != b'"' {
1402        return false;
1403    }
1404    length += 1;
1405
1406    // The following byte must be a valid start-of-identifier.
1407    if base + length >= total || !is_start_of_identifier(input.read_at(base + length)) {
1408        return false;
1409    }
1410    length += 1;
1411
1412    // Now scan the label. For double‑quoted heredoc, a terminating double quote is required.
1413    let mut terminated = false;
1414    loop {
1415        let pos = base + length;
1416        if pos >= total {
1417            return false;
1418        }
1419        let byte = input.read_at(pos);
1420        if *byte == b'\n' {
1421            // End of line: valid only if a closing double quote was encountered.
1422            return terminated;
1423        } else if *byte == b'\r' {
1424            // Handle CRLF sequences.
1425            return terminated && pos + 1 < total && *input.read_at(pos + 1) == b'\n';
1426        } else if !terminated && is_part_of_identifier(byte) {
1427            length += 1;
1428        } else if !terminated && *byte == b'"' {
1429            terminated = true;
1430            length += 1;
1431        } else {
1432            return false;
1433        }
1434    }
1435}
1436
1437#[inline]
1438fn matches_start_of_nowdoc_document(input: &Input, prefix_len: usize) -> bool {
1439    let total = input.len();
1440    let base = input.current_offset();
1441
1442    // Start after the prefix (if any) and the fixed opener (3 bytes) and skip whitespace.
1443    let mut length = 3 + prefix_len;
1444    while base + length < total && input.read_at(base + length).is_ascii_whitespace() {
1445        length += 1;
1446    }
1447
1448    // Now, the next byte must be a single quote.
1449    if base + length >= total || *input.read_at(base + length) != b'\'' {
1450        return false;
1451    }
1452    length += 1;
1453
1454    // The following byte must be a valid start-of-identifier.
1455    if base + length >= total || !is_start_of_identifier(input.read_at(base + length)) {
1456        return false;
1457    }
1458    length += 1;
1459
1460    // Read the label until a newline. A terminating single quote is required.
1461    let mut terminated = false;
1462    loop {
1463        let pos = base + length;
1464        if pos >= total {
1465            return false;
1466        }
1467        let byte = *input.read_at(pos);
1468        if byte == b'\n' {
1469            return terminated;
1470        } else if byte == b'\r' {
1471            return terminated && pos + 1 < total && *input.read_at(pos + 1) == b'\n';
1472        } else if !terminated && is_part_of_identifier(&byte) {
1473            length += 1;
1474        } else if !terminated && byte == b'\'' {
1475            terminated = true;
1476            length += 1;
1477        } else {
1478            return false;
1479        }
1480    }
1481}
1482
1483#[inline]
1484fn matches_literal_double_quote_string(input: &Input, prefix_len: usize) -> bool {
1485    let total = input.len();
1486    let base = input.current_offset();
1487
1488    // Start after the prefix (if any) and the initial double-quote.
1489    let mut pos = base + 1 + prefix_len;
1490    loop {
1491        if pos >= total {
1492            // Reached EOF: assume literal is complete.
1493            return true;
1494        }
1495        let byte = *input.read_at(pos);
1496        if byte == b'"' {
1497            // Encounter a closing double quote.
1498            return true;
1499        }
1500        if byte == b'\\' {
1501            // Skip an escape sequence: assume that the backslash and the escaped character form a pair.
1502            pos += 2;
1503            continue;
1504        }
1505
1506        // Check for variable interpolation or complex expression start:
1507        // If two-byte sequences match either "$" followed by a start-of-identifier or "{" and "$", then return false.
1508        if pos + 1 < total {
1509            let next = *input.read_at(pos + 1);
1510            if (byte == b'$' && (is_start_of_identifier(&next) || next == b'{')) || (byte == b'{' && next == b'$') {
1511                return false;
1512            }
1513        }
1514        pos += 1;
1515    }
1516}
1517
1518/// Measures the indentation of a heredoc/nowdoc closing marker by scanning the
1519/// body line by line, starting at `body_offset`, until the first line whose
1520/// leading whitespace is followed by the closing `label`. The returned width is
1521/// the number of leading space/tab bytes on that line; PHP 7.3 flexible
1522/// heredoc/nowdoc strips up to that many columns from every body line.
1523fn read_document_indentation(input: &Input, body_offset: usize, label: &[u8], interpolated: bool) -> usize {
1524    let total = input.len();
1525    let base = input.current_offset();
1526    let mut position = body_offset;
1527    loop {
1528        let mut width = 0;
1529        while position < total && matches!(*input.read_at(position), b' ' | b'\t') {
1530            width += 1;
1531            position += 1;
1532        }
1533
1534        if label_matches_at(input, position, label, total) {
1535            return width;
1536        }
1537
1538        let mut last_was_slash = false;
1539        while position < total && !matches!(*input.read_at(position), b'\n' | b'\r') {
1540            let byte = *input.read_at(position);
1541            let next = (position + 1 < total).then(|| *input.read_at(position + 1));
1542
1543            if interpolated && !last_was_slash {
1544                if (byte == b'$' && next == Some(b'{')) || (byte == b'{' && next == Some(b'$')) {
1545                    let from = (position - base) + 2;
1546                    position = base + read_until_end_of_brace_interpolation(input, from) as usize;
1547
1548                    continue;
1549                }
1550
1551                if byte == b'$' && next.is_some_and(|c| is_start_of_identifier(&c)) {
1552                    let from = (position - base) + 2;
1553                    position = base + read_until_end_of_variable_interpolation(input, from) as usize;
1554
1555                    continue;
1556                }
1557            }
1558
1559            last_was_slash = byte == b'\\' && !last_was_slash;
1560            position += 1;
1561        }
1562
1563        if position >= total {
1564            return 0;
1565        }
1566
1567        if *input.read_at(position) == b'\r' && position + 1 < total && *input.read_at(position + 1) == b'\n' {
1568            position += 2;
1569        } else {
1570            position += 1;
1571        }
1572    }
1573}
1574
1575/// Whether the closing `label` appears at the absolute `position`, not followed
1576/// by an identifier byte (so `EOT` matches but `EOTish` does not).
1577fn label_matches_at(input: &Input, position: usize, label: &[u8], total: usize) -> bool {
1578    if position + label.len() > total {
1579        return false;
1580    }
1581
1582    let mut index = 0;
1583    while index < label.len() {
1584        if *input.read_at(position + index) != label[index] {
1585            return false;
1586        }
1587        index += 1;
1588    }
1589
1590    let after = position + label.len();
1591    after >= total || !is_part_of_identifier(input.read_at(after))
1592}
1593
1594/// Whether the lexer is positioned at the start of a heredoc/nowdoc body line,
1595/// i.e. the previous byte is a line terminator.
1596#[inline]
1597fn document_body_at_line_start(input: &Input) -> bool {
1598    let offset = input.current_offset();
1599    offset == 0 || matches!(*input.read_at(offset - 1), b'\n' | b'\r')
1600}
1601
1602/// Counts the leading space/tab bytes at the current position, capped at `indent`.
1603#[inline]
1604fn document_leading_indent_width(input: &Input, indent: usize) -> usize {
1605    let mut width = 0;
1606    while width < indent && matches!(input.peek(width, 1), [b' ' | b'\t']) {
1607        width += 1;
1608    }
1609
1610    width
1611}
1612
1613/// Whether the current line, after `width` leading whitespace bytes, is the
1614/// closing marker. The marker's own indentation is left attached to the
1615/// `DocumentEnd` token rather than stripped as trivia.
1616#[inline]
1617fn document_line_is_closing_marker(input: &Input, width: usize, label: &[u8]) -> bool {
1618    input.peek(width, label.len()) == label
1619        && input.peek(width + label.len(), 1).first().is_none_or(|byte| !is_part_of_identifier(byte))
1620}
1621
1622/// Whether the line beginning at relative `offset` (just past a line terminator)
1623/// is the closing marker. Used to recognise the terminator that immediately
1624/// precedes the marker, which a heredoc/nowdoc drops from the string value.
1625#[inline]
1626fn document_next_line_is_closing_marker(input: &Input, offset: usize, label: &[u8]) -> bool {
1627    let mut width = 0;
1628    while matches!(input.peek(offset + width, 1), [b' ' | b'\t']) {
1629        width += 1;
1630    }
1631
1632    input.peek(offset + width, label.len()) == label
1633        && input.peek(offset + width + label.len(), 1).first().is_none_or(|byte| !is_part_of_identifier(byte))
1634}
1635
1636#[inline]
1637fn read_start_of_heredoc_document(input: &Input, double_quoted: bool, prefix_len: usize) -> (usize, usize, usize) {
1638    let total = input.len();
1639    let base = input.current_offset();
1640
1641    // Start reading after the prefix (if any) and the fixed opener (3 bytes).
1642    let mut pos = base + 3 + prefix_len;
1643    let mut whitespaces = 0;
1644    while pos < total && input.read_at(pos).is_ascii_whitespace() {
1645        whitespaces += 1;
1646        pos += 1;
1647    }
1648
1649    // The label (or delimiter) starts after:
1650    //   prefix + 3 bytes + whitespace bytes + an extra offset:
1651    //      if double-quoted: 2 bytes (opening and closing quotes around the label)
1652    //      else: 1 byte.
1653    let mut length = 3 + prefix_len + whitespaces + if double_quoted { 2 } else { 1 };
1654
1655    let mut label_length = 1; // Start with at least one byte for the label.
1656    let mut terminated = false; // For double-quoted heredoc, to track the closing quote.
1657    loop {
1658        let pos = base + length;
1659        // Bail out gracefully if we run past the input or hit a byte that the caller's
1660        // earlier validation should have rejected: returning the accumulated `(length,
1661        // whitespaces, label_length)` lets the lexer produce a malformed-heredoc token
1662        // instead of panicking.
1663        if pos >= total {
1664            return (length, whitespaces, label_length);
1665        }
1666
1667        let byte = *input.read_at(pos);
1668        if byte == b'\n' {
1669            // Newline ends the label.
1670            length += 1;
1671            return (length, whitespaces, label_length);
1672        } else if byte == b'\r' {
1673            // Handle CRLF sequences
1674            if pos + 1 < total && *input.read_at(pos + 1) == b'\n' {
1675                length += 2;
1676            } else {
1677                length += 1;
1678            }
1679            return (length, whitespaces, label_length);
1680        } else if is_part_of_identifier(&byte) && (!double_quoted || !terminated) {
1681            // For both unquoted and double-quoted (before the closing quote) heredoc,
1682            // a valid identifier character is part of the label.
1683            length += 1;
1684            label_length += 1;
1685        } else if double_quoted && !terminated && byte == b'"' {
1686            // In a double-quoted heredoc, a double quote terminates the label.
1687            length += 1;
1688            terminated = true;
1689        } else {
1690            // Malformed heredoc label: stop scanning and let the caller surface a parse error.
1691            return (length, whitespaces, label_length);
1692        }
1693    }
1694}
1695
1696#[inline]
1697fn read_start_of_nowdoc_document(input: &Input, prefix_len: usize) -> (usize, usize, usize) {
1698    let total = input.len();
1699    let base = input.current_offset();
1700
1701    let mut pos = base + 3 + prefix_len;
1702    let mut whitespaces = 0;
1703    while pos < total && input.read_at(pos).is_ascii_whitespace() {
1704        whitespaces += 1;
1705        pos += 1;
1706    }
1707
1708    // For nowdoc, the fixed extra offset is always 2.
1709    let mut length = 3 + prefix_len + whitespaces + 2;
1710
1711    let mut label_length = 1;
1712    let mut terminated = false;
1713    loop {
1714        let pos = base + length;
1715        if pos >= total {
1716            // Bail out gracefully on truncated input; surfacing the accumulated state lets the
1717            // lexer report a parse error instead of panicking.
1718            return (length, whitespaces, label_length);
1719        }
1720        let byte = *input.read_at(pos);
1721
1722        if byte == b'\n' {
1723            // A newline indicates the end of the label.
1724            length += 1;
1725            return (length, whitespaces, label_length);
1726        } else if byte == b'\r' {
1727            // Handle CRLF sequences
1728            if pos + 1 < total && *input.read_at(pos + 1) == b'\n' {
1729                length += 2;
1730            } else {
1731                length += 1;
1732            }
1733            return (length, whitespaces, label_length);
1734        } else if is_part_of_identifier(&byte) && !terminated {
1735            // For nowdoc, identifier characters contribute to the label until terminated.
1736            length += 1;
1737            label_length += 1;
1738        } else if !terminated && byte == b'\'' {
1739            // A single quote terminates the nowdoc label.
1740            length += 1;
1741            terminated = true;
1742        } else {
1743            // Malformed nowdoc label: stop scanning and let the caller surface a parse error.
1744            return (length, whitespaces, label_length);
1745        }
1746    }
1747}
1748
1749#[inline]
1750fn read_literal_string(input: &Input, quote: u8, prefix_len: usize) -> (TokenKind, usize) {
1751    let total = input.len();
1752    let start = input.current_offset();
1753    let skip = prefix_len + 1; // prefix + opening quote
1754    let mut length = skip;
1755
1756    let bytes = input.peek(skip, total - start - skip);
1757    loop {
1758        let scan_start = length - skip;
1759        match memchr2(quote, b'\\', &bytes[scan_start..]) {
1760            Some(pos) => {
1761                let abs_pos = scan_start + pos;
1762                let byte = bytes[abs_pos];
1763
1764                if byte == b'\\' {
1765                    length = skip + abs_pos + 2;
1766                    if length > total - start {
1767                        return (TokenKind::PartialLiteralString, total - start);
1768                    }
1769                } else {
1770                    length = skip + abs_pos + 1; // +1 for the closing quote
1771                    return (TokenKind::LiteralString, length);
1772                }
1773            }
1774            None => {
1775                // No quote or backslash found - EOF
1776                return (TokenKind::PartialLiteralString, total - start);
1777            }
1778        }
1779    }
1780}
1781
1782#[inline]
1783fn read_until_end_of_variable_interpolation(input: &Input, from: usize) -> u32 {
1784    let total = input.len();
1785    let base = input.current_offset();
1786    // `offset` is relative to the current position.
1787    let mut offset = from;
1788
1789    loop {
1790        let abs = base + offset;
1791        if abs >= total {
1792            // End of input.
1793            break;
1794        }
1795
1796        // Pattern 1: If the current byte is part of an identifier, simply advance.
1797        if is_part_of_identifier(input.read_at(abs)) {
1798            offset += 1;
1799            continue;
1800        }
1801
1802        // Pattern 2: If the current byte is a '[' then we enter a bracketed interpolation.
1803        if *input.read_at(abs) == b'[' {
1804            offset += 1;
1805            let mut nesting = 0;
1806            loop {
1807                let abs_inner = base + offset;
1808                if abs_inner >= total {
1809                    break;
1810                }
1811                let b = input.read_at(abs_inner);
1812                if *b == b']' {
1813                    offset += 1;
1814                    if nesting == 0 {
1815                        break;
1816                    }
1817
1818                    nesting -= 1;
1819                } else if *b == b'[' {
1820                    offset += 1;
1821                    nesting += 1;
1822                } else if b.is_ascii_whitespace() {
1823                    // Do not include whitespace.
1824                    break;
1825                } else {
1826                    offset += 1;
1827                }
1828            }
1829            // When bracketed interpolation is processed, exit the loop.
1830            break;
1831        }
1832
1833        // Pattern 3: Check for "->" followed by a valid identifier start.
1834        if base + offset + 2 < total
1835            && *input.read_at(abs) == b'-'
1836            && *input.read_at(base + offset + 1) == b'>'
1837            && is_start_of_identifier(input.read_at(base + offset + 2))
1838        {
1839            offset += 3;
1840            // Consume any following identifier characters.
1841            while base + offset < total && is_part_of_identifier(input.read_at(base + offset)) {
1842                offset += 1;
1843            }
1844            break;
1845        }
1846
1847        // Pattern 4: Check for "?->" followed by a valid identifier start.
1848        if base + offset + 3 < total
1849            && *input.read_at(abs) == b'?'
1850            && *input.read_at(base + offset + 1) == b'-'
1851            && *input.read_at(base + offset + 2) == b'>'
1852            && is_start_of_identifier(input.read_at(base + offset + 3))
1853        {
1854            offset += 4;
1855            while base + offset < total && is_part_of_identifier(input.read_at(base + offset)) {
1856                offset += 1;
1857            }
1858            break;
1859        }
1860
1861        // None of the expected patterns matched: exit the loop.
1862        break;
1863    }
1864
1865    offset as u32
1866}
1867
1868/// Scan forward from a `{$`/`${` brace interpolation to the offset just past
1869/// its matching `}` (tracking `{`/`}` nesting), or to end-of-input if the
1870/// interpolation is never closed.
1871///
1872/// The expression inside `{$...}` is real PHP and may contain nested string
1873/// literals - e.g. `"{$a["key"]}"` - whose own `{`/`}` bytes must not be
1874/// counted as interpolation braces (consider `"{$a["}"]}"`). Nested `'`, `"`,
1875/// and `` ` `` strings are therefore skipped wholesale, honouring `\` escapes,
1876/// so a `}` inside a nested string neither closes the interpolation early nor
1877/// is miscounted.
1878#[inline]
1879fn read_until_end_of_brace_interpolation(input: &Input, from: usize) -> u32 {
1880    let total = input.len();
1881    let base = input.current_offset();
1882    let mut offset = from;
1883    let mut nesting = 0;
1884
1885    loop {
1886        let abs = base + offset;
1887        if abs >= total {
1888            break;
1889        }
1890        match *input.read_at(abs) {
1891            b'}' => {
1892                offset += 1;
1893                if nesting == 0 {
1894                    break;
1895                }
1896
1897                nesting -= 1;
1898            }
1899            b'{' => {
1900                offset += 1;
1901                nesting += 1;
1902            }
1903            quote @ (b'\'' | b'"' | b'`') => {
1904                offset += 1;
1905                loop {
1906                    let abs = base + offset;
1907                    if abs >= total {
1908                        break;
1909                    }
1910                    match *input.read_at(abs) {
1911                        b'\\' => offset += 2,
1912                        b if b == quote => {
1913                            offset += 1;
1914                            break;
1915                        }
1916                        _ => offset += 1,
1917                    }
1918                }
1919            }
1920            _ => {
1921                offset += 1;
1922            }
1923        }
1924    }
1925
1926    offset as u32
1927}
1928
1929/// Scan a multi-line comment using SIMD-accelerated search.
1930/// Returns Some(length) including the closing */, or None if unterminated.
1931#[inline]
1932fn scan_multi_line_comment(bytes: &[u8]) -> Option<usize> {
1933    // Use SIMD to find */ quickly
1934    memmem::find(bytes, b"*/").map(|pos| pos + 2)
1935}
1936
1937/// Scan a single-line comment using SIMD-accelerated search.
1938/// Returns the length of the comment body (not including the //).
1939/// Stops at newline or ?>.
1940#[inline]
1941fn scan_single_line_comment(bytes: &[u8]) -> usize {
1942    let mut pos = 0;
1943    while pos < bytes.len() {
1944        match memchr::memchr3(b'\n', b'\r', b'?', &bytes[pos..]) {
1945            Some(offset) => {
1946                let found_pos = pos + offset;
1947                match bytes[found_pos] {
1948                    b'\n' | b'\r' => return found_pos,
1949                    b'?' => {
1950                        // Check if it's ?>
1951                        if found_pos + 1 < bytes.len() && bytes[found_pos + 1] == b'>' {
1952                            // Also check for whitespace before ?>
1953                            if found_pos > 0 && bytes[found_pos - 1].is_ascii_whitespace() {
1954                                return found_pos - 1;
1955                            }
1956                            return found_pos;
1957                        }
1958                        // Not ?>, continue searching
1959                        pos = found_pos + 1;
1960                    }
1961                    // `memchr3` only matches the three bytes we asked for; any other value here would
1962                    // indicate a memchr bug. Treat it as end-of-comment so the lexer keeps making progress.
1963                    _ => return found_pos,
1964                }
1965            }
1966            None => return bytes.len(),
1967        }
1968    }
1969
1970    bytes.len()
1971}