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
20}
21
22const fn make_ident_start_table() -> [bool; 256] {
23 let mut t = [false; 256];
24 let mut i = 0usize;
25 while i < 256 {
26 let b = i as u8;
27 t[i] = (b >= b'a' && b <= b'z') || (b >= b'A' && b <= b'Z') || b == b'_' || b >= 0x80;
28 i += 1;
29 }
30 t
31}
32
33const fn make_ident_continue_table() -> [bool; 256] {
34 let mut t = [false; 256];
35 let mut i = 0usize;
36 while i < 256 {
37 let b = i as u8;
38 t[i] = (b >= b'a' && b <= b'z')
39 || (b >= b'A' && b <= b'Z')
40 || (b >= b'0' && b <= b'9')
41 || b == b'_'
42 || b >= 0x80;
43 i += 1;
44 }
45 t
46}
47
48static IS_PHP_WHITESPACE: [bool; 256] = make_whitespace_table();
49static IS_IDENT_START: [bool; 256] = make_ident_start_table();
50static IS_IDENT_CONTINUE: [bool; 256] = make_ident_continue_table();
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum LexerErrorKind {
56 UnterminatedString,
58 FileTooLarge,
60 UnterminatedComment,
62 InvalidNumericLiteral,
64 UnexpectedCharacter,
66 Other,
68}
69
70#[derive(Debug, Clone, PartialEq)]
72#[non_exhaustive]
73pub struct LexerError {
74 pub kind: LexerErrorKind,
76 pub message: String,
78 pub span: Span,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct Token {
85 pub kind: TokenKind,
87 pub span: Span,
89}
90
91impl Token {
92 pub fn new(kind: TokenKind, span: Span) -> Self {
94 Self { kind, span }
95 }
96
97 pub fn eof(offset: u32) -> Self {
99 Self {
100 kind: TokenKind::Eof,
101 span: Span::new(offset, offset),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107enum LexerMode {
108 InlineHtml,
109 Php,
110}
111
112pub struct Lexer<'src> {
114 source: &'src str,
115 mode: LexerMode,
116 pos: usize,
117 peeked: Option<Token>,
118 peeked2: Option<Token>,
119 non_verbatim_strings: Vec<u32>,
122 verbatim_cursor: usize,
125 attributes: bool,
127 errors: Vec<LexerError>,
128}
129
130#[inline(always)]
131fn is_unexpected_character(b: u8) -> bool {
132 (b < 0x20 && !IS_PHP_WHITESPACE[b as usize]) || b == 0x7F
133}
134
135#[inline(always)]
136fn is_ident_start(b: u8) -> bool {
137 IS_IDENT_START[b as usize]
138}
139
140#[inline(always)]
141fn is_ident_continue(b: u8) -> bool {
142 IS_IDENT_CONTINUE[b as usize]
143}
144
145fn label_len(s: &str) -> usize {
147 let bytes = s.as_bytes();
148 if !bytes.first().copied().is_some_and(is_ident_start) {
149 return 0;
150 }
151 1 + bytes[1..]
152 .iter()
153 .take_while(|b| is_ident_continue(**b))
154 .count()
155}
156
157fn skip_complex_interp(bytes: &[u8], p: usize) -> usize {
163 interpolation_end(bytes, p).unwrap_or(bytes.len())
164}
165
166enum InterpFrame<'a> {
168 Code(u32),
170 Quoted(u8),
172 Heredoc(&'a [u8]),
174}
175
176#[doc(hidden)]
183pub fn interpolation_end(bytes: &[u8], mut p: usize) -> Option<usize> {
184 debug_assert!(bytes.get(p) == Some(&b'{'));
185 let mut stack = vec![InterpFrame::Code(0)];
186 while p < bytes.len() {
187 let b = bytes[p];
188 let (quote, heredoc) = match stack
189 .last_mut()
190 .expect("the scan returns when the stack empties")
191 {
192 InterpFrame::Code(depth) => {
193 match b {
194 b'{' => {
195 *depth += 1;
196 p += 1;
197 }
198 b'}' => {
199 *depth -= 1;
200 p += 1;
201 if *depth == 0 {
202 stack.pop();
203 if stack.is_empty() {
204 return Some(p);
205 }
206 }
207 }
208 b'"' | b'`' => {
209 stack.push(InterpFrame::Quoted(b));
210 p += 1;
211 }
212 b'\'' => p = skip_nested_squoted(bytes, p),
213 b'#' if bytes.get(p + 1) != Some(&b'[') => {
214 p += 1;
215 Lexer::skip_line_comment_body(bytes, &mut p);
216 }
217 b'/' if bytes.get(p + 1) == Some(&b'/') => {
218 p += 2;
219 Lexer::skip_line_comment_body(bytes, &mut p);
220 }
221 b'/' if bytes.get(p + 1) == Some(&b'*') => {
222 p = memmem::find(&bytes[p + 2..], b"*/").map_or(bytes.len(), |e| p + e + 4);
223 }
224 b'<' if bytes[p..].starts_with(b"<<<") => match heredoc_header(bytes, p + 3) {
225 Some((label, true, body)) => p = nowdoc_end(bytes, body, label),
226 Some((label, false, body)) => {
227 p = body;
228 match closing_label_end(bytes, p, label) {
229 Some(end) => p = end,
230 None => stack.push(InterpFrame::Heredoc(label)),
231 }
232 }
233 None => p += 3,
234 },
235 _ => p += 1,
236 }
237 continue;
238 }
239 InterpFrame::Quoted(q) => (Some(*q), None),
240 InterpFrame::Heredoc(label) => (None, Some(*label)),
241 };
242 match b {
243 b'\\' if heredoc.is_none() || newline_len(&bytes[p + 1..]).is_none() => {
244 p += 2;
245 }
246 b'{' if bytes.get(p + 1) == Some(&b'$') => stack.push(InterpFrame::Code(0)),
247 b'$' if bytes.get(p + 1) == Some(&b'{') => {
248 p += 1;
249 stack.push(InterpFrame::Code(0));
250 }
251 b'\n' | b'\r' if heredoc.is_some() => {
252 p += newline_len(&bytes[p..]).unwrap_or(1);
253 if let Some(end) = heredoc.and_then(|l| closing_label_end(bytes, p, l)) {
254 stack.pop();
255 p = end;
256 }
257 }
258 _ if quote == Some(b) => {
259 stack.pop();
260 p += 1;
261 }
262 _ => p += 1,
263 }
264 }
265 None
266}
267
268fn heredoc_header(bytes: &[u8], mut p: usize) -> Option<(&[u8], bool, usize)> {
270 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
271 p += 1;
272 }
273 let quote = bytes.get(p).copied().filter(|q| matches!(q, b'\'' | b'"'));
274 if quote.is_some() {
275 p += 1;
276 }
277 let label_start = p;
278 if !bytes.get(p).copied().is_some_and(is_ident_start) {
279 return None;
280 }
281 while bytes.get(p).copied().is_some_and(is_ident_continue) {
282 p += 1;
283 }
284 let label = &bytes[label_start..p];
285 if let Some(q) = quote {
286 if bytes.get(p) != Some(&q) {
287 return None;
288 }
289 p += 1;
290 }
291 let body = p + newline_len(&bytes[p..])?;
292 Some((label, quote == Some(b'\''), body))
293}
294
295fn closing_label_end(bytes: &[u8], mut p: usize, label: &[u8]) -> Option<usize> {
297 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
298 p += 1;
299 }
300 let end = p + label.len();
301 (bytes[p..].starts_with(label) && !bytes.get(end).copied().is_some_and(is_ident_continue))
302 .then_some(end)
303}
304
305fn nowdoc_end(bytes: &[u8], mut p: usize, label: &[u8]) -> usize {
307 while p < bytes.len() {
308 if let Some(end) = closing_label_end(bytes, p, label) {
309 return end;
310 }
311 p = memchr2(b'\n', b'\r', &bytes[p..]).map_or(bytes.len(), |e| {
312 p + e + newline_len(&bytes[p + e..]).unwrap_or(1)
313 });
314 }
315 bytes.len()
316}
317
318fn newline_len(bytes: &[u8]) -> Option<usize> {
320 match bytes {
321 [b'\r', b'\n', ..] => Some(2),
322 [b'\n' | b'\r', ..] => Some(1),
323 _ => None,
324 }
325}
326
327fn next_heredoc_line(bytes: &[u8], mut p: usize) -> usize {
330 while p < bytes.len() {
331 match bytes[p] {
332 b'\n' | b'\r' => return p + newline_len(&bytes[p..]).unwrap_or(1),
333 b'\\' if !matches!(bytes.get(p + 1), Some(b'\n' | b'\r')) => p += 2,
334 b'{' if bytes.get(p + 1) == Some(&b'$') => p = skip_complex_interp(bytes, p),
335 b'$' if bytes.get(p + 1) == Some(&b'{') => p = skip_complex_interp(bytes, p + 1),
336 _ => p += 1,
337 }
338 }
339 bytes.len()
340}
341
342fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
343 debug_assert!(bytes.get(p) == Some(&b'\''));
344 p += 1;
345 while p < bytes.len() {
346 match bytes[p] {
347 b'\\' => {
348 p += 1;
349 if p < bytes.len() {
350 p += 1;
351 }
352 }
353 b'\'' => return p + 1,
354 _ => p += 1,
355 }
356 }
357 p
358}
359
360fn starts_with_open_tag(rest: &[u8]) -> bool {
363 rest.starts_with(b"<?=")
364 || rest.len() >= 5
365 && rest.starts_with(b"<?")
366 && rest[2..5].eq_ignore_ascii_case(b"php")
367 && rest
368 .get(5)
369 .is_none_or(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
370}
371
372impl<'src> Lexer<'src> {
373 pub fn new(source: &'src str) -> Self {
378 if let Some(lexer) = Self::too_large(source) {
379 return lexer;
380 }
381
382 let pos = if source.starts_with("#!") {
384 source.find('\n').map(|p| p + 1).unwrap_or(source.len())
385 } else {
386 0
387 };
388
389 Self {
390 source,
391 mode: LexerMode::InlineHtml,
392 pos,
393 peeked: None,
394 peeked2: None,
395 non_verbatim_strings: Vec::new(),
396 verbatim_cursor: 0,
397 attributes: true,
398 errors: Vec::new(),
399 }
400 }
401
402 pub fn new_at(source: &'src str, offset: usize) -> Self {
409 if let Some(lexer) = Self::too_large(source) {
410 return lexer;
411 }
412
413 Self {
414 source,
415 mode: LexerMode::Php,
416 pos: offset,
417 peeked: None,
418 peeked2: None,
419 non_verbatim_strings: Vec::new(),
420 verbatim_cursor: 0,
421 attributes: true,
422 errors: Vec::new(),
423 }
424 }
425
426 pub fn with_attributes(mut self, enabled: bool) -> Self {
429 self.attributes = enabled;
430 self
431 }
432
433 fn too_large(source: &str) -> Option<Self> {
436 if source.len() <= u32::MAX as usize {
437 return None;
438 }
439 Some(Self {
440 source: "",
441 mode: LexerMode::Php,
442 pos: 0,
443 peeked: None,
444 peeked2: None,
445 non_verbatim_strings: Vec::new(),
446 verbatim_cursor: 0,
447 attributes: true,
448 errors: vec![LexerError {
449 kind: LexerErrorKind::FileTooLarge,
450 message: format!(
451 "source is {} bytes, which exceeds the maximum supported size of {} bytes",
452 source.len(),
453 u32::MAX
454 ),
455 span: Span::new(0, 0),
456 }],
457 })
458 }
459
460 pub fn errors(&self) -> &[LexerError] {
462 &self.errors
463 }
464
465 pub fn into_errors(self) -> Vec<LexerError> {
467 self.errors
468 }
469
470 #[doc(hidden)]
477 pub fn is_verbatim_string(&mut self, start: u32) -> bool {
478 let list = &self.non_verbatim_strings;
479 let mut i = self.verbatim_cursor;
480 if i > 0 && list.get(i - 1).is_some_and(|&prev| prev >= start) {
481 return list.binary_search(&start).is_err();
483 }
484 while list.get(i).is_some_and(|&next| next < start) {
485 i += 1;
486 }
487 self.verbatim_cursor = i;
488 list.get(i) != Some(&start)
489 }
490
491 #[doc(hidden)]
494 pub fn halt_at(&mut self, offset: usize) {
495 self.peeked = None;
496 self.peeked2 = None;
497 self.pos = self.source.len();
498 self.errors.retain(|e| (e.span.start as usize) < offset);
499 }
500
501 pub fn source(&self) -> &'src str {
503 self.source
504 }
505
506 pub fn peek(&mut self) -> &Token {
508 if self.peeked.is_none() {
509 self.peeked = Some(self.read_next_token());
510 }
511 self.peeked.as_ref().expect("peeked is Some: set above")
512 }
513
514 pub fn peek2(&mut self) -> &Token {
516 if self.peeked.is_none() {
518 self.peeked = Some(self.read_next_token());
519 }
520 if self.peeked2.is_none() {
521 self.peeked2 = Some(self.read_next_token());
522 }
523 self.peeked2.as_ref().expect("peeked2 is Some: set above")
524 }
525
526 pub fn next_token(&mut self) -> Token {
528 if let Some(token) = self.peeked.take() {
529 self.peeked = self.peeked2.take();
530 return token;
531 }
532 self.read_next_token()
533 }
534
535 pub fn token_text(&self, token: &Token) -> &'src str {
537 &self.source[token.span.start as usize..token.span.end as usize]
538 }
539
540 fn read_next_token(&mut self) -> Token {
541 if self.pos >= self.source.len() {
542 return Token::eof(self.source.len() as u32);
543 }
544
545 match self.mode {
546 LexerMode::InlineHtml => self.lex_inline_html(),
547 LexerMode::Php => self.lex_php(),
548 }
549 }
550
551 fn lex_inline_html(&mut self) -> Token {
552 let start = self.pos;
553 let bytes = self.source.as_bytes();
554
555 let mut search = self.pos;
559 let tag_pos = loop {
560 match memchr::memchr(b'<', &bytes[search..]) {
561 None => break None,
562 Some(offset) => {
563 let p = search + offset;
564 if starts_with_open_tag(&bytes[p..]) {
565 break Some(p - self.pos);
566 }
567 search = p + 1;
568 }
569 }
570 };
571
572 if let Some(tag_pos) = tag_pos {
573 if tag_pos == 0 {
574 self.pos = start + if self.check_at(2, b'=') { 3 } else { 5 };
575 self.mode = LexerMode::Php;
576 return self.tok(TokenKind::OpenTag, start);
577 }
578 let end = self.pos + tag_pos;
580 self.pos = end;
581 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
582 } else {
583 let end = self.source.len();
585 self.pos = end;
586 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
587 }
588 }
589
590 fn lex_php(&mut self) -> Token {
591 self.skip_whitespace();
593
594 if self.pos >= self.source.len() {
595 return Token::eof(self.source.len() as u32);
596 }
597
598 let bytes = self.source.as_bytes();
599 let start = self.pos;
600
601 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
604 self.pos += 2;
605 Self::skip_line_comment_body(bytes, &mut self.pos);
606 return self.tok(TokenKind::LineComment, start);
607 }
608
609 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
611 self.pos += 2;
612 let kind = if self.pos < bytes.len()
615 && bytes[self.pos] == b'*'
616 && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
617 {
618 TokenKind::DocComment
619 } else {
620 TokenKind::BlockComment
621 };
622 match memmem::find(&bytes[self.pos..], b"*/") {
623 Some(end) => self.pos += end + 2,
624 None => {
625 let span = Span::new(start as u32, self.source.len() as u32);
626 self.errors.push(LexerError {
627 kind: LexerErrorKind::UnterminatedComment,
628 message: "unterminated block comment".to_string(),
629 span,
630 });
631 self.pos = bytes.len();
632 }
633 }
634 return self.tok(kind, start);
635 }
636
637 if bytes[self.pos] == b'#' && !(self.attributes && bytes.get(self.pos + 1) == Some(&b'[')) {
640 self.pos += 1;
641 Self::skip_line_comment_body(bytes, &mut self.pos);
642 return self.tok(TokenKind::HashComment, start);
643 }
644
645 self.scan_token()
646 }
647
648 fn skip_whitespace(&mut self) {
650 let bytes = self.source.as_bytes();
651 while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
652 self.pos += 1;
653 }
654 }
655
656 fn scan_token(&mut self) -> Token {
658 let start = self.pos;
659 let bytes = self.source.as_bytes();
660 let b = bytes[start];
661
662 match b {
663 b'+' => {
665 if self.check_at(1, b'+') {
666 self.pos = start + 2;
667 self.tok(TokenKind::PlusPlus, start)
668 } else if self.check_at(1, b'=') {
669 self.pos = start + 2;
670 self.tok(TokenKind::PlusEquals, start)
671 } else {
672 self.pos = start + 1;
673 self.tok(TokenKind::Plus, start)
674 }
675 }
676 b'-' => {
677 if self.check_at(1, b'-') {
678 self.pos = start + 2;
679 self.tok(TokenKind::MinusMinus, start)
680 } else if self.check_at(1, b'=') {
681 self.pos = start + 2;
682 self.tok(TokenKind::MinusEquals, start)
683 } else if self.check_at(1, b'>') {
684 self.pos = start + 2;
685 self.tok(TokenKind::Arrow, start)
686 } else {
687 self.pos = start + 1;
688 self.tok(TokenKind::Minus, start)
689 }
690 }
691 b'*' => {
692 if self.check_at(1, b'*') {
693 if self.check_at(2, b'=') {
694 self.pos = start + 3;
695 self.tok(TokenKind::StarStarEquals, start)
696 } else {
697 self.pos = start + 2;
698 self.tok(TokenKind::StarStar, start)
699 }
700 } else if self.check_at(1, b'=') {
701 self.pos = start + 2;
702 self.tok(TokenKind::StarEquals, start)
703 } else {
704 self.pos = start + 1;
705 self.tok(TokenKind::Star, start)
706 }
707 }
708 b'/' => {
709 if self.check_at(1, b'=') {
711 self.pos = start + 2;
712 self.tok(TokenKind::SlashEquals, start)
713 } else {
714 self.pos = start + 1;
715 self.tok(TokenKind::Slash, start)
716 }
717 }
718 b'%' => {
719 if self.check_at(1, b'=') {
720 self.pos = start + 2;
721 self.tok(TokenKind::PercentEquals, start)
722 } else {
723 self.pos = start + 1;
724 self.tok(TokenKind::Percent, start)
725 }
726 }
727 b'.' => {
728 if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
730 self.pos = start + 1;
731 self.scan_digits(u8::is_ascii_digit);
732 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
734 self.try_scan_exponent();
735 }
736 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
738 self.consume_invalid_numeric_rest();
739 return self.invalid_numeric(start);
740 }
741 return self.tok(TokenKind::FloatLiteralLeadingDot, start);
742 }
743 if self.check_at(1, b'.') && self.check_at(2, b'.') {
744 self.pos = start + 3;
745 self.tok(TokenKind::Ellipsis, start)
746 } else if self.check_at(1, b'=') {
747 self.pos = start + 2;
748 self.tok(TokenKind::DotEquals, start)
749 } else {
750 self.pos = start + 1;
751 self.tok(TokenKind::Dot, start)
752 }
753 }
754 b'=' => {
755 if self.check_at(1, b'=') {
756 if self.check_at(2, b'=') {
757 self.pos = start + 3;
758 self.tok(TokenKind::EqualsEqualsEquals, start)
759 } else {
760 self.pos = start + 2;
761 self.tok(TokenKind::EqualsEquals, start)
762 }
763 } else if self.check_at(1, b'>') {
764 self.pos = start + 2;
765 self.tok(TokenKind::FatArrow, start)
766 } else {
767 self.pos = start + 1;
768 self.tok(TokenKind::Equals, start)
769 }
770 }
771 b'!' => {
772 if self.check_at(1, b'=') {
773 if self.check_at(2, b'=') {
774 self.pos = start + 3;
775 self.tok(TokenKind::BangEqualsEquals, start)
776 } else {
777 self.pos = start + 2;
778 self.tok(TokenKind::BangEquals, start)
779 }
780 } else {
781 self.pos = start + 1;
782 self.tok(TokenKind::Bang, start)
783 }
784 }
785 b'<' => self.scan_less_than(start),
786 b'>' => {
787 if self.check_at(1, b'>') {
788 if self.check_at(2, b'=') {
789 self.pos = start + 3;
790 self.tok(TokenKind::ShiftRightEquals, start)
791 } else {
792 self.pos = start + 2;
793 self.tok(TokenKind::ShiftRight, start)
794 }
795 } else if self.check_at(1, b'=') {
796 self.pos = start + 2;
797 self.tok(TokenKind::GreaterThanEquals, start)
798 } else {
799 self.pos = start + 1;
800 self.tok(TokenKind::GreaterThan, start)
801 }
802 }
803 b'&' => {
804 if self.check_at(1, b'&') {
805 self.pos = start + 2;
806 self.tok(TokenKind::AmpersandAmpersand, start)
807 } else if self.check_at(1, b'=') {
808 self.pos = start + 2;
809 self.tok(TokenKind::AmpersandEquals, start)
810 } else {
811 self.pos = start + 1;
812 self.tok(TokenKind::Ampersand, start)
813 }
814 }
815 b'|' => {
816 if self.check_at(1, b'|') {
817 self.pos = start + 2;
818 self.tok(TokenKind::PipePipe, start)
819 } else if self.check_at(1, b'=') {
820 self.pos = start + 2;
821 self.tok(TokenKind::PipeEquals, start)
822 } else if self.check_at(1, b'>') {
823 self.pos = start + 2;
824 self.tok(TokenKind::PipeArrow, start)
825 } else {
826 self.pos = start + 1;
827 self.tok(TokenKind::Pipe, start)
828 }
829 }
830 b'^' => {
831 if self.check_at(1, b'=') {
832 self.pos = start + 2;
833 self.tok(TokenKind::CaretEquals, start)
834 } else {
835 self.pos = start + 1;
836 self.tok(TokenKind::Caret, start)
837 }
838 }
839 b'~' => {
840 self.pos = start + 1;
841 self.tok(TokenKind::Tilde, start)
842 }
843 b'?' => {
844 if self.check_at(1, b'>') {
845 self.pos = start
847 + if self.check_at(2, b'\r') && self.check_at(3, b'\n') {
848 4
849 } else if self.check_at(2, b'\n') || self.check_at(2, b'\r') {
850 3
851 } else {
852 2
853 };
854 self.mode = LexerMode::InlineHtml;
855 self.tok(TokenKind::CloseTag, start)
856 } else if self.check_at(1, b'?') {
857 if self.check_at(2, b'=') {
858 self.pos = start + 3;
859 self.tok(TokenKind::CoalesceEquals, start)
860 } else {
861 self.pos = start + 2;
862 self.tok(TokenKind::QuestionQuestion, start)
863 }
864 } else if self.check_at(1, b'-') && self.check_at(2, b'>') {
865 self.pos = start + 3;
866 self.tok(TokenKind::NullsafeArrow, start)
867 } else {
868 self.pos = start + 1;
869 self.tok(TokenKind::Question, start)
870 }
871 }
872 b':' => {
873 if self.check_at(1, b':') {
874 self.pos = start + 2;
875 self.tok(TokenKind::DoubleColon, start)
876 } else {
877 self.pos = start + 1;
878 self.tok(TokenKind::Colon, start)
879 }
880 }
881 b'@' => {
882 self.pos = start + 1;
883 self.tok(TokenKind::At, start)
884 }
885 b'\\' => {
886 self.pos = start + 1;
887 self.tok(TokenKind::Backslash, start)
888 }
889 b'#' => {
890 if self.check_at(1, b'[') {
893 self.pos = start + 2;
894 self.tok(TokenKind::HashBracket, start)
895 } else {
896 self.pos = start + 1;
898 self.read_next_token()
899 }
900 }
901
902 b'(' => {
904 self.pos = start + 1;
905 self.tok(TokenKind::LeftParen, start)
906 }
907 b')' => {
908 self.pos = start + 1;
909 self.tok(TokenKind::RightParen, start)
910 }
911 b'[' => {
912 self.pos = start + 1;
913 self.tok(TokenKind::LeftBracket, start)
914 }
915 b']' => {
916 self.pos = start + 1;
917 self.tok(TokenKind::RightBracket, start)
918 }
919 b'{' => {
920 self.pos = start + 1;
921 self.tok(TokenKind::LeftBrace, start)
922 }
923 b'}' => {
924 self.pos = start + 1;
925 self.tok(TokenKind::RightBrace, start)
926 }
927 b';' => {
928 self.pos = start + 1;
929 self.tok(TokenKind::Semicolon, start)
930 }
931 b',' => {
932 self.pos = start + 1;
933 self.tok(TokenKind::Comma, start)
934 }
935
936 b'\'' => self.scan_single_quoted_string(),
938 b'"' => self.scan_double_quoted_string(),
939 b'`' => self.scan_backtick_string(),
940
941 b'$' => {
943 if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
944 self.pos = start + 2;
945 while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
946 self.pos += 1;
947 }
948 self.tok(TokenKind::Variable, start)
949 } else {
950 self.pos = start + 1;
951 self.tok(TokenKind::Dollar, start)
952 }
953 }
954
955 b'0'..=b'9' => self.scan_number(),
957
958 _ if is_ident_start(b) => {
960 if b == b'b' || b == b'B' {
962 if self.check_at(1, b'\'') {
963 return self.scan_single_quoted_string();
964 }
965 if self.check_at(1, b'"') {
966 return self.scan_double_quoted_string();
967 }
968 if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
969 let remaining = &self.source[self.pos..];
970 if let Some(token) = self.try_lex_heredoc(remaining) {
971 return token;
972 }
973 }
974 }
975 self.scan_identifier()
976 }
977
978 _ => {
980 let mut end = start + 1;
981 while bytes.get(end).is_some_and(|&b| is_unexpected_character(b)) {
982 end += 1;
983 }
984 self.errors.push(LexerError {
985 kind: LexerErrorKind::UnexpectedCharacter,
986 message: format!("unexpected character 0x{b:02X}"),
987 span: Span::new(start as u32, end as u32),
988 });
989 self.pos = end;
990 self.read_next_token()
991 }
992 }
993 }
994
995 fn scan_less_than(&mut self, start: usize) -> Token {
997 if self.check_at(1, b'<') {
998 if self.check_at(2, b'<') {
999 let remaining = &self.source[self.pos..];
1001 if let Some(token) = self.try_lex_heredoc(remaining) {
1002 return token;
1003 }
1004 }
1006 if self.check_at(2, b'=') {
1007 self.pos = start + 3;
1008 return self.tok(TokenKind::ShiftLeftEquals, start);
1009 }
1010 self.pos = start + 2;
1011 return self.tok(TokenKind::ShiftLeft, start);
1012 }
1013 if self.check_at(1, b'=') {
1014 if self.check_at(2, b'>') {
1015 self.pos = start + 3;
1016 return self.tok(TokenKind::Spaceship, start);
1017 }
1018 self.pos = start + 2;
1019 return self.tok(TokenKind::LessThanEquals, start);
1020 }
1021 if self.check_at(1, b'>') {
1023 self.pos = start + 2;
1024 return self.tok(TokenKind::BangEquals, start);
1025 }
1026 self.pos = start + 1;
1028 self.tok(TokenKind::LessThan, start)
1029 }
1030
1031 fn scan_single_quoted_string(&mut self) -> Token {
1034 let start = self.pos;
1035 let bytes = self.source.as_bytes();
1036 let mut p = self.pos;
1037 if bytes[p] == b'b' || bytes[p] == b'B' {
1039 p += 1;
1040 }
1041 p += 1; loop {
1043 match memchr2(b'\\', b'\'', &bytes[p..]) {
1044 None => {
1045 self.errors.push(LexerError {
1046 kind: LexerErrorKind::UnterminatedString,
1047 message: "unterminated string literal".to_string(),
1048 span: Span::new(start as u32, self.source.len() as u32),
1049 });
1050 self.mark_non_verbatim(start);
1051 self.pos = self.source.len();
1052 return self.tok(TokenKind::SingleQuotedString, start);
1053 }
1054 Some(offset) => {
1055 p += offset;
1056 match bytes[p] {
1057 b'\\' => {
1058 self.mark_non_verbatim(start);
1059 p += 1;
1060 if p < bytes.len() {
1061 p += 1;
1062 }
1063 }
1064 _ => {
1065 p += 1;
1067 break;
1068 }
1069 }
1070 }
1071 }
1072 }
1073 self.pos = p;
1074 self.tok(TokenKind::SingleQuotedString, start)
1075 }
1076
1077 fn scan_double_quoted_string(&mut self) -> Token {
1078 let start = self.pos;
1079 let bytes = self.source.as_bytes();
1080 let mut p = self.pos;
1081 if bytes[p] == b'b' || bytes[p] == b'B' {
1083 p += 1;
1084 }
1085 p += 1; let mut verbatim = true;
1087 loop {
1088 let segment = p;
1089 let Some(offset) = memchr::memchr3(b'\\', b'"', b'$', &bytes[p..]) else {
1090 self.errors.push(LexerError {
1091 kind: LexerErrorKind::UnterminatedString,
1092 message: "unterminated string literal".to_string(),
1093 span: Span::new(start as u32, self.source.len() as u32),
1094 });
1095 self.mark_non_verbatim(start);
1096 self.pos = self.source.len();
1097 return self.tok(TokenKind::DoubleQuotedString, start);
1098 };
1099 p += offset;
1100 match bytes[p] {
1101 b'\\' => {
1102 verbatim = false;
1103 p = (p + 2).min(bytes.len());
1104 }
1105 b'"' => {
1106 p += 1;
1107 break;
1108 }
1109 _ => {
1111 verbatim = false;
1112 if p > segment && bytes[p - 1] == b'{' {
1117 p = skip_complex_interp(bytes, p - 1);
1118 } else if bytes.get(p + 1) == Some(&b'{') {
1119 p = skip_complex_interp(bytes, p + 1);
1120 } else {
1121 p += 1;
1122 }
1123 }
1124 }
1125 }
1126 if !verbatim {
1127 self.mark_non_verbatim(start);
1128 }
1129 self.pos = p;
1130 self.tok(TokenKind::DoubleQuotedString, start)
1131 }
1132
1133 #[inline]
1134 fn mark_non_verbatim(&mut self, start: usize) {
1135 if self.non_verbatim_strings.last() != Some(&(start as u32)) {
1137 self.non_verbatim_strings.push(start as u32);
1138 }
1139 }
1140
1141 fn scan_backtick_string(&mut self) -> Token {
1142 let start = self.pos;
1143 let bytes = self.source.as_bytes();
1144 let mut p = self.pos;
1145 p += 1; loop {
1147 let segment = p;
1148 match memchr::memchr3(b'\\', b'`', b'$', &bytes[p..]) {
1149 None => {
1150 self.errors.push(LexerError {
1151 kind: LexerErrorKind::UnterminatedString,
1152 message: "unterminated string literal".to_string(),
1153 span: Span::new(start as u32, self.source.len() as u32),
1154 });
1155 self.pos = self.source.len();
1156 return self.tok(TokenKind::BacktickString, start);
1157 }
1158 Some(offset) => {
1159 p += offset;
1160 match bytes[p] {
1161 b'\\' => {
1162 p += 1;
1163 if p < bytes.len() {
1164 p += 1;
1165 }
1166 }
1167 b'$' if p > segment && bytes[p - 1] == b'{' => {
1168 p = skip_complex_interp(bytes, p - 1);
1169 }
1170 b'$' if bytes.get(p + 1) == Some(&b'{') => {
1171 p = skip_complex_interp(bytes, p + 1);
1172 }
1173 b'$' => p += 1,
1174 _ => {
1175 p += 1;
1177 break;
1178 }
1179 }
1180 }
1181 }
1182 }
1183 self.pos = p;
1184 self.tok(TokenKind::BacktickString, start)
1185 }
1186
1187 fn scan_number(&mut self) -> Token {
1190 let start = self.pos;
1191 let bytes = self.source.as_bytes();
1192
1193 if bytes[start] == b'0' && start + 1 < bytes.len() {
1195 match bytes[start + 1] {
1196 b'x' | b'X' => {
1197 self.pos = start + 2;
1198 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1199 self.consume_invalid_numeric_rest();
1200 return self.invalid_numeric(start);
1201 }
1202 if self.scan_digits(u8::is_ascii_hexdigit) {
1203 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1204 self.consume_invalid_numeric_rest();
1205 return self.invalid_numeric(start);
1206 }
1207 return self.tok(TokenKind::HexIntLiteral, start);
1208 }
1209 self.pos = start;
1211 }
1212 b'b' | b'B' => {
1213 self.pos = start + 2;
1214 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1215 self.consume_invalid_numeric_rest();
1216 return self.invalid_numeric(start);
1217 }
1218 if self.scan_digits(|b| b == &b'0' || b == &b'1') {
1219 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1220 self.consume_invalid_numeric_rest();
1221 return self.invalid_numeric(start);
1222 }
1223 return self.tok(TokenKind::BinIntLiteral, start);
1224 }
1225 self.pos = start;
1227 }
1228 b'o' | b'O' => {
1229 self.pos = start + 2;
1230 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1231 self.consume_invalid_numeric_rest();
1232 return self.invalid_numeric(start);
1233 }
1234 if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
1235 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1236 self.consume_invalid_numeric_rest();
1237 return self.invalid_numeric(start);
1238 }
1239 return self.tok(TokenKind::OctIntLiteralNew, start);
1240 }
1241 self.pos = start;
1243 }
1244 _ => {}
1245 }
1246 }
1247
1248 self.pos = start;
1250 self.scan_digits(u8::is_ascii_digit);
1251 let integer_end = self.pos;
1252 let mut kind = TokenKind::IntLiteral;
1253
1254 if bytes[start] == b'0' && integer_end > start + 1 {
1257 kind = TokenKind::OctIntLiteral;
1258 }
1259
1260 if self.pos < bytes.len() && bytes[self.pos] == b'.' {
1262 if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
1263 self.pos += 1; self.scan_digits(u8::is_ascii_digit);
1266 kind = TokenKind::FloatLiteralSimple;
1267 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1268 self.consume_invalid_numeric_rest();
1270 return self.invalid_numeric(start);
1271 } else {
1272 self.pos += 1; kind = TokenKind::FloatLiteralSimple;
1275 }
1276 }
1277
1278 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
1280 if self.try_scan_exponent() {
1281 kind = TokenKind::FloatLiteral;
1282 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1283 self.consume_invalid_numeric_rest();
1285 return self.invalid_numeric(start);
1286 }
1287 }
1288
1289 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1291 self.consume_invalid_numeric_rest();
1292 return self.invalid_numeric(start);
1293 }
1294
1295 self.tok(kind, start)
1296 }
1297
1298 fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
1301 let bytes = self.source.as_bytes();
1302 if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
1303 return false;
1304 }
1305 self.pos += 1;
1306 loop {
1307 if self.pos >= bytes.len() {
1308 break;
1309 }
1310 if is_valid(&bytes[self.pos]) {
1311 self.pos += 1;
1312 } else if bytes[self.pos] == b'_'
1313 && self.pos + 1 < bytes.len()
1314 && is_valid(&bytes[self.pos + 1])
1315 {
1316 self.pos += 2;
1317 } else {
1318 break;
1319 }
1320 }
1321 true
1322 }
1323
1324 fn try_scan_exponent(&mut self) -> bool {
1327 let bytes = self.source.as_bytes();
1328 let saved = self.pos;
1329 self.pos += 1; if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
1333 self.pos += 1;
1334 }
1335
1336 if self.scan_digits(u8::is_ascii_digit) {
1338 true
1339 } else {
1340 self.pos = saved;
1341 false
1342 }
1343 }
1344
1345 fn scan_identifier(&mut self) -> Token {
1348 let start = self.pos;
1349 let bytes = self.source.as_bytes();
1350 self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
1352 self.pos += 1;
1353 }
1354 let text = &self.source[start..self.pos];
1355 let starts_qualified_name = bytes.get(self.pos) == Some(&b'\\')
1358 && bytes.get(self.pos + 1).is_some_and(|&b| is_ident_start(b));
1359 let kind = match resolve_keyword(text) {
1360 Some(TokenKind::Namespace) => TokenKind::Namespace,
1361 Some(_) if starts_qualified_name => TokenKind::Identifier,
1362 Some(kind) => kind,
1363 None => TokenKind::Identifier,
1364 };
1365 self.tok(kind, start)
1366 }
1367
1368 #[inline]
1374 fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
1375 loop {
1376 match memchr::memchr3(b'\n', b'\r', b'?', &bytes[*pos..]) {
1377 None => {
1378 *pos = bytes.len();
1379 return;
1380 }
1381 Some(offset) => {
1382 let p = *pos + offset;
1383 if bytes[p] != b'?' {
1384 *pos = p; return;
1386 }
1387 if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
1389 *pos = p; return;
1391 }
1392 *pos = p + 1;
1394 }
1395 }
1396 }
1397 }
1398
1399 #[inline]
1400 fn check_at(&self, offset: usize, expected: u8) -> bool {
1401 self.source.as_bytes().get(self.pos + offset) == Some(&expected)
1402 }
1403
1404 #[inline]
1405 fn tok(&self, kind: TokenKind, start: usize) -> Token {
1406 Token::new(kind, Span::new(start as u32, self.pos as u32))
1407 }
1408
1409 fn invalid_numeric(&mut self, start: usize) -> Token {
1410 let span = Span::new(start as u32, self.pos as u32);
1411 self.errors.push(LexerError {
1412 kind: LexerErrorKind::InvalidNumericLiteral,
1413 message: "Invalid numeric literal".to_string(),
1414 span,
1415 });
1416 Token::new(TokenKind::InvalidNumericLiteral, span)
1417 }
1418
1419 fn consume_invalid_numeric_rest(&mut self) {
1421 let bytes = self.source.as_bytes();
1422 while self.pos < bytes.len() {
1423 let b = bytes[self.pos];
1424 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
1425 if (b == b'+' || b == b'-') && self.pos > 0 {
1427 let prev = bytes[self.pos - 1];
1428 if prev != b'e' && prev != b'E' {
1429 break;
1430 }
1431 }
1432 self.pos += 1;
1433 } else {
1434 break;
1435 }
1436 }
1437 }
1438
1439 fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
1443 let trimmed = remaining.trim_start_matches([' ', '\t', '\n', '\r']);
1445 let ws_len = remaining.len() - trimmed.len();
1446
1447 let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
1449 || trimmed.starts_with("B<<<"))
1450 && !trimmed[1..].starts_with("<<<>")
1451 {
1452 (&trimmed[1..], 1)
1453 } else {
1454 (trimmed, 0)
1455 };
1456
1457 if !after_prefix.starts_with("<<<") {
1458 return None;
1459 }
1460
1461 let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
1464 let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
1465 let arrows_offset =
1466 ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
1467
1468 let (label, is_nowdoc, label_line_end);
1470 if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
1471 let closing = label_len(after_quote);
1473 if after_quote.as_bytes().get(closing) != Some(&b'\'') {
1474 return None;
1475 }
1476 label = &after_quote[..closing];
1477 is_nowdoc = true;
1478 let after_label = &after_arrows_trimmed[2 + closing..];
1479 let nl = newline_len(after_label.as_bytes())?;
1480 label_line_end = arrows_offset + 2 + closing + nl - 1;
1481 } else {
1482 let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
1484 let closing = label_len(after_dquote);
1485 if after_dquote.as_bytes().get(closing) != Some(&b'"') {
1486 return None;
1487 }
1488 label = &after_dquote[..closing];
1489 &after_dquote[1 + closing..]
1490 } else {
1491 let end = label_len(after_arrows_trimmed);
1492 label = &after_arrows_trimmed[..end];
1493 &after_arrows_trimmed[end..]
1494 };
1495 let nl = newline_len(s.as_bytes())?;
1497 is_nowdoc = false;
1498 label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl - 1;
1499 };
1500
1501 if label.is_empty() {
1502 return None;
1503 }
1504
1505 let body_start_in_remaining = if label_line_end < remaining.len() {
1507 label_line_end + 1 } else {
1509 return None; };
1511
1512 let body = &remaining[body_start_in_remaining..];
1513
1514 let mut search_pos = 0;
1516 let end_marker_pos;
1517 loop {
1518 if search_pos >= body.len() {
1519 return None; }
1521 let line_start = search_pos;
1522 let line_end = body[line_start..]
1523 .find(['\n', '\r'])
1524 .map(|p| line_start + p)
1525 .unwrap_or(body.len());
1526 let line = &body[line_start..line_end];
1527 let trimmed_line = line.trim_start_matches([' ', '\t']);
1528
1529 if trimmed_line.len() >= label.len()
1534 && &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
1535 && !trimmed_line
1536 .as_bytes()
1537 .get(label.len())
1538 .copied()
1539 .is_some_and(is_ident_continue)
1540 {
1541 end_marker_pos = line_start;
1542 break;
1543 }
1544
1545 search_pos = if is_nowdoc {
1546 line_end + newline_len(&body.as_bytes()[line_end..]).unwrap_or(0)
1547 } else {
1548 next_heredoc_line(body.as_bytes(), line_start)
1549 };
1550 }
1551
1552 let end_marker_line = &body[end_marker_pos..];
1554 let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
1555 let indent_len = end_marker_line.len() - trimmed.len();
1556 let token_end_in_remaining =
1557 body_start_in_remaining + end_marker_pos + indent_len + label.len();
1558 self.pos = base_pos + token_end_in_remaining;
1559
1560 let span = Span::new(start as u32, self.pos as u32);
1561
1562 if is_nowdoc {
1563 Some(Token::new(TokenKind::Nowdoc, span))
1564 } else {
1565 Some(Token::new(TokenKind::Heredoc, span))
1566 }
1567 }
1568}
1569
1570pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
1578 let mut lexer = Lexer::new(source);
1579 let mut tokens = Vec::with_capacity(lexer.source().len() / 8);
1581
1582 loop {
1583 let tok = lexer.next_token();
1584 let is_eof = tok.kind == TokenKind::Eof;
1585 tokens.push(tok);
1586 if is_eof {
1587 break;
1588 }
1589 }
1590
1591 let eof_span = tokens
1595 .last()
1596 .expect("tokens guaranteed to contain at least the Eof token from lexer")
1597 .span;
1598 tokens.push(Token::new(TokenKind::Eof, eof_span));
1599
1600 let errors = lexer.errors;
1601 (tokens, errors)
1602}