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
11const 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
28const 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#[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: VecDeque<Token<'input>>,
93}
94
95impl<'input> Lexer<'input> {
96 const MAX_INTERPOLATION_DEPTH: u16 = 64;
98
99 #[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 #[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 #[must_use]
153 pub fn has_reached_eof(&self) -> bool {
154 self.input.has_reached_eof()
155 }
156
157 #[inline]
159 #[must_use]
160 pub const fn current_position(&self) -> Position {
161 self.input.current_position()
162 }
163
164 #[inline]
197 pub fn advance(&mut self) -> Option<Result<Token<'input>, SyntaxError>> {
198 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 if offset == 0
216 && self.input.len() >= 2
217 && unsafe { *self.input.read_at_unchecked(0) } == b'#'
219 && 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 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 let candidate = unsafe { bytes.get_unchecked(pos..) };
277
278 if candidate.len() >= 5
279 && (unsafe { *candidate.get_unchecked(2) } | 0x20) == b'p'
281 && (unsafe { *candidate.get_unchecked(3) } | 0x20) == b'h'
283 && (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 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 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 }
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; } 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 [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 let lower = b | 0x20; 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 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 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 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 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 None
1136 }
1137 }
1138 }
1139 }
1140 }
1141
1142 #[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 #[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 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 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 let mut length = 3 + prefix_len;
1357 while base + length < total && input.read_at(base + length).is_ascii_whitespace() {
1359 length += 1;
1360 }
1361
1362 if base + length >= total || !is_start_of_identifier(input.read_at(base + length)) {
1364 return false;
1365 }
1366 length += 1; loop {
1370 let pos = base + length;
1371 if pos >= total {
1372 return false; }
1374
1375 let byte = *input.read_at(pos);
1376 if byte == b'\n' {
1377 return true; } else if byte == b'\r' {
1379 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; }
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 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 if base + length >= total || *input.read_at(base + length) != b'"' {
1402 return false;
1403 }
1404 length += 1;
1405
1406 if base + length >= total || !is_start_of_identifier(input.read_at(base + length)) {
1408 return false;
1409 }
1410 length += 1;
1411
1412 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 return terminated;
1423 } else if *byte == b'\r' {
1424 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 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 if base + length >= total || *input.read_at(base + length) != b'\'' {
1450 return false;
1451 }
1452 length += 1;
1453
1454 if base + length >= total || !is_start_of_identifier(input.read_at(base + length)) {
1456 return false;
1457 }
1458 length += 1;
1459
1460 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 let mut pos = base + 1 + prefix_len;
1490 loop {
1491 if pos >= total {
1492 return true;
1494 }
1495 let byte = *input.read_at(pos);
1496 if byte == b'"' {
1497 return true;
1499 }
1500 if byte == b'\\' {
1501 pos += 2;
1503 continue;
1504 }
1505
1506 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
1518fn 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
1575fn 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#[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#[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#[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#[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 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 let mut length = 3 + prefix_len + whitespaces + if double_quoted { 2 } else { 1 };
1654
1655 let mut label_length = 1; let mut terminated = false; loop {
1658 let pos = base + length;
1659 if pos >= total {
1664 return (length, whitespaces, label_length);
1665 }
1666
1667 let byte = *input.read_at(pos);
1668 if byte == b'\n' {
1669 length += 1;
1671 return (length, whitespaces, label_length);
1672 } else if byte == b'\r' {
1673 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 length += 1;
1684 label_length += 1;
1685 } else if double_quoted && !terminated && byte == b'"' {
1686 length += 1;
1688 terminated = true;
1689 } else {
1690 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 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 return (length, whitespaces, label_length);
1719 }
1720 let byte = *input.read_at(pos);
1721
1722 if byte == b'\n' {
1723 length += 1;
1725 return (length, whitespaces, label_length);
1726 } else if byte == b'\r' {
1727 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 length += 1;
1737 label_length += 1;
1738 } else if !terminated && byte == b'\'' {
1739 length += 1;
1741 terminated = true;
1742 } else {
1743 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; 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; return (TokenKind::LiteralString, length);
1772 }
1773 }
1774 None => {
1775 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 let mut offset = from;
1788
1789 loop {
1790 let abs = base + offset;
1791 if abs >= total {
1792 break;
1794 }
1795
1796 if is_part_of_identifier(input.read_at(abs)) {
1798 offset += 1;
1799 continue;
1800 }
1801
1802 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 break;
1825 } else {
1826 offset += 1;
1827 }
1828 }
1829 break;
1831 }
1832
1833 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 while base + offset < total && is_part_of_identifier(input.read_at(base + offset)) {
1842 offset += 1;
1843 }
1844 break;
1845 }
1846
1847 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 break;
1863 }
1864
1865 offset as u32
1866}
1867
1868#[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#[inline]
1932fn scan_multi_line_comment(bytes: &[u8]) -> Option<usize> {
1933 memmem::find(bytes, b"*/").map(|pos| pos + 2)
1935}
1936
1937#[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 if found_pos + 1 < bytes.len() && bytes[found_pos + 1] == b'>' {
1952 if found_pos > 0 && bytes[found_pos - 1].is_ascii_whitespace() {
1954 return found_pos - 1;
1955 }
1956 return found_pos;
1957 }
1958 pos = found_pos + 1;
1960 }
1961 _ => return found_pos,
1964 }
1965 }
1966 None => return bytes.len(),
1967 }
1968 }
1969
1970 bytes.len()
1971}