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)]
67pub struct LexerError {
68 pub kind: LexerErrorKind,
70 pub message: String,
72 pub span: Span,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct Token {
79 pub kind: TokenKind,
81 pub span: Span,
83}
84
85impl Token {
86 pub fn new(kind: TokenKind, span: Span) -> Self {
88 Self { kind, span }
89 }
90
91 pub fn eof(offset: u32) -> Self {
93 Self {
94 kind: TokenKind::Eof,
95 span: Span::new(offset, offset),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101enum LexerMode {
102 InlineHtml,
103 Php,
104}
105
106pub struct Lexer<'src> {
108 source: &'src str,
109 mode: LexerMode,
110 pos: usize,
111 peeked: Option<Token>,
112 peeked2: Option<Token>,
113 non_verbatim_strings: Vec<u32>,
116 verbatim_cursor: usize,
119 pub 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 skip_complex_interp(bytes: &[u8], mut p: usize) -> usize {
143 debug_assert!(bytes.get(p) == Some(&b'{'));
144 let mut in_dquote = false;
148 let mut depth = 0i32;
149 let mut suspended: Vec<i32> = Vec::new();
150 while p < bytes.len() {
151 match bytes[p] {
152 b'\\' => {
153 p += 1;
154 if p < bytes.len() {
155 p += 1;
156 }
157 }
158 b'"' if in_dquote => {
159 p += 1;
160 depth = suspended
161 .pop()
162 .expect("a nested string is entered from an interpolation");
163 in_dquote = false;
164 }
165 b'{' if in_dquote => {
166 if bytes.get(p + 1) == Some(&b'$') {
167 in_dquote = false;
169 depth = 0;
170 } else {
171 p += 1;
172 }
173 }
174 _ if in_dquote => p += 1,
175 b'{' => {
176 depth += 1;
177 p += 1;
178 }
179 b'}' => {
180 depth -= 1;
181 p += 1;
182 if depth == 0 {
183 if suspended.is_empty() {
184 return p;
185 }
186 in_dquote = true;
187 }
188 }
189 b'"' => {
190 suspended.push(depth);
191 in_dquote = true;
192 p += 1;
193 }
194 b'\'' => p = skip_nested_squoted(bytes, p),
195 _ => p += 1,
196 }
197 }
198 p
199}
200
201fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
202 debug_assert!(bytes.get(p) == Some(&b'\''));
203 p += 1;
204 while p < bytes.len() {
205 match bytes[p] {
206 b'\\' => {
207 p += 1;
208 if p < bytes.len() {
209 p += 1;
210 }
211 }
212 b'\'' => return p + 1,
213 _ => p += 1,
214 }
215 }
216 p
217}
218
219impl<'src> Lexer<'src> {
220 pub fn new(source: &'src str) -> Self {
225 if let Some(lexer) = Self::too_large(source) {
226 return lexer;
227 }
228
229 let pos = if source.starts_with("#!") {
231 source.find('\n').map(|p| p + 1).unwrap_or(source.len())
232 } else {
233 0
234 };
235
236 let remaining = &source[pos..];
238 let rem_bytes = remaining.as_bytes();
239 let mode = if (rem_bytes.len() >= 5
240 && rem_bytes[0] == b'<'
241 && rem_bytes[1] == b'?'
242 && rem_bytes[2..5].eq_ignore_ascii_case(b"php"))
243 || remaining.starts_with("<?=")
244 {
245 LexerMode::Php
246 } else {
247 LexerMode::InlineHtml
248 };
249
250 Self {
251 source,
252 mode,
253 pos,
254 peeked: None,
255 peeked2: None,
256 non_verbatim_strings: Vec::new(),
257 verbatim_cursor: 0,
258 errors: Vec::new(),
259 }
260 }
261
262 pub fn new_at(source: &'src str, offset: usize) -> Self {
269 if let Some(lexer) = Self::too_large(source) {
270 return lexer;
271 }
272
273 Self {
274 source,
275 mode: LexerMode::Php,
276 pos: offset,
277 peeked: None,
278 peeked2: None,
279 non_verbatim_strings: Vec::new(),
280 verbatim_cursor: 0,
281 errors: Vec::new(),
282 }
283 }
284
285 fn too_large(source: &str) -> Option<Self> {
288 if source.len() <= u32::MAX as usize {
289 return None;
290 }
291 Some(Self {
292 source: "",
293 mode: LexerMode::Php,
294 pos: 0,
295 peeked: None,
296 peeked2: None,
297 non_verbatim_strings: Vec::new(),
298 verbatim_cursor: 0,
299 errors: vec![LexerError {
300 kind: LexerErrorKind::FileTooLarge,
301 message: format!(
302 "source is {} bytes, which exceeds the maximum supported size of {} bytes",
303 source.len(),
304 u32::MAX
305 ),
306 span: Span::new(0, 0),
307 }],
308 })
309 }
310
311 pub fn is_verbatim_string(&mut self, start: u32) -> bool {
317 let list = &self.non_verbatim_strings;
318 let mut i = self.verbatim_cursor;
319 if i > 0 && list.get(i - 1).is_some_and(|&prev| prev >= start) {
320 return list.binary_search(&start).is_err();
322 }
323 while list.get(i).is_some_and(|&next| next < start) {
324 i += 1;
325 }
326 self.verbatim_cursor = i;
327 list.get(i) != Some(&start)
328 }
329
330 pub fn source(&self) -> &'src str {
332 self.source
333 }
334
335 pub fn peek(&mut self) -> &Token {
337 if self.peeked.is_none() {
338 self.peeked = Some(self.read_next_token());
339 }
340 self.peeked.as_ref().expect("peeked is Some: set above")
341 }
342
343 pub fn peek2(&mut self) -> &Token {
345 if self.peeked.is_none() {
347 self.peeked = Some(self.read_next_token());
348 }
349 if self.peeked2.is_none() {
350 self.peeked2 = Some(self.read_next_token());
351 }
352 self.peeked2.as_ref().expect("peeked2 is Some: set above")
353 }
354
355 pub fn next_token(&mut self) -> Token {
357 if let Some(token) = self.peeked.take() {
358 self.peeked = self.peeked2.take();
359 return token;
360 }
361 self.read_next_token()
362 }
363
364 pub fn token_text(&self, token: &Token) -> &'src str {
366 &self.source[token.span.start as usize..token.span.end as usize]
367 }
368
369 fn read_next_token(&mut self) -> Token {
370 if self.pos >= self.source.len() {
371 return Token::eof(self.source.len() as u32);
372 }
373
374 match self.mode {
375 LexerMode::InlineHtml => self.lex_inline_html(),
376 LexerMode::Php => self.lex_php(),
377 }
378 }
379
380 fn lex_inline_html(&mut self) -> Token {
381 let start = self.pos;
382 let bytes = self.source.as_bytes();
383
384 let mut search = self.pos;
388 let tag_pos = loop {
389 match memchr::memchr(b'<', &bytes[search..]) {
390 None => break None,
391 Some(offset) => {
392 let p = search + offset;
393 let rest = &bytes[p..];
394 if (rest.len() >= 5
395 && rest[0] == b'<'
396 && rest[1] == b'?'
397 && rest[2..5].eq_ignore_ascii_case(b"php"))
398 || rest.starts_with(b"<?=")
399 {
400 break Some(p - self.pos);
401 }
402 search = p + 1;
403 }
404 }
405 };
406
407 if let Some(tag_pos) = tag_pos {
408 if tag_pos == 0 {
409 self.mode = LexerMode::Php;
411 return self.lex_php();
412 }
413 let end = self.pos + tag_pos;
415 self.pos = end;
416 self.mode = LexerMode::Php;
417 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
418 } else {
419 let end = self.source.len();
421 self.pos = end;
422 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
423 }
424 }
425
426 fn lex_php(&mut self) -> Token {
427 self.skip_whitespace();
429
430 if self.pos >= self.source.len() {
431 return Token::eof(self.source.len() as u32);
432 }
433
434 let bytes = self.source.as_bytes();
435 let start = self.pos;
436
437 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
440 self.pos += 2;
441 Self::skip_line_comment_body(bytes, &mut self.pos);
442 return self.tok(TokenKind::LineComment, start);
443 }
444
445 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
447 self.pos += 2;
448 let kind = if self.pos < bytes.len()
451 && bytes[self.pos] == b'*'
452 && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
453 {
454 TokenKind::DocComment
455 } else {
456 TokenKind::BlockComment
457 };
458 match memmem::find(&bytes[self.pos..], b"*/") {
459 Some(end) => self.pos += end + 2,
460 None => {
461 let span = Span::new(start as u32, self.source.len() as u32);
462 self.errors.push(LexerError {
463 kind: LexerErrorKind::Other,
464 message: "unterminated block comment".to_string(),
465 span,
466 });
467 self.pos = bytes.len();
468 }
469 }
470 return self.tok(kind, start);
471 }
472
473 if bytes[self.pos] == b'#' && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'[') {
476 self.pos += 1;
477 Self::skip_line_comment_body(bytes, &mut self.pos);
478 return self.tok(TokenKind::HashComment, start);
479 }
480
481 self.scan_token()
482 }
483
484 fn skip_whitespace(&mut self) {
486 let bytes = self.source.as_bytes();
487 while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
488 self.pos += 1;
489 }
490 }
491
492 fn scan_token(&mut self) -> Token {
494 let start = self.pos;
495 let bytes = self.source.as_bytes();
496 let b = bytes[start];
497
498 match b {
499 b'+' => {
501 if self.check_at(1, b'+') {
502 self.pos = start + 2;
503 self.tok(TokenKind::PlusPlus, start)
504 } else if self.check_at(1, b'=') {
505 self.pos = start + 2;
506 self.tok(TokenKind::PlusEquals, start)
507 } else {
508 self.pos = start + 1;
509 self.tok(TokenKind::Plus, start)
510 }
511 }
512 b'-' => {
513 if self.check_at(1, b'-') {
514 self.pos = start + 2;
515 self.tok(TokenKind::MinusMinus, start)
516 } else if self.check_at(1, b'=') {
517 self.pos = start + 2;
518 self.tok(TokenKind::MinusEquals, start)
519 } else if self.check_at(1, b'>') {
520 self.pos = start + 2;
521 self.tok(TokenKind::Arrow, start)
522 } else {
523 self.pos = start + 1;
524 self.tok(TokenKind::Minus, start)
525 }
526 }
527 b'*' => {
528 if self.check_at(1, b'*') {
529 if self.check_at(2, b'=') {
530 self.pos = start + 3;
531 self.tok(TokenKind::StarStarEquals, start)
532 } else {
533 self.pos = start + 2;
534 self.tok(TokenKind::StarStar, start)
535 }
536 } else if self.check_at(1, b'=') {
537 self.pos = start + 2;
538 self.tok(TokenKind::StarEquals, start)
539 } else {
540 self.pos = start + 1;
541 self.tok(TokenKind::Star, start)
542 }
543 }
544 b'/' => {
545 if self.check_at(1, b'=') {
547 self.pos = start + 2;
548 self.tok(TokenKind::SlashEquals, start)
549 } else {
550 self.pos = start + 1;
551 self.tok(TokenKind::Slash, start)
552 }
553 }
554 b'%' => {
555 if self.check_at(1, b'=') {
556 self.pos = start + 2;
557 self.tok(TokenKind::PercentEquals, start)
558 } else {
559 self.pos = start + 1;
560 self.tok(TokenKind::Percent, start)
561 }
562 }
563 b'.' => {
564 if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
566 self.pos = start + 1;
567 self.scan_digits(u8::is_ascii_digit);
568 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
570 self.try_scan_exponent();
571 }
572 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
574 self.consume_invalid_numeric_rest();
575 return self.invalid_numeric(start);
576 }
577 return self.tok(TokenKind::FloatLiteralLeadingDot, start);
578 }
579 if self.check_at(1, b'.') && self.check_at(2, b'.') {
580 self.pos = start + 3;
581 self.tok(TokenKind::Ellipsis, start)
582 } else if self.check_at(1, b'=') {
583 self.pos = start + 2;
584 self.tok(TokenKind::DotEquals, start)
585 } else {
586 self.pos = start + 1;
587 self.tok(TokenKind::Dot, start)
588 }
589 }
590 b'=' => {
591 if self.check_at(1, b'=') {
592 if self.check_at(2, b'=') {
593 self.pos = start + 3;
594 self.tok(TokenKind::EqualsEqualsEquals, start)
595 } else {
596 self.pos = start + 2;
597 self.tok(TokenKind::EqualsEquals, start)
598 }
599 } else if self.check_at(1, b'>') {
600 self.pos = start + 2;
601 self.tok(TokenKind::FatArrow, start)
602 } else {
603 self.pos = start + 1;
604 self.tok(TokenKind::Equals, start)
605 }
606 }
607 b'!' => {
608 if self.check_at(1, b'=') {
609 if self.check_at(2, b'=') {
610 self.pos = start + 3;
611 self.tok(TokenKind::BangEqualsEquals, start)
612 } else {
613 self.pos = start + 2;
614 self.tok(TokenKind::BangEquals, start)
615 }
616 } else {
617 self.pos = start + 1;
618 self.tok(TokenKind::Bang, start)
619 }
620 }
621 b'<' => self.scan_less_than(start),
622 b'>' => {
623 if self.check_at(1, b'>') {
624 if self.check_at(2, b'=') {
625 self.pos = start + 3;
626 self.tok(TokenKind::ShiftRightEquals, start)
627 } else {
628 self.pos = start + 2;
629 self.tok(TokenKind::ShiftRight, start)
630 }
631 } else if self.check_at(1, b'=') {
632 self.pos = start + 2;
633 self.tok(TokenKind::GreaterThanEquals, start)
634 } else {
635 self.pos = start + 1;
636 self.tok(TokenKind::GreaterThan, start)
637 }
638 }
639 b'&' => {
640 if self.check_at(1, b'&') {
641 self.pos = start + 2;
642 self.tok(TokenKind::AmpersandAmpersand, start)
643 } else if self.check_at(1, b'=') {
644 self.pos = start + 2;
645 self.tok(TokenKind::AmpersandEquals, start)
646 } else {
647 self.pos = start + 1;
648 self.tok(TokenKind::Ampersand, start)
649 }
650 }
651 b'|' => {
652 if self.check_at(1, b'|') {
653 self.pos = start + 2;
654 self.tok(TokenKind::PipePipe, start)
655 } else if self.check_at(1, b'=') {
656 self.pos = start + 2;
657 self.tok(TokenKind::PipeEquals, start)
658 } else if self.check_at(1, b'>') {
659 self.pos = start + 2;
660 self.tok(TokenKind::PipeArrow, start)
661 } else {
662 self.pos = start + 1;
663 self.tok(TokenKind::Pipe, start)
664 }
665 }
666 b'^' => {
667 if self.check_at(1, b'=') {
668 self.pos = start + 2;
669 self.tok(TokenKind::CaretEquals, start)
670 } else {
671 self.pos = start + 1;
672 self.tok(TokenKind::Caret, start)
673 }
674 }
675 b'~' => {
676 self.pos = start + 1;
677 self.tok(TokenKind::Tilde, start)
678 }
679 b'?' => {
680 if self.check_at(1, b'>') {
681 self.pos = start + 2;
682 self.mode = LexerMode::InlineHtml;
683 self.tok(TokenKind::CloseTag, start)
684 } else if self.check_at(1, b'?') {
685 if self.check_at(2, b'=') {
686 self.pos = start + 3;
687 self.tok(TokenKind::CoalesceEquals, start)
688 } else {
689 self.pos = start + 2;
690 self.tok(TokenKind::QuestionQuestion, start)
691 }
692 } else if self.check_at(1, b'-') && self.check_at(2, b'>') {
693 self.pos = start + 3;
694 self.tok(TokenKind::NullsafeArrow, start)
695 } else {
696 self.pos = start + 1;
697 self.tok(TokenKind::Question, start)
698 }
699 }
700 b':' => {
701 if self.check_at(1, b':') {
702 self.pos = start + 2;
703 self.tok(TokenKind::DoubleColon, start)
704 } else {
705 self.pos = start + 1;
706 self.tok(TokenKind::Colon, start)
707 }
708 }
709 b'@' => {
710 self.pos = start + 1;
711 self.tok(TokenKind::At, start)
712 }
713 b'\\' => {
714 self.pos = start + 1;
715 self.tok(TokenKind::Backslash, start)
716 }
717 b'#' => {
718 if self.check_at(1, b'[') {
721 self.pos = start + 2;
722 self.tok(TokenKind::HashBracket, start)
723 } else {
724 self.pos = start + 1;
726 self.read_next_token()
727 }
728 }
729
730 b'(' => {
732 self.pos = start + 1;
733 self.tok(TokenKind::LeftParen, start)
734 }
735 b')' => {
736 self.pos = start + 1;
737 self.tok(TokenKind::RightParen, start)
738 }
739 b'[' => {
740 self.pos = start + 1;
741 self.tok(TokenKind::LeftBracket, start)
742 }
743 b']' => {
744 self.pos = start + 1;
745 self.tok(TokenKind::RightBracket, start)
746 }
747 b'{' => {
748 self.pos = start + 1;
749 self.tok(TokenKind::LeftBrace, start)
750 }
751 b'}' => {
752 self.pos = start + 1;
753 self.tok(TokenKind::RightBrace, start)
754 }
755 b';' => {
756 self.pos = start + 1;
757 self.tok(TokenKind::Semicolon, start)
758 }
759 b',' => {
760 self.pos = start + 1;
761 self.tok(TokenKind::Comma, start)
762 }
763
764 b'\'' => self.scan_single_quoted_string(),
766 b'"' => self.scan_double_quoted_string(),
767 b'`' => self.scan_backtick_string(),
768
769 b'$' => {
771 if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
772 self.pos = start + 2;
773 while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
774 self.pos += 1;
775 }
776 self.tok(TokenKind::Variable, start)
777 } else {
778 self.pos = start + 1;
779 self.tok(TokenKind::Dollar, start)
780 }
781 }
782
783 b'0'..=b'9' => self.scan_number(),
785
786 _ if is_ident_start(b) => {
788 if b == b'b' || b == b'B' {
790 if self.check_at(1, b'\'') {
791 return self.scan_single_quoted_string();
792 }
793 if self.check_at(1, b'"') {
794 return self.scan_double_quoted_string();
795 }
796 if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
797 let remaining = &self.source[self.pos..];
798 if let Some(token) = self.try_lex_heredoc(remaining) {
799 return token;
800 }
801 }
802 }
803 self.scan_identifier()
804 }
805
806 _ => {
808 self.pos = start + 1;
809 self.read_next_token()
810 }
811 }
812 }
813
814 fn scan_less_than(&mut self, start: usize) -> Token {
816 if self.check_at(1, b'<') {
817 if self.check_at(2, b'<') {
818 let remaining = &self.source[self.pos..];
820 if let Some(token) = self.try_lex_heredoc(remaining) {
821 return token;
822 }
823 }
825 if self.check_at(2, b'=') {
826 self.pos = start + 3;
827 return self.tok(TokenKind::ShiftLeftEquals, start);
828 }
829 self.pos = start + 2;
830 return self.tok(TokenKind::ShiftLeft, start);
831 }
832 if self.check_at(1, b'=') {
833 if self.check_at(2, b'>') {
834 self.pos = start + 3;
835 return self.tok(TokenKind::Spaceship, start);
836 }
837 self.pos = start + 2;
838 return self.tok(TokenKind::LessThanEquals, start);
839 }
840 if self.check_at(1, b'>') {
842 self.pos = start + 2;
843 return self.tok(TokenKind::BangEquals, start);
844 }
845 if self.check_at(1, b'?') {
846 let bytes = self.source.as_bytes();
847 if bytes.len() >= self.pos + 5
848 && bytes[self.pos + 2..self.pos + 5].eq_ignore_ascii_case(b"php")
849 {
850 self.pos = start + 5;
851 return self.tok(TokenKind::OpenTag, start);
852 }
853 if self.source[self.pos..].starts_with("<?=") {
854 self.pos = start + 3;
855 return self.tok(TokenKind::OpenTag, start);
856 }
857 }
858 self.pos = start + 1;
859 self.tok(TokenKind::LessThan, start)
860 }
861
862 fn scan_single_quoted_string(&mut self) -> Token {
865 let start = self.pos;
866 let bytes = self.source.as_bytes();
867 let mut p = self.pos;
868 if bytes[p] == b'b' || bytes[p] == b'B' {
870 p += 1;
871 }
872 p += 1; loop {
874 match memchr2(b'\\', b'\'', &bytes[p..]) {
875 None => {
876 self.errors.push(LexerError {
877 kind: LexerErrorKind::UnterminatedString,
878 message: "unterminated string literal".to_string(),
879 span: Span::new(start as u32, self.source.len() as u32),
880 });
881 self.mark_non_verbatim(start);
882 self.pos = self.source.len();
883 return self.tok(TokenKind::SingleQuotedString, start);
884 }
885 Some(offset) => {
886 p += offset;
887 match bytes[p] {
888 b'\\' => {
889 self.mark_non_verbatim(start);
890 p += 1;
891 if p < bytes.len() {
892 p += 1;
893 }
894 }
895 _ => {
896 p += 1;
898 break;
899 }
900 }
901 }
902 }
903 }
904 self.pos = p;
905 self.tok(TokenKind::SingleQuotedString, start)
906 }
907
908 fn scan_double_quoted_string(&mut self) -> Token {
909 let start = self.pos;
910 let bytes = self.source.as_bytes();
911 let mut p = self.pos;
912 if bytes[p] == b'b' || bytes[p] == b'B' {
914 p += 1;
915 }
916 p += 1; let mut verbatim = true;
918 loop {
919 let segment = p;
920 let Some(offset) = memchr::memchr3(b'\\', b'"', b'$', &bytes[p..]) else {
921 self.errors.push(LexerError {
922 kind: LexerErrorKind::UnterminatedString,
923 message: "unterminated string literal".to_string(),
924 span: Span::new(start as u32, self.source.len() as u32),
925 });
926 self.mark_non_verbatim(start);
927 self.pos = self.source.len();
928 return self.tok(TokenKind::DoubleQuotedString, start);
929 };
930 p += offset;
931 match bytes[p] {
932 b'\\' => {
933 verbatim = false;
934 p = (p + 2).min(bytes.len());
935 }
936 b'"' => {
937 p += 1;
938 break;
939 }
940 _ => {
942 verbatim = false;
943 if p > segment && bytes[p - 1] == b'{' {
948 p = skip_complex_interp(bytes, p - 1);
949 } else {
950 p += 1;
951 }
952 }
953 }
954 }
955 if !verbatim {
956 self.mark_non_verbatim(start);
957 }
958 self.pos = p;
959 self.tok(TokenKind::DoubleQuotedString, start)
960 }
961
962 #[inline]
963 fn mark_non_verbatim(&mut self, start: usize) {
964 if self.non_verbatim_strings.last() != Some(&(start as u32)) {
966 self.non_verbatim_strings.push(start as u32);
967 }
968 }
969
970 fn scan_backtick_string(&mut self) -> Token {
971 let start = self.pos;
972 let bytes = self.source.as_bytes();
973 let mut p = self.pos;
974 p += 1; loop {
976 match memchr2(b'\\', b'`', &bytes[p..]) {
977 None => {
978 self.errors.push(LexerError {
979 kind: LexerErrorKind::UnterminatedString,
980 message: "unterminated string literal".to_string(),
981 span: Span::new(start as u32, self.source.len() as u32),
982 });
983 self.pos = self.source.len();
984 return self.tok(TokenKind::BacktickString, start);
985 }
986 Some(offset) => {
987 p += offset;
988 match bytes[p] {
989 b'\\' => {
990 p += 1;
991 if p < bytes.len() {
992 p += 1;
993 }
994 }
995 _ => {
996 p += 1;
998 break;
999 }
1000 }
1001 }
1002 }
1003 }
1004 self.pos = p;
1005 self.tok(TokenKind::BacktickString, start)
1006 }
1007
1008 fn scan_number(&mut self) -> Token {
1011 let start = self.pos;
1012 let bytes = self.source.as_bytes();
1013
1014 if bytes[start] == b'0' && start + 1 < bytes.len() {
1016 match bytes[start + 1] {
1017 b'x' | b'X' => {
1018 self.pos = start + 2;
1019 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1020 self.consume_invalid_numeric_rest();
1021 return self.invalid_numeric(start);
1022 }
1023 if self.scan_digits(u8::is_ascii_hexdigit) {
1024 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1025 self.consume_invalid_numeric_rest();
1026 return self.invalid_numeric(start);
1027 }
1028 return self.tok(TokenKind::HexIntLiteral, start);
1029 }
1030 self.pos = start;
1032 }
1033 b'b' | b'B' => {
1034 self.pos = start + 2;
1035 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1036 self.consume_invalid_numeric_rest();
1037 return self.invalid_numeric(start);
1038 }
1039 if self.scan_digits(|b| b == &b'0' || b == &b'1') {
1040 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1041 self.consume_invalid_numeric_rest();
1042 return self.invalid_numeric(start);
1043 }
1044 return self.tok(TokenKind::BinIntLiteral, start);
1045 }
1046 self.pos = start;
1048 }
1049 b'o' | b'O' => {
1050 self.pos = start + 2;
1051 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1052 self.consume_invalid_numeric_rest();
1053 return self.invalid_numeric(start);
1054 }
1055 if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
1056 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1057 self.consume_invalid_numeric_rest();
1058 return self.invalid_numeric(start);
1059 }
1060 return self.tok(TokenKind::OctIntLiteralNew, start);
1061 }
1062 self.pos = start;
1064 }
1065 _ => {}
1066 }
1067 }
1068
1069 self.pos = start;
1071 self.scan_digits(u8::is_ascii_digit);
1072 let integer_end = self.pos;
1073 let mut kind = TokenKind::IntLiteral;
1074
1075 if bytes[start] == b'0' && integer_end > start + 1 {
1078 kind = TokenKind::OctIntLiteral;
1079 }
1080
1081 if self.pos < bytes.len() && bytes[self.pos] == b'.' {
1083 if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
1084 self.pos += 1; self.scan_digits(u8::is_ascii_digit);
1087 kind = TokenKind::FloatLiteralSimple;
1088 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1089 self.consume_invalid_numeric_rest();
1091 return self.invalid_numeric(start);
1092 } else if self.pos + 1 >= bytes.len() || bytes[self.pos + 1] != b'.' {
1093 self.pos += 1; kind = TokenKind::FloatLiteralSimple;
1096 }
1097 }
1098
1099 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
1101 if self.try_scan_exponent() {
1102 kind = TokenKind::FloatLiteral;
1103 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1104 self.consume_invalid_numeric_rest();
1106 return self.invalid_numeric(start);
1107 }
1108 }
1109
1110 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1112 self.consume_invalid_numeric_rest();
1113 return self.invalid_numeric(start);
1114 }
1115
1116 self.tok(kind, start)
1117 }
1118
1119 fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
1122 let bytes = self.source.as_bytes();
1123 if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
1124 return false;
1125 }
1126 self.pos += 1;
1127 loop {
1128 if self.pos >= bytes.len() {
1129 break;
1130 }
1131 if is_valid(&bytes[self.pos]) {
1132 self.pos += 1;
1133 } else if bytes[self.pos] == b'_'
1134 && self.pos + 1 < bytes.len()
1135 && is_valid(&bytes[self.pos + 1])
1136 {
1137 self.pos += 2;
1138 } else {
1139 break;
1140 }
1141 }
1142 true
1143 }
1144
1145 fn try_scan_exponent(&mut self) -> bool {
1148 let bytes = self.source.as_bytes();
1149 let saved = self.pos;
1150 self.pos += 1; if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
1154 self.pos += 1;
1155 }
1156
1157 if self.scan_digits(u8::is_ascii_digit) {
1159 true
1160 } else {
1161 self.pos = saved;
1162 false
1163 }
1164 }
1165
1166 fn scan_identifier(&mut self) -> Token {
1169 let start = self.pos;
1170 let bytes = self.source.as_bytes();
1171 self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
1173 self.pos += 1;
1174 }
1175 let text = &self.source[start..self.pos];
1176 let kind = resolve_keyword(text).unwrap_or(TokenKind::Identifier);
1177 self.tok(kind, start)
1178 }
1179
1180 #[inline]
1186 fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
1187 loop {
1188 match memchr2(b'\n', b'?', &bytes[*pos..]) {
1189 None => {
1190 *pos = bytes.len();
1191 return;
1192 }
1193 Some(offset) => {
1194 let p = *pos + offset;
1195 if bytes[p] == b'\n' {
1196 *pos = p; return;
1198 }
1199 if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
1201 *pos = p; return;
1203 }
1204 *pos = p + 1;
1206 }
1207 }
1208 }
1209 }
1210
1211 #[inline]
1212 fn check_at(&self, offset: usize, expected: u8) -> bool {
1213 self.source.as_bytes().get(self.pos + offset) == Some(&expected)
1214 }
1215
1216 #[inline]
1217 fn tok(&self, kind: TokenKind, start: usize) -> Token {
1218 Token::new(kind, Span::new(start as u32, self.pos as u32))
1219 }
1220
1221 fn invalid_numeric(&mut self, start: usize) -> Token {
1222 let span = Span::new(start as u32, self.pos as u32);
1223 self.errors.push(LexerError {
1224 kind: LexerErrorKind::Other,
1225 message: "Invalid numeric literal".to_string(),
1226 span,
1227 });
1228 Token::new(TokenKind::InvalidNumericLiteral, span)
1229 }
1230
1231 fn consume_invalid_numeric_rest(&mut self) {
1233 let bytes = self.source.as_bytes();
1234 while self.pos < bytes.len() {
1235 let b = bytes[self.pos];
1236 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
1237 if (b == b'+' || b == b'-') && self.pos > 0 {
1239 let prev = bytes[self.pos - 1];
1240 if prev != b'e' && prev != b'E' {
1241 break;
1242 }
1243 }
1244 self.pos += 1;
1245 } else {
1246 break;
1247 }
1248 }
1249 }
1250
1251 fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
1255 let trimmed = remaining.trim_start_matches(|c: char| {
1257 c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\x0C'
1258 });
1259 let ws_len = remaining.len() - trimmed.len();
1260
1261 let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
1263 || trimmed.starts_with("B<<<"))
1264 && !trimmed[1..].starts_with("<<<>")
1265 {
1266 (&trimmed[1..], 1)
1267 } else {
1268 (trimmed, 0)
1269 };
1270
1271 if !after_prefix.starts_with("<<<") {
1272 return None;
1273 }
1274
1275 let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
1278 let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
1279 let arrows_offset =
1280 ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
1281
1282 let (label, is_nowdoc, label_line_end);
1284 if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
1285 let closing = after_quote.find('\'')?;
1287 label = &after_quote[..closing];
1288 is_nowdoc = true;
1289 let after_label = &after_arrows_trimmed[2 + closing..];
1290 let nl = after_label.find('\n').unwrap_or(after_label.len());
1292 label_line_end = arrows_offset + 2 + closing + nl;
1293 if label_line_end < remaining.len() {
1294 }
1296 } else {
1297 let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
1299 let closing = after_dquote.find('"')?;
1300 label = &after_dquote[..closing];
1301 &after_dquote[1 + closing..]
1302 } else {
1303 let end = after_arrows_trimmed
1305 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1306 .unwrap_or(after_arrows_trimmed.len());
1307 if end == 0 {
1308 return None;
1309 }
1310 label = &after_arrows_trimmed[..end];
1311 &after_arrows_trimmed[end..]
1312 };
1313 is_nowdoc = false;
1314 let nl = s.find('\n').unwrap_or(s.len());
1315 label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl;
1316 };
1317
1318 if label.is_empty() {
1319 return None;
1320 }
1321
1322 let body_start_in_remaining = if label_line_end < remaining.len() {
1324 label_line_end + 1 } else {
1326 return None; };
1328
1329 let body = &remaining[body_start_in_remaining..];
1330
1331 let mut search_pos = 0;
1333 let end_marker_pos;
1334 loop {
1335 if search_pos >= body.len() {
1336 return None; }
1338 let line_start = search_pos;
1339 let line_end = body[line_start..]
1340 .find('\n')
1341 .map(|p| line_start + p)
1342 .unwrap_or(body.len());
1343 let line = &body[line_start..line_end];
1344 let trimmed_line = line.trim_start_matches([' ', '\t']);
1345
1346 if trimmed_line.len() >= label.len()
1351 && &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
1352 && !trimmed_line
1353 .as_bytes()
1354 .get(label.len())
1355 .copied()
1356 .is_some_and(is_ident_continue)
1357 {
1358 end_marker_pos = line_start;
1359 break;
1360 }
1361
1362 search_pos = if line_end < body.len() {
1363 line_end + 1
1364 } else {
1365 body.len()
1366 };
1367 }
1368
1369 let end_marker_line = &body[end_marker_pos..];
1371 let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
1372 let indent_len = end_marker_line.len() - trimmed.len();
1373 let token_end_in_remaining =
1374 body_start_in_remaining + end_marker_pos + indent_len + label.len();
1375 self.pos = base_pos + token_end_in_remaining;
1376
1377 let span = Span::new(start as u32, self.pos as u32);
1378
1379 if is_nowdoc {
1380 Some(Token::new(TokenKind::Nowdoc, span))
1381 } else {
1382 Some(Token::new(TokenKind::Heredoc, span))
1383 }
1384 }
1385}
1386
1387pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
1395 let mut lexer = Lexer::new(source);
1396 let mut tokens = Vec::with_capacity(lexer.source().len() / 8);
1398
1399 loop {
1400 let tok = lexer.next_token();
1401 let is_eof = tok.kind == TokenKind::Eof;
1402 tokens.push(tok);
1403 if is_eof {
1404 break;
1405 }
1406 }
1407
1408 let eof_span = tokens
1412 .last()
1413 .expect("tokens guaranteed to contain at least the Eof token from lexer")
1414 .span;
1415 tokens.push(Token::new(TokenKind::Eof, eof_span));
1416
1417 let errors = lexer.errors;
1418 (tokens, errors)
1419}