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