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)]
55pub enum LexerErrorKind {
56 UnterminatedString,
58 FileTooLarge,
60 Other,
62}
63
64#[derive(Debug, Clone, PartialEq)]
66pub struct LexerError {
67 pub kind: LexerErrorKind,
69 pub message: String,
71 pub span: Span,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct Token {
78 pub kind: TokenKind,
80 pub span: Span,
82}
83
84impl Token {
85 pub fn new(kind: TokenKind, span: Span) -> Self {
87 Self { kind, span }
88 }
89
90 pub fn eof(offset: u32) -> Self {
92 Self {
93 kind: TokenKind::Eof,
94 span: Span::new(offset, offset),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100enum LexerMode {
101 InlineHtml,
102 Php,
103}
104
105pub struct Lexer<'src> {
107 source: &'src str,
108 mode: LexerMode,
109 pos: usize,
110 peeked: Option<Token>,
111 peeked2: Option<Token>,
112 pub errors: Vec<LexerError>,
114}
115
116#[inline(always)]
117fn is_ident_start(b: u8) -> bool {
118 IS_IDENT_START[b as usize]
119}
120
121#[inline(always)]
122fn is_ident_continue(b: u8) -> bool {
123 IS_IDENT_CONTINUE[b as usize]
124}
125
126fn skip_complex_interp(bytes: &[u8], mut p: usize) -> usize {
132 debug_assert!(bytes.get(p) == Some(&b'{'));
133 let mut depth = 0i32;
134 while p < bytes.len() {
135 match bytes[p] {
136 b'{' => {
137 depth += 1;
138 p += 1;
139 }
140 b'}' => {
141 depth -= 1;
142 p += 1;
143 if depth == 0 {
144 return p;
145 }
146 }
147 b'\\' => {
148 p += 1;
149 if p < bytes.len() {
150 p += 1;
151 }
152 }
153 b'"' => p = skip_nested_dquoted(bytes, p),
154 b'\'' => p = skip_nested_squoted(bytes, p),
155 _ => p += 1,
156 }
157 }
158 p
159}
160
161fn skip_nested_dquoted(bytes: &[u8], mut p: usize) -> usize {
162 debug_assert!(bytes.get(p) == Some(&b'"'));
163 p += 1;
164 while p < bytes.len() {
165 match bytes[p] {
166 b'\\' => {
167 p += 1;
168 if p < bytes.len() {
169 p += 1;
170 }
171 }
172 b'"' => return p + 1,
173 b'{' if bytes.get(p + 1) == Some(&b'$') => p = skip_complex_interp(bytes, p),
174 _ => p += 1,
175 }
176 }
177 p
178}
179
180fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
181 debug_assert!(bytes.get(p) == Some(&b'\''));
182 p += 1;
183 while p < bytes.len() {
184 match bytes[p] {
185 b'\\' => {
186 p += 1;
187 if p < bytes.len() {
188 p += 1;
189 }
190 }
191 b'\'' => return p + 1,
192 _ => p += 1,
193 }
194 }
195 p
196}
197
198impl<'src> Lexer<'src> {
199 pub fn new(source: &'src str) -> Self {
201 debug_assert!(
202 source.len() <= u32::MAX as usize,
203 "source is {} bytes, which exceeds the u32::MAX span limit",
204 source.len()
205 );
206
207 let pos = if source.starts_with("#!") {
209 source.find('\n').map(|p| p + 1).unwrap_or(source.len())
210 } else {
211 0
212 };
213
214 let remaining = &source[pos..];
216 let rem_bytes = remaining.as_bytes();
217 let mode = if (rem_bytes.len() >= 5
218 && rem_bytes[0] == b'<'
219 && rem_bytes[1] == b'?'
220 && rem_bytes[2..5].eq_ignore_ascii_case(b"php"))
221 || remaining.starts_with("<?=")
222 {
223 LexerMode::Php
224 } else {
225 LexerMode::InlineHtml
226 };
227
228 Self {
229 source,
230 mode,
231 pos,
232 peeked: None,
233 peeked2: None,
234 errors: Vec::new(),
235 }
236 }
237
238 pub fn new_at(source: &'src str, offset: usize) -> Self {
243 debug_assert!(
244 source.len() <= u32::MAX as usize,
245 "source is {} bytes, which exceeds the u32::MAX span limit",
246 source.len()
247 );
248
249 Self {
250 source,
251 mode: LexerMode::Php,
252 pos: offset,
253 peeked: None,
254 peeked2: None,
255 errors: Vec::new(),
256 }
257 }
258
259 pub fn source(&self) -> &'src str {
261 self.source
262 }
263
264 pub fn peek(&mut self) -> &Token {
266 if self.peeked.is_none() {
267 self.peeked = Some(self.read_next_token());
268 }
269 self.peeked.as_ref().expect("peeked is Some: set above")
270 }
271
272 pub fn peek2(&mut self) -> &Token {
274 if self.peeked.is_none() {
276 self.peeked = Some(self.read_next_token());
277 }
278 if self.peeked2.is_none() {
279 self.peeked2 = Some(self.read_next_token());
280 }
281 self.peeked2.as_ref().expect("peeked2 is Some: set above")
282 }
283
284 pub fn next_token(&mut self) -> Token {
286 if let Some(token) = self.peeked.take() {
287 self.peeked = self.peeked2.take();
288 return token;
289 }
290 self.read_next_token()
291 }
292
293 pub fn token_text(&self, token: &Token) -> &'src str {
295 &self.source[token.span.start as usize..token.span.end as usize]
296 }
297
298 fn read_next_token(&mut self) -> Token {
299 if self.pos >= self.source.len() {
300 return Token::eof(self.source.len() as u32);
301 }
302
303 match self.mode {
304 LexerMode::InlineHtml => self.lex_inline_html(),
305 LexerMode::Php => self.lex_php(),
306 }
307 }
308
309 fn lex_inline_html(&mut self) -> Token {
310 let start = self.pos;
311 let bytes = self.source.as_bytes();
312
313 let mut search = self.pos;
317 let tag_pos = loop {
318 match memchr::memchr(b'<', &bytes[search..]) {
319 None => break None,
320 Some(offset) => {
321 let p = search + offset;
322 let rest = &bytes[p..];
323 if (rest.len() >= 5
324 && rest[0] == b'<'
325 && rest[1] == b'?'
326 && rest[2..5].eq_ignore_ascii_case(b"php"))
327 || rest.starts_with(b"<?=")
328 {
329 break Some(p - self.pos);
330 }
331 search = p + 1;
332 }
333 }
334 };
335
336 if let Some(tag_pos) = tag_pos {
337 if tag_pos == 0 {
338 self.mode = LexerMode::Php;
340 return self.lex_php();
341 }
342 let end = self.pos + tag_pos;
344 self.pos = end;
345 self.mode = LexerMode::Php;
346 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
347 } else {
348 let end = self.source.len();
350 self.pos = end;
351 Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
352 }
353 }
354
355 fn lex_php(&mut self) -> Token {
356 self.skip_whitespace();
358
359 if self.pos >= self.source.len() {
360 return Token::eof(self.source.len() as u32);
361 }
362
363 let bytes = self.source.as_bytes();
364 let start = self.pos;
365
366 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
369 self.pos += 2;
370 Self::skip_line_comment_body(bytes, &mut self.pos);
371 return self.tok(TokenKind::LineComment, start);
372 }
373
374 if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
376 self.pos += 2;
377 let kind = if self.pos < bytes.len()
380 && bytes[self.pos] == b'*'
381 && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
382 {
383 TokenKind::DocComment
384 } else {
385 TokenKind::BlockComment
386 };
387 match memmem::find(&bytes[self.pos..], b"*/") {
388 Some(end) => self.pos += end + 2,
389 None => {
390 let span = Span::new(start as u32, self.source.len() as u32);
391 self.errors.push(LexerError {
392 kind: LexerErrorKind::Other,
393 message: "unterminated block comment".to_string(),
394 span,
395 });
396 self.pos = bytes.len();
397 }
398 }
399 return self.tok(kind, start);
400 }
401
402 if bytes[self.pos] == b'#' && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'[') {
405 self.pos += 1;
406 Self::skip_line_comment_body(bytes, &mut self.pos);
407 return self.tok(TokenKind::HashComment, start);
408 }
409
410 self.scan_token()
411 }
412
413 fn skip_whitespace(&mut self) {
415 let bytes = self.source.as_bytes();
416 while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
417 self.pos += 1;
418 }
419 }
420
421 fn scan_token(&mut self) -> Token {
423 let start = self.pos;
424 let bytes = self.source.as_bytes();
425 let b = bytes[start];
426
427 match b {
428 b'+' => {
430 if self.check_at(1, b'+') {
431 self.pos = start + 2;
432 self.tok(TokenKind::PlusPlus, start)
433 } else if self.check_at(1, b'=') {
434 self.pos = start + 2;
435 self.tok(TokenKind::PlusEquals, start)
436 } else {
437 self.pos = start + 1;
438 self.tok(TokenKind::Plus, start)
439 }
440 }
441 b'-' => {
442 if self.check_at(1, b'-') {
443 self.pos = start + 2;
444 self.tok(TokenKind::MinusMinus, start)
445 } else if self.check_at(1, b'=') {
446 self.pos = start + 2;
447 self.tok(TokenKind::MinusEquals, start)
448 } else if self.check_at(1, b'>') {
449 self.pos = start + 2;
450 self.tok(TokenKind::Arrow, start)
451 } else {
452 self.pos = start + 1;
453 self.tok(TokenKind::Minus, start)
454 }
455 }
456 b'*' => {
457 if self.check_at(1, b'*') {
458 if self.check_at(2, b'=') {
459 self.pos = start + 3;
460 self.tok(TokenKind::StarStarEquals, start)
461 } else {
462 self.pos = start + 2;
463 self.tok(TokenKind::StarStar, start)
464 }
465 } else if self.check_at(1, b'=') {
466 self.pos = start + 2;
467 self.tok(TokenKind::StarEquals, start)
468 } else {
469 self.pos = start + 1;
470 self.tok(TokenKind::Star, start)
471 }
472 }
473 b'/' => {
474 if self.check_at(1, b'=') {
476 self.pos = start + 2;
477 self.tok(TokenKind::SlashEquals, start)
478 } else {
479 self.pos = start + 1;
480 self.tok(TokenKind::Slash, start)
481 }
482 }
483 b'%' => {
484 if self.check_at(1, b'=') {
485 self.pos = start + 2;
486 self.tok(TokenKind::PercentEquals, start)
487 } else {
488 self.pos = start + 1;
489 self.tok(TokenKind::Percent, start)
490 }
491 }
492 b'.' => {
493 if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
495 self.pos = start + 1;
496 self.scan_digits(u8::is_ascii_digit);
497 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
499 self.try_scan_exponent();
500 }
501 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
503 self.consume_invalid_numeric_rest();
504 return self.invalid_numeric(start);
505 }
506 return self.tok(TokenKind::FloatLiteralLeadingDot, start);
507 }
508 if self.check_at(1, b'.') && self.check_at(2, b'.') {
509 self.pos = start + 3;
510 self.tok(TokenKind::Ellipsis, start)
511 } else if self.check_at(1, b'=') {
512 self.pos = start + 2;
513 self.tok(TokenKind::DotEquals, start)
514 } else {
515 self.pos = start + 1;
516 self.tok(TokenKind::Dot, start)
517 }
518 }
519 b'=' => {
520 if self.check_at(1, b'=') {
521 if self.check_at(2, b'=') {
522 self.pos = start + 3;
523 self.tok(TokenKind::EqualsEqualsEquals, start)
524 } else {
525 self.pos = start + 2;
526 self.tok(TokenKind::EqualsEquals, start)
527 }
528 } else if self.check_at(1, b'>') {
529 self.pos = start + 2;
530 self.tok(TokenKind::FatArrow, start)
531 } else {
532 self.pos = start + 1;
533 self.tok(TokenKind::Equals, start)
534 }
535 }
536 b'!' => {
537 if self.check_at(1, b'=') {
538 if self.check_at(2, b'=') {
539 self.pos = start + 3;
540 self.tok(TokenKind::BangEqualsEquals, start)
541 } else {
542 self.pos = start + 2;
543 self.tok(TokenKind::BangEquals, start)
544 }
545 } else {
546 self.pos = start + 1;
547 self.tok(TokenKind::Bang, start)
548 }
549 }
550 b'<' => self.scan_less_than(start),
551 b'>' => {
552 if self.check_at(1, b'>') {
553 if self.check_at(2, b'=') {
554 self.pos = start + 3;
555 self.tok(TokenKind::ShiftRightEquals, start)
556 } else {
557 self.pos = start + 2;
558 self.tok(TokenKind::ShiftRight, start)
559 }
560 } else if self.check_at(1, b'=') {
561 self.pos = start + 2;
562 self.tok(TokenKind::GreaterThanEquals, start)
563 } else {
564 self.pos = start + 1;
565 self.tok(TokenKind::GreaterThan, start)
566 }
567 }
568 b'&' => {
569 if self.check_at(1, b'&') {
570 self.pos = start + 2;
571 self.tok(TokenKind::AmpersandAmpersand, start)
572 } else if self.check_at(1, b'=') {
573 self.pos = start + 2;
574 self.tok(TokenKind::AmpersandEquals, start)
575 } else {
576 self.pos = start + 1;
577 self.tok(TokenKind::Ampersand, start)
578 }
579 }
580 b'|' => {
581 if self.check_at(1, b'|') {
582 self.pos = start + 2;
583 self.tok(TokenKind::PipePipe, start)
584 } else if self.check_at(1, b'=') {
585 self.pos = start + 2;
586 self.tok(TokenKind::PipeEquals, start)
587 } else if self.check_at(1, b'>') {
588 self.pos = start + 2;
589 self.tok(TokenKind::PipeArrow, start)
590 } else {
591 self.pos = start + 1;
592 self.tok(TokenKind::Pipe, start)
593 }
594 }
595 b'^' => {
596 if self.check_at(1, b'=') {
597 self.pos = start + 2;
598 self.tok(TokenKind::CaretEquals, start)
599 } else {
600 self.pos = start + 1;
601 self.tok(TokenKind::Caret, start)
602 }
603 }
604 b'~' => {
605 self.pos = start + 1;
606 self.tok(TokenKind::Tilde, start)
607 }
608 b'?' => {
609 if self.check_at(1, b'>') {
610 self.pos = start + 2;
611 self.mode = LexerMode::InlineHtml;
612 self.tok(TokenKind::CloseTag, start)
613 } else if self.check_at(1, b'?') {
614 if self.check_at(2, b'=') {
615 self.pos = start + 3;
616 self.tok(TokenKind::CoalesceEquals, start)
617 } else {
618 self.pos = start + 2;
619 self.tok(TokenKind::QuestionQuestion, start)
620 }
621 } else if self.check_at(1, b'-') && self.check_at(2, b'>') {
622 self.pos = start + 3;
623 self.tok(TokenKind::NullsafeArrow, start)
624 } else {
625 self.pos = start + 1;
626 self.tok(TokenKind::Question, start)
627 }
628 }
629 b':' => {
630 if self.check_at(1, b':') {
631 self.pos = start + 2;
632 self.tok(TokenKind::DoubleColon, start)
633 } else {
634 self.pos = start + 1;
635 self.tok(TokenKind::Colon, start)
636 }
637 }
638 b'@' => {
639 self.pos = start + 1;
640 self.tok(TokenKind::At, start)
641 }
642 b'\\' => {
643 self.pos = start + 1;
644 self.tok(TokenKind::Backslash, start)
645 }
646 b'#' => {
647 if self.check_at(1, b'[') {
650 self.pos = start + 2;
651 self.tok(TokenKind::HashBracket, start)
652 } else {
653 self.pos = start + 1;
655 self.read_next_token()
656 }
657 }
658
659 b'(' => {
661 self.pos = start + 1;
662 self.tok(TokenKind::LeftParen, start)
663 }
664 b')' => {
665 self.pos = start + 1;
666 self.tok(TokenKind::RightParen, start)
667 }
668 b'[' => {
669 self.pos = start + 1;
670 self.tok(TokenKind::LeftBracket, start)
671 }
672 b']' => {
673 self.pos = start + 1;
674 self.tok(TokenKind::RightBracket, start)
675 }
676 b'{' => {
677 self.pos = start + 1;
678 self.tok(TokenKind::LeftBrace, start)
679 }
680 b'}' => {
681 self.pos = start + 1;
682 self.tok(TokenKind::RightBrace, start)
683 }
684 b';' => {
685 self.pos = start + 1;
686 self.tok(TokenKind::Semicolon, start)
687 }
688 b',' => {
689 self.pos = start + 1;
690 self.tok(TokenKind::Comma, start)
691 }
692
693 b'\'' => self.scan_single_quoted_string(),
695 b'"' => self.scan_double_quoted_string(),
696 b'`' => self.scan_backtick_string(),
697
698 b'$' => {
700 if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
701 self.pos = start + 2;
702 while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
703 self.pos += 1;
704 }
705 self.tok(TokenKind::Variable, start)
706 } else {
707 self.pos = start + 1;
708 self.tok(TokenKind::Dollar, start)
709 }
710 }
711
712 b'0'..=b'9' => self.scan_number(),
714
715 _ if is_ident_start(b) => {
717 if b == b'b' || b == b'B' {
719 if self.check_at(1, b'\'') {
720 return self.scan_single_quoted_string();
721 }
722 if self.check_at(1, b'"') {
723 return self.scan_double_quoted_string();
724 }
725 if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
726 let remaining = &self.source[self.pos..];
727 if let Some(token) = self.try_lex_heredoc(remaining) {
728 return token;
729 }
730 }
731 }
732 self.scan_identifier()
733 }
734
735 _ => {
737 self.pos = start + 1;
738 self.read_next_token()
739 }
740 }
741 }
742
743 fn scan_less_than(&mut self, start: usize) -> Token {
745 if self.check_at(1, b'<') {
746 if self.check_at(2, b'<') {
747 let remaining = &self.source[self.pos..];
749 if let Some(token) = self.try_lex_heredoc(remaining) {
750 return token;
751 }
752 }
754 if self.check_at(2, b'=') {
755 self.pos = start + 3;
756 return self.tok(TokenKind::ShiftLeftEquals, start);
757 }
758 self.pos = start + 2;
759 return self.tok(TokenKind::ShiftLeft, start);
760 }
761 if self.check_at(1, b'=') {
762 if self.check_at(2, b'>') {
763 self.pos = start + 3;
764 return self.tok(TokenKind::Spaceship, start);
765 }
766 self.pos = start + 2;
767 return self.tok(TokenKind::LessThanEquals, start);
768 }
769 if self.check_at(1, b'>') {
771 self.pos = start + 2;
772 return self.tok(TokenKind::BangEquals, start);
773 }
774 if self.check_at(1, b'?') {
775 let bytes = self.source.as_bytes();
776 if bytes.len() >= self.pos + 5
777 && bytes[self.pos + 2..self.pos + 5].eq_ignore_ascii_case(b"php")
778 {
779 self.pos = start + 5;
780 return self.tok(TokenKind::OpenTag, start);
781 }
782 if self.source[self.pos..].starts_with("<?=") {
783 self.pos = start + 3;
784 return self.tok(TokenKind::OpenTag, start);
785 }
786 }
787 self.pos = start + 1;
788 self.tok(TokenKind::LessThan, start)
789 }
790
791 fn scan_single_quoted_string(&mut self) -> Token {
794 let start = self.pos;
795 let bytes = self.source.as_bytes();
796 let mut p = self.pos;
797 if bytes[p] == b'b' || bytes[p] == b'B' {
799 p += 1;
800 }
801 p += 1; loop {
803 match memchr2(b'\\', b'\'', &bytes[p..]) {
804 None => {
805 self.errors.push(LexerError {
806 kind: LexerErrorKind::UnterminatedString,
807 message: "unterminated string literal".to_string(),
808 span: Span::new(start as u32, self.source.len() as u32),
809 });
810 self.pos = self.source.len();
811 return self.tok(TokenKind::SingleQuotedString, start);
812 }
813 Some(offset) => {
814 p += offset;
815 match bytes[p] {
816 b'\\' => {
817 p += 1;
818 if p < bytes.len() {
819 p += 1;
820 }
821 }
822 _ => {
823 p += 1;
825 break;
826 }
827 }
828 }
829 }
830 }
831 self.pos = p;
832 self.tok(TokenKind::SingleQuotedString, start)
833 }
834
835 fn scan_double_quoted_string(&mut self) -> Token {
836 let start = self.pos;
837 let bytes = self.source.as_bytes();
838 let mut p = self.pos;
839 if bytes[p] == b'b' || bytes[p] == b'B' {
841 p += 1;
842 }
843 p += 1; loop {
845 if p >= bytes.len() {
846 self.errors.push(LexerError {
847 kind: LexerErrorKind::UnterminatedString,
848 message: "unterminated string literal".to_string(),
849 span: Span::new(start as u32, self.source.len() as u32),
850 });
851 self.pos = self.source.len();
852 return self.tok(TokenKind::DoubleQuotedString, start);
853 }
854 match bytes[p] {
855 b'\\' => {
856 p += 1;
857 if p < bytes.len() {
858 p += 1;
859 }
860 }
861 b'"' => {
862 p += 1;
863 break;
864 }
865 b'{' if p + 1 < bytes.len() && bytes[p + 1] == b'$' => {
869 p = skip_complex_interp(bytes, p);
870 }
871 _ => {
872 p += 1;
873 }
874 }
875 }
876 self.pos = p;
877 self.tok(TokenKind::DoubleQuotedString, start)
878 }
879
880 fn scan_backtick_string(&mut self) -> Token {
881 let start = self.pos;
882 let bytes = self.source.as_bytes();
883 let mut p = self.pos;
884 p += 1; loop {
886 match memchr2(b'\\', b'`', &bytes[p..]) {
887 None => {
888 self.errors.push(LexerError {
889 kind: LexerErrorKind::UnterminatedString,
890 message: "unterminated string literal".to_string(),
891 span: Span::new(start as u32, self.source.len() as u32),
892 });
893 self.pos = self.source.len();
894 return self.tok(TokenKind::BacktickString, start);
895 }
896 Some(offset) => {
897 p += offset;
898 match bytes[p] {
899 b'\\' => {
900 p += 1;
901 if p < bytes.len() {
902 p += 1;
903 }
904 }
905 _ => {
906 p += 1;
908 break;
909 }
910 }
911 }
912 }
913 }
914 self.pos = p;
915 self.tok(TokenKind::BacktickString, start)
916 }
917
918 fn scan_number(&mut self) -> Token {
921 let start = self.pos;
922 let bytes = self.source.as_bytes();
923
924 if bytes[start] == b'0' && start + 1 < bytes.len() {
926 match bytes[start + 1] {
927 b'x' | b'X' => {
928 self.pos = start + 2;
929 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
930 self.consume_invalid_numeric_rest();
931 return self.invalid_numeric(start);
932 }
933 if self.scan_digits(u8::is_ascii_hexdigit) {
934 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
935 self.consume_invalid_numeric_rest();
936 return self.invalid_numeric(start);
937 }
938 return self.tok(TokenKind::HexIntLiteral, start);
939 }
940 self.pos = start;
942 }
943 b'b' | b'B' => {
944 self.pos = start + 2;
945 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
946 self.consume_invalid_numeric_rest();
947 return self.invalid_numeric(start);
948 }
949 if self.scan_digits(|b| b == &b'0' || b == &b'1') {
950 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
951 self.consume_invalid_numeric_rest();
952 return self.invalid_numeric(start);
953 }
954 return self.tok(TokenKind::BinIntLiteral, start);
955 }
956 self.pos = start;
958 }
959 b'o' | b'O' => {
960 self.pos = start + 2;
961 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
962 self.consume_invalid_numeric_rest();
963 return self.invalid_numeric(start);
964 }
965 if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
966 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
967 self.consume_invalid_numeric_rest();
968 return self.invalid_numeric(start);
969 }
970 return self.tok(TokenKind::OctIntLiteralNew, start);
971 }
972 self.pos = start;
974 }
975 _ => {}
976 }
977 }
978
979 self.pos = start;
981 self.scan_digits(u8::is_ascii_digit);
982 let integer_end = self.pos;
983 let mut kind = TokenKind::IntLiteral;
984
985 if bytes[start] == b'0' && integer_end > start + 1 {
988 kind = TokenKind::OctIntLiteral;
989 }
990
991 if self.pos < bytes.len() && bytes[self.pos] == b'.' {
993 if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
994 self.pos += 1; self.scan_digits(u8::is_ascii_digit);
997 kind = TokenKind::FloatLiteralSimple;
998 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
999 self.consume_invalid_numeric_rest();
1001 return self.invalid_numeric(start);
1002 } else if self.pos + 1 >= bytes.len() || bytes[self.pos + 1] != b'.' {
1003 self.pos += 1; kind = TokenKind::FloatLiteralSimple;
1006 }
1007 }
1008
1009 if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
1011 if self.try_scan_exponent() {
1012 kind = TokenKind::FloatLiteral;
1013 } else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
1014 self.consume_invalid_numeric_rest();
1016 return self.invalid_numeric(start);
1017 }
1018 }
1019
1020 if self.pos < bytes.len() && bytes[self.pos] == b'_' {
1022 self.consume_invalid_numeric_rest();
1023 return self.invalid_numeric(start);
1024 }
1025
1026 self.tok(kind, start)
1027 }
1028
1029 fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
1032 let bytes = self.source.as_bytes();
1033 if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
1034 return false;
1035 }
1036 self.pos += 1;
1037 loop {
1038 if self.pos >= bytes.len() {
1039 break;
1040 }
1041 if is_valid(&bytes[self.pos]) {
1042 self.pos += 1;
1043 } else if bytes[self.pos] == b'_'
1044 && self.pos + 1 < bytes.len()
1045 && is_valid(&bytes[self.pos + 1])
1046 {
1047 self.pos += 2;
1048 } else {
1049 break;
1050 }
1051 }
1052 true
1053 }
1054
1055 fn try_scan_exponent(&mut self) -> bool {
1058 let bytes = self.source.as_bytes();
1059 let saved = self.pos;
1060 self.pos += 1; if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
1064 self.pos += 1;
1065 }
1066
1067 if self.scan_digits(u8::is_ascii_digit) {
1069 true
1070 } else {
1071 self.pos = saved;
1072 false
1073 }
1074 }
1075
1076 fn scan_identifier(&mut self) -> Token {
1079 let start = self.pos;
1080 let bytes = self.source.as_bytes();
1081 self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
1083 self.pos += 1;
1084 }
1085 let text = &self.source[start..self.pos];
1086 let kind = resolve_keyword(text).unwrap_or(TokenKind::Identifier);
1087 self.tok(kind, start)
1088 }
1089
1090 #[inline]
1096 fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
1097 loop {
1098 match memchr2(b'\n', b'?', &bytes[*pos..]) {
1099 None => {
1100 *pos = bytes.len();
1101 return;
1102 }
1103 Some(offset) => {
1104 let p = *pos + offset;
1105 if bytes[p] == b'\n' {
1106 *pos = p; return;
1108 }
1109 if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
1111 *pos = p; return;
1113 }
1114 *pos = p + 1;
1116 }
1117 }
1118 }
1119 }
1120
1121 #[inline]
1122 fn check_at(&self, offset: usize, expected: u8) -> bool {
1123 self.source.as_bytes().get(self.pos + offset) == Some(&expected)
1124 }
1125
1126 #[inline]
1127 fn tok(&self, kind: TokenKind, start: usize) -> Token {
1128 Token::new(kind, Span::new(start as u32, self.pos as u32))
1129 }
1130
1131 fn invalid_numeric(&mut self, start: usize) -> Token {
1132 let span = Span::new(start as u32, self.pos as u32);
1133 self.errors.push(LexerError {
1134 kind: LexerErrorKind::Other,
1135 message: "Invalid numeric literal".to_string(),
1136 span,
1137 });
1138 Token::new(TokenKind::InvalidNumericLiteral, span)
1139 }
1140
1141 fn consume_invalid_numeric_rest(&mut self) {
1143 let bytes = self.source.as_bytes();
1144 while self.pos < bytes.len() {
1145 let b = bytes[self.pos];
1146 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
1147 if (b == b'+' || b == b'-') && self.pos > 0 {
1149 let prev = bytes[self.pos - 1];
1150 if prev != b'e' && prev != b'E' {
1151 break;
1152 }
1153 }
1154 self.pos += 1;
1155 } else {
1156 break;
1157 }
1158 }
1159 }
1160
1161 fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
1165 let trimmed = remaining.trim_start_matches(|c: char| {
1167 c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\x0C'
1168 });
1169 let ws_len = remaining.len() - trimmed.len();
1170
1171 let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
1173 || trimmed.starts_with("B<<<"))
1174 && !trimmed[1..].starts_with("<<<>")
1175 {
1176 (&trimmed[1..], 1)
1177 } else {
1178 (trimmed, 0)
1179 };
1180
1181 if !after_prefix.starts_with("<<<") {
1182 return None;
1183 }
1184
1185 let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
1188 let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
1189 let arrows_offset =
1190 ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
1191
1192 let (label, is_nowdoc, label_line_end);
1194 if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
1195 let closing = after_quote.find('\'')?;
1197 label = &after_quote[..closing];
1198 is_nowdoc = true;
1199 let after_label = &after_arrows_trimmed[2 + closing..];
1200 let nl = after_label.find('\n').unwrap_or(after_label.len());
1202 label_line_end = arrows_offset + 2 + closing + nl;
1203 if label_line_end < remaining.len() {
1204 }
1206 } else {
1207 let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
1209 let closing = after_dquote.find('"')?;
1210 label = &after_dquote[..closing];
1211 &after_dquote[1 + closing..]
1212 } else {
1213 let end = after_arrows_trimmed
1215 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1216 .unwrap_or(after_arrows_trimmed.len());
1217 if end == 0 {
1218 return None;
1219 }
1220 label = &after_arrows_trimmed[..end];
1221 &after_arrows_trimmed[end..]
1222 };
1223 is_nowdoc = false;
1224 let nl = s.find('\n').unwrap_or(s.len());
1225 label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl;
1226 };
1227
1228 if label.is_empty() {
1229 return None;
1230 }
1231
1232 let body_start_in_remaining = if label_line_end < remaining.len() {
1234 label_line_end + 1 } else {
1236 return None; };
1238
1239 let body = &remaining[body_start_in_remaining..];
1240
1241 let mut search_pos = 0;
1243 let end_marker_pos;
1244 loop {
1245 if search_pos >= body.len() {
1246 return None; }
1248 let line_start = search_pos;
1249 let line_end = body[line_start..]
1250 .find('\n')
1251 .map(|p| line_start + p)
1252 .unwrap_or(body.len());
1253 let line = &body[line_start..line_end];
1254 let trimmed_line = line.trim_start_matches([' ', '\t']);
1255
1256 if trimmed_line.len() >= label.len()
1261 && &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
1262 && !trimmed_line
1263 .as_bytes()
1264 .get(label.len())
1265 .copied()
1266 .is_some_and(is_ident_continue)
1267 {
1268 end_marker_pos = line_start;
1269 break;
1270 }
1271
1272 search_pos = if line_end < body.len() {
1273 line_end + 1
1274 } else {
1275 body.len()
1276 };
1277 }
1278
1279 let end_marker_line = &body[end_marker_pos..];
1281 let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
1282 let indent_len = end_marker_line.len() - trimmed.len();
1283 let token_end_in_remaining =
1284 body_start_in_remaining + end_marker_pos + indent_len + label.len();
1285 self.pos = base_pos + token_end_in_remaining;
1286
1287 let span = Span::new(start as u32, self.pos as u32);
1288
1289 if is_nowdoc {
1290 Some(Token::new(TokenKind::Nowdoc, span))
1291 } else {
1292 Some(Token::new(TokenKind::Heredoc, span))
1293 }
1294 }
1295}
1296
1297pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
1305 if source.len() > u32::MAX as usize {
1306 let error = LexerError {
1307 kind: LexerErrorKind::FileTooLarge,
1308 message: format!(
1309 "source is {} bytes, which exceeds the maximum supported size of {} bytes",
1310 source.len(),
1311 u32::MAX
1312 ),
1313 span: Span::new(0, 0),
1314 };
1315 let eof = Token::eof(0);
1316 return (vec![eof, eof], vec![error]);
1317 }
1318
1319 let mut lexer = Lexer::new(source);
1320 let mut tokens = Vec::with_capacity(source.len() / 8);
1321
1322 loop {
1323 let tok = lexer.next_token();
1324 let is_eof = tok.kind == TokenKind::Eof;
1325 tokens.push(tok);
1326 if is_eof {
1327 break;
1328 }
1329 }
1330
1331 let eof_span = tokens
1335 .last()
1336 .expect("tokens guaranteed to contain at least the Eof token from lexer")
1337 .span;
1338 tokens.push(Token::new(TokenKind::Eof, eof_span));
1339
1340 let errors = lexer.errors;
1341 (tokens, errors)
1342}