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 UnterminatedComment,
63 InvalidNumericLiteral,
65 Other,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71#[non_exhaustive]
72pub struct LexerError {
73 pub kind: LexerErrorKind,
75 pub message: String,
77 pub span: Span,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct Token {
84 pub kind: TokenKind,
86 pub span: Span,
88}
89
90impl Token {
91 pub fn new(kind: TokenKind, span: Span) -> Self {
93 Self { kind, span }
94 }
95
96 pub fn eof(offset: u32) -> Self {
98 Self {
99 kind: TokenKind::Eof,
100 span: Span::new(offset, offset),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106enum LexerMode {
107 InlineHtml,
108 Php,
109}
110
111pub struct Lexer<'src> {
113 source: &'src str,
114 mode: LexerMode,
115 pos: usize,
116 peeked: Option<Token>,
117 peeked2: Option<Token>,
118 non_verbatim_strings: Vec<u32>,
121 verbatim_cursor: usize,
124 errors: Vec<LexerError>,
125}
126
127#[inline(always)]
128fn is_ident_start(b: u8) -> bool {
129 IS_IDENT_START[b as usize]
130}
131
132#[inline(always)]
133fn is_ident_continue(b: u8) -> bool {
134 IS_IDENT_CONTINUE[b as usize]
135}
136
137fn label_len(s: &str) -> usize {
139 let bytes = s.as_bytes();
140 if !bytes.first().copied().is_some_and(is_ident_start) {
141 return 0;
142 }
143 1 + bytes[1..]
144 .iter()
145 .take_while(|b| is_ident_continue(**b))
146 .count()
147}
148
149fn skip_complex_interp(bytes: &[u8], p: usize) -> usize {
155 interpolation_end(bytes, p).unwrap_or(bytes.len())
156}
157
158enum InterpFrame<'a> {
160 Code(u32),
162 Quoted(u8),
164 Heredoc(&'a [u8]),
166}
167
168#[doc(hidden)]
175pub fn interpolation_end(bytes: &[u8], mut p: usize) -> Option<usize> {
176 debug_assert!(bytes.get(p) == Some(&b'{'));
177 let mut stack = vec![InterpFrame::Code(0)];
178 while p < bytes.len() {
179 let b = bytes[p];
180 let (quote, heredoc) = match stack
181 .last_mut()
182 .expect("the scan returns when the stack empties")
183 {
184 InterpFrame::Code(depth) => {
185 match b {
186 b'{' => {
187 *depth += 1;
188 p += 1;
189 }
190 b'}' => {
191 *depth -= 1;
192 p += 1;
193 if *depth == 0 {
194 stack.pop();
195 if stack.is_empty() {
196 return Some(p);
197 }
198 }
199 }
200 b'"' | b'`' => {
201 stack.push(InterpFrame::Quoted(b));
202 p += 1;
203 }
204 b'\'' => p = skip_nested_squoted(bytes, p),
205 b'#' if bytes.get(p + 1) != Some(&b'[') => {
206 p += 1;
207 Lexer::skip_line_comment_body(bytes, &mut p);
208 }
209 b'/' if bytes.get(p + 1) == Some(&b'/') => {
210 p += 2;
211 Lexer::skip_line_comment_body(bytes, &mut p);
212 }
213 b'/' if bytes.get(p + 1) == Some(&b'*') => {
214 p = memmem::find(&bytes[p + 2..], b"*/").map_or(bytes.len(), |e| p + e + 4);
215 }
216 b'<' if bytes[p..].starts_with(b"<<<") => match heredoc_header(bytes, p + 3) {
217 Some((label, true, body)) => p = nowdoc_end(bytes, body, label),
218 Some((label, false, body)) => {
219 p = body;
220 match closing_label_end(bytes, p, label) {
221 Some(end) => p = end,
222 None => stack.push(InterpFrame::Heredoc(label)),
223 }
224 }
225 None => p += 3,
226 },
227 _ => p += 1,
228 }
229 continue;
230 }
231 InterpFrame::Quoted(q) => (Some(*q), None),
232 InterpFrame::Heredoc(label) => (None, Some(*label)),
233 };
234 match b {
235 b'\\' if heredoc.is_none() || newline_len(&bytes[p + 1..]).is_none() => {
236 p += 2;
237 }
238 b'{' if bytes.get(p + 1) == Some(&b'$') => stack.push(InterpFrame::Code(0)),
239 b'$' if bytes.get(p + 1) == Some(&b'{') => {
240 p += 1;
241 stack.push(InterpFrame::Code(0));
242 }
243 b'\n' | b'\r' if heredoc.is_some() => {
244 p += newline_len(&bytes[p..]).unwrap_or(1);
245 if let Some(end) = heredoc.and_then(|l| closing_label_end(bytes, p, l)) {
246 stack.pop();
247 p = end;
248 }
249 }
250 _ if quote == Some(b) => {
251 stack.pop();
252 p += 1;
253 }
254 _ => p += 1,
255 }
256 }
257 None
258}
259
260fn heredoc_header(bytes: &[u8], mut p: usize) -> Option<(&[u8], bool, usize)> {
262 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
263 p += 1;
264 }
265 let quote = bytes.get(p).copied().filter(|q| matches!(q, b'\'' | b'"'));
266 if quote.is_some() {
267 p += 1;
268 }
269 let label_start = p;
270 if !bytes.get(p).copied().is_some_and(is_ident_start) {
271 return None;
272 }
273 while bytes.get(p).copied().is_some_and(is_ident_continue) {
274 p += 1;
275 }
276 let label = &bytes[label_start..p];
277 if let Some(q) = quote {
278 if bytes.get(p) != Some(&q) {
279 return None;
280 }
281 p += 1;
282 }
283 let body = p + newline_len(&bytes[p..])?;
284 Some((label, quote == Some(b'\''), body))
285}
286
287fn closing_label_end(bytes: &[u8], mut p: usize, label: &[u8]) -> Option<usize> {
289 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
290 p += 1;
291 }
292 let end = p + label.len();
293 (bytes[p..].starts_with(label) && !bytes.get(end).copied().is_some_and(is_ident_continue))
294 .then_some(end)
295}
296
297fn nowdoc_end(bytes: &[u8], mut p: usize, label: &[u8]) -> usize {
299 while p < bytes.len() {
300 if let Some(end) = closing_label_end(bytes, p, label) {
301 return end;
302 }
303 p = memchr2(b'\n', b'\r', &bytes[p..]).map_or(bytes.len(), |e| {
304 p + e + newline_len(&bytes[p + e..]).unwrap_or(1)
305 });
306 }
307 bytes.len()
308}
309
310fn newline_len(bytes: &[u8]) -> Option<usize> {
312 match bytes {
313 [b'\r', b'\n', ..] => Some(2),
314 [b'\n' | b'\r', ..] => Some(1),
315 _ => None,
316 }
317}
318
319fn next_heredoc_line(bytes: &[u8], mut p: usize) -> usize {
322 while p < bytes.len() {
323 match bytes[p] {
324 b'\n' | b'\r' => return p + newline_len(&bytes[p..]).unwrap_or(1),
325 b'\\' if !matches!(bytes.get(p + 1), Some(b'\n' | b'\r')) => p += 2,
326 b'{' if bytes.get(p + 1) == Some(&b'$') => p = skip_complex_interp(bytes, p),
327 b'$' if bytes.get(p + 1) == Some(&b'{') => p = skip_complex_interp(bytes, p + 1),
328 _ => p += 1,
329 }
330 }
331 bytes.len()
332}
333
334fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
335 debug_assert!(bytes.get(p) == Some(&b'\''));
336 p += 1;
337 while p < bytes.len() {
338 match bytes[p] {
339 b'\\' => {
340 p += 1;
341 if p < bytes.len() {
342 p += 1;
343 }
344 }
345 b'\'' => return p + 1,
346 _ => p += 1,
347 }
348 }
349 p
350}
351
352fn starts_with_open_tag(rest: &[u8]) -> bool {
355 rest.starts_with(b"<?=")
356 || rest.len() >= 5
357 && rest.starts_with(b"<?")
358 && rest[2..5].eq_ignore_ascii_case(b"php")
359 && rest
360 .get(5)
361 .is_none_or(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
362}
363
364impl<'src> Lexer<'src> {
365 pub fn new(source: &'src str) -> Self {
370 if let Some(lexer) = Self::too_large(source) {
371 return lexer;
372 }
373
374 let pos = if source.starts_with("#!") {
376 source.find('\n').map(|p| p + 1).unwrap_or(source.len())
377 } else {
378 0
379 };
380
381 let mode = if starts_with_open_tag(&source.as_bytes()[pos..]) {
383 LexerMode::Php
384 } else {
385 LexerMode::InlineHtml
386 };
387
388 Self {
389 source,
390 mode,
391 pos,
392 peeked: None,
393 peeked2: None,
394 non_verbatim_strings: Vec::new(),
395 verbatim_cursor: 0,
396 errors: Vec::new(),
397 }
398 }
399
400 pub fn new_at(source: &'src str, offset: usize) -> Self {
407 if let Some(lexer) = Self::too_large(source) {
408 return lexer;
409 }
410
411 Self {
412 source,
413 mode: LexerMode::Php,
414 pos: offset,
415 peeked: None,
416 peeked2: None,
417 non_verbatim_strings: Vec::new(),
418 verbatim_cursor: 0,
419 errors: Vec::new(),
420 }
421 }
422
423 fn too_large(source: &str) -> Option<Self> {
426 if source.len() <= u32::MAX as usize {
427 return None;
428 }
429 Some(Self {
430 source: "",
431 mode: LexerMode::Php,
432 pos: 0,
433 peeked: None,
434 peeked2: None,
435 non_verbatim_strings: Vec::new(),
436 verbatim_cursor: 0,
437 errors: vec![LexerError {
438 kind: LexerErrorKind::FileTooLarge,
439 message: format!(
440 "source is {} bytes, which exceeds the maximum supported size of {} bytes",
441 source.len(),
442 u32::MAX
443 ),
444 span: Span::new(0, 0),
445 }],
446 })
447 }
448
449 pub fn errors(&self) -> &[LexerError] {
451 &self.errors
452 }
453
454 pub fn into_errors(self) -> Vec<LexerError> {
456 self.errors
457 }
458
459 #[doc(hidden)]
466 pub fn is_verbatim_string(&mut self, start: u32) -> bool {
467 let list = &self.non_verbatim_strings;
468 let mut i = self.verbatim_cursor;
469 if i > 0 && list.get(i - 1).is_some_and(|&prev| prev >= start) {
470 return list.binary_search(&start).is_err();
472 }
473 while list.get(i).is_some_and(|&next| next < start) {
474 i += 1;
475 }
476 self.verbatim_cursor = i;
477 list.get(i) != Some(&start)
478 }
479
480 #[doc(hidden)]
483 pub fn halt_at(&mut self, offset: usize) {
484 self.peeked = None;
485 self.peeked2 = None;
486 self.pos = self.source.len();
487 self.errors.retain(|e| (e.span.start as usize) < offset);
488 }
489
490 pub fn source(&self) -> &'src str {
492 self.source
493 }
494
495 pub fn peek(&mut self) -> &Token {
497 if self.peeked.is_none() {
498 self.peeked = Some(self.read_next_token());
499 }
500 self.peeked.as_ref().expect("peeked is Some: set above")
501 }
502
503 pub fn peek2(&mut self) -> &Token {
505 if self.peeked.is_none() {
507 self.peeked = Some(self.read_next_token());
508 }
509 if self.peeked2.is_none() {
510 self.peeked2 = Some(self.read_next_token());
511 }
512 self.peeked2.as_ref().expect("peeked2 is Some: set above")
513 }
514
515 pub fn next_token(&mut self) -> Token {
517 if let Some(token) = self.peeked.take() {
518 self.peeked = self.peeked2.take();
519 return token;
520 }
521 self.read_next_token()
522 }
523
524 pub fn token_text(&self, token: &Token) -> &'src str {
526 &self.source[token.span.start as usize..token.span.end as usize]
527 }
528
529 fn read_next_token(&mut self) -> Token {
530 if self.pos >= self.source.len() {
531 return Token::eof(self.source.len() as u32);
532 }
533
534 match self.mode {
535 LexerMode::InlineHtml => self.lex_inline_html(),
536 LexerMode::Php => self.lex_php(),
537 }
538 }
539
540 fn lex_inline_html(&mut self) -> Token {
541 let start = self.pos;
542 let bytes = self.source.as_bytes();
543
544 let mut search = self.pos;
548 let tag_pos = loop {
549 match memchr::memchr(b'<', &bytes[search..]) {
550 None => break None,
551 Some(offset) => {
552 let p = search + offset;
553 if starts_with_open_tag(&bytes[p..]) {
554 break Some(p - self.pos);
555 }
556 search = p + 1;
557 }
558 }
559 };
560
561 if let Some(tag_pos) = tag_pos {
562 if tag_pos == 0 {
563 self.mode = LexerMode::Php;
565 return self.lex_php();
566 }
567 let end = self.pos + tag_pos;
569 self.pos = end;
570 self.mode = LexerMode::Php;
571 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
572 } else {
573 let end = self.source.len();
575 self.pos = end;
576 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
577 }
578 }
579
580 fn lex_php(&mut self) -> Token {
581 self.skip_whitespace();
583
584 if self.pos >= self.source.len() {
585 return Token::eof(self.source.len() as u32);
586 }
587
588 let bytes = self.source.as_bytes();
589 let start = self.pos;
590
591 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
594 self.pos += 2;
595 Self::skip_line_comment_body(bytes, &mut self.pos);
596 return self.tok(TokenKind::LineComment, start);
597 }
598
599 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
601 self.pos += 2;
602 let kind = if self.pos < bytes.len()
605 && bytes[self.pos] == b'*'
606 && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
607 {
608 TokenKind::DocComment
609 } else {
610 TokenKind::BlockComment
611 };
612 match memmem::find(&bytes[self.pos..], b"*/") {
613 Some(end) => self.pos += end + 2,
614 None => {
615 let span = Span::new(start as u32, self.source.len() as u32);
616 self.errors.push(LexerError {
617 kind: LexerErrorKind::UnterminatedComment,
618 message: "unterminated block comment".to_string(),
619 span,
620 });
621 self.pos = bytes.len();
622 }
623 }
624 return self.tok(kind, start);
625 }
626
627 if bytes[self.pos] == b'#' && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'[') {
630 self.pos += 1;
631 Self::skip_line_comment_body(bytes, &mut self.pos);
632 return self.tok(TokenKind::HashComment, start);
633 }
634
635 self.scan_token()
636 }
637
638 fn skip_whitespace(&mut self) {
640 let bytes = self.source.as_bytes();
641 while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
642 self.pos += 1;
643 }
644 }
645
646 fn scan_token(&mut self) -> Token {
648 let start = self.pos;
649 let bytes = self.source.as_bytes();
650 let b = bytes[start];
651
652 match b {
653 b'+' => {
655 if self.check_at(1, b'+') {
656 self.pos = start + 2;
657 self.tok(TokenKind::PlusPlus, start)
658 } else if self.check_at(1, b'=') {
659 self.pos = start + 2;
660 self.tok(TokenKind::PlusEquals, start)
661 } else {
662 self.pos = start + 1;
663 self.tok(TokenKind::Plus, start)
664 }
665 }
666 b'-' => {
667 if self.check_at(1, b'-') {
668 self.pos = start + 2;
669 self.tok(TokenKind::MinusMinus, start)
670 } else if self.check_at(1, b'=') {
671 self.pos = start + 2;
672 self.tok(TokenKind::MinusEquals, start)
673 } else if self.check_at(1, b'>') {
674 self.pos = start + 2;
675 self.tok(TokenKind::Arrow, start)
676 } else {
677 self.pos = start + 1;
678 self.tok(TokenKind::Minus, start)
679 }
680 }
681 b'*' => {
682 if self.check_at(1, b'*') {
683 if self.check_at(2, b'=') {
684 self.pos = start + 3;
685 self.tok(TokenKind::StarStarEquals, start)
686 } else {
687 self.pos = start + 2;
688 self.tok(TokenKind::StarStar, start)
689 }
690 } else if self.check_at(1, b'=') {
691 self.pos = start + 2;
692 self.tok(TokenKind::StarEquals, start)
693 } else {
694 self.pos = start + 1;
695 self.tok(TokenKind::Star, start)
696 }
697 }
698 b'/' => {
699 if self.check_at(1, b'=') {
701 self.pos = start + 2;
702 self.tok(TokenKind::SlashEquals, start)
703 } else {
704 self.pos = start + 1;
705 self.tok(TokenKind::Slash, start)
706 }
707 }
708 b'%' => {
709 if self.check_at(1, b'=') {
710 self.pos = start + 2;
711 self.tok(TokenKind::PercentEquals, start)
712 } else {
713 self.pos = start + 1;
714 self.tok(TokenKind::Percent, start)
715 }
716 }
717 b'.' => {
718 if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
720 self.pos = start + 1;
721 self.scan_digits(u8::is_ascii_digit);
722 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
724 self.try_scan_exponent();
725 }
726 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
728 self.consume_invalid_numeric_rest();
729 return self.invalid_numeric(start);
730 }
731 return self.tok(TokenKind::FloatLiteralLeadingDot, start);
732 }
733 if self.check_at(1, b'.') && self.check_at(2, b'.') {
734 self.pos = start + 3;
735 self.tok(TokenKind::Ellipsis, start)
736 } else if self.check_at(1, b'=') {
737 self.pos = start + 2;
738 self.tok(TokenKind::DotEquals, start)
739 } else {
740 self.pos = start + 1;
741 self.tok(TokenKind::Dot, start)
742 }
743 }
744 b'=' => {
745 if self.check_at(1, b'=') {
746 if self.check_at(2, b'=') {
747 self.pos = start + 3;
748 self.tok(TokenKind::EqualsEqualsEquals, start)
749 } else {
750 self.pos = start + 2;
751 self.tok(TokenKind::EqualsEquals, start)
752 }
753 } else if self.check_at(1, b'>') {
754 self.pos = start + 2;
755 self.tok(TokenKind::FatArrow, start)
756 } else {
757 self.pos = start + 1;
758 self.tok(TokenKind::Equals, start)
759 }
760 }
761 b'!' => {
762 if self.check_at(1, b'=') {
763 if self.check_at(2, b'=') {
764 self.pos = start + 3;
765 self.tok(TokenKind::BangEqualsEquals, start)
766 } else {
767 self.pos = start + 2;
768 self.tok(TokenKind::BangEquals, start)
769 }
770 } else {
771 self.pos = start + 1;
772 self.tok(TokenKind::Bang, start)
773 }
774 }
775 b'<' => self.scan_less_than(start),
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::ShiftRightEquals, start)
781 } else {
782 self.pos = start + 2;
783 self.tok(TokenKind::ShiftRight, start)
784 }
785 } else if self.check_at(1, b'=') {
786 self.pos = start + 2;
787 self.tok(TokenKind::GreaterThanEquals, start)
788 } else {
789 self.pos = start + 1;
790 self.tok(TokenKind::GreaterThan, start)
791 }
792 }
793 b'&' => {
794 if self.check_at(1, b'&') {
795 self.pos = start + 2;
796 self.tok(TokenKind::AmpersandAmpersand, start)
797 } else if self.check_at(1, b'=') {
798 self.pos = start + 2;
799 self.tok(TokenKind::AmpersandEquals, start)
800 } else {
801 self.pos = start + 1;
802 self.tok(TokenKind::Ampersand, start)
803 }
804 }
805 b'|' => {
806 if self.check_at(1, b'|') {
807 self.pos = start + 2;
808 self.tok(TokenKind::PipePipe, start)
809 } else if self.check_at(1, b'=') {
810 self.pos = start + 2;
811 self.tok(TokenKind::PipeEquals, start)
812 } else if self.check_at(1, b'>') {
813 self.pos = start + 2;
814 self.tok(TokenKind::PipeArrow, start)
815 } else {
816 self.pos = start + 1;
817 self.tok(TokenKind::Pipe, start)
818 }
819 }
820 b'^' => {
821 if self.check_at(1, b'=') {
822 self.pos = start + 2;
823 self.tok(TokenKind::CaretEquals, start)
824 } else {
825 self.pos = start + 1;
826 self.tok(TokenKind::Caret, start)
827 }
828 }
829 b'~' => {
830 self.pos = start + 1;
831 self.tok(TokenKind::Tilde, start)
832 }
833 b'?' => {
834 if self.check_at(1, b'>') {
835 self.pos = start + 2;
836 self.mode = LexerMode::InlineHtml;
837 self.tok(TokenKind::CloseTag, start)
838 } else if self.check_at(1, b'?') {
839 if self.check_at(2, b'=') {
840 self.pos = start + 3;
841 self.tok(TokenKind::CoalesceEquals, start)
842 } else {
843 self.pos = start + 2;
844 self.tok(TokenKind::QuestionQuestion, start)
845 }
846 } else if self.check_at(1, b'-') && self.check_at(2, b'>') {
847 self.pos = start + 3;
848 self.tok(TokenKind::NullsafeArrow, start)
849 } else {
850 self.pos = start + 1;
851 self.tok(TokenKind::Question, start)
852 }
853 }
854 b':' => {
855 if self.check_at(1, b':') {
856 self.pos = start + 2;
857 self.tok(TokenKind::DoubleColon, start)
858 } else {
859 self.pos = start + 1;
860 self.tok(TokenKind::Colon, start)
861 }
862 }
863 b'@' => {
864 self.pos = start + 1;
865 self.tok(TokenKind::At, start)
866 }
867 b'\\' => {
868 self.pos = start + 1;
869 self.tok(TokenKind::Backslash, start)
870 }
871 b'#' => {
872 if self.check_at(1, b'[') {
875 self.pos = start + 2;
876 self.tok(TokenKind::HashBracket, start)
877 } else {
878 self.pos = start + 1;
880 self.read_next_token()
881 }
882 }
883
884 b'(' => {
886 self.pos = start + 1;
887 self.tok(TokenKind::LeftParen, start)
888 }
889 b')' => {
890 self.pos = start + 1;
891 self.tok(TokenKind::RightParen, start)
892 }
893 b'[' => {
894 self.pos = start + 1;
895 self.tok(TokenKind::LeftBracket, start)
896 }
897 b']' => {
898 self.pos = start + 1;
899 self.tok(TokenKind::RightBracket, start)
900 }
901 b'{' => {
902 self.pos = start + 1;
903 self.tok(TokenKind::LeftBrace, start)
904 }
905 b'}' => {
906 self.pos = start + 1;
907 self.tok(TokenKind::RightBrace, start)
908 }
909 b';' => {
910 self.pos = start + 1;
911 self.tok(TokenKind::Semicolon, start)
912 }
913 b',' => {
914 self.pos = start + 1;
915 self.tok(TokenKind::Comma, start)
916 }
917
918 b'\'' => self.scan_single_quoted_string(),
920 b'"' => self.scan_double_quoted_string(),
921 b'`' => self.scan_backtick_string(),
922
923 b'$' => {
925 if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
926 self.pos = start + 2;
927 while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
928 self.pos += 1;
929 }
930 self.tok(TokenKind::Variable, start)
931 } else {
932 self.pos = start + 1;
933 self.tok(TokenKind::Dollar, start)
934 }
935 }
936
937 b'0'..=b'9' => self.scan_number(),
939
940 _ if is_ident_start(b) => {
942 if b == b'b' || b == b'B' {
944 if self.check_at(1, b'\'') {
945 return self.scan_single_quoted_string();
946 }
947 if self.check_at(1, b'"') {
948 return self.scan_double_quoted_string();
949 }
950 if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
951 let remaining = &self.source[self.pos..];
952 if let Some(token) = self.try_lex_heredoc(remaining) {
953 return token;
954 }
955 }
956 }
957 self.scan_identifier()
958 }
959
960 _ => {
962 self.pos = start + 1;
963 self.read_next_token()
964 }
965 }
966 }
967
968 fn scan_less_than(&mut self, start: usize) -> Token {
970 if self.check_at(1, b'<') {
971 if self.check_at(2, b'<') {
972 let remaining = &self.source[self.pos..];
974 if let Some(token) = self.try_lex_heredoc(remaining) {
975 return token;
976 }
977 }
979 if self.check_at(2, b'=') {
980 self.pos = start + 3;
981 return self.tok(TokenKind::ShiftLeftEquals, start);
982 }
983 self.pos = start + 2;
984 return self.tok(TokenKind::ShiftLeft, start);
985 }
986 if self.check_at(1, b'=') {
987 if self.check_at(2, b'>') {
988 self.pos = start + 3;
989 return self.tok(TokenKind::Spaceship, start);
990 }
991 self.pos = start + 2;
992 return self.tok(TokenKind::LessThanEquals, start);
993 }
994 if self.check_at(1, b'>') {
996 self.pos = start + 2;
997 return self.tok(TokenKind::BangEquals, start);
998 }
999 if starts_with_open_tag(&self.source.as_bytes()[start..]) {
1000 self.pos = start + if self.check_at(2, b'=') { 3 } else { 5 };
1001 return self.tok(TokenKind::OpenTag, start);
1002 }
1003 self.pos = start + 1;
1004 self.tok(TokenKind::LessThan, start)
1005 }
1006
1007 fn scan_single_quoted_string(&mut self) -> Token {
1010 let start = self.pos;
1011 let bytes = self.source.as_bytes();
1012 let mut p = self.pos;
1013 if bytes[p] == b'b' || bytes[p] == b'B' {
1015 p += 1;
1016 }
1017 p += 1; loop {
1019 match memchr2(b'\\', b'\'', &bytes[p..]) {
1020 None => {
1021 self.errors.push(LexerError {
1022 kind: LexerErrorKind::UnterminatedString,
1023 message: "unterminated string literal".to_string(),
1024 span: Span::new(start as u32, self.source.len() as u32),
1025 });
1026 self.mark_non_verbatim(start);
1027 self.pos = self.source.len();
1028 return self.tok(TokenKind::SingleQuotedString, start);
1029 }
1030 Some(offset) => {
1031 p += offset;
1032 match bytes[p] {
1033 b'\\' => {
1034 self.mark_non_verbatim(start);
1035 p += 1;
1036 if p < bytes.len() {
1037 p += 1;
1038 }
1039 }
1040 _ => {
1041 p += 1;
1043 break;
1044 }
1045 }
1046 }
1047 }
1048 }
1049 self.pos = p;
1050 self.tok(TokenKind::SingleQuotedString, start)
1051 }
1052
1053 fn scan_double_quoted_string(&mut self) -> Token {
1054 let start = self.pos;
1055 let bytes = self.source.as_bytes();
1056 let mut p = self.pos;
1057 if bytes[p] == b'b' || bytes[p] == b'B' {
1059 p += 1;
1060 }
1061 p += 1; let mut verbatim = true;
1063 loop {
1064 let segment = p;
1065 let Some(offset) = memchr::memchr3(b'\\', b'"', b'$', &bytes[p..]) else {
1066 self.errors.push(LexerError {
1067 kind: LexerErrorKind::UnterminatedString,
1068 message: "unterminated string literal".to_string(),
1069 span: Span::new(start as u32, self.source.len() as u32),
1070 });
1071 self.mark_non_verbatim(start);
1072 self.pos = self.source.len();
1073 return self.tok(TokenKind::DoubleQuotedString, start);
1074 };
1075 p += offset;
1076 match bytes[p] {
1077 b'\\' => {
1078 verbatim = false;
1079 p = (p + 2).min(bytes.len());
1080 }
1081 b'"' => {
1082 p += 1;
1083 break;
1084 }
1085 _ => {
1087 verbatim = false;
1088 if p > segment && bytes[p - 1] == b'{' {
1093 p = skip_complex_interp(bytes, p - 1);
1094 } else if bytes.get(p + 1) == Some(&b'{') {
1095 p = skip_complex_interp(bytes, p + 1);
1096 } else {
1097 p += 1;
1098 }
1099 }
1100 }
1101 }
1102 if !verbatim {
1103 self.mark_non_verbatim(start);
1104 }
1105 self.pos = p;
1106 self.tok(TokenKind::DoubleQuotedString, start)
1107 }
1108
1109 #[inline]
1110 fn mark_non_verbatim(&mut self, start: usize) {
1111 if self.non_verbatim_strings.last() != Some(&(start as u32)) {
1113 self.non_verbatim_strings.push(start as u32);
1114 }
1115 }
1116
1117 fn scan_backtick_string(&mut self) -> Token {
1118 let start = self.pos;
1119 let bytes = self.source.as_bytes();
1120 let mut p = self.pos;
1121 p += 1; loop {
1123 let segment = p;
1124 match memchr::memchr3(b'\\', b'`', b'$', &bytes[p..]) {
1125 None => {
1126 self.errors.push(LexerError {
1127 kind: LexerErrorKind::UnterminatedString,
1128 message: "unterminated string literal".to_string(),
1129 span: Span::new(start as u32, self.source.len() as u32),
1130 });
1131 self.pos = self.source.len();
1132 return self.tok(TokenKind::BacktickString, start);
1133 }
1134 Some(offset) => {
1135 p += offset;
1136 match bytes[p] {
1137 b'\\' => {
1138 p += 1;
1139 if p < bytes.len() {
1140 p += 1;
1141 }
1142 }
1143 b'$' if p > segment && bytes[p - 1] == b'{' => {
1144 p = skip_complex_interp(bytes, p - 1);
1145 }
1146 b'$' if bytes.get(p + 1) == Some(&b'{') => {
1147 p = skip_complex_interp(bytes, p + 1);
1148 }
1149 b'$' => p += 1,
1150 _ => {
1151 p += 1;
1153 break;
1154 }
1155 }
1156 }
1157 }
1158 }
1159 self.pos = p;
1160 self.tok(TokenKind::BacktickString, start)
1161 }
1162
1163 fn scan_number(&mut self) -> Token {
1166 let start = self.pos;
1167 let bytes = self.source.as_bytes();
1168
1169 if bytes[start] == b'0' && start + 1 < bytes.len() {
1171 match bytes[start + 1] {
1172 b'x' | b'X' => {
1173 self.pos = start + 2;
1174 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1175 self.consume_invalid_numeric_rest();
1176 return self.invalid_numeric(start);
1177 }
1178 if self.scan_digits(u8::is_ascii_hexdigit) {
1179 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1180 self.consume_invalid_numeric_rest();
1181 return self.invalid_numeric(start);
1182 }
1183 return self.tok(TokenKind::HexIntLiteral, start);
1184 }
1185 self.pos = start;
1187 }
1188 b'b' | b'B' => {
1189 self.pos = start + 2;
1190 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1191 self.consume_invalid_numeric_rest();
1192 return self.invalid_numeric(start);
1193 }
1194 if self.scan_digits(|b| b == &b'0' || b == &b'1') {
1195 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1196 self.consume_invalid_numeric_rest();
1197 return self.invalid_numeric(start);
1198 }
1199 return self.tok(TokenKind::BinIntLiteral, start);
1200 }
1201 self.pos = start;
1203 }
1204 b'o' | b'O' => {
1205 self.pos = start + 2;
1206 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1207 self.consume_invalid_numeric_rest();
1208 return self.invalid_numeric(start);
1209 }
1210 if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
1211 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1212 self.consume_invalid_numeric_rest();
1213 return self.invalid_numeric(start);
1214 }
1215 return self.tok(TokenKind::OctIntLiteralNew, start);
1216 }
1217 self.pos = start;
1219 }
1220 _ => {}
1221 }
1222 }
1223
1224 self.pos = start;
1226 self.scan_digits(u8::is_ascii_digit);
1227 let integer_end = self.pos;
1228 let mut kind = TokenKind::IntLiteral;
1229
1230 if bytes[start] == b'0' && integer_end > start + 1 {
1233 kind = TokenKind::OctIntLiteral;
1234 }
1235
1236 if self.pos < bytes.len() && bytes[self.pos] == b'.' {
1238 if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
1239 self.pos += 1; self.scan_digits(u8::is_ascii_digit);
1242 kind = TokenKind::FloatLiteralSimple;
1243 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1244 self.consume_invalid_numeric_rest();
1246 return self.invalid_numeric(start);
1247 } else {
1248 self.pos += 1; kind = TokenKind::FloatLiteralSimple;
1251 }
1252 }
1253
1254 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
1256 if self.try_scan_exponent() {
1257 kind = TokenKind::FloatLiteral;
1258 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1259 self.consume_invalid_numeric_rest();
1261 return self.invalid_numeric(start);
1262 }
1263 }
1264
1265 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1267 self.consume_invalid_numeric_rest();
1268 return self.invalid_numeric(start);
1269 }
1270
1271 self.tok(kind, start)
1272 }
1273
1274 fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
1277 let bytes = self.source.as_bytes();
1278 if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
1279 return false;
1280 }
1281 self.pos += 1;
1282 loop {
1283 if self.pos >= bytes.len() {
1284 break;
1285 }
1286 if is_valid(&bytes[self.pos]) {
1287 self.pos += 1;
1288 } else if bytes[self.pos] == b'_'
1289 && self.pos + 1 < bytes.len()
1290 && is_valid(&bytes[self.pos + 1])
1291 {
1292 self.pos += 2;
1293 } else {
1294 break;
1295 }
1296 }
1297 true
1298 }
1299
1300 fn try_scan_exponent(&mut self) -> bool {
1303 let bytes = self.source.as_bytes();
1304 let saved = self.pos;
1305 self.pos += 1; if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
1309 self.pos += 1;
1310 }
1311
1312 if self.scan_digits(u8::is_ascii_digit) {
1314 true
1315 } else {
1316 self.pos = saved;
1317 false
1318 }
1319 }
1320
1321 fn scan_identifier(&mut self) -> Token {
1324 let start = self.pos;
1325 let bytes = self.source.as_bytes();
1326 self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
1328 self.pos += 1;
1329 }
1330 let text = &self.source[start..self.pos];
1331 let starts_qualified_name = bytes.get(self.pos) == Some(&b'\\')
1334 && bytes.get(self.pos + 1).is_some_and(|&b| is_ident_start(b));
1335 let kind = match resolve_keyword(text) {
1336 Some(TokenKind::Namespace) => TokenKind::Namespace,
1337 Some(_) if starts_qualified_name => TokenKind::Identifier,
1338 Some(kind) => kind,
1339 None => TokenKind::Identifier,
1340 };
1341 self.tok(kind, start)
1342 }
1343
1344 #[inline]
1350 fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
1351 loop {
1352 match memchr::memchr3(b'\n', b'\r', b'?', &bytes[*pos..]) {
1353 None => {
1354 *pos = bytes.len();
1355 return;
1356 }
1357 Some(offset) => {
1358 let p = *pos + offset;
1359 if bytes[p] != b'?' {
1360 *pos = p; return;
1362 }
1363 if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
1365 *pos = p; return;
1367 }
1368 *pos = p + 1;
1370 }
1371 }
1372 }
1373 }
1374
1375 #[inline]
1376 fn check_at(&self, offset: usize, expected: u8) -> bool {
1377 self.source.as_bytes().get(self.pos + offset) == Some(&expected)
1378 }
1379
1380 #[inline]
1381 fn tok(&self, kind: TokenKind, start: usize) -> Token {
1382 Token::new(kind, Span::new(start as u32, self.pos as u32))
1383 }
1384
1385 fn invalid_numeric(&mut self, start: usize) -> Token {
1386 let span = Span::new(start as u32, self.pos as u32);
1387 self.errors.push(LexerError {
1388 kind: LexerErrorKind::InvalidNumericLiteral,
1389 message: "Invalid numeric literal".to_string(),
1390 span,
1391 });
1392 Token::new(TokenKind::InvalidNumericLiteral, span)
1393 }
1394
1395 fn consume_invalid_numeric_rest(&mut self) {
1397 let bytes = self.source.as_bytes();
1398 while self.pos < bytes.len() {
1399 let b = bytes[self.pos];
1400 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
1401 if (b == b'+' || b == b'-') && self.pos > 0 {
1403 let prev = bytes[self.pos - 1];
1404 if prev != b'e' && prev != b'E' {
1405 break;
1406 }
1407 }
1408 self.pos += 1;
1409 } else {
1410 break;
1411 }
1412 }
1413 }
1414
1415 fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
1419 let trimmed = remaining.trim_start_matches(|c: char| {
1421 c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\x0C'
1422 });
1423 let ws_len = remaining.len() - trimmed.len();
1424
1425 let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
1427 || trimmed.starts_with("B<<<"))
1428 && !trimmed[1..].starts_with("<<<>")
1429 {
1430 (&trimmed[1..], 1)
1431 } else {
1432 (trimmed, 0)
1433 };
1434
1435 if !after_prefix.starts_with("<<<") {
1436 return None;
1437 }
1438
1439 let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
1442 let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
1443 let arrows_offset =
1444 ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
1445
1446 let (label, is_nowdoc, label_line_end);
1448 if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
1449 let closing = label_len(after_quote);
1451 if after_quote.as_bytes().get(closing) != Some(&b'\'') {
1452 return None;
1453 }
1454 label = &after_quote[..closing];
1455 is_nowdoc = true;
1456 let after_label = &after_arrows_trimmed[2 + closing..];
1457 let nl = newline_len(after_label.as_bytes())?;
1458 label_line_end = arrows_offset + 2 + closing + nl - 1;
1459 } else {
1460 let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
1462 let closing = label_len(after_dquote);
1463 if after_dquote.as_bytes().get(closing) != Some(&b'"') {
1464 return None;
1465 }
1466 label = &after_dquote[..closing];
1467 &after_dquote[1 + closing..]
1468 } else {
1469 let end = label_len(after_arrows_trimmed);
1470 label = &after_arrows_trimmed[..end];
1471 &after_arrows_trimmed[end..]
1472 };
1473 let nl = newline_len(s.as_bytes())?;
1475 is_nowdoc = false;
1476 label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl - 1;
1477 };
1478
1479 if label.is_empty() {
1480 return None;
1481 }
1482
1483 let body_start_in_remaining = if label_line_end < remaining.len() {
1485 label_line_end + 1 } else {
1487 return None; };
1489
1490 let body = &remaining[body_start_in_remaining..];
1491
1492 let mut search_pos = 0;
1494 let end_marker_pos;
1495 loop {
1496 if search_pos >= body.len() {
1497 return None; }
1499 let line_start = search_pos;
1500 let line_end = body[line_start..]
1501 .find(['\n', '\r'])
1502 .map(|p| line_start + p)
1503 .unwrap_or(body.len());
1504 let line = &body[line_start..line_end];
1505 let trimmed_line = line.trim_start_matches([' ', '\t']);
1506
1507 if trimmed_line.len() >= label.len()
1512 && &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
1513 && !trimmed_line
1514 .as_bytes()
1515 .get(label.len())
1516 .copied()
1517 .is_some_and(is_ident_continue)
1518 {
1519 end_marker_pos = line_start;
1520 break;
1521 }
1522
1523 search_pos = if is_nowdoc {
1524 line_end + newline_len(&body.as_bytes()[line_end..]).unwrap_or(0)
1525 } else {
1526 next_heredoc_line(body.as_bytes(), line_start)
1527 };
1528 }
1529
1530 let end_marker_line = &body[end_marker_pos..];
1532 let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
1533 let indent_len = end_marker_line.len() - trimmed.len();
1534 let token_end_in_remaining =
1535 body_start_in_remaining + end_marker_pos + indent_len + label.len();
1536 self.pos = base_pos + token_end_in_remaining;
1537
1538 let span = Span::new(start as u32, self.pos as u32);
1539
1540 if is_nowdoc {
1541 Some(Token::new(TokenKind::Nowdoc, span))
1542 } else {
1543 Some(Token::new(TokenKind::Heredoc, span))
1544 }
1545 }
1546}
1547
1548pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
1556 let mut lexer = Lexer::new(source);
1557 let mut tokens = Vec::with_capacity(lexer.source().len() / 8);
1559
1560 loop {
1561 let tok = lexer.next_token();
1562 let is_eof = tok.kind == TokenKind::Eof;
1563 tokens.push(tok);
1564 if is_eof {
1565 break;
1566 }
1567 }
1568
1569 let eof_span = tokens
1573 .last()
1574 .expect("tokens guaranteed to contain at least the Eof token from lexer")
1575 .span;
1576 tokens.push(Token::new(TokenKind::Eof, eof_span));
1577
1578 let errors = lexer.errors;
1579 (tokens, errors)
1580}