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, attributes: bool) -> usize {
163 interpolation_end(bytes, p, attributes).unwrap_or(bytes.len())
164}
165
166enum InterpFrame<'a> {
168 Code(u32),
170 Quoted(u8),
172 Heredoc(&'a [u8]),
174}
175
176#[doc(hidden)]
184pub fn interpolation_end(bytes: &[u8], mut p: usize, attributes: bool) -> Option<usize> {
185 debug_assert!(bytes.get(p) == Some(&b'{'));
186 let mut stack = vec![InterpFrame::Code(0)];
187 while p < bytes.len() {
188 let b = bytes[p];
189 let (quote, heredoc) = match stack
190 .last_mut()
191 .expect("the scan returns when the stack empties")
192 {
193 InterpFrame::Code(depth) => {
194 match b {
195 b'{' => {
196 *depth += 1;
197 p += 1;
198 }
199 b'}' => {
200 *depth -= 1;
201 p += 1;
202 if *depth == 0 {
203 stack.pop();
204 if stack.is_empty() {
205 return Some(p);
206 }
207 }
208 }
209 b'"' | b'`' => {
210 stack.push(InterpFrame::Quoted(b));
211 p += 1;
212 }
213 b'\'' => p = skip_nested_squoted(bytes, p),
214 b'#' if !(attributes && bytes.get(p + 1) == Some(&b'[')) => {
215 p += 1;
216 Lexer::skip_line_comment_body(bytes, &mut p);
217 }
218 b'/' if bytes.get(p + 1) == Some(&b'/') => {
219 p += 2;
220 Lexer::skip_line_comment_body(bytes, &mut p);
221 }
222 b'/' if bytes.get(p + 1) == Some(&b'*') => {
223 p = memmem::find(&bytes[p + 2..], b"*/").map_or(bytes.len(), |e| p + e + 4);
224 }
225 b'<' if bytes[p..].starts_with(b"<<<") => match heredoc_header(bytes, p + 3) {
226 Some((label, true, body)) => p = nowdoc_end(bytes, body, label),
227 Some((label, false, body)) => {
228 p = body;
229 match closing_label_end(bytes, p, label) {
230 Some(end) => p = end,
231 None => stack.push(InterpFrame::Heredoc(label)),
232 }
233 }
234 None => p += 3,
235 },
236 _ => p += 1,
237 }
238 continue;
239 }
240 InterpFrame::Quoted(q) => (Some(*q), None),
241 InterpFrame::Heredoc(label) => (None, Some(*label)),
242 };
243 match b {
244 b'\\' if heredoc.is_none() || newline_len(&bytes[p + 1..]).is_none() => {
245 p += 2;
246 }
247 b'{' if bytes.get(p + 1) == Some(&b'$') => stack.push(InterpFrame::Code(0)),
248 b'$' if bytes.get(p + 1) == Some(&b'{') => {
249 p += 1;
250 stack.push(InterpFrame::Code(0));
251 }
252 b'\n' | b'\r' if heredoc.is_some() => {
253 p += newline_len(&bytes[p..]).unwrap_or(1);
254 if let Some(end) = heredoc.and_then(|l| closing_label_end(bytes, p, l)) {
255 stack.pop();
256 p = end;
257 }
258 }
259 _ if quote == Some(b) => {
260 stack.pop();
261 p += 1;
262 }
263 _ => p += 1,
264 }
265 }
266 None
267}
268
269fn heredoc_header(bytes: &[u8], mut p: usize) -> Option<(&[u8], bool, usize)> {
271 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
272 p += 1;
273 }
274 let quote = bytes.get(p).copied().filter(|q| matches!(q, b'\'' | b'"'));
275 if quote.is_some() {
276 p += 1;
277 }
278 let label_start = p;
279 if !bytes.get(p).copied().is_some_and(is_ident_start) {
280 return None;
281 }
282 while bytes.get(p).copied().is_some_and(is_ident_continue) {
283 p += 1;
284 }
285 let label = &bytes[label_start..p];
286 if let Some(q) = quote {
287 if bytes.get(p) != Some(&q) {
288 return None;
289 }
290 p += 1;
291 }
292 let body = p + newline_len(&bytes[p..])?;
293 Some((label, quote == Some(b'\''), body))
294}
295
296fn closing_label_end(bytes: &[u8], mut p: usize, label: &[u8]) -> Option<usize> {
298 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
299 p += 1;
300 }
301 let end = p + label.len();
302 (bytes[p..].starts_with(label) && !bytes.get(end).copied().is_some_and(is_ident_continue))
303 .then_some(end)
304}
305
306fn nowdoc_end(bytes: &[u8], mut p: usize, label: &[u8]) -> usize {
308 while p < bytes.len() {
309 if let Some(end) = closing_label_end(bytes, p, label) {
310 return end;
311 }
312 p = memchr2(b'\n', b'\r', &bytes[p..]).map_or(bytes.len(), |e| {
313 p + e + newline_len(&bytes[p + e..]).unwrap_or(1)
314 });
315 }
316 bytes.len()
317}
318
319fn newline_len(bytes: &[u8]) -> Option<usize> {
321 match bytes {
322 [b'\r', b'\n', ..] => Some(2),
323 [b'\n' | b'\r', ..] => Some(1),
324 _ => None,
325 }
326}
327
328fn next_heredoc_line(bytes: &[u8], mut p: usize, attributes: bool) -> usize {
331 while p < bytes.len() {
332 match bytes[p] {
333 b'\n' | b'\r' => return p + newline_len(&bytes[p..]).unwrap_or(1),
334 b'\\' if !matches!(bytes.get(p + 1), Some(b'\n' | b'\r')) => p += 2,
335 b'{' if bytes.get(p + 1) == Some(&b'$') => {
336 p = skip_complex_interp(bytes, p, attributes)
337 }
338 b'$' if bytes.get(p + 1) == Some(&b'{') => {
339 p = skip_complex_interp(bytes, p + 1, attributes)
340 }
341 _ => p += 1,
342 }
343 }
344 bytes.len()
345}
346
347fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
348 debug_assert!(bytes.get(p) == Some(&b'\''));
349 p += 1;
350 while p < bytes.len() {
351 match bytes[p] {
352 b'\\' => {
353 p += 1;
354 if p < bytes.len() {
355 p += 1;
356 }
357 }
358 b'\'' => return p + 1,
359 _ => p += 1,
360 }
361 }
362 p
363}
364
365fn starts_with_open_tag(rest: &[u8]) -> bool {
368 rest.starts_with(b"<?=")
369 || rest.len() >= 5
370 && rest.starts_with(b"<?")
371 && rest[2..5].eq_ignore_ascii_case(b"php")
372 && rest
373 .get(5)
374 .is_none_or(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
375}
376
377impl<'src> Lexer<'src> {
378 pub fn new(source: &'src str) -> Self {
383 if let Some(lexer) = Self::too_large(source) {
384 return lexer;
385 }
386
387 let pos = if source.starts_with("#!") {
389 source.find('\n').map(|p| p + 1).unwrap_or(source.len())
390 } else {
391 0
392 };
393
394 Self {
395 source,
396 mode: LexerMode::InlineHtml,
397 pos,
398 peeked: None,
399 peeked2: None,
400 non_verbatim_strings: Vec::new(),
401 verbatim_cursor: 0,
402 attributes: true,
403 errors: Vec::new(),
404 }
405 }
406
407 pub fn new_at(source: &'src str, offset: usize) -> Self {
414 if let Some(lexer) = Self::too_large(source) {
415 return lexer;
416 }
417
418 Self {
419 source,
420 mode: LexerMode::Php,
421 pos: offset,
422 peeked: None,
423 peeked2: None,
424 non_verbatim_strings: Vec::new(),
425 verbatim_cursor: 0,
426 attributes: true,
427 errors: Vec::new(),
428 }
429 }
430
431 pub fn with_attributes(mut self, enabled: bool) -> Self {
434 self.attributes = enabled;
435 self
436 }
437
438 fn too_large(source: &str) -> Option<Self> {
441 if source.len() <= u32::MAX as usize {
442 return None;
443 }
444 Some(Self {
445 source: "",
446 mode: LexerMode::Php,
447 pos: 0,
448 peeked: None,
449 peeked2: None,
450 non_verbatim_strings: Vec::new(),
451 verbatim_cursor: 0,
452 attributes: true,
453 errors: vec![LexerError {
454 kind: LexerErrorKind::FileTooLarge,
455 message: format!(
456 "source is {} bytes, which exceeds the maximum supported size of {} bytes",
457 source.len(),
458 u32::MAX
459 ),
460 span: Span::new(0, 0),
461 }],
462 })
463 }
464
465 pub fn errors(&self) -> &[LexerError] {
467 &self.errors
468 }
469
470 pub fn into_errors(self) -> Vec<LexerError> {
472 self.errors
473 }
474
475 #[doc(hidden)]
482 pub fn is_verbatim_string(&mut self, start: u32) -> bool {
483 let list = &self.non_verbatim_strings;
484 let mut i = self.verbatim_cursor;
485 if i > 0 && list.get(i - 1).is_some_and(|&prev| prev >= start) {
486 return list.binary_search(&start).is_err();
488 }
489 while list.get(i).is_some_and(|&next| next < start) {
490 i += 1;
491 }
492 self.verbatim_cursor = i;
493 list.get(i) != Some(&start)
494 }
495
496 #[doc(hidden)]
499 pub fn halt_at(&mut self, offset: usize) {
500 self.peeked = None;
501 self.peeked2 = None;
502 self.pos = self.source.len();
503 self.errors.retain(|e| (e.span.start as usize) < offset);
504 }
505
506 pub fn source(&self) -> &'src str {
508 self.source
509 }
510
511 pub fn peek(&mut self) -> &Token {
513 if self.peeked.is_none() {
514 self.peeked = Some(self.read_next_token());
515 }
516 self.peeked.as_ref().expect("peeked is Some: set above")
517 }
518
519 pub fn peek2(&mut self) -> &Token {
521 if self.peeked.is_none() {
523 self.peeked = Some(self.read_next_token());
524 }
525 if self.peeked2.is_none() {
526 self.peeked2 = Some(self.read_next_token());
527 }
528 self.peeked2.as_ref().expect("peeked2 is Some: set above")
529 }
530
531 pub fn next_token(&mut self) -> Token {
533 if let Some(token) = self.peeked.take() {
534 self.peeked = self.peeked2.take();
535 return token;
536 }
537 self.read_next_token()
538 }
539
540 pub fn token_text(&self, token: &Token) -> &'src str {
542 &self.source[token.span.start as usize..token.span.end as usize]
543 }
544
545 fn read_next_token(&mut self) -> Token {
546 if self.pos >= self.source.len() {
547 return Token::eof(self.source.len() as u32);
548 }
549
550 match self.mode {
551 LexerMode::InlineHtml => self.lex_inline_html(),
552 LexerMode::Php => self.lex_php(),
553 }
554 }
555
556 fn lex_inline_html(&mut self) -> Token {
557 let start = self.pos;
558 let bytes = self.source.as_bytes();
559
560 let mut search = self.pos;
564 let tag_pos = loop {
565 match memchr::memchr(b'<', &bytes[search..]) {
566 None => break None,
567 Some(offset) => {
568 let p = search + offset;
569 if starts_with_open_tag(&bytes[p..]) {
570 break Some(p - self.pos);
571 }
572 search = p + 1;
573 }
574 }
575 };
576
577 if let Some(tag_pos) = tag_pos {
578 if tag_pos == 0 {
579 self.pos = start + if self.check_at(2, b'=') { 3 } else { 5 };
580 self.mode = LexerMode::Php;
581 return self.tok(TokenKind::OpenTag, start);
582 }
583 let end = self.pos + tag_pos;
585 self.pos = end;
586 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
587 } else {
588 let end = self.source.len();
590 self.pos = end;
591 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
592 }
593 }
594
595 fn lex_php(&mut self) -> Token {
596 self.skip_whitespace();
598
599 if self.pos >= self.source.len() {
600 return Token::eof(self.source.len() as u32);
601 }
602
603 let bytes = self.source.as_bytes();
604 let start = self.pos;
605
606 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
609 self.pos += 2;
610 Self::skip_line_comment_body(bytes, &mut self.pos);
611 return self.tok(TokenKind::LineComment, start);
612 }
613
614 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
616 self.pos += 2;
617 let kind = if self.pos < bytes.len()
620 && bytes[self.pos] == b'*'
621 && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
622 {
623 TokenKind::DocComment
624 } else {
625 TokenKind::BlockComment
626 };
627 match memmem::find(&bytes[self.pos..], b"*/") {
628 Some(end) => self.pos += end + 2,
629 None => {
630 let span = Span::new(start as u32, self.source.len() as u32);
631 self.errors.push(LexerError {
632 kind: LexerErrorKind::UnterminatedComment,
633 message: "unterminated block comment".to_string(),
634 span,
635 });
636 self.pos = bytes.len();
637 }
638 }
639 return self.tok(kind, start);
640 }
641
642 if bytes[self.pos] == b'#' && !(self.attributes && bytes.get(self.pos + 1) == Some(&b'[')) {
645 self.pos += 1;
646 Self::skip_line_comment_body(bytes, &mut self.pos);
647 return self.tok(TokenKind::HashComment, start);
648 }
649
650 self.scan_token()
651 }
652
653 fn skip_whitespace(&mut self) {
655 let bytes = self.source.as_bytes();
656 while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
657 self.pos += 1;
658 }
659 }
660
661 fn scan_token(&mut self) -> Token {
663 let start = self.pos;
664 let bytes = self.source.as_bytes();
665 let b = bytes[start];
666
667 match b {
668 b'+' => {
670 if self.check_at(1, b'+') {
671 self.pos = start + 2;
672 self.tok(TokenKind::PlusPlus, start)
673 } else if self.check_at(1, b'=') {
674 self.pos = start + 2;
675 self.tok(TokenKind::PlusEquals, start)
676 } else {
677 self.pos = start + 1;
678 self.tok(TokenKind::Plus, start)
679 }
680 }
681 b'-' => {
682 if self.check_at(1, b'-') {
683 self.pos = start + 2;
684 self.tok(TokenKind::MinusMinus, start)
685 } else if self.check_at(1, b'=') {
686 self.pos = start + 2;
687 self.tok(TokenKind::MinusEquals, start)
688 } else if self.check_at(1, b'>') {
689 self.pos = start + 2;
690 self.tok(TokenKind::Arrow, start)
691 } else {
692 self.pos = start + 1;
693 self.tok(TokenKind::Minus, start)
694 }
695 }
696 b'*' => {
697 if self.check_at(1, b'*') {
698 if self.check_at(2, b'=') {
699 self.pos = start + 3;
700 self.tok(TokenKind::StarStarEquals, start)
701 } else {
702 self.pos = start + 2;
703 self.tok(TokenKind::StarStar, start)
704 }
705 } else if self.check_at(1, b'=') {
706 self.pos = start + 2;
707 self.tok(TokenKind::StarEquals, start)
708 } else {
709 self.pos = start + 1;
710 self.tok(TokenKind::Star, start)
711 }
712 }
713 b'/' => {
714 if self.check_at(1, b'=') {
716 self.pos = start + 2;
717 self.tok(TokenKind::SlashEquals, start)
718 } else {
719 self.pos = start + 1;
720 self.tok(TokenKind::Slash, start)
721 }
722 }
723 b'%' => {
724 if self.check_at(1, b'=') {
725 self.pos = start + 2;
726 self.tok(TokenKind::PercentEquals, start)
727 } else {
728 self.pos = start + 1;
729 self.tok(TokenKind::Percent, start)
730 }
731 }
732 b'.' => {
733 if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
735 self.pos = start + 1;
736 self.scan_digits(u8::is_ascii_digit);
737 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
739 self.try_scan_exponent();
740 }
741 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
743 self.consume_invalid_numeric_rest();
744 return self.invalid_numeric(start);
745 }
746 return self.tok(TokenKind::FloatLiteralLeadingDot, start);
747 }
748 if self.check_at(1, b'.') && self.check_at(2, b'.') {
749 self.pos = start + 3;
750 self.tok(TokenKind::Ellipsis, start)
751 } else if self.check_at(1, b'=') {
752 self.pos = start + 2;
753 self.tok(TokenKind::DotEquals, start)
754 } else {
755 self.pos = start + 1;
756 self.tok(TokenKind::Dot, start)
757 }
758 }
759 b'=' => {
760 if self.check_at(1, b'=') {
761 if self.check_at(2, b'=') {
762 self.pos = start + 3;
763 self.tok(TokenKind::EqualsEqualsEquals, start)
764 } else {
765 self.pos = start + 2;
766 self.tok(TokenKind::EqualsEquals, start)
767 }
768 } else if self.check_at(1, b'>') {
769 self.pos = start + 2;
770 self.tok(TokenKind::FatArrow, start)
771 } else {
772 self.pos = start + 1;
773 self.tok(TokenKind::Equals, start)
774 }
775 }
776 b'!' => {
777 if self.check_at(1, b'=') {
778 if self.check_at(2, b'=') {
779 self.pos = start + 3;
780 self.tok(TokenKind::BangEqualsEquals, start)
781 } else {
782 self.pos = start + 2;
783 self.tok(TokenKind::BangEquals, start)
784 }
785 } else {
786 self.pos = start + 1;
787 self.tok(TokenKind::Bang, start)
788 }
789 }
790 b'<' => self.scan_less_than(start),
791 b'>' => {
792 if self.check_at(1, b'>') {
793 if self.check_at(2, b'=') {
794 self.pos = start + 3;
795 self.tok(TokenKind::ShiftRightEquals, start)
796 } else {
797 self.pos = start + 2;
798 self.tok(TokenKind::ShiftRight, start)
799 }
800 } else if self.check_at(1, b'=') {
801 self.pos = start + 2;
802 self.tok(TokenKind::GreaterThanEquals, start)
803 } else {
804 self.pos = start + 1;
805 self.tok(TokenKind::GreaterThan, start)
806 }
807 }
808 b'&' => {
809 if self.check_at(1, b'&') {
810 self.pos = start + 2;
811 self.tok(TokenKind::AmpersandAmpersand, start)
812 } else if self.check_at(1, b'=') {
813 self.pos = start + 2;
814 self.tok(TokenKind::AmpersandEquals, start)
815 } else {
816 self.pos = start + 1;
817 self.tok(TokenKind::Ampersand, start)
818 }
819 }
820 b'|' => {
821 if self.check_at(1, b'|') {
822 self.pos = start + 2;
823 self.tok(TokenKind::PipePipe, start)
824 } else if self.check_at(1, b'=') {
825 self.pos = start + 2;
826 self.tok(TokenKind::PipeEquals, start)
827 } else if self.check_at(1, b'>') {
828 self.pos = start + 2;
829 self.tok(TokenKind::PipeArrow, start)
830 } else {
831 self.pos = start + 1;
832 self.tok(TokenKind::Pipe, start)
833 }
834 }
835 b'^' => {
836 if self.check_at(1, b'=') {
837 self.pos = start + 2;
838 self.tok(TokenKind::CaretEquals, start)
839 } else {
840 self.pos = start + 1;
841 self.tok(TokenKind::Caret, start)
842 }
843 }
844 b'~' => {
845 self.pos = start + 1;
846 self.tok(TokenKind::Tilde, start)
847 }
848 b'?' => {
849 if self.check_at(1, b'>') {
850 self.pos = start
852 + if self.check_at(2, b'\r') && self.check_at(3, b'\n') {
853 4
854 } else if self.check_at(2, b'\n') || self.check_at(2, b'\r') {
855 3
856 } else {
857 2
858 };
859 self.mode = LexerMode::InlineHtml;
860 self.tok(TokenKind::CloseTag, start)
861 } else if self.check_at(1, b'?') {
862 if self.check_at(2, b'=') {
863 self.pos = start + 3;
864 self.tok(TokenKind::CoalesceEquals, start)
865 } else {
866 self.pos = start + 2;
867 self.tok(TokenKind::QuestionQuestion, start)
868 }
869 } else if self.check_at(1, b'-') && self.check_at(2, b'>') {
870 self.pos = start + 3;
871 self.tok(TokenKind::NullsafeArrow, start)
872 } else {
873 self.pos = start + 1;
874 self.tok(TokenKind::Question, start)
875 }
876 }
877 b':' => {
878 if self.check_at(1, b':') {
879 self.pos = start + 2;
880 self.tok(TokenKind::DoubleColon, start)
881 } else {
882 self.pos = start + 1;
883 self.tok(TokenKind::Colon, start)
884 }
885 }
886 b'@' => {
887 self.pos = start + 1;
888 self.tok(TokenKind::At, start)
889 }
890 b'\\' => {
891 self.pos = start + 1;
892 self.tok(TokenKind::Backslash, start)
893 }
894 b'#' => {
895 if self.check_at(1, b'[') {
898 self.pos = start + 2;
899 self.tok(TokenKind::HashBracket, start)
900 } else {
901 self.pos = start + 1;
903 self.read_next_token()
904 }
905 }
906
907 b'(' => {
909 self.pos = start + 1;
910 self.tok(TokenKind::LeftParen, start)
911 }
912 b')' => {
913 self.pos = start + 1;
914 self.tok(TokenKind::RightParen, start)
915 }
916 b'[' => {
917 self.pos = start + 1;
918 self.tok(TokenKind::LeftBracket, start)
919 }
920 b']' => {
921 self.pos = start + 1;
922 self.tok(TokenKind::RightBracket, start)
923 }
924 b'{' => {
925 self.pos = start + 1;
926 self.tok(TokenKind::LeftBrace, start)
927 }
928 b'}' => {
929 self.pos = start + 1;
930 self.tok(TokenKind::RightBrace, start)
931 }
932 b';' => {
933 self.pos = start + 1;
934 self.tok(TokenKind::Semicolon, start)
935 }
936 b',' => {
937 self.pos = start + 1;
938 self.tok(TokenKind::Comma, start)
939 }
940
941 b'\'' => self.scan_single_quoted_string(),
943 b'"' => self.scan_double_quoted_string(),
944 b'`' => self.scan_backtick_string(),
945
946 b'$' => {
948 if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
949 self.pos = start + 2;
950 while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
951 self.pos += 1;
952 }
953 self.tok(TokenKind::Variable, start)
954 } else {
955 self.pos = start + 1;
956 self.tok(TokenKind::Dollar, start)
957 }
958 }
959
960 b'0'..=b'9' => self.scan_number(),
962
963 _ if is_ident_start(b) => {
965 if b == b'b' || b == b'B' {
967 if self.check_at(1, b'\'') {
968 return self.scan_single_quoted_string();
969 }
970 if self.check_at(1, b'"') {
971 return self.scan_double_quoted_string();
972 }
973 if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
974 let remaining = &self.source[self.pos..];
975 if let Some(token) = self.try_lex_heredoc(remaining) {
976 return token;
977 }
978 }
979 }
980 self.scan_identifier()
981 }
982
983 _ => {
985 let mut end = start + 1;
986 while bytes.get(end).is_some_and(|&b| is_unexpected_character(b)) {
987 end += 1;
988 }
989 self.errors.push(LexerError {
990 kind: LexerErrorKind::UnexpectedCharacter,
991 message: format!("unexpected character 0x{b:02X}"),
992 span: Span::new(start as u32, end as u32),
993 });
994 self.pos = end;
995 self.read_next_token()
996 }
997 }
998 }
999
1000 fn scan_less_than(&mut self, start: usize) -> Token {
1002 if self.check_at(1, b'<') {
1003 if self.check_at(2, b'<') {
1004 let remaining = &self.source[self.pos..];
1006 if let Some(token) = self.try_lex_heredoc(remaining) {
1007 return token;
1008 }
1009 }
1011 if self.check_at(2, b'=') {
1012 self.pos = start + 3;
1013 return self.tok(TokenKind::ShiftLeftEquals, start);
1014 }
1015 self.pos = start + 2;
1016 return self.tok(TokenKind::ShiftLeft, start);
1017 }
1018 if self.check_at(1, b'=') {
1019 if self.check_at(2, b'>') {
1020 self.pos = start + 3;
1021 return self.tok(TokenKind::Spaceship, start);
1022 }
1023 self.pos = start + 2;
1024 return self.tok(TokenKind::LessThanEquals, start);
1025 }
1026 if self.check_at(1, b'>') {
1028 self.pos = start + 2;
1029 return self.tok(TokenKind::BangEquals, start);
1030 }
1031 self.pos = start + 1;
1033 self.tok(TokenKind::LessThan, start)
1034 }
1035
1036 fn scan_single_quoted_string(&mut self) -> Token {
1039 let start = self.pos;
1040 let bytes = self.source.as_bytes();
1041 let mut p = self.pos;
1042 if bytes[p] == b'b' || bytes[p] == b'B' {
1044 p += 1;
1045 }
1046 p += 1; loop {
1048 match memchr2(b'\\', b'\'', &bytes[p..]) {
1049 None => {
1050 self.errors.push(LexerError {
1051 kind: LexerErrorKind::UnterminatedString,
1052 message: "unterminated string literal".to_string(),
1053 span: Span::new(start as u32, self.source.len() as u32),
1054 });
1055 self.mark_non_verbatim(start);
1056 self.pos = self.source.len();
1057 return self.tok(TokenKind::SingleQuotedString, start);
1058 }
1059 Some(offset) => {
1060 p += offset;
1061 match bytes[p] {
1062 b'\\' => {
1063 self.mark_non_verbatim(start);
1064 p += 1;
1065 if p < bytes.len() {
1066 p += 1;
1067 }
1068 }
1069 _ => {
1070 p += 1;
1072 break;
1073 }
1074 }
1075 }
1076 }
1077 }
1078 self.pos = p;
1079 self.tok(TokenKind::SingleQuotedString, start)
1080 }
1081
1082 fn scan_double_quoted_string(&mut self) -> Token {
1083 let start = self.pos;
1084 let bytes = self.source.as_bytes();
1085 let mut p = self.pos;
1086 if bytes[p] == b'b' || bytes[p] == b'B' {
1088 p += 1;
1089 }
1090 p += 1; let mut verbatim = true;
1092 loop {
1093 let segment = p;
1094 let Some(offset) = memchr::memchr3(b'\\', b'"', b'$', &bytes[p..]) else {
1095 self.errors.push(LexerError {
1096 kind: LexerErrorKind::UnterminatedString,
1097 message: "unterminated string literal".to_string(),
1098 span: Span::new(start as u32, self.source.len() as u32),
1099 });
1100 self.mark_non_verbatim(start);
1101 self.pos = self.source.len();
1102 return self.tok(TokenKind::DoubleQuotedString, start);
1103 };
1104 p += offset;
1105 match bytes[p] {
1106 b'\\' => {
1107 verbatim = false;
1108 p = (p + 2).min(bytes.len());
1109 }
1110 b'"' => {
1111 p += 1;
1112 break;
1113 }
1114 _ => {
1116 verbatim = false;
1117 if p > segment && bytes[p - 1] == b'{' {
1122 p = skip_complex_interp(bytes, p - 1, self.attributes);
1123 } else if bytes.get(p + 1) == Some(&b'{') {
1124 p = skip_complex_interp(bytes, p + 1, self.attributes);
1125 } else {
1126 p += 1;
1127 }
1128 }
1129 }
1130 }
1131 if !verbatim {
1132 self.mark_non_verbatim(start);
1133 }
1134 self.pos = p;
1135 self.tok(TokenKind::DoubleQuotedString, start)
1136 }
1137
1138 #[inline]
1139 fn mark_non_verbatim(&mut self, start: usize) {
1140 if self.non_verbatim_strings.last() != Some(&(start as u32)) {
1142 self.non_verbatim_strings.push(start as u32);
1143 }
1144 }
1145
1146 fn scan_backtick_string(&mut self) -> Token {
1147 let start = self.pos;
1148 let bytes = self.source.as_bytes();
1149 let mut p = self.pos;
1150 p += 1; loop {
1152 let segment = p;
1153 match memchr::memchr3(b'\\', b'`', b'$', &bytes[p..]) {
1154 None => {
1155 self.errors.push(LexerError {
1156 kind: LexerErrorKind::UnterminatedString,
1157 message: "unterminated string literal".to_string(),
1158 span: Span::new(start as u32, self.source.len() as u32),
1159 });
1160 self.pos = self.source.len();
1161 return self.tok(TokenKind::BacktickString, start);
1162 }
1163 Some(offset) => {
1164 p += offset;
1165 match bytes[p] {
1166 b'\\' => {
1167 p += 1;
1168 if p < bytes.len() {
1169 p += 1;
1170 }
1171 }
1172 b'$' if p > segment && bytes[p - 1] == b'{' => {
1173 p = skip_complex_interp(bytes, p - 1, self.attributes);
1174 }
1175 b'$' if bytes.get(p + 1) == Some(&b'{') => {
1176 p = skip_complex_interp(bytes, p + 1, self.attributes);
1177 }
1178 b'$' => p += 1,
1179 _ => {
1180 p += 1;
1182 break;
1183 }
1184 }
1185 }
1186 }
1187 }
1188 self.pos = p;
1189 self.tok(TokenKind::BacktickString, start)
1190 }
1191
1192 fn scan_number(&mut self) -> Token {
1195 let start = self.pos;
1196 let bytes = self.source.as_bytes();
1197
1198 if bytes[start] == b'0' && start + 1 < bytes.len() {
1200 match bytes[start + 1] {
1201 b'x' | b'X' => {
1202 self.pos = start + 2;
1203 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1204 self.consume_invalid_numeric_rest();
1205 return self.invalid_numeric(start);
1206 }
1207 if self.scan_digits(u8::is_ascii_hexdigit) {
1208 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1209 self.consume_invalid_numeric_rest();
1210 return self.invalid_numeric(start);
1211 }
1212 return self.tok(TokenKind::HexIntLiteral, start);
1213 }
1214 self.pos = start;
1216 }
1217 b'b' | b'B' => {
1218 self.pos = start + 2;
1219 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1220 self.consume_invalid_numeric_rest();
1221 return self.invalid_numeric(start);
1222 }
1223 if self.scan_digits(|b| b == &b'0' || b == &b'1') {
1224 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1225 self.consume_invalid_numeric_rest();
1226 return self.invalid_numeric(start);
1227 }
1228 return self.tok(TokenKind::BinIntLiteral, start);
1229 }
1230 self.pos = start;
1232 }
1233 b'o' | b'O' => {
1234 self.pos = start + 2;
1235 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1236 self.consume_invalid_numeric_rest();
1237 return self.invalid_numeric(start);
1238 }
1239 if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
1240 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1241 self.consume_invalid_numeric_rest();
1242 return self.invalid_numeric(start);
1243 }
1244 return self.tok(TokenKind::OctIntLiteralNew, start);
1245 }
1246 self.pos = start;
1248 }
1249 _ => {}
1250 }
1251 }
1252
1253 self.pos = start;
1255 self.scan_digits(u8::is_ascii_digit);
1256 let integer_end = self.pos;
1257 let mut kind = TokenKind::IntLiteral;
1258
1259 if bytes[start] == b'0' && integer_end > start + 1 {
1262 kind = TokenKind::OctIntLiteral;
1263 }
1264
1265 if self.pos < bytes.len() && bytes[self.pos] == b'.' {
1267 if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
1268 self.pos += 1; self.scan_digits(u8::is_ascii_digit);
1271 kind = TokenKind::FloatLiteralSimple;
1272 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1273 self.consume_invalid_numeric_rest();
1275 return self.invalid_numeric(start);
1276 } else {
1277 self.pos += 1; kind = TokenKind::FloatLiteralSimple;
1280 }
1281 }
1282
1283 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
1285 if self.try_scan_exponent() {
1286 kind = TokenKind::FloatLiteral;
1287 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1288 self.consume_invalid_numeric_rest();
1290 return self.invalid_numeric(start);
1291 }
1292 }
1293
1294 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1296 self.consume_invalid_numeric_rest();
1297 return self.invalid_numeric(start);
1298 }
1299
1300 self.tok(kind, start)
1301 }
1302
1303 fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
1306 let bytes = self.source.as_bytes();
1307 if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
1308 return false;
1309 }
1310 self.pos += 1;
1311 loop {
1312 if self.pos >= bytes.len() {
1313 break;
1314 }
1315 if is_valid(&bytes[self.pos]) {
1316 self.pos += 1;
1317 } else if bytes[self.pos] == b'_'
1318 && self.pos + 1 < bytes.len()
1319 && is_valid(&bytes[self.pos + 1])
1320 {
1321 self.pos += 2;
1322 } else {
1323 break;
1324 }
1325 }
1326 true
1327 }
1328
1329 fn try_scan_exponent(&mut self) -> bool {
1332 let bytes = self.source.as_bytes();
1333 let saved = self.pos;
1334 self.pos += 1; if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
1338 self.pos += 1;
1339 }
1340
1341 if self.scan_digits(u8::is_ascii_digit) {
1343 true
1344 } else {
1345 self.pos = saved;
1346 false
1347 }
1348 }
1349
1350 fn scan_identifier(&mut self) -> Token {
1353 let start = self.pos;
1354 let bytes = self.source.as_bytes();
1355 self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
1357 self.pos += 1;
1358 }
1359 let text = &self.source[start..self.pos];
1360 let starts_qualified_name = bytes.get(self.pos) == Some(&b'\\')
1363 && bytes.get(self.pos + 1).is_some_and(|&b| is_ident_start(b));
1364 let kind = match resolve_keyword(text) {
1365 Some(TokenKind::Namespace) => TokenKind::Namespace,
1366 Some(_) if starts_qualified_name => TokenKind::Identifier,
1367 Some(kind) => kind,
1368 None => TokenKind::Identifier,
1369 };
1370 self.tok(kind, start)
1371 }
1372
1373 #[inline]
1379 fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
1380 loop {
1381 match memchr::memchr3(b'\n', b'\r', b'?', &bytes[*pos..]) {
1382 None => {
1383 *pos = bytes.len();
1384 return;
1385 }
1386 Some(offset) => {
1387 let p = *pos + offset;
1388 if bytes[p] != b'?' {
1389 *pos = p; return;
1391 }
1392 if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
1394 *pos = p; return;
1396 }
1397 *pos = p + 1;
1399 }
1400 }
1401 }
1402 }
1403
1404 #[inline]
1405 fn check_at(&self, offset: usize, expected: u8) -> bool {
1406 self.source.as_bytes().get(self.pos + offset) == Some(&expected)
1407 }
1408
1409 #[inline]
1410 fn tok(&self, kind: TokenKind, start: usize) -> Token {
1411 Token::new(kind, Span::new(start as u32, self.pos as u32))
1412 }
1413
1414 fn invalid_numeric(&mut self, start: usize) -> Token {
1415 let span = Span::new(start as u32, self.pos as u32);
1416 self.errors.push(LexerError {
1417 kind: LexerErrorKind::InvalidNumericLiteral,
1418 message: "Invalid numeric literal".to_string(),
1419 span,
1420 });
1421 Token::new(TokenKind::InvalidNumericLiteral, span)
1422 }
1423
1424 fn consume_invalid_numeric_rest(&mut self) {
1426 let bytes = self.source.as_bytes();
1427 while self.pos < bytes.len() {
1428 let b = bytes[self.pos];
1429 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
1430 if (b == b'+' || b == b'-') && self.pos > 0 {
1432 let prev = bytes[self.pos - 1];
1433 if prev != b'e' && prev != b'E' {
1434 break;
1435 }
1436 }
1437 self.pos += 1;
1438 } else {
1439 break;
1440 }
1441 }
1442 }
1443
1444 fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
1448 let trimmed = remaining.trim_start_matches([' ', '\t', '\n', '\r']);
1450 let ws_len = remaining.len() - trimmed.len();
1451
1452 let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
1454 || trimmed.starts_with("B<<<"))
1455 && !trimmed[1..].starts_with("<<<>")
1456 {
1457 (&trimmed[1..], 1)
1458 } else {
1459 (trimmed, 0)
1460 };
1461
1462 if !after_prefix.starts_with("<<<") {
1463 return None;
1464 }
1465
1466 let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
1469 let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
1470 let arrows_offset =
1471 ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
1472
1473 let (label, is_nowdoc, label_line_end);
1475 if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
1476 let closing = label_len(after_quote);
1478 if after_quote.as_bytes().get(closing) != Some(&b'\'') {
1479 return None;
1480 }
1481 label = &after_quote[..closing];
1482 is_nowdoc = true;
1483 let after_label = &after_arrows_trimmed[2 + closing..];
1484 let nl = newline_len(after_label.as_bytes())?;
1485 label_line_end = arrows_offset + 2 + closing + nl - 1;
1486 } else {
1487 let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
1489 let closing = label_len(after_dquote);
1490 if after_dquote.as_bytes().get(closing) != Some(&b'"') {
1491 return None;
1492 }
1493 label = &after_dquote[..closing];
1494 &after_dquote[1 + closing..]
1495 } else {
1496 let end = label_len(after_arrows_trimmed);
1497 label = &after_arrows_trimmed[..end];
1498 &after_arrows_trimmed[end..]
1499 };
1500 let nl = newline_len(s.as_bytes())?;
1502 is_nowdoc = false;
1503 label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl - 1;
1504 };
1505
1506 if label.is_empty() {
1507 return None;
1508 }
1509
1510 let body_start_in_remaining = if label_line_end < remaining.len() {
1512 label_line_end + 1 } else {
1514 return None; };
1516
1517 let body = &remaining[body_start_in_remaining..];
1518
1519 let mut search_pos = 0;
1521 let end_marker_pos;
1522 loop {
1523 if search_pos >= body.len() {
1524 return None; }
1526 let line_start = search_pos;
1527 let line_end = body[line_start..]
1528 .find(['\n', '\r'])
1529 .map(|p| line_start + p)
1530 .unwrap_or(body.len());
1531 let line = &body[line_start..line_end];
1532 let trimmed_line = line.trim_start_matches([' ', '\t']);
1533
1534 if trimmed_line.len() >= label.len()
1539 && &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
1540 && !trimmed_line
1541 .as_bytes()
1542 .get(label.len())
1543 .copied()
1544 .is_some_and(is_ident_continue)
1545 {
1546 end_marker_pos = line_start;
1547 break;
1548 }
1549
1550 search_pos = if is_nowdoc {
1551 line_end + newline_len(&body.as_bytes()[line_end..]).unwrap_or(0)
1552 } else {
1553 next_heredoc_line(body.as_bytes(), line_start, self.attributes)
1554 };
1555 }
1556
1557 let end_marker_line = &body[end_marker_pos..];
1559 let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
1560 let indent_len = end_marker_line.len() - trimmed.len();
1561 let token_end_in_remaining =
1562 body_start_in_remaining + end_marker_pos + indent_len + label.len();
1563 self.pos = base_pos + token_end_in_remaining;
1564
1565 let span = Span::new(start as u32, self.pos as u32);
1566
1567 if is_nowdoc {
1568 Some(Token::new(TokenKind::Nowdoc, span))
1569 } else {
1570 Some(Token::new(TokenKind::Heredoc, span))
1571 }
1572 }
1573}
1574
1575pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
1583 let mut lexer = Lexer::new(source);
1584 let mut tokens = Vec::with_capacity(lexer.source().len() / 8);
1586
1587 loop {
1588 let tok = lexer.next_token();
1589 let is_eof = tok.kind == TokenKind::Eof;
1590 tokens.push(tok);
1591 if is_eof {
1592 break;
1593 }
1594 }
1595
1596 let eof_span = tokens
1600 .last()
1601 .expect("tokens guaranteed to contain at least the Eof token from lexer")
1602 .span;
1603 tokens.push(Token::new(TokenKind::Eof, eof_span));
1604
1605 let errors = lexer.errors;
1606 (tokens, errors)
1607}