use memchr::{memchr2, memmem};
use php_ast::Span;
use crate::token::{resolve_keyword, TokenKind};
const fn make_whitespace_table() -> [bool; 256] {
let mut t = [false; 256];
t[b' ' as usize] = true;
t[b'\t' as usize] = true;
t[b'\r' as usize] = true;
t[b'\n' as usize] = true;
t[0x0C] = true; t
}
const fn make_ident_start_table() -> [bool; 256] {
let mut t = [false; 256];
let mut i = 0usize;
while i < 256 {
let b = i as u8;
t[i] = (b >= b'a' && b <= b'z') || (b >= b'A' && b <= b'Z') || b == b'_' || b >= 0x80;
i += 1;
}
t
}
const fn make_ident_continue_table() -> [bool; 256] {
let mut t = [false; 256];
let mut i = 0usize;
while i < 256 {
let b = i as u8;
t[i] = (b >= b'a' && b <= b'z')
|| (b >= b'A' && b <= b'Z')
|| (b >= b'0' && b <= b'9')
|| b == b'_'
|| b >= 0x80;
i += 1;
}
t
}
static IS_PHP_WHITESPACE: [bool; 256] = make_whitespace_table();
static IS_IDENT_START: [bool; 256] = make_ident_start_table();
static IS_IDENT_CONTINUE: [bool; 256] = make_ident_continue_table();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LexerErrorKind {
UnterminatedString,
FileTooLarge,
UnterminatedComment,
InvalidNumericLiteral,
Other,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct LexerError {
pub kind: LexerErrorKind,
pub message: String,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
impl Token {
pub fn new(kind: TokenKind, span: Span) -> Self {
Self { kind, span }
}
pub fn eof(offset: u32) -> Self {
Self {
kind: TokenKind::Eof,
span: Span::new(offset, offset),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LexerMode {
InlineHtml,
Php,
}
pub struct Lexer<'src> {
source: &'src str,
mode: LexerMode,
pos: usize,
peeked: Option<Token>,
peeked2: Option<Token>,
non_verbatim_strings: Vec<u32>,
verbatim_cursor: usize,
errors: Vec<LexerError>,
}
#[inline(always)]
fn is_ident_start(b: u8) -> bool {
IS_IDENT_START[b as usize]
}
#[inline(always)]
fn is_ident_continue(b: u8) -> bool {
IS_IDENT_CONTINUE[b as usize]
}
fn label_len(s: &str) -> usize {
let bytes = s.as_bytes();
if !bytes.first().copied().is_some_and(is_ident_start) {
return 0;
}
1 + bytes[1..]
.iter()
.take_while(|b| is_ident_continue(**b))
.count()
}
fn skip_complex_interp(bytes: &[u8], p: usize) -> usize {
interpolation_end(bytes, p).unwrap_or(bytes.len())
}
enum InterpFrame<'a> {
Code(u32),
Quoted(u8),
Heredoc(&'a [u8]),
}
#[doc(hidden)]
pub fn interpolation_end(bytes: &[u8], mut p: usize) -> Option<usize> {
debug_assert!(bytes.get(p) == Some(&b'{'));
let mut stack = vec![InterpFrame::Code(0)];
while p < bytes.len() {
let b = bytes[p];
let (quote, heredoc) = match stack
.last_mut()
.expect("the scan returns when the stack empties")
{
InterpFrame::Code(depth) => {
match b {
b'{' => {
*depth += 1;
p += 1;
}
b'}' => {
*depth -= 1;
p += 1;
if *depth == 0 {
stack.pop();
if stack.is_empty() {
return Some(p);
}
}
}
b'"' | b'`' => {
stack.push(InterpFrame::Quoted(b));
p += 1;
}
b'\'' => p = skip_nested_squoted(bytes, p),
b'#' if bytes.get(p + 1) != Some(&b'[') => {
p += 1;
Lexer::skip_line_comment_body(bytes, &mut p);
}
b'/' if bytes.get(p + 1) == Some(&b'/') => {
p += 2;
Lexer::skip_line_comment_body(bytes, &mut p);
}
b'/' if bytes.get(p + 1) == Some(&b'*') => {
p = memmem::find(&bytes[p + 2..], b"*/").map_or(bytes.len(), |e| p + e + 4);
}
b'<' if bytes[p..].starts_with(b"<<<") => match heredoc_header(bytes, p + 3) {
Some((label, true, body)) => p = nowdoc_end(bytes, body, label),
Some((label, false, body)) => {
p = body;
match closing_label_end(bytes, p, label) {
Some(end) => p = end,
None => stack.push(InterpFrame::Heredoc(label)),
}
}
None => p += 3,
},
_ => p += 1,
}
continue;
}
InterpFrame::Quoted(q) => (Some(*q), None),
InterpFrame::Heredoc(label) => (None, Some(*label)),
};
match b {
b'\\' if heredoc.is_none() || newline_len(&bytes[p + 1..]).is_none() => {
p += 2;
}
b'{' if bytes.get(p + 1) == Some(&b'$') => stack.push(InterpFrame::Code(0)),
b'$' if bytes.get(p + 1) == Some(&b'{') => {
p += 1;
stack.push(InterpFrame::Code(0));
}
b'\n' | b'\r' if heredoc.is_some() => {
p += newline_len(&bytes[p..]).unwrap_or(1);
if let Some(end) = heredoc.and_then(|l| closing_label_end(bytes, p, l)) {
stack.pop();
p = end;
}
}
_ if quote == Some(b) => {
stack.pop();
p += 1;
}
_ => p += 1,
}
}
None
}
fn heredoc_header(bytes: &[u8], mut p: usize) -> Option<(&[u8], bool, usize)> {
while matches!(bytes.get(p), Some(b' ' | b'\t')) {
p += 1;
}
let quote = bytes.get(p).copied().filter(|q| matches!(q, b'\'' | b'"'));
if quote.is_some() {
p += 1;
}
let label_start = p;
if !bytes.get(p).copied().is_some_and(is_ident_start) {
return None;
}
while bytes.get(p).copied().is_some_and(is_ident_continue) {
p += 1;
}
let label = &bytes[label_start..p];
if let Some(q) = quote {
if bytes.get(p) != Some(&q) {
return None;
}
p += 1;
}
let body = p + newline_len(&bytes[p..])?;
Some((label, quote == Some(b'\''), body))
}
fn closing_label_end(bytes: &[u8], mut p: usize, label: &[u8]) -> Option<usize> {
while matches!(bytes.get(p), Some(b' ' | b'\t')) {
p += 1;
}
let end = p + label.len();
(bytes[p..].starts_with(label) && !bytes.get(end).copied().is_some_and(is_ident_continue))
.then_some(end)
}
fn nowdoc_end(bytes: &[u8], mut p: usize, label: &[u8]) -> usize {
while p < bytes.len() {
if let Some(end) = closing_label_end(bytes, p, label) {
return end;
}
p = memchr2(b'\n', b'\r', &bytes[p..]).map_or(bytes.len(), |e| {
p + e + newline_len(&bytes[p + e..]).unwrap_or(1)
});
}
bytes.len()
}
fn newline_len(bytes: &[u8]) -> Option<usize> {
match bytes {
[b'\r', b'\n', ..] => Some(2),
[b'\n' | b'\r', ..] => Some(1),
_ => None,
}
}
fn next_heredoc_line(bytes: &[u8], mut p: usize) -> usize {
while p < bytes.len() {
match bytes[p] {
b'\n' | b'\r' => return p + newline_len(&bytes[p..]).unwrap_or(1),
b'\\' if !matches!(bytes.get(p + 1), Some(b'\n' | b'\r')) => p += 2,
b'{' if bytes.get(p + 1) == Some(&b'$') => p = skip_complex_interp(bytes, p),
b'$' if bytes.get(p + 1) == Some(&b'{') => p = skip_complex_interp(bytes, p + 1),
_ => p += 1,
}
}
bytes.len()
}
fn skip_nested_squoted(bytes: &[u8], mut p: usize) -> usize {
debug_assert!(bytes.get(p) == Some(&b'\''));
p += 1;
while p < bytes.len() {
match bytes[p] {
b'\\' => {
p += 1;
if p < bytes.len() {
p += 1;
}
}
b'\'' => return p + 1,
_ => p += 1,
}
}
p
}
fn starts_with_open_tag(rest: &[u8]) -> bool {
rest.starts_with(b"<?=")
|| rest.len() >= 5
&& rest.starts_with(b"<?")
&& rest[2..5].eq_ignore_ascii_case(b"php")
&& rest
.get(5)
.is_none_or(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
}
impl<'src> Lexer<'src> {
pub fn new(source: &'src str) -> Self {
if let Some(lexer) = Self::too_large(source) {
return lexer;
}
let pos = if source.starts_with("#!") {
source.find('\n').map(|p| p + 1).unwrap_or(source.len())
} else {
0
};
let mode = if starts_with_open_tag(&source.as_bytes()[pos..]) {
LexerMode::Php
} else {
LexerMode::InlineHtml
};
Self {
source,
mode,
pos,
peeked: None,
peeked2: None,
non_verbatim_strings: Vec::new(),
verbatim_cursor: 0,
errors: Vec::new(),
}
}
pub fn new_at(source: &'src str, offset: usize) -> Self {
if let Some(lexer) = Self::too_large(source) {
return lexer;
}
Self {
source,
mode: LexerMode::Php,
pos: offset,
peeked: None,
peeked2: None,
non_verbatim_strings: Vec::new(),
verbatim_cursor: 0,
errors: Vec::new(),
}
}
fn too_large(source: &str) -> Option<Self> {
if source.len() <= u32::MAX as usize {
return None;
}
Some(Self {
source: "",
mode: LexerMode::Php,
pos: 0,
peeked: None,
peeked2: None,
non_verbatim_strings: Vec::new(),
verbatim_cursor: 0,
errors: vec![LexerError {
kind: LexerErrorKind::FileTooLarge,
message: format!(
"source is {} bytes, which exceeds the maximum supported size of {} bytes",
source.len(),
u32::MAX
),
span: Span::new(0, 0),
}],
})
}
pub fn errors(&self) -> &[LexerError] {
&self.errors
}
pub fn into_errors(self) -> Vec<LexerError> {
self.errors
}
#[doc(hidden)]
pub fn is_verbatim_string(&mut self, start: u32) -> bool {
let list = &self.non_verbatim_strings;
let mut i = self.verbatim_cursor;
if i > 0 && list.get(i - 1).is_some_and(|&prev| prev >= start) {
return list.binary_search(&start).is_err();
}
while list.get(i).is_some_and(|&next| next < start) {
i += 1;
}
self.verbatim_cursor = i;
list.get(i) != Some(&start)
}
#[doc(hidden)]
pub fn halt_at(&mut self, offset: usize) {
self.peeked = None;
self.peeked2 = None;
self.pos = self.source.len();
self.errors.retain(|e| (e.span.start as usize) < offset);
}
pub fn source(&self) -> &'src str {
self.source
}
pub fn peek(&mut self) -> &Token {
if self.peeked.is_none() {
self.peeked = Some(self.read_next_token());
}
self.peeked.as_ref().expect("peeked is Some: set above")
}
pub fn peek2(&mut self) -> &Token {
if self.peeked.is_none() {
self.peeked = Some(self.read_next_token());
}
if self.peeked2.is_none() {
self.peeked2 = Some(self.read_next_token());
}
self.peeked2.as_ref().expect("peeked2 is Some: set above")
}
pub fn next_token(&mut self) -> Token {
if let Some(token) = self.peeked.take() {
self.peeked = self.peeked2.take();
return token;
}
self.read_next_token()
}
pub fn token_text(&self, token: &Token) -> &'src str {
&self.source[token.span.start as usize..token.span.end as usize]
}
fn read_next_token(&mut self) -> Token {
if self.pos >= self.source.len() {
return Token::eof(self.source.len() as u32);
}
match self.mode {
LexerMode::InlineHtml => self.lex_inline_html(),
LexerMode::Php => self.lex_php(),
}
}
fn lex_inline_html(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
let mut search = self.pos;
let tag_pos = loop {
match memchr::memchr(b'<', &bytes[search..]) {
None => break None,
Some(offset) => {
let p = search + offset;
if starts_with_open_tag(&bytes[p..]) {
break Some(p - self.pos);
}
search = p + 1;
}
}
};
if let Some(tag_pos) = tag_pos {
if tag_pos == 0 {
self.mode = LexerMode::Php;
return self.lex_php();
}
let end = self.pos + tag_pos;
self.pos = end;
self.mode = LexerMode::Php;
Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
} else {
let end = self.source.len();
self.pos = end;
Token::new(TokenKind::InlineHtml, Span::new(start as u32, end as u32))
}
}
fn lex_php(&mut self) -> Token {
self.skip_whitespace();
if self.pos >= self.source.len() {
return Token::eof(self.source.len() as u32);
}
let bytes = self.source.as_bytes();
let start = self.pos;
if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/' {
self.pos += 2;
Self::skip_line_comment_body(bytes, &mut self.pos);
return self.tok(TokenKind::LineComment, start);
}
if bytes[self.pos] == b'/' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'*' {
self.pos += 2;
let kind = if self.pos < bytes.len()
&& bytes[self.pos] == b'*'
&& !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'/')
{
TokenKind::DocComment
} else {
TokenKind::BlockComment
};
match memmem::find(&bytes[self.pos..], b"*/") {
Some(end) => self.pos += end + 2,
None => {
let span = Span::new(start as u32, self.source.len() as u32);
self.errors.push(LexerError {
kind: LexerErrorKind::UnterminatedComment,
message: "unterminated block comment".to_string(),
span,
});
self.pos = bytes.len();
}
}
return self.tok(kind, start);
}
if bytes[self.pos] == b'#' && !(self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'[') {
self.pos += 1;
Self::skip_line_comment_body(bytes, &mut self.pos);
return self.tok(TokenKind::HashComment, start);
}
self.scan_token()
}
fn skip_whitespace(&mut self) {
let bytes = self.source.as_bytes();
while self.pos < bytes.len() && IS_PHP_WHITESPACE[bytes[self.pos] as usize] {
self.pos += 1;
}
}
fn scan_token(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
let b = bytes[start];
match b {
b'+' => {
if self.check_at(1, b'+') {
self.pos = start + 2;
self.tok(TokenKind::PlusPlus, start)
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::PlusEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Plus, start)
}
}
b'-' => {
if self.check_at(1, b'-') {
self.pos = start + 2;
self.tok(TokenKind::MinusMinus, start)
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::MinusEquals, start)
} else if self.check_at(1, b'>') {
self.pos = start + 2;
self.tok(TokenKind::Arrow, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Minus, start)
}
}
b'*' => {
if self.check_at(1, b'*') {
if self.check_at(2, b'=') {
self.pos = start + 3;
self.tok(TokenKind::StarStarEquals, start)
} else {
self.pos = start + 2;
self.tok(TokenKind::StarStar, start)
}
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::StarEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Star, start)
}
}
b'/' => {
if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::SlashEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Slash, start)
}
}
b'%' => {
if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::PercentEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Percent, start)
}
}
b'.' => {
if start + 1 < bytes.len() && bytes[start + 1].is_ascii_digit() {
self.pos = start + 1;
self.scan_digits(u8::is_ascii_digit);
if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
self.try_scan_exponent();
}
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
return self.tok(TokenKind::FloatLiteralLeadingDot, start);
}
if self.check_at(1, b'.') && self.check_at(2, b'.') {
self.pos = start + 3;
self.tok(TokenKind::Ellipsis, start)
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::DotEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Dot, start)
}
}
b'=' => {
if self.check_at(1, b'=') {
if self.check_at(2, b'=') {
self.pos = start + 3;
self.tok(TokenKind::EqualsEqualsEquals, start)
} else {
self.pos = start + 2;
self.tok(TokenKind::EqualsEquals, start)
}
} else if self.check_at(1, b'>') {
self.pos = start + 2;
self.tok(TokenKind::FatArrow, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Equals, start)
}
}
b'!' => {
if self.check_at(1, b'=') {
if self.check_at(2, b'=') {
self.pos = start + 3;
self.tok(TokenKind::BangEqualsEquals, start)
} else {
self.pos = start + 2;
self.tok(TokenKind::BangEquals, start)
}
} else {
self.pos = start + 1;
self.tok(TokenKind::Bang, start)
}
}
b'<' => self.scan_less_than(start),
b'>' => {
if self.check_at(1, b'>') {
if self.check_at(2, b'=') {
self.pos = start + 3;
self.tok(TokenKind::ShiftRightEquals, start)
} else {
self.pos = start + 2;
self.tok(TokenKind::ShiftRight, start)
}
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::GreaterThanEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::GreaterThan, start)
}
}
b'&' => {
if self.check_at(1, b'&') {
self.pos = start + 2;
self.tok(TokenKind::AmpersandAmpersand, start)
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::AmpersandEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Ampersand, start)
}
}
b'|' => {
if self.check_at(1, b'|') {
self.pos = start + 2;
self.tok(TokenKind::PipePipe, start)
} else if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::PipeEquals, start)
} else if self.check_at(1, b'>') {
self.pos = start + 2;
self.tok(TokenKind::PipeArrow, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Pipe, start)
}
}
b'^' => {
if self.check_at(1, b'=') {
self.pos = start + 2;
self.tok(TokenKind::CaretEquals, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Caret, start)
}
}
b'~' => {
self.pos = start + 1;
self.tok(TokenKind::Tilde, start)
}
b'?' => {
if self.check_at(1, b'>') {
self.pos = start + 2;
self.mode = LexerMode::InlineHtml;
self.tok(TokenKind::CloseTag, start)
} else if self.check_at(1, b'?') {
if self.check_at(2, b'=') {
self.pos = start + 3;
self.tok(TokenKind::CoalesceEquals, start)
} else {
self.pos = start + 2;
self.tok(TokenKind::QuestionQuestion, start)
}
} else if self.check_at(1, b'-') && self.check_at(2, b'>') {
self.pos = start + 3;
self.tok(TokenKind::NullsafeArrow, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Question, start)
}
}
b':' => {
if self.check_at(1, b':') {
self.pos = start + 2;
self.tok(TokenKind::DoubleColon, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Colon, start)
}
}
b'@' => {
self.pos = start + 1;
self.tok(TokenKind::At, start)
}
b'\\' => {
self.pos = start + 1;
self.tok(TokenKind::Backslash, start)
}
b'#' => {
if self.check_at(1, b'[') {
self.pos = start + 2;
self.tok(TokenKind::HashBracket, start)
} else {
self.pos = start + 1;
self.read_next_token()
}
}
b'(' => {
self.pos = start + 1;
self.tok(TokenKind::LeftParen, start)
}
b')' => {
self.pos = start + 1;
self.tok(TokenKind::RightParen, start)
}
b'[' => {
self.pos = start + 1;
self.tok(TokenKind::LeftBracket, start)
}
b']' => {
self.pos = start + 1;
self.tok(TokenKind::RightBracket, start)
}
b'{' => {
self.pos = start + 1;
self.tok(TokenKind::LeftBrace, start)
}
b'}' => {
self.pos = start + 1;
self.tok(TokenKind::RightBrace, start)
}
b';' => {
self.pos = start + 1;
self.tok(TokenKind::Semicolon, start)
}
b',' => {
self.pos = start + 1;
self.tok(TokenKind::Comma, start)
}
b'\'' => self.scan_single_quoted_string(),
b'"' => self.scan_double_quoted_string(),
b'`' => self.scan_backtick_string(),
b'$' => {
if start + 1 < bytes.len() && is_ident_start(bytes[start + 1]) {
self.pos = start + 2;
while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
self.pos += 1;
}
self.tok(TokenKind::Variable, start)
} else {
self.pos = start + 1;
self.tok(TokenKind::Dollar, start)
}
}
b'0'..=b'9' => self.scan_number(),
_ if is_ident_start(b) => {
if b == b'b' || b == b'B' {
if self.check_at(1, b'\'') {
return self.scan_single_quoted_string();
}
if self.check_at(1, b'"') {
return self.scan_double_quoted_string();
}
if self.check_at(1, b'<') && self.check_at(2, b'<') && self.check_at(3, b'<') {
let remaining = &self.source[self.pos..];
if let Some(token) = self.try_lex_heredoc(remaining) {
return token;
}
}
}
self.scan_identifier()
}
_ => {
self.pos = start + 1;
self.read_next_token()
}
}
}
fn scan_less_than(&mut self, start: usize) -> Token {
if self.check_at(1, b'<') {
if self.check_at(2, b'<') {
let remaining = &self.source[self.pos..];
if let Some(token) = self.try_lex_heredoc(remaining) {
return token;
}
}
if self.check_at(2, b'=') {
self.pos = start + 3;
return self.tok(TokenKind::ShiftLeftEquals, start);
}
self.pos = start + 2;
return self.tok(TokenKind::ShiftLeft, start);
}
if self.check_at(1, b'=') {
if self.check_at(2, b'>') {
self.pos = start + 3;
return self.tok(TokenKind::Spaceship, start);
}
self.pos = start + 2;
return self.tok(TokenKind::LessThanEquals, start);
}
if self.check_at(1, b'>') {
self.pos = start + 2;
return self.tok(TokenKind::BangEquals, start);
}
if starts_with_open_tag(&self.source.as_bytes()[start..]) {
self.pos = start + if self.check_at(2, b'=') { 3 } else { 5 };
return self.tok(TokenKind::OpenTag, start);
}
self.pos = start + 1;
self.tok(TokenKind::LessThan, start)
}
fn scan_single_quoted_string(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
let mut p = self.pos;
if bytes[p] == b'b' || bytes[p] == b'B' {
p += 1;
}
p += 1; loop {
match memchr2(b'\\', b'\'', &bytes[p..]) {
None => {
self.errors.push(LexerError {
kind: LexerErrorKind::UnterminatedString,
message: "unterminated string literal".to_string(),
span: Span::new(start as u32, self.source.len() as u32),
});
self.mark_non_verbatim(start);
self.pos = self.source.len();
return self.tok(TokenKind::SingleQuotedString, start);
}
Some(offset) => {
p += offset;
match bytes[p] {
b'\\' => {
self.mark_non_verbatim(start);
p += 1;
if p < bytes.len() {
p += 1;
}
}
_ => {
p += 1;
break;
}
}
}
}
}
self.pos = p;
self.tok(TokenKind::SingleQuotedString, start)
}
fn scan_double_quoted_string(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
let mut p = self.pos;
if bytes[p] == b'b' || bytes[p] == b'B' {
p += 1;
}
p += 1; let mut verbatim = true;
loop {
let segment = p;
let Some(offset) = memchr::memchr3(b'\\', b'"', b'$', &bytes[p..]) else {
self.errors.push(LexerError {
kind: LexerErrorKind::UnterminatedString,
message: "unterminated string literal".to_string(),
span: Span::new(start as u32, self.source.len() as u32),
});
self.mark_non_verbatim(start);
self.pos = self.source.len();
return self.tok(TokenKind::DoubleQuotedString, start);
};
p += offset;
match bytes[p] {
b'\\' => {
verbatim = false;
p = (p + 2).min(bytes.len());
}
b'"' => {
p += 1;
break;
}
_ => {
verbatim = false;
if p > segment && bytes[p - 1] == b'{' {
p = skip_complex_interp(bytes, p - 1);
} else if bytes.get(p + 1) == Some(&b'{') {
p = skip_complex_interp(bytes, p + 1);
} else {
p += 1;
}
}
}
}
if !verbatim {
self.mark_non_verbatim(start);
}
self.pos = p;
self.tok(TokenKind::DoubleQuotedString, start)
}
#[inline]
fn mark_non_verbatim(&mut self, start: usize) {
if self.non_verbatim_strings.last() != Some(&(start as u32)) {
self.non_verbatim_strings.push(start as u32);
}
}
fn scan_backtick_string(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
let mut p = self.pos;
p += 1; loop {
let segment = p;
match memchr::memchr3(b'\\', b'`', b'$', &bytes[p..]) {
None => {
self.errors.push(LexerError {
kind: LexerErrorKind::UnterminatedString,
message: "unterminated string literal".to_string(),
span: Span::new(start as u32, self.source.len() as u32),
});
self.pos = self.source.len();
return self.tok(TokenKind::BacktickString, start);
}
Some(offset) => {
p += offset;
match bytes[p] {
b'\\' => {
p += 1;
if p < bytes.len() {
p += 1;
}
}
b'$' if p > segment && bytes[p - 1] == b'{' => {
p = skip_complex_interp(bytes, p - 1);
}
b'$' if bytes.get(p + 1) == Some(&b'{') => {
p = skip_complex_interp(bytes, p + 1);
}
b'$' => p += 1,
_ => {
p += 1;
break;
}
}
}
}
}
self.pos = p;
self.tok(TokenKind::BacktickString, start)
}
fn scan_number(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
if bytes[start] == b'0' && start + 1 < bytes.len() {
match bytes[start + 1] {
b'x' | b'X' => {
self.pos = start + 2;
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
if self.scan_digits(u8::is_ascii_hexdigit) {
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
return self.tok(TokenKind::HexIntLiteral, start);
}
self.pos = start;
}
b'b' | b'B' => {
self.pos = start + 2;
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
if self.scan_digits(|b| b == &b'0' || b == &b'1') {
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
return self.tok(TokenKind::BinIntLiteral, start);
}
self.pos = start;
}
b'o' | b'O' => {
self.pos = start + 2;
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
if self.scan_digits(|b| (b'0'..=b'7').contains(b)) {
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
return self.tok(TokenKind::OctIntLiteralNew, start);
}
self.pos = start;
}
_ => {}
}
}
self.pos = start;
self.scan_digits(u8::is_ascii_digit);
let integer_end = self.pos;
let mut kind = TokenKind::IntLiteral;
if bytes[start] == b'0' && integer_end > start + 1 {
kind = TokenKind::OctIntLiteral;
}
if self.pos < bytes.len() && bytes[self.pos] == b'.' {
if self.pos + 1 < bytes.len() && bytes[self.pos + 1].is_ascii_digit() {
self.pos += 1; self.scan_digits(u8::is_ascii_digit);
kind = TokenKind::FloatLiteralSimple;
} else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
} else {
self.pos += 1; kind = TokenKind::FloatLiteralSimple;
}
}
if self.pos < bytes.len() && matches!(bytes[self.pos], b'e' | b'E') {
if self.try_scan_exponent() {
kind = TokenKind::FloatLiteral;
} else if self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
}
if self.pos < bytes.len() && bytes[self.pos] == b'_' {
self.consume_invalid_numeric_rest();
return self.invalid_numeric(start);
}
self.tok(kind, start)
}
fn scan_digits(&mut self, is_valid: fn(&u8) -> bool) -> bool {
let bytes = self.source.as_bytes();
if self.pos >= bytes.len() || !is_valid(&bytes[self.pos]) {
return false;
}
self.pos += 1;
loop {
if self.pos >= bytes.len() {
break;
}
if is_valid(&bytes[self.pos]) {
self.pos += 1;
} else if bytes[self.pos] == b'_'
&& self.pos + 1 < bytes.len()
&& is_valid(&bytes[self.pos + 1])
{
self.pos += 2;
} else {
break;
}
}
true
}
fn try_scan_exponent(&mut self) -> bool {
let bytes = self.source.as_bytes();
let saved = self.pos;
self.pos += 1;
if self.pos < bytes.len() && matches!(bytes[self.pos], b'+' | b'-') {
self.pos += 1;
}
if self.scan_digits(u8::is_ascii_digit) {
true
} else {
self.pos = saved;
false
}
}
fn scan_identifier(&mut self) -> Token {
let start = self.pos;
let bytes = self.source.as_bytes();
self.pos += 1; while self.pos < bytes.len() && is_ident_continue(bytes[self.pos]) {
self.pos += 1;
}
let text = &self.source[start..self.pos];
let starts_qualified_name = bytes.get(self.pos) == Some(&b'\\')
&& bytes.get(self.pos + 1).is_some_and(|&b| is_ident_start(b));
let kind = match resolve_keyword(text) {
Some(TokenKind::Namespace) => TokenKind::Namespace,
Some(_) if starts_qualified_name => TokenKind::Identifier,
Some(kind) => kind,
None => TokenKind::Identifier,
};
self.tok(kind, start)
}
#[inline]
fn skip_line_comment_body(bytes: &[u8], pos: &mut usize) {
loop {
match memchr::memchr3(b'\n', b'\r', b'?', &bytes[*pos..]) {
None => {
*pos = bytes.len();
return;
}
Some(offset) => {
let p = *pos + offset;
if bytes[p] != b'?' {
*pos = p; return;
}
if p + 1 < bytes.len() && bytes[p + 1] == b'>' {
*pos = p; return;
}
*pos = p + 1;
}
}
}
}
#[inline]
fn check_at(&self, offset: usize, expected: u8) -> bool {
self.source.as_bytes().get(self.pos + offset) == Some(&expected)
}
#[inline]
fn tok(&self, kind: TokenKind, start: usize) -> Token {
Token::new(kind, Span::new(start as u32, self.pos as u32))
}
fn invalid_numeric(&mut self, start: usize) -> Token {
let span = Span::new(start as u32, self.pos as u32);
self.errors.push(LexerError {
kind: LexerErrorKind::InvalidNumericLiteral,
message: "Invalid numeric literal".to_string(),
span,
});
Token::new(TokenKind::InvalidNumericLiteral, span)
}
fn consume_invalid_numeric_rest(&mut self) {
let bytes = self.source.as_bytes();
while self.pos < bytes.len() {
let b = bytes[self.pos];
if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'+' || b == b'-' {
if (b == b'+' || b == b'-') && self.pos > 0 {
let prev = bytes[self.pos - 1];
if prev != b'e' && prev != b'E' {
break;
}
}
self.pos += 1;
} else {
break;
}
}
}
fn try_lex_heredoc(&mut self, remaining: &str) -> Option<Token> {
let trimmed = remaining.trim_start_matches(|c: char| {
c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\x0C'
});
let ws_len = remaining.len() - trimmed.len();
let (after_prefix, prefix_len) = if (trimmed.starts_with("b<<<")
|| trimmed.starts_with("B<<<"))
&& !trimmed[1..].starts_with("<<<>")
{
(&trimmed[1..], 1)
} else {
(trimmed, 0)
};
if !after_prefix.starts_with("<<<") {
return None;
}
let base_pos = self.pos; let start = base_pos + ws_len; let after_arrows = &after_prefix[3..];
let after_arrows_trimmed = after_arrows.trim_start_matches([' ', '\t']);
let arrows_offset =
ws_len + prefix_len + 3 + (after_arrows.len() - after_arrows_trimmed.len());
let (label, is_nowdoc, label_line_end);
if let Some(after_quote) = after_arrows_trimmed.strip_prefix('\'') {
let closing = label_len(after_quote);
if after_quote.as_bytes().get(closing) != Some(&b'\'') {
return None;
}
label = &after_quote[..closing];
is_nowdoc = true;
let after_label = &after_arrows_trimmed[2 + closing..];
let nl = newline_len(after_label.as_bytes())?;
label_line_end = arrows_offset + 2 + closing + nl - 1;
} else {
let s = if let Some(after_dquote) = after_arrows_trimmed.strip_prefix('"') {
let closing = label_len(after_dquote);
if after_dquote.as_bytes().get(closing) != Some(&b'"') {
return None;
}
label = &after_dquote[..closing];
&after_dquote[1 + closing..]
} else {
let end = label_len(after_arrows_trimmed);
label = &after_arrows_trimmed[..end];
&after_arrows_trimmed[end..]
};
let nl = newline_len(s.as_bytes())?;
is_nowdoc = false;
label_line_end = arrows_offset + (after_arrows_trimmed.len() - s.len()) + nl - 1;
};
if label.is_empty() {
return None;
}
let body_start_in_remaining = if label_line_end < remaining.len() {
label_line_end + 1 } else {
return None; };
let body = &remaining[body_start_in_remaining..];
let mut search_pos = 0;
let end_marker_pos;
loop {
if search_pos >= body.len() {
return None; }
let line_start = search_pos;
let line_end = body[line_start..]
.find(['\n', '\r'])
.map(|p| line_start + p)
.unwrap_or(body.len());
let line = &body[line_start..line_end];
let trimmed_line = line.trim_start_matches([' ', '\t']);
if trimmed_line.len() >= label.len()
&& &trimmed_line.as_bytes()[..label.len()] == label.as_bytes()
&& !trimmed_line
.as_bytes()
.get(label.len())
.copied()
.is_some_and(is_ident_continue)
{
end_marker_pos = line_start;
break;
}
search_pos = if is_nowdoc {
line_end + newline_len(&body.as_bytes()[line_end..]).unwrap_or(0)
} else {
next_heredoc_line(body.as_bytes(), line_start)
};
}
let end_marker_line = &body[end_marker_pos..];
let trimmed = end_marker_line.trim_start_matches([' ', '\t']);
let indent_len = end_marker_line.len() - trimmed.len();
let token_end_in_remaining =
body_start_in_remaining + end_marker_pos + indent_len + label.len();
self.pos = base_pos + token_end_in_remaining;
let span = Span::new(start as u32, self.pos as u32);
if is_nowdoc {
Some(Token::new(TokenKind::Nowdoc, span))
} else {
Some(Token::new(TokenKind::Heredoc, span))
}
}
}
pub fn lex_all(source: &str) -> (Vec<Token>, Vec<LexerError>) {
let mut lexer = Lexer::new(source);
let mut tokens = Vec::with_capacity(lexer.source().len() / 8);
loop {
let tok = lexer.next_token();
let is_eof = tok.kind == TokenKind::Eof;
tokens.push(tok);
if is_eof {
break;
}
}
let eof_span = tokens
.last()
.expect("tokens guaranteed to contain at least the Eof token from lexer")
.span;
tokens.push(Token::new(TokenKind::Eof, eof_span));
let errors = lexer.errors;
(tokens, errors)
}