1use memchr::{memchr2, memmem};
2use php_ast::Span;
3
4use crate::token::{resolve_keyword, TokenKind};
5
6const fn make_whitespace_table() -> [bool; 256] {
14 let mut t = [false; 256];
15 t[b' ' as usize] = true;
16 t[b'\t' as usize] = true;
17 t[b'\r' as usize] = true;
18 t[b'\n' as usize] = true;
19 t[0x0C] = true; t
21}
22
23const fn make_ident_start_table() -> [bool; 256] {
24 let mut t = [false; 256];
25 let mut i = 0usize;
26 while i < 256 {
27 let b = i as u8;
28 t[i] = (b >= b'a' && b <= b'z') || (b >= b'A' && b <= b'Z') || b == b'_' || b >= 0x80;
29 i += 1;
30 }
31 t
32}
33
34const fn make_ident_continue_table() -> [bool; 256] {
35 let mut t = [false; 256];
36 let mut i = 0usize;
37 while i < 256 {
38 let b = i as u8;
39 t[i] = (b >= b'a' && b <= b'z')
40 || (b >= b'A' && b <= b'Z')
41 || (b >= b'0' && b <= b'9')
42 || b == b'_'
43 || b >= 0x80;
44 i += 1;
45 }
46 t
47}
48
49static IS_PHP_WHITESPACE: [bool; 256] = make_whitespace_table();
50static IS_IDENT_START: [bool; 256] = make_ident_start_table();
51static IS_IDENT_CONTINUE: [bool; 256] = make_ident_continue_table();
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum LexerErrorKind {
57 UnterminatedString,
59 FileTooLarge,
61 Other,
63}
64
65#[derive(Debug, Clone, PartialEq)]
67#[non_exhaustive]
68pub struct LexerError {
69 pub kind: LexerErrorKind,
71 pub message: String,
73 pub span: Span,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct Token {
80 pub kind: TokenKind,
82 pub span: Span,
84}
85
86impl Token {
87 pub fn new(kind: TokenKind, span: Span) -> Self {
89 Self { kind, span }
90 }
91
92 pub fn eof(offset: u32) -> Self {
94 Self {
95 kind: TokenKind::Eof,
96 span: Span::new(offset, offset),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102enum LexerMode {
103 InlineHtml,
104 Php,
105}
106
107pub struct Lexer<'src> {
109 source: &'src str,
110 mode: LexerMode,
111 pos: usize,
112 peeked: Option<Token>,
113 peeked2: Option<Token>,
114 non_verbatim_strings: Vec<u32>,
117 verbatim_cursor: usize,
120 errors: Vec<LexerError>,
121}
122
123#[inline(always)]
124fn is_ident_start(b: u8) -> bool {
125 IS_IDENT_START[b as usize]
126}
127
128#[inline(always)]
129fn is_ident_continue(b: u8) -> bool {
130 IS_IDENT_CONTINUE[b as usize]
131}
132
133fn label_len(s: &str) -> usize {
135 let bytes = s.as_bytes();
136 if !bytes.first().copied().is_some_and(is_ident_start) {
137 return 0;
138 }
139 1 + bytes[1..]
140 .iter()
141 .take_while(|b| is_ident_continue(**b))
142 .count()
143}
144
145fn skip_complex_interp(bytes: &[u8], mut p: usize) -> usize {
155 debug_assert!(bytes.get(p) == Some(&b'{'));
156 let mut in_dquote = false;
160 let mut depth = 0i32;
161 let mut suspended: Vec<i32> = Vec::new();
162 while p < bytes.len() {
163 match bytes[p] {
164 b'\\' => {
165 p += 1;
166 if p < bytes.len() {
167 p += 1;
168 }
169 }
170 b'"' if in_dquote => {
171 p += 1;
172 depth = suspended
173 .pop()
174 .expect("a nested string is entered from an interpolation");
175 in_dquote = false;
176 }
177 b'{' if in_dquote => {
178 if bytes.get(p + 1) == Some(&b'$') {
179 in_dquote = false;
181 depth = 0;
182 } else {
183 p += 1;
184 }
185 }
186 b'$' if in_dquote && bytes.get(p + 1) == Some(&b'{') => {
187 p += 1;
188 in_dquote = false;
189 depth = 0;
190 }
191 _ if in_dquote => p += 1,
192 b'{' => {
193 depth += 1;
194 p += 1;
195 }
196 b'}' => {
197 depth -= 1;
198 p += 1;
199 if depth == 0 {
200 if suspended.is_empty() {
201 return p;
202 }
203 in_dquote = true;
204 }
205 }
206 b'"' => {
207 suspended.push(depth);
208 in_dquote = true;
209 p += 1;
210 }
211 b'\'' => p = skip_nested_squoted(bytes, p),
212 _ => p += 1,
213 }
214 }
215 p
216}
217
218fn next_heredoc_line(bytes: &[u8], mut p: usize) -> usize {
221 while p < bytes.len() {
222 match bytes[p] {
223 b'\n' => return p + 1,
224 b'\\' if bytes.get(p + 1) != Some(&b'\n') => p += 2,
225 b'{' if bytes.get(p + 1) == Some(&b'$') => p = skip_complex_interp(bytes, p),
226 b'$' if bytes.get(p + 1) == Some(&b'{') => p = skip_complex_interp(bytes, p + 1),
227 _ => p += 1,
228 }
229 }
230 bytes.len()
231}
232
233fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
234 debug_assert!(bytes.get(p) == Some(&b'\''));
235 p += 1;
236 while p < bytes.len() {
237 match bytes[p] {
238 b'\\' => {
239 p += 1;
240 if p < bytes.len() {
241 p += 1;
242 }
243 }
244 b'\'' => return p + 1,
245 _ => p += 1,
246 }
247 }
248 p
249}
250
251fn starts_with_open_tag(rest: &[u8]) -> bool {
254 rest.starts_with(b"<?=")
255 || rest.len() >= 5
256 && rest.starts_with(b"<?")
257 && rest[2..5].eq_ignore_ascii_case(b"php")
258 && rest
259 .get(5)
260 .is_none_or(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
261}
262
263impl<'src> Lexer<'src> {
264 pub fn new(source: &'src str) -> Self {
269 if let Some(lexer) = Self::too_large(source) {
270 return lexer;
271 }
272
273 let pos = if source.starts_with("#!") {
275 source.find('\n').map(|p| p + 1).unwrap_or(source.len())
276 } else {
277 0
278 };
279
280 let mode = if starts_with_open_tag(&source.as_bytes()[pos..]) {
282 LexerMode::Php
283 } else {
284 LexerMode::InlineHtml
285 };
286
287 Self {
288 source,
289 mode,
290 pos,
291 peeked: None,
292 peeked2: None,
293 non_verbatim_strings: Vec::new(),
294 verbatim_cursor: 0,
295 errors: Vec::new(),
296 }
297 }
298
299 pub fn new_at(source: &'src str, offset: usize) -> Self {
306 if let Some(lexer) = Self::too_large(source) {
307 return lexer;
308 }
309
310 Self {
311 source,
312 mode: LexerMode::Php,
313 pos: offset,
314 peeked: None,
315 peeked2: None,
316 non_verbatim_strings: Vec::new(),
317 verbatim_cursor: 0,
318 errors: Vec::new(),
319 }
320 }
321
322 fn too_large(source: &str) -> Option<Self> {
325 if source.len() <= u32::MAX as usize {
326 return None;
327 }
328 Some(Self {
329 source: "",
330 mode: LexerMode::Php,
331 pos: 0,
332 peeked: None,
333 peeked2: None,
334 non_verbatim_strings: Vec::new(),
335 verbatim_cursor: 0,
336 errors: vec![LexerError {
337 kind: LexerErrorKind::FileTooLarge,
338 message: format!(
339 "source is {} bytes, which exceeds the maximum supported size of {} bytes",
340 source.len(),
341 u32::MAX
342 ),
343 span: Span::new(0, 0),
344 }],
345 })
346 }
347
348 pub fn errors(&self) -> &[LexerError] {
350 &self.errors
351 }
352
353 pub fn into_errors(self) -> Vec<LexerError> {
355 self.errors
356 }
357
358 #[doc(hidden)]
365 pub fn is_verbatim_string(&mut self, start: u32) -> bool {
366 let list = &self.non_verbatim_strings;
367 let mut i = self.verbatim_cursor;
368 if i > 0 && list.get(i - 1).is_some_and(|&prev| prev >= start) {
369 return list.binary_search(&start).is_err();
371 }
372 while list.get(i).is_some_and(|&next| next < start) {
373 i += 1;
374 }
375 self.verbatim_cursor = i;
376 list.get(i) != Some(&start)
377 }
378
379 pub fn source(&self) -> &'src str {
381 self.source
382 }
383
384 pub fn peek(&mut self) -> &Token {
386 if self.peeked.is_none() {
387 self.peeked = Some(self.read_next_token());
388 }
389 self.peeked.as_ref().expect("peeked is Some: set above")
390 }
391
392 pub fn peek2(&mut self) -> &Token {
394 if self.peeked.is_none() {
396 self.peeked = Some(self.read_next_token());
397 }
398 if self.peeked2.is_none() {
399 self.peeked2 = Some(self.read_next_token());
400 }
401 self.peeked2.as_ref().expect("peeked2 is Some: set above")
402 }
403
404 pub fn next_token(&mut self) -> Token {
406 if let Some(token) = self.peeked.take() {
407 self.peeked = self.peeked2.take();
408 return token;
409 }
410 self.read_next_token()
411 }
412
413 pub fn token_text(&self, token: &Token) -> &'src str {
415 &self.source[token.span.start as usize..token.span.end as usize]
416 }
417
418 fn read_next_token(&mut self) -> Token {
419 if self.pos >= self.source.len() {
420 return Token::eof(self.source.len() as u32);
421 }
422
423 match self.mode {
424 LexerMode::InlineHtml => self.lex_inline_html(),
425 LexerMode::Php => self.lex_php(),
426 }
427 }
428
429 fn lex_inline_html(&mut self) -> Token {
430 let start = self.pos;
431 let bytes = self.source.as_bytes();
432
433 let mut search = self.pos;
437 let tag_pos = loop {
438 match memchr::memchr(b'<', &bytes[search..]) {
439 None => break None,
440 Some(offset) => {
441 let p = search + offset;
442 if starts_with_open_tag(&bytes[p..]) {
443 break Some(p - self.pos);
444 }
445 search = p + 1;
446 }
447 }
448 };
449
450 if let Some(tag_pos) = tag_pos {
451 if tag_pos == 0 {
452 self.mode = LexerMode::Php;
454 return self.lex_php();
455 }
456 let end = self.pos + tag_pos;
458 self.pos = end;
459 self.mode = LexerMode::Php;
460 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
461 } else {
462 let end = self.source.len();
464 self.pos = end;
465 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
466 }
467 }
468
469 fn lex_php(&mut self) -> Token {
470 self.skip_whitespace();
472
473 if self.pos >= self.source.len() {
474 return Token::eof(self.source.len() as u32);
475 }
476
477 let bytes = self.source.as_bytes();
478 let start = self.pos;
479
480 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
483 self.pos += 2;
484 Self::skip_line_comment_body(bytes, &mut self.pos);
485 return self.tok(TokenKind::LineComment, start);
486 }
487
488 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
490 self.pos += 2;
491 let kind = if self.pos < bytes.len()
494 && bytes[self.pos] == b'*'
495 && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
496 {
497 TokenKind::DocComment
498 } else {
499 TokenKind::BlockComment
500 };
501 match memmem::find(&bytes[self.pos..], b"*/") {
502 Some(end) => self.pos += end + 2,
503 None => {
504 let span = Span::new(start as u32, self.source.len() as u32);
505 self.errors.push(LexerError {
506 kind: LexerErrorKind::Other,
507 message: "unterminated block comment".to_string(),
508 span,
509 });
510 self.pos = bytes.len();
511 }
512 }
513 return self.tok(kind, start);
514 }
515
516 if bytes[self.pos] == b'#' && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'[') {
519 self.pos += 1;
520 Self::skip_line_comment_body(bytes, &mut self.pos);
521 return self.tok(TokenKind::HashComment, start);
522 }
523
524 self.scan_token()
525 }
526
527 fn skip_whitespace(&mut self) {
529 let bytes = self.source.as_bytes();
530 while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
531 self.pos += 1;
532 }
533 }
534
535 fn scan_token(&mut self) -> Token {
537 let start = self.pos;
538 let bytes = self.source.as_bytes();
539 let b = bytes[start];
540
541 match b {
542 b'+' => {
544 if self.check_at(1, b'+') {
545 self.pos = start + 2;
546 self.tok(TokenKind::PlusPlus, start)
547 } else if self.check_at(1, b'=') {
548 self.pos = start + 2;
549 self.tok(TokenKind::PlusEquals, start)
550 } else {
551 self.pos = start + 1;
552 self.tok(TokenKind::Plus, start)
553 }
554 }
555 b'-' => {
556 if self.check_at(1, b'-') {
557 self.pos = start + 2;
558 self.tok(TokenKind::MinusMinus, start)
559 } else if self.check_at(1, b'=') {
560 self.pos = start + 2;
561 self.tok(TokenKind::MinusEquals, start)
562 } else if self.check_at(1, b'>') {
563 self.pos = start + 2;
564 self.tok(TokenKind::Arrow, start)
565 } else {
566 self.pos = start + 1;
567 self.tok(TokenKind::Minus, start)
568 }
569 }
570 b'*' => {
571 if self.check_at(1, b'*') {
572 if self.check_at(2, b'=') {
573 self.pos = start + 3;
574 self.tok(TokenKind::StarStarEquals, start)
575 } else {
576 self.pos = start + 2;
577 self.tok(TokenKind::StarStar, start)
578 }
579 } else if self.check_at(1, b'=') {
580 self.pos = start + 2;
581 self.tok(TokenKind::StarEquals, start)
582 } else {
583 self.pos = start + 1;
584 self.tok(TokenKind::Star, start)
585 }
586 }
587 b'/' => {
588 if self.check_at(1, b'=') {
590 self.pos = start + 2;
591 self.tok(TokenKind::SlashEquals, start)
592 } else {
593 self.pos = start + 1;
594 self.tok(TokenKind::Slash, start)
595 }
596 }
597 b'%' => {
598 if self.check_at(1, b'=') {
599 self.pos = start + 2;
600 self.tok(TokenKind::PercentEquals, start)
601 } else {
602 self.pos = start + 1;
603 self.tok(TokenKind::Percent, start)
604 }
605 }
606 b'.' => {
607 if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
609 self.pos = start + 1;
610 self.scan_digits(u8::is_ascii_digit);
611 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
613 self.try_scan_exponent();
614 }
615 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
617 self.consume_invalid_numeric_rest();
618 return self.invalid_numeric(start);
619 }
620 return self.tok(TokenKind::FloatLiteralLeadingDot, start);
621 }
622 if self.check_at(1, b'.') && self.check_at(2, b'.') {
623 self.pos = start + 3;
624 self.tok(TokenKind::Ellipsis, start)
625 } else if self.check_at(1, b'=') {
626 self.pos = start + 2;
627 self.tok(TokenKind::DotEquals, start)
628 } else {
629 self.pos = start + 1;
630 self.tok(TokenKind::Dot, start)
631 }
632 }
633 b'=' => {
634 if self.check_at(1, b'=') {
635 if self.check_at(2, b'=') {
636 self.pos = start + 3;
637 self.tok(TokenKind::EqualsEqualsEquals, start)
638 } else {
639 self.pos = start + 2;
640 self.tok(TokenKind::EqualsEquals, start)
641 }
642 } else if self.check_at(1, b'>') {
643 self.pos = start + 2;
644 self.tok(TokenKind::FatArrow, start)
645 } else {
646 self.pos = start + 1;
647 self.tok(TokenKind::Equals, start)
648 }
649 }
650 b'!' => {
651 if self.check_at(1, b'=') {
652 if self.check_at(2, b'=') {
653 self.pos = start + 3;
654 self.tok(TokenKind::BangEqualsEquals, start)
655 } else {
656 self.pos = start + 2;
657 self.tok(TokenKind::BangEquals, start)
658 }
659 } else {
660 self.pos = start + 1;
661 self.tok(TokenKind::Bang, start)
662 }
663 }
664 b'<' => self.scan_less_than(start),
665 b'>' => {
666 if self.check_at(1, b'>') {
667 if self.check_at(2, b'=') {
668 self.pos = start + 3;
669 self.tok(TokenKind::ShiftRightEquals, start)
670 } else {
671 self.pos = start + 2;
672 self.tok(TokenKind::ShiftRight, start)
673 }
674 } else if self.check_at(1, b'=') {
675 self.pos = start + 2;
676 self.tok(TokenKind::GreaterThanEquals, start)
677 } else {
678 self.pos = start + 1;
679 self.tok(TokenKind::GreaterThan, start)
680 }
681 }
682 b'&' => {
683 if self.check_at(1, b'&') {
684 self.pos = start + 2;
685 self.tok(TokenKind::AmpersandAmpersand, start)
686 } else if self.check_at(1, b'=') {
687 self.pos = start + 2;
688 self.tok(TokenKind::AmpersandEquals, start)
689 } else {
690 self.pos = start + 1;
691 self.tok(TokenKind::Ampersand, start)
692 }
693 }
694 b'|' => {
695 if self.check_at(1, b'|') {
696 self.pos = start + 2;
697 self.tok(TokenKind::PipePipe, start)
698 } else if self.check_at(1, b'=') {
699 self.pos = start + 2;
700 self.tok(TokenKind::PipeEquals, start)
701 } else if self.check_at(1, b'>') {
702 self.pos = start + 2;
703 self.tok(TokenKind::PipeArrow, start)
704 } else {
705 self.pos = start + 1;
706 self.tok(TokenKind::Pipe, start)
707 }
708 }
709 b'^' => {
710 if self.check_at(1, b'=') {
711 self.pos = start + 2;
712 self.tok(TokenKind::CaretEquals, start)
713 } else {
714 self.pos = start + 1;
715 self.tok(TokenKind::Caret, start)
716 }
717 }
718 b'~' => {
719 self.pos = start + 1;
720 self.tok(TokenKind::Tilde, start)
721 }
722 b'?' => {
723 if self.check_at(1, b'>') {
724 self.pos = start + 2;
725 self.mode = LexerMode::InlineHtml;
726 self.tok(TokenKind::CloseTag, start)
727 } else if self.check_at(1, b'?') {
728 if self.check_at(2, b'=') {
729 self.pos = start + 3;
730 self.tok(TokenKind::CoalesceEquals, start)
731 } else {
732 self.pos = start + 2;
733 self.tok(TokenKind::QuestionQuestion, start)
734 }
735 } else if self.check_at(1, b'-') && self.check_at(2, b'>') {
736 self.pos = start + 3;
737 self.tok(TokenKind::NullsafeArrow, start)
738 } else {
739 self.pos = start + 1;
740 self.tok(TokenKind::Question, start)
741 }
742 }
743 b':' => {
744 if self.check_at(1, b':') {
745 self.pos = start + 2;
746 self.tok(TokenKind::DoubleColon, start)
747 } else {
748 self.pos = start + 1;
749 self.tok(TokenKind::Colon, start)
750 }
751 }
752 b'@' => {
753 self.pos = start + 1;
754 self.tok(TokenKind::At, start)
755 }
756 b'\\' => {
757 self.pos = start + 1;
758 self.tok(TokenKind::Backslash, start)
759 }
760 b'#' => {
761 if self.check_at(1, b'[') {
764 self.pos = start + 2;
765 self.tok(TokenKind::HashBracket, start)
766 } else {
767 self.pos = start + 1;
769 self.read_next_token()
770 }
771 }
772
773 b'(' => {
775 self.pos = start + 1;
776 self.tok(TokenKind::LeftParen, start)
777 }
778 b')' => {
779 self.pos = start + 1;
780 self.tok(TokenKind::RightParen, start)
781 }
782 b'[' => {
783 self.pos = start + 1;
784 self.tok(TokenKind::LeftBracket, start)
785 }
786 b']' => {
787 self.pos = start + 1;
788 self.tok(TokenKind::RightBracket, start)
789 }
790 b'{' => {
791 self.pos = start + 1;
792 self.tok(TokenKind::LeftBrace, start)
793 }
794 b'}' => {
795 self.pos = start + 1;
796 self.tok(TokenKind::RightBrace, start)
797 }
798 b';' => {
799 self.pos = start + 1;
800 self.tok(TokenKind::Semicolon, start)
801 }
802 b',' => {
803 self.pos = start + 1;
804 self.tok(TokenKind::Comma, start)
805 }
806
807 b'\'' => self.scan_single_quoted_string(),
809 b'"' => self.scan_double_quoted_string(),
810 b'`' => self.scan_backtick_string(),
811
812 b'$' => {
814 if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
815 self.pos = start + 2;
816 while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
817 self.pos += 1;
818 }
819 self.tok(TokenKind::Variable, start)
820 } else {
821 self.pos = start + 1;
822 self.tok(TokenKind::Dollar, start)
823 }
824 }
825
826 b'0'..=b'9' => self.scan_number(),
828
829 _ if is_ident_start(b) => {
831 if b == b'b' || b == b'B' {
833 if self.check_at(1, b'\'') {
834 return self.scan_single_quoted_string();
835 }
836 if self.check_at(1, b'"') {
837 return self.scan_double_quoted_string();
838 }
839 if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
840 let remaining = &self.source[self.pos..];
841 if let Some(token) = self.try_lex_heredoc(remaining) {
842 return token;
843 }
844 }
845 }
846 self.scan_identifier()
847 }
848
849 _ => {
851 self.pos = start + 1;
852 self.read_next_token()
853 }
854 }
855 }
856
857 fn scan_less_than(&mut self, start: usize) -> Token {
859 if self.check_at(1, b'<') {
860 if self.check_at(2, b'<') {
861 let remaining = &self.source[self.pos..];
863 if let Some(token) = self.try_lex_heredoc(remaining) {
864 return token;
865 }
866 }
868 if self.check_at(2, b'=') {
869 self.pos = start + 3;
870 return self.tok(TokenKind::ShiftLeftEquals, start);
871 }
872 self.pos = start + 2;
873 return self.tok(TokenKind::ShiftLeft, start);
874 }
875 if self.check_at(1, b'=') {
876 if self.check_at(2, b'>') {
877 self.pos = start + 3;
878 return self.tok(TokenKind::Spaceship, start);
879 }
880 self.pos = start + 2;
881 return self.tok(TokenKind::LessThanEquals, start);
882 }
883 if self.check_at(1, b'>') {
885 self.pos = start + 2;
886 return self.tok(TokenKind::BangEquals, start);
887 }
888 if starts_with_open_tag(&self.source.as_bytes()[start..]) {
889 self.pos = start + if self.check_at(2, b'=') { 3 } else { 5 };
890 return self.tok(TokenKind::OpenTag, start);
891 }
892 self.pos = start + 1;
893 self.tok(TokenKind::LessThan, start)
894 }
895
896 fn scan_single_quoted_string(&mut self) -> Token {
899 let start = self.pos;
900 let bytes = self.source.as_bytes();
901 let mut p = self.pos;
902 if bytes[p] == b'b' || bytes[p] == b'B' {
904 p += 1;
905 }
906 p += 1; loop {
908 match memchr2(b'\\', b'\'', &bytes[p..]) {
909 None => {
910 self.errors.push(LexerError {
911 kind: LexerErrorKind::UnterminatedString,
912 message: "unterminated string literal".to_string(),
913 span: Span::new(start as u32, self.source.len() as u32),
914 });
915 self.mark_non_verbatim(start);
916 self.pos = self.source.len();
917 return self.tok(TokenKind::SingleQuotedString, start);
918 }
919 Some(offset) => {
920 p += offset;
921 match bytes[p] {
922 b'\\' => {
923 self.mark_non_verbatim(start);
924 p += 1;
925 if p < bytes.len() {
926 p += 1;
927 }
928 }
929 _ => {
930 p += 1;
932 break;
933 }
934 }
935 }
936 }
937 }
938 self.pos = p;
939 self.tok(TokenKind::SingleQuotedString, start)
940 }
941
942 fn scan_double_quoted_string(&mut self) -> Token {
943 let start = self.pos;
944 let bytes = self.source.as_bytes();
945 let mut p = self.pos;
946 if bytes[p] == b'b' || bytes[p] == b'B' {
948 p += 1;
949 }
950 p += 1; let mut verbatim = true;
952 loop {
953 let segment = p;
954 let Some(offset) = memchr::memchr3(b'\\', b'"', b'$', &bytes[p..]) else {
955 self.errors.push(LexerError {
956 kind: LexerErrorKind::UnterminatedString,
957 message: "unterminated string literal".to_string(),
958 span: Span::new(start as u32, self.source.len() as u32),
959 });
960 self.mark_non_verbatim(start);
961 self.pos = self.source.len();
962 return self.tok(TokenKind::DoubleQuotedString, start);
963 };
964 p += offset;
965 match bytes[p] {
966 b'\\' => {
967 verbatim = false;
968 p = (p + 2).min(bytes.len());
969 }
970 b'"' => {
971 p += 1;
972 break;
973 }
974 _ => {
976 verbatim = false;
977 if p > segment && bytes[p - 1] == b'{' {
982 p = skip_complex_interp(bytes, p - 1);
983 } else if bytes.get(p + 1) == Some(&b'{') {
984 p = skip_complex_interp(bytes, p + 1);
985 } else {
986 p += 1;
987 }
988 }
989 }
990 }
991 if !verbatim {
992 self.mark_non_verbatim(start);
993 }
994 self.pos = p;
995 self.tok(TokenKind::DoubleQuotedString, start)
996 }
997
998 #[inline]
999 fn mark_non_verbatim(&mut self, start: usize) {
1000 if self.non_verbatim_strings.last() != Some(&(start as u32)) {
1002 self.non_verbatim_strings.push(start as u32);
1003 }
1004 }
1005
1006 fn scan_backtick_string(&mut self) -> Token {
1007 let start = self.pos;
1008 let bytes = self.source.as_bytes();
1009 let mut p = self.pos;
1010 p += 1; loop {
1012 match memchr2(b'\\', b'`', &bytes[p..]) {
1013 None => {
1014 self.errors.push(LexerError {
1015 kind: LexerErrorKind::UnterminatedString,
1016 message: "unterminated string literal".to_string(),
1017 span: Span::new(start as u32, self.source.len() as u32),
1018 });
1019 self.pos = self.source.len();
1020 return self.tok(TokenKind::BacktickString, start);
1021 }
1022 Some(offset) => {
1023 p += offset;
1024 match bytes[p] {
1025 b'\\' => {
1026 p += 1;
1027 if p < bytes.len() {
1028 p += 1;
1029 }
1030 }
1031 _ => {
1032 p += 1;
1034 break;
1035 }
1036 }
1037 }
1038 }
1039 }
1040 self.pos = p;
1041 self.tok(TokenKind::BacktickString, start)
1042 }
1043
1044 fn scan_number(&mut self) -> Token {
1047 let start = self.pos;
1048 let bytes = self.source.as_bytes();
1049
1050 if bytes[start] == b'0' && start + 1 < bytes.len() {
1052 match bytes[start + 1] {
1053 b'x' | b'X' => {
1054 self.pos = start + 2;
1055 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1056 self.consume_invalid_numeric_rest();
1057 return self.invalid_numeric(start);
1058 }
1059 if self.scan_digits(u8::is_ascii_hexdigit) {
1060 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1061 self.consume_invalid_numeric_rest();
1062 return self.invalid_numeric(start);
1063 }
1064 return self.tok(TokenKind::HexIntLiteral, start);
1065 }
1066 self.pos = start;
1068 }
1069 b'b' | b'B' => {
1070 self.pos = start + 2;
1071 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1072 self.consume_invalid_numeric_rest();
1073 return self.invalid_numeric(start);
1074 }
1075 if self.scan_digits(|b| b == &b'0' || b == &b'1') {
1076 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1077 self.consume_invalid_numeric_rest();
1078 return self.invalid_numeric(start);
1079 }
1080 return self.tok(TokenKind::BinIntLiteral, start);
1081 }
1082 self.pos = start;
1084 }
1085 b'o' | b'O' => {
1086 self.pos = start + 2;
1087 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1088 self.consume_invalid_numeric_rest();
1089 return self.invalid_numeric(start);
1090 }
1091 if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
1092 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1093 self.consume_invalid_numeric_rest();
1094 return self.invalid_numeric(start);
1095 }
1096 return self.tok(TokenKind::OctIntLiteralNew, start);
1097 }
1098 self.pos = start;
1100 }
1101 _ => {}
1102 }
1103 }
1104
1105 self.pos = start;
1107 self.scan_digits(u8::is_ascii_digit);
1108 let integer_end = self.pos;
1109 let mut kind = TokenKind::IntLiteral;
1110
1111 if bytes[start] == b'0' && integer_end > start + 1 {
1114 kind = TokenKind::OctIntLiteral;
1115 }
1116
1117 if self.pos < bytes.len() && bytes[self.pos] == b'.' {
1119 if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
1120 self.pos += 1; self.scan_digits(u8::is_ascii_digit);
1123 kind = TokenKind::FloatLiteralSimple;
1124 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1125 self.consume_invalid_numeric_rest();
1127 return self.invalid_numeric(start);
1128 } else {
1129 self.pos += 1; kind = TokenKind::FloatLiteralSimple;
1132 }
1133 }
1134
1135 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
1137 if self.try_scan_exponent() {
1138 kind = TokenKind::FloatLiteral;
1139 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1140 self.consume_invalid_numeric_rest();
1142 return self.invalid_numeric(start);
1143 }
1144 }
1145
1146 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1148 self.consume_invalid_numeric_rest();
1149 return self.invalid_numeric(start);
1150 }
1151
1152 self.tok(kind, start)
1153 }
1154
1155 fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
1158 let bytes = self.source.as_bytes();
1159 if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
1160 return false;
1161 }
1162 self.pos += 1;
1163 loop {
1164 if self.pos >= bytes.len() {
1165 break;
1166 }
1167 if is_valid(&bytes[self.pos]) {
1168 self.pos += 1;
1169 } else if bytes[self.pos] == b'_'
1170 && self.pos + 1 < bytes.len()
1171 && is_valid(&bytes[self.pos + 1])
1172 {
1173 self.pos += 2;
1174 } else {
1175 break;
1176 }
1177 }
1178 true
1179 }
1180
1181 fn try_scan_exponent(&mut self) -> bool {
1184 let bytes = self.source.as_bytes();
1185 let saved = self.pos;
1186 self.pos += 1; if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
1190 self.pos += 1;
1191 }
1192
1193 if self.scan_digits(u8::is_ascii_digit) {
1195 true
1196 } else {
1197 self.pos = saved;
1198 false
1199 }
1200 }
1201
1202 fn scan_identifier(&mut self) -> Token {
1205 let start = self.pos;
1206 let bytes = self.source.as_bytes();
1207 self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
1209 self.pos += 1;
1210 }
1211 let text = &self.source[start..self.pos];
1212 let starts_qualified_name = bytes.get(self.pos) == Some(&b'\\')
1215 && bytes.get(self.pos + 1).is_some_and(|&b| is_ident_start(b));
1216 let kind = match resolve_keyword(text) {
1217 Some(TokenKind::Namespace) => TokenKind::Namespace,
1218 Some(_) if starts_qualified_name => TokenKind::Identifier,
1219 Some(kind) => kind,
1220 None => TokenKind::Identifier,
1221 };
1222 self.tok(kind, start)
1223 }
1224
1225 #[inline]
1231 fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
1232 loop {
1233 match memchr2(b'\n', b'?', &bytes[*pos..]) {
1234 None => {
1235 *pos = bytes.len();
1236 return;
1237 }
1238 Some(offset) => {
1239 let p = *pos + offset;
1240 if bytes[p] == b'\n' {
1241 *pos = p; return;
1243 }
1244 if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
1246 *pos = p; return;
1248 }
1249 *pos = p + 1;
1251 }
1252 }
1253 }
1254 }
1255
1256 #[inline]
1257 fn check_at(&self, offset: usize, expected: u8) -> bool {
1258 self.source.as_bytes().get(self.pos + offset) == Some(&expected)
1259 }
1260
1261 #[inline]
1262 fn tok(&self, kind: TokenKind, start: usize) -> Token {
1263 Token::new(kind, Span::new(start as u32, self.pos as u32))
1264 }
1265
1266 fn invalid_numeric(&mut self, start: usize) -> Token {
1267 let span = Span::new(start as u32, self.pos as u32);
1268 self.errors.push(LexerError {
1269 kind: LexerErrorKind::Other,
1270 message: "Invalid numeric literal".to_string(),
1271 span,
1272 });
1273 Token::new(TokenKind::InvalidNumericLiteral, span)
1274 }
1275
1276 fn consume_invalid_numeric_rest(&mut self) {
1278 let bytes = self.source.as_bytes();
1279 while self.pos < bytes.len() {
1280 let b = bytes[self.pos];
1281 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
1282 if (b == b'+' || b == b'-') && self.pos > 0 {
1284 let prev = bytes[self.pos - 1];
1285 if prev != b'e' && prev != b'E' {
1286 break;
1287 }
1288 }
1289 self.pos += 1;
1290 } else {
1291 break;
1292 }
1293 }
1294 }
1295
1296 fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
1300 let trimmed = remaining.trim_start_matches(|c: char| {
1302 c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\x0C'
1303 });
1304 let ws_len = remaining.len() - trimmed.len();
1305
1306 let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
1308 || trimmed.starts_with("B<<<"))
1309 && !trimmed[1..].starts_with("<<<>")
1310 {
1311 (&trimmed[1..], 1)
1312 } else {
1313 (trimmed, 0)
1314 };
1315
1316 if !after_prefix.starts_with("<<<") {
1317 return None;
1318 }
1319
1320 let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
1323 let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
1324 let arrows_offset =
1325 ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
1326
1327 let (label, is_nowdoc, label_line_end);
1329 if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
1330 let closing = label_len(after_quote);
1332 if after_quote.as_bytes().get(closing) != Some(&b'\'') {
1333 return None;
1334 }
1335 label = &after_quote[..closing];
1336 is_nowdoc = true;
1337 let after_label = &after_arrows_trimmed[2 + closing..];
1338 if !(after_label.starts_with('\n') || after_label.starts_with("\r\n")) {
1339 return None;
1340 }
1341 let nl = after_label.find('\n').unwrap_or(after_label.len());
1342 label_line_end = arrows_offset + 2 + closing + nl;
1343 if label_line_end < remaining.len() {
1344 }
1346 } else {
1347 let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
1349 let closing = label_len(after_dquote);
1350 if after_dquote.as_bytes().get(closing) != Some(&b'"') {
1351 return None;
1352 }
1353 label = &after_dquote[..closing];
1354 &after_dquote[1 + closing..]
1355 } else {
1356 let end = label_len(after_arrows_trimmed);
1357 label = &after_arrows_trimmed[..end];
1358 &after_arrows_trimmed[end..]
1359 };
1360 if !(s.starts_with('\n') || s.starts_with("\r\n")) {
1362 return None;
1363 }
1364 is_nowdoc = false;
1365 let nl = s.find('\n').unwrap_or(s.len());
1366 label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl;
1367 };
1368
1369 if label.is_empty() {
1370 return None;
1371 }
1372
1373 let body_start_in_remaining = if label_line_end < remaining.len() {
1375 label_line_end + 1 } else {
1377 return None; };
1379
1380 let body = &remaining[body_start_in_remaining..];
1381
1382 let mut search_pos = 0;
1384 let end_marker_pos;
1385 loop {
1386 if search_pos >= body.len() {
1387 return None; }
1389 let line_start = search_pos;
1390 let line_end = body[line_start..]
1391 .find('\n')
1392 .map(|p| line_start + p)
1393 .unwrap_or(body.len());
1394 let line = &body[line_start..line_end];
1395 let trimmed_line = line.trim_start_matches([' ', '\t']);
1396
1397 if trimmed_line.len() >= label.len()
1402 && &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
1403 && !trimmed_line
1404 .as_bytes()
1405 .get(label.len())
1406 .copied()
1407 .is_some_and(is_ident_continue)
1408 {
1409 end_marker_pos = line_start;
1410 break;
1411 }
1412
1413 search_pos = if is_nowdoc {
1414 if line_end < body.len() {
1415 line_end + 1
1416 } else {
1417 body.len()
1418 }
1419 } else {
1420 next_heredoc_line(body.as_bytes(), line_start)
1421 };
1422 }
1423
1424 let end_marker_line = &body[end_marker_pos..];
1426 let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
1427 let indent_len = end_marker_line.len() - trimmed.len();
1428 let token_end_in_remaining =
1429 body_start_in_remaining + end_marker_pos + indent_len + label.len();
1430 self.pos = base_pos + token_end_in_remaining;
1431
1432 let span = Span::new(start as u32, self.pos as u32);
1433
1434 if is_nowdoc {
1435 Some(Token::new(TokenKind::Nowdoc, span))
1436 } else {
1437 Some(Token::new(TokenKind::Heredoc, span))
1438 }
1439 }
1440}
1441
1442pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
1450 let mut lexer = Lexer::new(source);
1451 let mut tokens = Vec::with_capacity(lexer.source().len() / 8);
1453
1454 loop {
1455 let tok = lexer.next_token();
1456 let is_eof = tok.kind == TokenKind::Eof;
1457 tokens.push(tok);
1458 if is_eof {
1459 break;
1460 }
1461 }
1462
1463 let eof_span = tokens
1467 .last()
1468 .expect("tokens guaranteed to contain at least the Eof token from lexer")
1469 .span;
1470 tokens.push(Token::new(TokenKind::Eof, eof_span));
1471
1472 let errors = lexer.errors;
1473 (tokens, errors)
1474}