use std::borrow::Cow;
use php_ast::*;
use php_lexer::TokenKind;
use crate::diagnostics::ParseError;
use crate::instrument;
use crate::parser::{Parser, MAX_DEPTH};
use crate::precedence::{self, ASSIGNMENT_BP, NULL_COALESCE_LEFT_BP, TERNARY_BP};
use crate::stmt;
use crate::version::PhpVersion;
fn is_nonassoc_token(kind: TokenKind) -> bool {
matches!(
kind,
TokenKind::EqualsEquals
| TokenKind::BangEquals
| TokenKind::EqualsEqualsEquals
| TokenKind::BangEqualsEquals
| TokenKind::Spaceship
| TokenKind::LessThan
| TokenKind::GreaterThan
| TokenKind::LessThanEquals
| TokenKind::GreaterThanEquals
| TokenKind::Instanceof
)
}
fn is_nonassoc_binary_op(op: BinaryOp) -> bool {
matches!(
op,
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Identical
| BinaryOp::NotIdentical
| BinaryOp::Spaceship
| BinaryOp::Less
| BinaryOp::Greater
| BinaryOp::LessOrEqual
| BinaryOp::GreaterOrEqual
| BinaryOp::Instanceof
)
}
const CAST_KEYWORDS: &[(&str, CastKind)] = &[
("int", CastKind::Int),
("integer", CastKind::Int),
("float", CastKind::Float),
("double", CastKind::Float),
("real", CastKind::Float),
("string", CastKind::String),
("binary", CastKind::String),
("bool", CastKind::Bool),
("boolean", CastKind::Bool),
("array", CastKind::Array),
("object", CastKind::Object),
("unset", CastKind::Unset),
("void", CastKind::Void),
];
fn parse_assign_continuation<'arena, 'src>(
parser: &mut Parser<'arena, 'src>,
lhs: Expr<'arena, 'src>,
) -> Expr<'arena, 'src> {
debug_assert!(parser.current_kind().is_assignment_op());
let op_token = parser.advance();
let by_ref = op_token.kind == TokenKind::Equals && parser.check(TokenKind::Ampersand);
if by_ref {
parser.advance();
}
let op = match op_token.kind {
TokenKind::Equals => AssignOp::Assign,
TokenKind::PlusEquals => AssignOp::Plus,
TokenKind::MinusEquals => AssignOp::Minus,
TokenKind::StarEquals => AssignOp::Mul,
TokenKind::SlashEquals => AssignOp::Div,
TokenKind::PercentEquals => AssignOp::Mod,
TokenKind::StarStarEquals => AssignOp::Pow,
TokenKind::DotEquals => AssignOp::Concat,
TokenKind::AmpersandEquals => AssignOp::BitwiseAnd,
TokenKind::PipeEquals => AssignOp::BitwiseOr,
TokenKind::CaretEquals => AssignOp::BitwiseXor,
TokenKind::ShiftLeftEquals => AssignOp::ShiftLeft,
TokenKind::ShiftRightEquals => AssignOp::ShiftRight,
TokenKind::CoalesceEquals => AssignOp::Coalesce,
_ => unreachable!(
"is_assignment_op() guarantees one of the listed variants, got {:?}",
op_token.kind
),
};
let rhs = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = lhs.span.merge(rhs.span);
Expr {
kind: ExprKind::Assign(AssignExpr {
target: parser.alloc(lhs),
op,
value: parser.alloc(rhs),
by_ref,
}),
span,
}
}
pub fn parse_expr<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
instrument::record_parse_expr();
parse_expr_bp(parser, 0)
}
pub fn parse_expr_bp<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
min_bp: u8,
) -> Expr<'arena, 'src> {
if min_bp != 0 {
instrument::record_parse_expr_bp_recursive();
}
parser.expr_depth += 1;
if parser.expr_depth > MAX_DEPTH {
parser.expr_depth -= 1;
let span = parser.current_span();
parser.error(ParseError::Forbidden {
message: "maximum expression nesting depth exceeded".into(),
span,
});
return Expr {
kind: ExprKind::Error,
span,
};
}
let mut lhs = parse_atom(parser);
loop {
let kind = parser.current_kind();
let should_continue = match kind {
TokenKind::PlusPlus | TokenKind::MinusMinus => {
if let Some(left_bp) = precedence::postfix_binding_power(kind) {
if left_bp < min_bp {
break;
}
let op_token = parser.advance();
let op = match op_token.kind {
TokenKind::PlusPlus => UnaryPostfixOp::PostIncrement,
TokenKind::MinusMinus => UnaryPostfixOp::PostDecrement,
_ => unreachable!(
"outer arm already matched PlusPlus/MinusMinus, got {:?}",
op_token.kind
),
};
let span = lhs.span.merge(op_token.span);
lhs = Expr {
kind: ExprKind::UnaryPostfix(UnaryPostfixExpr {
operand: parser.alloc(lhs),
op,
}),
span,
};
true
} else {
false
}
}
TokenKind::Question => {
if TERNARY_BP < min_bp {
break;
}
if parser.version >= PhpVersion::Php80 && matches!(lhs.kind, ExprKind::Ternary(_)) {
let span = parser.current_span();
parser.error(ParseError::Forbidden {
message: "Unparenthesized `a ? b : c ? d : e` is not supported. \
Use parentheses to make the order explicit."
.into(),
span,
});
}
parser.advance();
let then_expr = if parser.check(TokenKind::Colon) {
None
} else {
let e = parse_expr_bp(parser, 0);
if parser.version >= PhpVersion::Php80 && matches!(e.kind, ExprKind::Ternary(_))
{
parser.error(ParseError::Forbidden {
message: "Unparenthesized `a ? b ? c : d : e` is not supported. \
Use parentheses to make the order explicit."
.into(),
span: e.span,
});
}
Some(parser.alloc(e))
};
parser.expect(TokenKind::Colon);
let mut else_expr = parse_expr_bp(parser, TERNARY_BP + 1);
if parser.current_kind().is_assignment_op() {
else_expr = parse_assign_continuation(parser, else_expr);
}
let span = lhs.span.merge(else_expr.span);
lhs = Expr {
kind: ExprKind::Ternary(TernaryExpr {
condition: parser.alloc(lhs),
then_expr,
else_expr: parser.alloc(else_expr),
}),
span,
};
true
}
TokenKind::Arrow | TokenKind::NullsafeArrow => {
if 44u8 < min_bp {
break;
}
let is_nullsafe = kind == TokenKind::NullsafeArrow;
if is_nullsafe {
let span = parser.current_span();
parser.require_version(PhpVersion::Php80, "nullsafe operator (?->)", span);
}
parser.advance();
let member = parse_member_name(parser);
if parser.check(TokenKind::LeftParen) {
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
let callable_kind = if is_nullsafe {
CallableCreateKind::NullsafeMethod {
object: parser.alloc(lhs),
method: parser.alloc(member),
}
} else {
CallableCreateKind::Method {
object: parser.alloc(lhs),
method: parser.alloc(member),
}
};
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: callable_kind,
}),
span,
};
}
ArgListResult::Args(args) => {
let span = Span::new(lhs.span.start, parser.previous_end());
let call = parser.alloc(MethodCallExpr {
object: parser.alloc(lhs),
method: parser.alloc(member),
args,
});
let expr_kind = if is_nullsafe {
ExprKind::NullsafeMethodCall(call)
} else {
ExprKind::MethodCall(call)
};
lhs = Expr {
kind: expr_kind,
span,
};
}
}
} else {
let span = Span::new(lhs.span.start, member.span.end);
let expr_kind = if is_nullsafe {
ExprKind::NullsafePropertyAccess(PropertyAccessExpr {
object: parser.alloc(lhs),
property: parser.alloc(member),
})
} else {
ExprKind::PropertyAccess(PropertyAccessExpr {
object: parser.alloc(lhs),
property: parser.alloc(member),
})
};
lhs = Expr {
kind: expr_kind,
span,
};
}
true
}
_ => false,
};
if should_continue {
continue;
}
if kind.is_assignment_op() {
if ASSIGNMENT_BP < min_bp {
break;
}
if matches!(
lhs.kind,
ExprKind::UnaryPrefix(UnaryPrefixExpr {
op: UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement,
..
}) | ExprKind::UnaryPostfix(UnaryPostfixExpr {
op: UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement,
..
})
) {
let span = parser.current_span();
parser.error(ParseError::Forbidden {
message: "Cannot use increment/decrement as an assignment target.".into(),
span,
});
}
let op_token = parser.advance();
let by_ref = op_token.kind == TokenKind::Equals && parser.check(TokenKind::Ampersand);
if by_ref {
parser.advance(); }
let op = match op_token.kind {
TokenKind::Equals => AssignOp::Assign,
TokenKind::PlusEquals => AssignOp::Plus,
TokenKind::MinusEquals => AssignOp::Minus,
TokenKind::StarEquals => AssignOp::Mul,
TokenKind::SlashEquals => AssignOp::Div,
TokenKind::PercentEquals => AssignOp::Mod,
TokenKind::StarStarEquals => AssignOp::Pow,
TokenKind::DotEquals => AssignOp::Concat,
TokenKind::AmpersandEquals => AssignOp::BitwiseAnd,
TokenKind::PipeEquals => AssignOp::BitwiseOr,
TokenKind::CaretEquals => AssignOp::BitwiseXor,
TokenKind::ShiftLeftEquals => AssignOp::ShiftLeft,
TokenKind::ShiftRightEquals => AssignOp::ShiftRight,
TokenKind::CoalesceEquals => AssignOp::Coalesce,
_ => unreachable!(
"is_assignment_op() guarantees one of the listed variants, got {:?}",
kind
),
};
let rhs = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = lhs.span.merge(rhs.span);
lhs = Expr {
kind: ExprKind::Assign(AssignExpr {
target: parser.alloc(lhs),
op,
value: parser.alloc(rhs),
by_ref,
}),
span,
};
continue;
}
if kind == TokenKind::DoubleColon {
if 90u8 < min_bp {
break;
}
parser.advance();
if parser.check(TokenKind::Variable) {
let token = parser.advance();
let member = parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::Src(parser.variable_name(token))),
span: token.span,
});
let span = Span::new(lhs.span.start, token.span.end);
lhs = Expr {
kind: ExprKind::StaticPropertyAccess(StaticAccessExpr {
class: parser.alloc(lhs),
member,
}),
span,
};
} else if parser.check(TokenKind::Dollar) {
let member = parse_atom(parser);
let span = Span::new(lhs.span.start, member.span.end);
lhs = Expr {
kind: ExprKind::StaticPropertyAccessDynamic {
class: parser.alloc(lhs),
member: parser.alloc(member),
},
span,
};
} else if parser.check(TokenKind::LeftBrace) {
let brace_span = parser.current_span();
parser.require_version(
PhpVersion::Php83,
"dynamic class constant fetch",
brace_span,
);
parser.advance(); let member = parse_expr(parser);
parser.expect(TokenKind::RightBrace);
if parser.check(TokenKind::LeftParen) {
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::StaticMethod {
class: parser.alloc(lhs),
method: parser.alloc(member),
},
}),
span,
};
}
ArgListResult::Args(args) => {
let lhs_start = lhs.span.start;
let callee = Expr {
kind: ExprKind::ClassConstAccessDynamic {
class: parser.alloc(lhs),
member: parser.alloc(member),
},
span: Span::new(lhs_start, parser.previous_end()),
};
let span = Span::new(lhs_start, parser.previous_end());
lhs = Expr {
kind: ExprKind::FunctionCall(FunctionCallExpr {
name: parser.alloc(callee),
args,
}),
span,
};
}
}
} else {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ClassConstAccessDynamic {
class: parser.alloc(lhs),
member: parser.alloc(member),
},
span,
};
}
} else if parser.check(TokenKind::Class) {
let token = parser.advance();
let span = Span::new(lhs.span.start, token.span.end);
lhs = Expr {
kind: ExprKind::ClassConstAccess(StaticAccessExpr {
class: parser.alloc(lhs),
member: parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::Src("class")),
span: token.span,
}),
}),
span,
};
} else {
let (member_name, member_span) =
if let Some(result) = parser.eat_identifier_or_keyword() {
result
} else {
let span = parser.current_span();
parser.error(ParseError::Expected {
expected: "identifier".into(),
found: parser.current_kind(),
span,
});
("<error>", span)
};
let member = parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::Src(member_name)),
span: member_span,
});
if parser.check(TokenKind::LeftParen) {
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::StaticMethod {
class: parser.alloc(lhs),
method: member,
},
}),
span,
};
}
ArgListResult::Args(args) => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::StaticMethodCall(parser.alloc(
StaticMethodCallExpr {
class: parser.alloc(lhs),
method: member,
args,
},
)),
span,
};
}
}
} else {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ClassConstAccess(StaticAccessExpr {
class: parser.alloc(lhs),
member,
}),
span,
};
}
}
continue;
}
if kind == TokenKind::LeftBracket {
if 44u8 < min_bp {
break;
}
parser.advance(); let index = if parser.check(TokenKind::RightBracket) {
None
} else {
let e = parse_expr(parser);
Some(parser.alloc(e))
};
parser.expect(TokenKind::RightBracket);
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ArrayAccess(ArrayAccessExpr {
array: parser.alloc(lhs),
index,
}),
span,
};
continue;
}
if kind == TokenKind::LeftBrace {
if 44u8 < min_bp {
break;
}
parser.advance(); let index = if parser.check(TokenKind::RightBrace) {
None
} else {
let e = parse_expr(parser);
Some(parser.alloc(e))
};
parser.expect(TokenKind::RightBrace);
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ArrayAccess(ArrayAccessExpr {
array: parser.alloc(lhs),
index,
}),
span,
};
continue;
}
if kind == TokenKind::LeftParen {
if 44u8 < min_bp {
break;
}
lhs = parse_function_call(parser, lhs);
continue;
}
if kind == TokenKind::QuestionQuestion {
if NULL_COALESCE_LEFT_BP < min_bp {
break;
}
parser.advance();
let mut rhs = parse_expr_bp(parser, TERNARY_BP + 1);
if parser.current_kind().is_assignment_op() {
rhs = parse_assign_continuation(parser, rhs);
}
let span = lhs.span.merge(rhs.span);
lhs = Expr {
kind: ExprKind::NullCoalesce(NullCoalesceExpr {
left: parser.alloc(lhs),
right: parser.alloc(rhs),
}),
span,
};
continue;
}
if let Some((left_bp, right_bp)) = precedence::infix_binding_power(kind) {
if left_bp < min_bp {
break;
}
if parser.version >= PhpVersion::Php80
&& is_nonassoc_token(kind)
&& matches!(&lhs.kind, ExprKind::Binary(b) if is_nonassoc_binary_op(b.op))
{
let span = parser.current_span();
parser.error(ParseError::Forbidden {
message: "Chaining non-associative operators requires explicit parentheses."
.into(),
span,
});
}
let op_token = parser.advance();
if op_token.kind == TokenKind::PipeArrow {
parser.require_version(PhpVersion::Php85, "pipe operator (|>)", op_token.span);
}
let op = token_to_binary_op(op_token.kind).unwrap_or_else(|| {
unreachable!(
"infix_binding_power returned Some for {:?} but token_to_binary_op returned None",
op_token.kind
)
});
let rhs = parse_expr_bp(parser, right_bp);
let span = lhs.span.merge(rhs.span);
lhs = Expr {
kind: ExprKind::Binary(BinaryExpr {
left: parser.alloc(lhs),
op,
right: parser.alloc(rhs),
}),
span,
};
continue;
}
break;
}
parser.expr_depth -= 1;
lhs
}
fn parse_member_name<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
match parser.current_kind() {
TokenKind::Identifier
| TokenKind::Class
| TokenKind::Abstract
| TokenKind::Final
| TokenKind::Interface
| TokenKind::Trait
| TokenKind::Extends
| TokenKind::Implements
| TokenKind::Public
| TokenKind::Protected
| TokenKind::Private
| TokenKind::Static
| TokenKind::Const
| TokenKind::Fn_
| TokenKind::Match_
| TokenKind::Namespace
| TokenKind::Use
| TokenKind::Readonly
| TokenKind::Enum_
| TokenKind::From
| TokenKind::Self_
| TokenKind::Parent_
| TokenKind::New
| TokenKind::Yield_
| TokenKind::Throw
| TokenKind::Try
| TokenKind::Catch
| TokenKind::Finally
| TokenKind::Instanceof
| TokenKind::Array
| TokenKind::List
| TokenKind::Switch
| TokenKind::Case
| TokenKind::Default
| TokenKind::If
| TokenKind::Else
| TokenKind::ElseIf
| TokenKind::While
| TokenKind::Do
| TokenKind::For
| TokenKind::Foreach
| TokenKind::As
| TokenKind::Function
| TokenKind::Return
| TokenKind::Echo
| TokenKind::Print
| TokenKind::Break
| TokenKind::Continue
| TokenKind::Goto
| TokenKind::Declare
| TokenKind::Unset
| TokenKind::Global
| TokenKind::Clone
| TokenKind::Isset
| TokenKind::Empty
| TokenKind::Include
| TokenKind::IncludeOnce
| TokenKind::Require
| TokenKind::RequireOnce
| TokenKind::Eval
| TokenKind::Exit
| TokenKind::Die
| TokenKind::True
| TokenKind::False
| TokenKind::Null
| TokenKind::And
| TokenKind::Or
| TokenKind::Xor
| TokenKind::MagicClass
| TokenKind::MagicDir
| TokenKind::MagicFile
| TokenKind::MagicFunction
| TokenKind::MagicLine
| TokenKind::MagicMethod
| TokenKind::MagicNamespace
| TokenKind::MagicTrait
| TokenKind::MagicProperty => {
let token = parser.advance();
let text = &parser.source[token.span.start as usize..token.span.end as usize];
Expr {
kind: ExprKind::Identifier(NameStr::Src(text)),
span: token.span,
}
}
TokenKind::Variable => {
let token = parser.advance();
let name = parser.variable_name(token);
Expr {
kind: ExprKind::Variable(NameStr::Src(name)),
span: token.span,
}
}
TokenKind::LeftBrace => {
parser.advance();
let inner = parse_expr(parser);
parser.expect(TokenKind::RightBrace);
inner
}
_ => {
let span = parser.current_span();
parser.error(ParseError::Expected {
expected: "member name".into(),
found: parser.current_kind(),
span,
});
Expr {
kind: ExprKind::Error,
span,
}
}
}
}
fn parse_atom<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
instrument::record_parse_atom();
let kind = parser.current_kind();
if parser.is_semi_reserved_keyword()
&& !matches!(
kind,
TokenKind::New | TokenKind::Throw | TokenKind::Yield_ | TokenKind::Instanceof
)
&& parser.peek_kind() == Some(TokenKind::Backslash)
{
let token = parser.advance();
let src = parser.source;
let first = &src[token.span.start as usize..token.span.end as usize];
let mut parts: Vec<&str> = vec![first];
while parser.eat(TokenKind::Backslash).is_some() {
if let Some((part, _)) = parser.eat_identifier_or_keyword() {
parts.push(part);
}
}
let span = Span::new(token.span.start, parser.previous_end());
let ident = if parts.len() == 1 {
NameStr::Src(parts[0])
} else {
NameStr::Arena(parser.arena.alloc_str(&parts.join("\\")))
};
return Expr {
kind: ExprKind::Identifier(ident),
span,
};
}
if kind == TokenKind::HashBracket {
let start = parser.start_span();
let attributes = parser.parse_attributes();
if parser.check(TokenKind::Static) {
parser.advance();
if parser.check(TokenKind::Function) {
return parse_closure(parser, true, start, attributes);
}
if parser.check(TokenKind::Fn_) {
return parse_arrow_function(parser, true, start, attributes);
}
let span = parser.current_span();
parser.error(ParseError::Expected {
expected: "'function' or 'fn'".into(),
found: parser.current_kind(),
span,
});
return Expr {
kind: ExprKind::Error,
span: Span::new(start, span.end),
};
}
if parser.check(TokenKind::Function) {
return parse_closure(parser, false, start, attributes);
}
if parser.check(TokenKind::Fn_) {
return parse_arrow_function(parser, false, start, attributes);
}
let span = parser.current_span();
parser.error(ParseError::Expected {
expected: "'function', 'fn', or 'static'".into(),
found: parser.current_kind(),
span,
});
return Expr {
kind: ExprKind::Error,
span: Span::new(start, span.end),
};
}
if kind == TokenKind::At {
let token = parser.advance();
let mut operand = parse_expr_bp(parser, 41); if parser.current_kind().is_assignment_op() {
operand = parse_assign_continuation(parser, operand);
}
let span = token.span.merge(operand.span);
return Expr {
kind: ExprKind::ErrorSuppress(parser.alloc(operand)),
span,
};
}
if let Some(right_bp) = precedence::prefix_binding_power(kind) {
let op_token = parser.advance();
let mut operand = parse_expr_bp(parser, right_bp);
if parser.current_kind().is_assignment_op()
&& !matches!(op_token.kind, TokenKind::PlusPlus | TokenKind::MinusMinus)
{
operand = parse_assign_continuation(parser, operand);
}
let op = match op_token.kind {
TokenKind::Minus => UnaryPrefixOp::Negate,
TokenKind::Plus => UnaryPrefixOp::Plus,
TokenKind::Bang => UnaryPrefixOp::BooleanNot,
TokenKind::Tilde => UnaryPrefixOp::BitwiseNot,
TokenKind::PlusPlus => UnaryPrefixOp::PreIncrement,
TokenKind::MinusMinus => UnaryPrefixOp::PreDecrement,
_ => unreachable!(
"prefix_binding_power only returns Some for -, +, !, ~, ++, --, got {:?}",
op_token.kind
),
};
let span = op_token.span.merge(operand.span);
return Expr {
kind: ExprKind::UnaryPrefix(UnaryPrefixExpr {
op,
operand: parser.alloc(operand),
}),
span,
};
}
match kind {
TokenKind::IntLiteral => {
let token = parser.advance();
let text = &parser.source()[token.span.start as usize..token.span.end as usize];
match parse_int_no_alloc(text.as_bytes(), 10) {
Some(value) => Expr {
kind: ExprKind::Int(value),
span: token.span,
},
None => Expr {
kind: ExprKind::Float(parse_float_no_alloc(text)),
span: token.span,
},
}
}
TokenKind::HexIntLiteral => {
let token = parser.advance();
let text = &parser.source()[token.span.start as usize..token.span.end as usize];
match parse_int_no_alloc(&text.as_bytes()[2..], 16) {
Some(value) => Expr {
kind: ExprKind::Int(value),
span: token.span,
},
None => Expr {
kind: ExprKind::Float(parse_int_as_float(&text.as_bytes()[2..], 16.0)),
span: token.span,
},
}
}
TokenKind::BinIntLiteral => {
let token = parser.advance();
let text = &parser.source()[token.span.start as usize..token.span.end as usize];
match parse_int_no_alloc(&text.as_bytes()[2..], 2) {
Some(value) => Expr {
kind: ExprKind::Int(value),
span: token.span,
},
None => Expr {
kind: ExprKind::Float(parse_int_as_float(&text.as_bytes()[2..], 2.0)),
span: token.span,
},
}
}
TokenKind::OctIntLiteral => {
let token = parser.advance();
let text = &parser.source()[token.span.start as usize..token.span.end as usize];
match parse_int_no_alloc(&text.as_bytes()[1..], 8) {
Some(value) => Expr {
kind: ExprKind::Int(value),
span: token.span,
},
None => Expr {
kind: ExprKind::Float(parse_int_as_float(&text.as_bytes()[1..], 8.0)),
span: token.span,
},
}
}
TokenKind::OctIntLiteralNew => {
let token = parser.advance();
let text = &parser.source()[token.span.start as usize..token.span.end as usize];
match parse_int_no_alloc(&text.as_bytes()[2..], 8) {
Some(value) => Expr {
kind: ExprKind::Int(value),
span: token.span,
},
None => Expr {
kind: ExprKind::Float(parse_int_as_float(&text.as_bytes()[2..], 8.0)),
span: token.span,
},
}
}
TokenKind::InvalidNumericLiteral => {
let token = parser.advance();
Expr {
kind: ExprKind::Int(0),
span: token.span,
}
}
TokenKind::FloatLiteral
| TokenKind::FloatLiteralSimple
| TokenKind::FloatLiteralLeadingDot => {
let token = parser.advance();
let text = &parser.source()[token.span.start as usize..token.span.end as usize];
let value = parse_float_no_alloc(text);
Expr {
kind: ExprKind::Float(value),
span: token.span,
}
}
TokenKind::SingleQuotedString => {
let token = parser.advance();
let src = parser.source();
let text = &src[token.span.start as usize..token.span.end as usize];
let text = text
.strip_prefix('b')
.or_else(|| text.strip_prefix('B'))
.unwrap_or(text);
let without_open = text.strip_prefix('\'').unwrap_or(text);
let inner = without_open.strip_suffix('\'').unwrap_or(without_open);
let value: &'arena str = if !inner.contains('\\') {
let offset = inner.as_ptr() as usize - src.as_ptr() as usize;
parser.arena.alloc_str(&src[offset..offset + inner.len()])
} else {
let mut decoded = String::with_capacity(inner.len());
let bytes = inner.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'\'' => {
decoded.push('\'');
i += 2;
}
b'\\' => {
decoded.push('\\');
i += 2;
}
_ => {
decoded.push('\\');
i += 1;
}
}
} else {
let rest = &inner[i..];
let ch = rest.chars().next().unwrap();
decoded.push(ch);
i += ch.len_utf8();
}
}
parser.arena.alloc_str(&decoded)
};
Expr {
kind: ExprKind::String(value),
span: token.span,
}
}
TokenKind::DoubleQuotedString => {
let token = parser.advance();
let src = parser.source();
let text = &src[token.span.start as usize..token.span.end as usize];
let stripped = text
.strip_prefix('b')
.or_else(|| text.strip_prefix('B'))
.unwrap_or(text);
let Some(without_open) = stripped.strip_prefix('"') else {
return Expr {
kind: ExprKind::String(parser.arena.alloc_str("")),
span: token.span,
};
};
let inner = without_open.strip_suffix('"').unwrap_or(without_open);
if crate::interpolation::has_interpolation(inner) {
let inner_offset = (inner.as_ptr() as usize - src.as_ptr() as usize) as u32;
let parts = crate::interpolation::parse_interpolated_parts(
parser.arena,
src,
inner,
inner_offset,
parser.version,
);
Expr {
kind: ExprKind::InterpolatedString(parts),
span: token.span,
}
} else if !inner.contains('\\') {
let offset = inner.as_ptr() as usize - src.as_ptr() as usize;
Expr {
kind: ExprKind::String(
parser.arena.alloc_str(&src[offset..offset + inner.len()]),
),
span: token.span,
}
} else {
let inner_offset = token.span.end - 1 - inner.len() as u32;
let parts = crate::interpolation::parse_interpolated_parts(
parser.arena,
src,
inner,
inner_offset,
parser.version,
);
if parts.len() == 1 {
match parts
.into_iter()
.next()
.expect("parts.len() == 1 checked above")
{
StringPart::Literal(s) => Expr {
kind: ExprKind::String(s),
span: token.span,
},
part => {
let mut v = parser.alloc_vec_with_capacity(1);
v.push(part);
Expr {
kind: ExprKind::InterpolatedString(v),
span: token.span,
}
}
}
} else {
Expr {
kind: ExprKind::InterpolatedString(parts),
span: token.span,
}
}
}
}
TokenKind::BacktickString => {
let token = parser.advance();
let src = parser.source();
let text = &src[token.span.start as usize..token.span.end as usize];
let Some(without_open) = text.strip_prefix('`') else {
return Expr {
kind: ExprKind::ShellExec(parser.alloc_vec_with_capacity(0)),
span: token.span,
};
};
let inner = without_open.strip_suffix('`').unwrap_or(without_open);
if crate::interpolation::has_interpolation(inner) {
let inner_offset = token.span.start + 1;
let parts = crate::interpolation::parse_interpolated_parts(
parser.arena,
src,
inner,
inner_offset,
parser.version,
);
Expr {
kind: ExprKind::ShellExec(parts),
span: token.span,
}
} else if !inner.contains('\\') {
let offset = inner.as_ptr() as usize - src.as_ptr() as usize;
let mut parts = parser.alloc_vec_with_capacity(1);
parts.push(StringPart::Literal(
parser.arena.alloc_str(&src[offset..offset + inner.len()]),
));
Expr {
kind: ExprKind::ShellExec(parts),
span: token.span,
}
} else {
let inner_offset = token.span.start + 1;
let parts = crate::interpolation::parse_interpolated_parts(
parser.arena,
src,
inner,
inner_offset,
parser.version,
);
Expr {
kind: ExprKind::ShellExec(parts),
span: token.span,
}
}
}
TokenKind::Heredoc => {
let token = parser.advance();
let src = parser.source();
let text = &src[token.span.start as usize..token.span.end as usize];
let (label, body_start_in_text, body_end_in_text, indent) = parse_heredoc_content(text);
let body_offset = token.span.start + body_start_in_text as u32;
let raw_body = &src[body_offset as usize..token.span.start as usize + body_end_in_text];
if crate::interpolation::has_interpolation(raw_body) {
if !indent.is_empty() {
let parts = crate::interpolation::parse_interpolated_parts_indented(
parser.arena,
src,
raw_body,
body_offset,
&indent,
parser.version,
);
Expr {
kind: ExprKind::Heredoc { label, parts },
span: token.span,
}
} else {
let parts = crate::interpolation::parse_interpolated_parts(
parser.arena,
src,
raw_body,
body_offset,
parser.version,
);
Expr {
kind: ExprKind::Heredoc { label, parts },
span: token.span,
}
}
} else {
let body_str = if !indent.is_empty() {
raw_body
.lines()
.map(|line| line.strip_prefix(&indent).unwrap_or(line))
.collect::<Vec<_>>()
.join("\n")
} else {
raw_body.to_string()
};
let mut parts = parser.alloc_vec_with_capacity(1);
parts.push(StringPart::Literal(parser.arena.alloc_str(&body_str)));
Expr {
kind: ExprKind::Heredoc { label, parts },
span: token.span,
}
}
}
TokenKind::Nowdoc => {
let token = parser.advance();
let src = parser.source();
let text = &src[token.span.start as usize..token.span.end as usize];
let (label, body_start_in_text, body_end_in_text, indent) = parse_heredoc_content(text);
let raw_body = &text[body_start_in_text..body_end_in_text];
let value: &'arena str = if !indent.is_empty() {
let s = raw_body
.lines()
.map(|line| line.strip_prefix(&indent).unwrap_or(line))
.collect::<Vec<_>>()
.join("\n");
parser.arena.alloc_str(&s)
} else {
parser.arena.alloc_str(raw_body)
};
Expr {
kind: ExprKind::Nowdoc { label, value },
span: token.span,
}
}
TokenKind::True => {
let token = parser.advance();
Expr {
kind: ExprKind::Bool(true),
span: token.span,
}
}
TokenKind::False => {
let token = parser.advance();
Expr {
kind: ExprKind::Bool(false),
span: token.span,
}
}
TokenKind::Null => {
let token = parser.advance();
Expr {
kind: ExprKind::Null,
span: token.span,
}
}
TokenKind::Variable => {
let token = parser.advance();
let name = parser.variable_name(token);
Expr {
kind: ExprKind::Variable(NameStr::Src(name)),
span: token.span,
}
}
TokenKind::Dollar => {
let token = parser.advance(); let inner = if parser.check(TokenKind::LeftBrace) {
parser.advance(); let expr = parse_expr(parser);
parser.expect(TokenKind::RightBrace);
expr
} else {
parse_atom(parser)
};
let span = Span::new(token.span.start, inner.span.end);
Expr {
kind: ExprKind::VariableVariable(parser.alloc(inner)),
span,
}
}
TokenKind::Identifier => {
let token = parser.advance();
let src = parser.source;
let text = &src[token.span.start as usize..token.span.end as usize];
if parser.check(TokenKind::Backslash) {
let mut parts: Vec<&str> = vec![text];
while parser.eat(TokenKind::Backslash).is_some() {
if let Some((part, _)) = parser.eat_identifier_or_keyword() {
parts.push(part);
}
}
let span = Span::new(token.span.start, parser.previous_end());
Expr {
kind: ExprKind::Identifier(NameStr::Arena(
parser.arena.alloc_str(&parts.join("\\")),
)),
span,
}
} else {
Expr {
kind: ExprKind::Identifier(NameStr::Src(text)),
span: token.span,
}
}
}
TokenKind::Backslash => {
let start = parser.start_span();
let name = parser.parse_name();
let ident = match name.to_string_repr() {
Cow::Borrowed(s) => NameStr::Src(s),
Cow::Owned(ref s) => NameStr::Arena(parser.arena.alloc_str(s)),
};
Expr {
kind: ExprKind::Identifier(ident),
span: Span::new(start, name.span().end),
}
}
TokenKind::Self_ => {
let token = parser.advance();
Expr {
kind: ExprKind::Identifier(NameStr::Arena("self")),
span: token.span,
}
}
TokenKind::Parent_ => {
let token = parser.advance();
Expr {
kind: ExprKind::Identifier(NameStr::Arena("parent")),
span: token.span,
}
}
TokenKind::Static => {
let token = parser.advance();
if parser.check(TokenKind::Function) {
return parse_closure(parser, true, token.span.start, parser.alloc_vec());
}
if parser.check(TokenKind::Fn_) {
return parse_arrow_function(parser, true, token.span.start, parser.alloc_vec());
}
Expr {
kind: ExprKind::Identifier(NameStr::Arena("static")),
span: token.span,
}
}
TokenKind::Print => {
let token = parser.advance();
let expr = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = token.span.merge(expr.span);
Expr {
kind: ExprKind::Print(parser.alloc(expr)),
span,
}
}
TokenKind::New => parse_new_expr(parser),
TokenKind::Function => {
let start = parser.start_span();
parse_closure(parser, false, start, parser.alloc_vec())
}
TokenKind::Fn_ => {
let start = parser.start_span();
parse_arrow_function(parser, false, start, parser.alloc_vec())
}
TokenKind::Match_ => {
let span = parser.current_span();
parser.require_version(PhpVersion::Php80, "match expressions", span);
parse_match_expr(parser)
}
TokenKind::Throw => {
let token = parser.advance();
let span = token.span;
parser.require_version(PhpVersion::Php80, "throw expressions", span);
let expr = parse_expr_bp(parser, ASSIGNMENT_BP);
let merged = span.merge(expr.span);
Expr {
kind: ExprKind::ThrowExpr(parser.alloc(expr)),
span: merged,
}
}
TokenKind::Yield_ => parse_yield_expr(parser),
TokenKind::LeftParen => {
if let Some(cast_kind) = try_parse_cast(parser) {
return cast_kind;
}
let start = parser.start_span();
let open = parser.advance(); let inner = parse_expr(parser);
parser.expect_closing(TokenKind::RightParen, open.span);
let span = Span::new(start, parser.previous_end());
Expr {
kind: ExprKind::Parenthesized(parser.alloc(inner)),
span,
}
}
TokenKind::LeftBracket => parse_array_literal(parser),
TokenKind::Array => parse_array_call(parser),
TokenKind::List => parse_list_expr(parser),
TokenKind::Isset => {
let start = parser.start_span();
parser.advance();
parser.expect(TokenKind::LeftParen);
let mut exprs = parser.alloc_vec();
exprs.push(parse_expr(parser));
while parser.eat(TokenKind::Comma).is_some() {
if parser.check(TokenKind::RightParen) {
break;
}
exprs.push(parse_expr(parser));
}
parser.expect(TokenKind::RightParen);
let end = parser.previous_end();
Expr {
kind: ExprKind::Isset(exprs),
span: Span::new(start, end),
}
}
TokenKind::Empty => {
let start = parser.start_span();
parser.advance();
parser.expect(TokenKind::LeftParen);
let inner = parse_expr(parser);
parser.expect(TokenKind::RightParen);
let end = parser.previous_end();
Expr {
kind: ExprKind::Empty(parser.alloc(inner)),
span: Span::new(start, end),
}
}
TokenKind::Eval => {
let start = parser.start_span();
parser.advance();
parser.expect(TokenKind::LeftParen);
let inner = parse_expr(parser);
parser.expect(TokenKind::RightParen);
let end = parser.previous_end();
Expr {
kind: ExprKind::Eval(parser.alloc(inner)),
span: Span::new(start, end),
}
}
TokenKind::Include => {
let token = parser.advance();
let inner = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = token.span.merge(inner.span);
Expr {
kind: ExprKind::Include(IncludeKind::Include, parser.alloc(inner)),
span,
}
}
TokenKind::IncludeOnce => {
let token = parser.advance();
let inner = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = token.span.merge(inner.span);
Expr {
kind: ExprKind::Include(IncludeKind::IncludeOnce, parser.alloc(inner)),
span,
}
}
TokenKind::Require => {
let token = parser.advance();
let inner = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = token.span.merge(inner.span);
Expr {
kind: ExprKind::Include(IncludeKind::Require, parser.alloc(inner)),
span,
}
}
TokenKind::RequireOnce => {
let token = parser.advance();
let inner = parse_expr_bp(parser, ASSIGNMENT_BP);
let span = token.span.merge(inner.span);
Expr {
kind: ExprKind::Include(IncludeKind::RequireOnce, parser.alloc(inner)),
span,
}
}
TokenKind::Exit | TokenKind::Die => {
let token = parser.advance();
let src = parser.source;
let name_text = NameStr::Src(&src[token.span.start as usize..token.span.end as usize]);
if parser.check(TokenKind::LeftParen) {
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let callee = Expr {
kind: ExprKind::Identifier(name_text),
span: token.span,
};
let span = Span::new(token.span.start, parser.previous_end());
Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::Function(parser.alloc(callee)),
}),
span,
}
}
ArgListResult::Args(args) => {
let span = Span::new(token.span.start, parser.previous_end());
if args.is_empty() {
Expr {
kind: ExprKind::Exit(None),
span,
}
} else if args.len() == 1 && args[0].name.is_none() && !args[0].unpack {
let value = args
.into_iter()
.next()
.expect("args.len() == 1 checked above")
.value;
Expr {
kind: ExprKind::Exit(Some(parser.alloc(value))),
span,
}
} else {
let callee = Expr {
kind: ExprKind::Identifier(name_text),
span: token.span,
};
Expr {
kind: ExprKind::FunctionCall(FunctionCallExpr {
name: parser.alloc(callee),
args,
}),
span,
}
}
}
}
} else {
Expr {
kind: ExprKind::Exit(None),
span: token.span,
}
}
}
TokenKind::Clone => {
let token = parser.advance();
if parser.check(TokenKind::LeftParen) {
let src = parser.source;
let name_text =
NameStr::Src(&src[token.span.start as usize..token.span.end as usize]);
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
parser.require_version(
PhpVersion::Php85,
"clone(...) first-class callable",
token.span,
);
let span = Span::new(token.span.start, parser.previous_end());
let callee = Expr {
kind: ExprKind::Identifier(name_text),
span: token.span,
};
Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::Function(parser.alloc(callee)),
}),
span,
}
}
ArgListResult::Args(args) => {
let span = Span::new(token.span.start, parser.previous_end());
let is_simple =
args.len() == 1 && args[0].name.is_none() && !args[0].unpack;
let is_clone_with = args.len() == 2
&& args[0].name.is_none()
&& !args[0].unpack
&& args[1].name.is_none()
&& !args[1].unpack;
if is_simple {
let object = args
.into_iter()
.next()
.expect("is_simple: args.len() == 1 checked above")
.value;
Expr {
kind: ExprKind::Clone(parser.alloc(object)),
span,
}
} else if is_clone_with {
parser.require_version(
PhpVersion::Php85,
"clone with property overrides",
token.span,
);
let mut iter = args.into_iter();
let object = iter
.next()
.expect("is_clone_with: args.len() == 2 checked above")
.value;
let overrides = iter
.next()
.expect("is_clone_with: args.len() == 2 checked above")
.value;
Expr {
kind: ExprKind::CloneWith(
parser.alloc(object),
parser.alloc(overrides),
),
span,
}
} else {
let callee = Expr {
kind: ExprKind::Identifier(name_text),
span: token.span,
};
Expr {
kind: ExprKind::FunctionCall(FunctionCallExpr {
name: parser.alloc(callee),
args,
}),
span,
}
}
}
}
} else {
let operand = parse_expr_bp(parser, 41);
let span = token.span.merge(operand.span);
Expr {
kind: ExprKind::Clone(parser.alloc(operand)),
span,
}
}
}
TokenKind::MagicClass => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Class),
span: t.span,
}
}
TokenKind::MagicDir => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Dir),
span: t.span,
}
}
TokenKind::MagicFile => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::File),
span: t.span,
}
}
TokenKind::MagicFunction => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Function),
span: t.span,
}
}
TokenKind::MagicLine => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Line),
span: t.span,
}
}
TokenKind::MagicMethod => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Method),
span: t.span,
}
}
TokenKind::MagicNamespace => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Namespace),
span: t.span,
}
}
TokenKind::MagicTrait => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Trait),
span: t.span,
}
}
TokenKind::MagicProperty => {
let t = parser.advance();
Expr {
kind: ExprKind::MagicConst(MagicConstKind::Property),
span: t.span,
}
}
TokenKind::Namespace if parser.peek_kind() == Some(TokenKind::Backslash) => {
let start = parser.start_span();
let name = parser.parse_name();
let text = parser
.arena
.alloc_str(&format!("namespace\\{}", name.join_parts()));
Expr {
kind: ExprKind::Identifier(NameStr::Arena(text)),
span: Span::new(start, name.span().end),
}
}
TokenKind::Namespace => {
let span = parser.current_span();
parser.error(ParseError::ExpectedExpression { span });
Expr {
kind: ExprKind::Error,
span,
}
}
TokenKind::Readonly => {
let token = parser.advance();
Expr {
kind: ExprKind::Identifier(NameStr::Arena("readonly")),
span: token.span,
}
}
_ => {
let span = parser.current_span();
parser.error(ParseError::ExpectedExpression { span });
Expr {
kind: ExprKind::Error,
span,
}
}
}
}
fn parse_new_expr<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
let start = parser.start_span();
parser.advance();
let anon_attributes = if parser.check(TokenKind::HashBracket) {
parser.parse_attributes()
} else {
parser.alloc_vec()
};
let anon_readonly =
parser.check(TokenKind::Readonly) && parser.peek_kind() == Some(TokenKind::Class);
if parser.check(TokenKind::Class) || anon_readonly {
if anon_readonly {
parser.advance(); }
parser.advance();
let args = if parser.check(TokenKind::LeftParen) {
parse_arg_list(parser)
} else {
parser.alloc_vec()
};
let extends = if parser.eat(TokenKind::Extends).is_some() {
Some(parser.parse_name())
} else {
None
};
let implements = if parser.eat(TokenKind::Implements).is_some() {
stmt::parse_name_list(parser)
} else {
parser.alloc_vec()
};
parser.expect(TokenKind::LeftBrace);
let members = stmt::parse_class_members(parser, false);
parser.expect(TokenKind::RightBrace);
let end = parser.previous_end();
let class_decl = ClassDecl {
name: None,
modifiers: ClassModifiers {
is_readonly: anon_readonly,
..Default::default()
},
extends,
implements,
members,
attributes: anon_attributes,
doc_comment: None,
};
let anon_class_expr = Expr {
kind: ExprKind::AnonymousClass(parser.alloc(class_decl)),
span: Span::new(start, end),
};
return Expr {
kind: ExprKind::New(NewExpr {
class: parser.alloc(anon_class_expr),
args,
}),
span: Span::new(start, end),
};
}
let class = match parser.current_kind() {
TokenKind::Self_ => {
let t = parser.advance();
Expr {
kind: ExprKind::Identifier(NameStr::Arena("self")),
span: t.span,
}
}
TokenKind::Parent_ => {
let t = parser.advance();
Expr {
kind: ExprKind::Identifier(NameStr::Arena("parent")),
span: t.span,
}
}
TokenKind::Static => {
let t = parser.advance();
Expr {
kind: ExprKind::Identifier(NameStr::Arena("static")),
span: t.span,
}
}
TokenKind::Variable => {
let t = parser.advance();
Expr {
kind: ExprKind::Variable(NameStr::Src(parser.variable_name(t))),
span: t.span,
}
}
TokenKind::LeftParen => {
let paren_start = parser.start_span();
let open = parser.advance(); let inner = parse_expr(parser);
parser.expect_closing(TokenKind::RightParen, open.span);
let paren_span = Span::new(paren_start, parser.previous_end());
Expr {
kind: ExprKind::Parenthesized(parser.alloc(inner)),
span: paren_span,
}
}
_ => {
let name = parser.parse_name();
let ident = match name.to_string_repr() {
Cow::Borrowed(s) => NameStr::Src(s),
Cow::Owned(ref s) => NameStr::Arena(parser.arena.alloc_str(s)),
};
Expr {
kind: ExprKind::Identifier(ident),
span: name.span(),
}
}
};
let args = if parser.check(TokenKind::LeftParen) {
parse_arg_list(parser)
} else {
parser.alloc_vec()
};
let span = Span::new(start, parser.previous_end());
Expr {
kind: ExprKind::New(NewExpr {
class: parser.alloc(class),
args,
}),
span,
}
}
fn parse_closure<'arena, 'src>(
parser: &mut Parser<'arena, 'src>,
is_static: bool,
start: u32,
attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
) -> Expr<'arena, 'src> {
if !is_static {
parser.advance(); } else {
parser.advance(); }
let by_ref = parser.eat(TokenKind::Ampersand).is_some();
parser.expect(TokenKind::LeftParen);
let params = stmt::parse_param_list(parser);
parser.expect(TokenKind::RightParen);
let use_vars = if parser.eat(TokenKind::Use).is_some() {
parser.expect(TokenKind::LeftParen);
let vars = parse_closure_use_list(parser);
parser.expect(TokenKind::RightParen);
vars
} else {
parser.alloc_vec()
};
let return_type = if parser.eat(TokenKind::Colon).is_some() {
Some(parser.parse_type_hint())
} else {
None
};
parser.expect(TokenKind::LeftBrace);
let mut body = parser.alloc_vec_with_capacity(16);
while !parser.check(TokenKind::RightBrace) && !parser.check(TokenKind::Eof) {
let span_before = parser.current_span();
body.push(stmt::parse_stmt(parser));
if parser.current_span() == span_before {
parser.advance();
}
}
parser.expect(TokenKind::RightBrace);
let end = parser.previous_end();
Expr {
kind: ExprKind::Closure(parser.alloc(ClosureExpr {
is_static,
by_ref,
params,
use_vars,
return_type,
body,
attributes,
})),
span: Span::new(start, end),
}
}
fn parse_closure_use_list<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> ArenaVec<'arena, ClosureUseVar<'src>> {
let mut vars = parser.alloc_vec_with_capacity(2);
loop {
if parser.check(TokenKind::RightParen) {
break;
}
let var_start = parser.start_span();
let by_ref = parser.eat(TokenKind::Ampersand).is_some();
if let Some(token) = parser.eat(TokenKind::Variable) {
let name = parser.variable_name(token);
let span = Span::new(var_start, token.span.end);
vars.push(ClosureUseVar { name, by_ref, span });
}
if parser.eat(TokenKind::Comma).is_none() {
break;
}
}
vars
}
fn parse_arrow_function<'arena, 'src>(
parser: &mut Parser<'arena, 'src>,
is_static: bool,
start: u32,
attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
) -> Expr<'arena, 'src> {
parser.advance();
let by_ref = parser.eat(TokenKind::Ampersand).is_some();
parser.expect(TokenKind::LeftParen);
let params = stmt::parse_param_list(parser);
parser.expect(TokenKind::RightParen);
let return_type = if parser.eat(TokenKind::Colon).is_some() {
Some(parser.parse_type_hint())
} else {
None
};
parser.expect(TokenKind::FatArrow);
let body = parse_expr(parser);
let span = Span::new(start, body.span.end);
Expr {
kind: ExprKind::ArrowFunction(parser.alloc(ArrowFunctionExpr {
is_static,
by_ref,
params,
return_type,
body: parser.alloc(body),
attributes,
})),
span,
}
}
fn parse_match_expr<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
let start = parser.start_span();
parser.advance();
parser.expect(TokenKind::LeftParen);
let subject = parse_expr(parser);
parser.expect(TokenKind::RightParen);
parser.expect(TokenKind::LeftBrace);
let mut arms = parser.alloc_vec_with_capacity(4);
let mut seen_default = false;
while !parser.check(TokenKind::RightBrace) && !parser.check(TokenKind::Eof) {
let arm_start = parser.start_span();
let conditions = if let Some(tok) = parser.eat(TokenKind::Default) {
if seen_default {
parser.error(ParseError::Forbidden {
message: "match expression may only contain one default arm".into(),
span: tok.span,
});
}
seen_default = true;
parser.eat(TokenKind::Comma);
None
} else {
let mut conds = parser.alloc_vec_with_capacity(2);
conds.push(parse_expr(parser));
while parser.eat(TokenKind::Comma).is_some() {
if parser.check(TokenKind::FatArrow) {
break;
}
conds.push(parse_expr(parser));
}
Some(conds)
};
parser.expect(TokenKind::FatArrow);
let body = parse_expr(parser);
let arm_span = Span::new(arm_start, body.span.end);
arms.push(MatchArm {
conditions,
body,
span: arm_span,
});
if parser.eat(TokenKind::Comma).is_none() {
break;
}
}
parser.expect(TokenKind::RightBrace);
let end = parser.previous_end();
Expr {
kind: ExprKind::Match(MatchExpr {
subject: parser.alloc(subject),
arms,
}),
span: Span::new(start, end),
}
}
fn parse_yield_expr<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
let start = parser.start_span();
parser.advance();
if parser.check(TokenKind::From) {
parser.advance();
let value = parse_expr(parser);
let span = Span::new(start, value.span.end);
return Expr {
kind: ExprKind::Yield(YieldExpr {
key: None,
value: Some(parser.alloc(value)),
is_from: true,
}),
span,
};
}
let kind = parser.current_kind();
let is_binary_only = precedence::infix_binding_power(kind).is_some()
&& precedence::prefix_binding_power(kind).is_none();
if parser.check(TokenKind::Semicolon)
|| parser.check(TokenKind::RightParen)
|| parser.check(TokenKind::RightBracket)
|| parser.check(TokenKind::RightBrace)
|| parser.check(TokenKind::Comma)
|| is_binary_only
{
let span = Span::new(start, parser.previous_end());
return Expr {
kind: ExprKind::Yield(YieldExpr {
key: None,
value: None,
is_from: false,
}),
span,
};
}
let first = parse_expr(parser);
if parser.eat(TokenKind::FatArrow).is_some() {
let value = parse_expr(parser);
let span = Span::new(start, value.span.end);
return Expr {
kind: ExprKind::Yield(YieldExpr {
key: Some(parser.alloc(first)),
value: Some(parser.alloc(value)),
is_from: false,
}),
span,
};
}
let span = Span::new(start, first.span.end);
Expr {
kind: ExprKind::Yield(YieldExpr {
key: None,
value: Some(parser.alloc(first)),
is_from: false,
}),
span,
}
}
enum ArgListResult<'arena, 'src> {
Args(ArenaVec<'arena, Arg<'arena, 'src>>),
CallableMarker,
}
fn parse_arg_list_or_callable<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> ArgListResult<'arena, 'src> {
parser.advance();
if parser.check(TokenKind::Ellipsis) && parser.peek_kind() == Some(TokenKind::RightParen) {
let span = parser.current_span();
parser.require_version(PhpVersion::Php81, "first-class callable syntax", span);
parser.advance(); parser.advance(); return ArgListResult::CallableMarker;
}
let mut args = parser.alloc_vec_with_capacity(4);
let mut seen_named = false;
if !parser.check(TokenKind::RightParen) {
loop {
if parser.check(TokenKind::RightParen) {
break; }
let arg = parse_arg(parser);
if arg.name.is_some() {
seen_named = true;
} else if seen_named {
parser.error(ParseError::Forbidden {
message: "cannot use positional argument after named argument".into(),
span: arg.span,
});
}
args.push(arg);
if parser.eat(TokenKind::Comma).is_none() {
break;
}
}
}
parser.expect(TokenKind::RightParen);
ArgListResult::Args(args)
}
pub fn parse_arg_list<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> ArenaVec<'arena, Arg<'arena, 'src>> {
match parse_arg_list_or_callable(parser) {
ArgListResult::Args(args) => args,
ArgListResult::CallableMarker => parser.alloc_vec(), }
}
fn parse_arg<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Arg<'arena, 'src> {
let start = parser.start_span();
let name = if parser.peek_kind() == Some(TokenKind::Colon) {
let kind = parser.current_kind();
if kind == TokenKind::Identifier
|| parser.is_semi_reserved_keyword()
|| matches!(
kind,
TokenKind::Array
| TokenKind::List
| TokenKind::Match_
| TokenKind::Fn_
| TokenKind::Static
| TokenKind::Abstract
| TokenKind::Final
| TokenKind::Readonly
| TokenKind::Class
| TokenKind::Interface
| TokenKind::Trait
| TokenKind::Enum_
| TokenKind::Extends
| TokenKind::Implements
| TokenKind::Const
| TokenKind::Use
| TokenKind::Namespace
| TokenKind::New
| TokenKind::Try
| TokenKind::Catch
| TokenKind::Finally
| TokenKind::Throw
| TokenKind::Instanceof
| TokenKind::Yield_
| TokenKind::Switch
| TokenKind::Public
| TokenKind::Protected
| TokenKind::Private
)
{
let name_token = parser.advance();
let span = name_token.span;
parser.require_version(PhpVersion::Php80, "named arguments", span);
parser.advance(); let src = parser.source;
Some(Name::Simple {
value: &src[name_token.span.start as usize..name_token.span.end as usize],
span: name_token.span,
})
} else {
None
}
} else {
None
};
let unpack = parser.eat(TokenKind::Ellipsis).is_some();
let by_ref = parser.eat(TokenKind::Ampersand).is_some();
let value = parse_expr(parser);
let span = Span::new(start, value.span.end);
Arg {
name,
value,
unpack,
by_ref,
span,
}
}
fn parse_function_call<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
callee: Expr<'arena, 'src>,
) -> Expr<'arena, 'src> {
let start = callee.span.start;
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(start, parser.previous_end());
Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::Function(parser.alloc(callee)),
}),
span,
}
}
ArgListResult::Args(args) => {
let span = Span::new(start, parser.previous_end());
Expr {
kind: ExprKind::FunctionCall(FunctionCallExpr {
name: parser.alloc(callee),
args,
}),
span,
}
}
}
}
fn parse_array_literal<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
instrument::record_parse_array();
let start = parser.start_span();
parser.advance();
let mut elements = parser.alloc_vec_with_capacity(0);
if !parser.check(TokenKind::RightBracket) {
loop {
if parser.check(TokenKind::RightBracket) {
break; }
if parser.check(TokenKind::Comma) {
let span = parser.current_span();
elements.push(ArrayElement {
key: None,
value: Expr {
kind: ExprKind::Omit,
span,
},
unpack: false,
by_ref: false,
span,
});
} else {
elements.push(parse_array_element(parser));
}
if parser.eat(TokenKind::Comma).is_none() {
break;
}
}
}
instrument::record_parse_array_element_count(elements.len());
parser.expect(TokenKind::RightBracket);
let end = parser.previous_end();
let span = Span::new(start, end);
Expr {
kind: ExprKind::Array(elements),
span,
}
}
fn parse_array_call<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
instrument::record_parse_array();
let start = parser.start_span();
parser.advance(); parser.expect(TokenKind::LeftParen);
let mut elements = parser.alloc_vec_with_capacity(0);
if !parser.check(TokenKind::RightParen) {
loop {
if parser.check(TokenKind::RightParen) {
break; }
elements.push(parse_array_element(parser));
if parser.eat(TokenKind::Comma).is_none() {
break;
}
}
}
instrument::record_parse_array_element_count(elements.len());
parser.expect(TokenKind::RightParen);
let end = parser.previous_end();
let span = Span::new(start, end);
Expr {
kind: ExprKind::Array(elements),
span,
}
}
fn parse_array_element<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> ArrayElement<'arena, 'src> {
instrument::record_parse_array_element();
let elem_start = parser.start_span();
let unpack = parser.eat(TokenKind::Ellipsis).is_some();
let by_ref_value = !unpack && parser.check(TokenKind::Ampersand) && {
parser.advance();
true
};
instrument::record_parse_expr_array_first();
let first_expr = parse_expr(parser);
if !unpack && !by_ref_value && parser.eat(TokenKind::FatArrow).is_some() {
instrument::record_parse_array_element_with_arrow();
let by_ref = parser.check(TokenKind::Ampersand) && {
parser.advance();
true
};
instrument::record_parse_expr_array_second();
let value = parse_expr(parser);
let elem_span = Span::new(elem_start, value.span.end);
ArrayElement {
key: Some(first_expr),
value,
unpack: false,
by_ref,
span: elem_span,
}
} else {
instrument::record_parse_array_simple_value();
let elem_span = Span::new(elem_start, first_expr.span.end);
ArrayElement {
key: None,
value: first_expr,
unpack,
by_ref: by_ref_value,
span: elem_span,
}
}
}
fn parse_list_expr<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
let start = parser.start_span();
parser.advance(); parser.expect(TokenKind::LeftParen);
let mut elements = parser.alloc_vec_with_capacity(4);
if !parser.check(TokenKind::RightParen) {
loop {
if parser.check(TokenKind::RightParen) {
break;
}
if parser.check(TokenKind::Comma) {
let span = parser.current_span();
elements.push(ArrayElement {
key: None,
value: Expr {
kind: ExprKind::Omit,
span,
},
unpack: false,
by_ref: false,
span,
});
} else {
elements.push(parse_list_element(parser));
}
if parser.eat(TokenKind::Comma).is_none() {
break;
}
}
}
parser.expect(TokenKind::RightParen);
let end = parser.previous_end();
let span = Span::new(start, end);
Expr {
kind: ExprKind::Array(elements),
span,
}
}
fn parse_list_element<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> ArrayElement<'arena, 'src> {
let elem_start = parser.start_span();
if parser.check(TokenKind::Ampersand) {
parser.advance();
let value = parse_expr(parser);
let elem_span = Span::new(elem_start, value.span.end);
return ArrayElement {
key: None,
value,
unpack: false,
by_ref: true,
span: elem_span,
};
}
let first = parse_expr(parser);
if parser.eat(TokenKind::FatArrow).is_some() {
let by_ref = parser.check(TokenKind::Ampersand) && {
parser.advance();
true
};
let value = parse_expr(parser);
let elem_span = Span::new(elem_start, value.span.end);
ArrayElement {
key: Some(first),
value,
unpack: false,
by_ref,
span: elem_span,
}
} else {
let elem_span = Span::new(elem_start, first.span.end);
ArrayElement {
key: None,
value: first,
unpack: false,
by_ref: false,
span: elem_span,
}
}
}
fn try_parse_cast<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> Option<Expr<'arena, 'src>> {
let peeked = parser.peek_kind();
let cast_kind = match peeked {
Some(TokenKind::Identifier) => {
let peek_text = parser.peek_text()?;
CAST_KEYWORDS
.iter()
.find(|(kw, _)| kw.eq_ignore_ascii_case(peek_text))
.map(|(_, ck)| *ck)
}
Some(TokenKind::Array) => Some(CastKind::Array),
Some(TokenKind::Unset) => Some(CastKind::Unset),
_ => None,
};
let cast_kind = cast_kind?;
if parser.peek2_kind() != Some(TokenKind::RightParen) {
return None;
}
let start = parser.start_span();
parser.advance(); let kw_span = parser.current_span();
parser.advance(); parser.eat(TokenKind::RightParen)?;
if cast_kind == CastKind::Unset && parser.version >= PhpVersion::Php80 {
parser.error(ParseError::Forbidden {
message: "the (unset) cast is no longer supported".into(),
span: kw_span,
});
}
if cast_kind == CastKind::Void {
parser.require_version(PhpVersion::Php85, "void cast", kw_span);
}
let kw_text = &parser.source[kw_span.start as usize..kw_span.end as usize];
if kw_text.eq_ignore_ascii_case("real") && parser.version >= PhpVersion::Php80 {
parser.error(ParseError::Forbidden {
message: "the (real) cast is no longer supported, use (float) instead".into(),
span: kw_span,
});
}
let mut operand = parse_expr_bp(parser, 41);
if parser.current_kind().is_assignment_op() {
operand = parse_assign_continuation(parser, operand);
}
let span = Span::new(start, operand.span.end);
Some(Expr {
kind: ExprKind::Cast(cast_kind, parser.alloc(operand)),
span,
})
}
fn token_to_binary_op(kind: TokenKind) -> Option<BinaryOp> {
match kind {
TokenKind::Plus => Some(BinaryOp::Add),
TokenKind::Minus => Some(BinaryOp::Sub),
TokenKind::Star => Some(BinaryOp::Mul),
TokenKind::Slash => Some(BinaryOp::Div),
TokenKind::Percent => Some(BinaryOp::Mod),
TokenKind::StarStar => Some(BinaryOp::Pow),
TokenKind::Dot => Some(BinaryOp::Concat),
TokenKind::EqualsEquals => Some(BinaryOp::Equal),
TokenKind::BangEquals => Some(BinaryOp::NotEqual),
TokenKind::EqualsEqualsEquals => Some(BinaryOp::Identical),
TokenKind::BangEqualsEquals => Some(BinaryOp::NotIdentical),
TokenKind::LessThan => Some(BinaryOp::Less),
TokenKind::GreaterThan => Some(BinaryOp::Greater),
TokenKind::LessThanEquals => Some(BinaryOp::LessOrEqual),
TokenKind::GreaterThanEquals => Some(BinaryOp::GreaterOrEqual),
TokenKind::Spaceship => Some(BinaryOp::Spaceship),
TokenKind::AmpersandAmpersand => Some(BinaryOp::BooleanAnd),
TokenKind::PipePipe => Some(BinaryOp::BooleanOr),
TokenKind::Ampersand => Some(BinaryOp::BitwiseAnd),
TokenKind::Pipe => Some(BinaryOp::BitwiseOr),
TokenKind::Caret => Some(BinaryOp::BitwiseXor),
TokenKind::ShiftLeft => Some(BinaryOp::ShiftLeft),
TokenKind::ShiftRight => Some(BinaryOp::ShiftRight),
TokenKind::And => Some(BinaryOp::LogicalAnd),
TokenKind::Or => Some(BinaryOp::LogicalOr),
TokenKind::Xor => Some(BinaryOp::LogicalXor),
TokenKind::Instanceof => Some(BinaryOp::Instanceof),
TokenKind::PipeArrow => Some(BinaryOp::Pipe),
_ => None,
}
}
fn parse_heredoc_content(text: &str) -> (&str, usize, usize, String) {
let b_prefix = if text.starts_with('b') { 1 } else { 0 };
let prefix_len = b_prefix + 3; let after_prefix = &text[prefix_len..];
let trim_len = after_prefix.len() - after_prefix.trim_start_matches([' ', '\t']).len();
let after = &after_prefix[trim_len..];
let after_start = prefix_len + trim_len;
let (label, label_consumed) = if let Some(stripped) = after.strip_prefix('\'') {
let end = stripped.find('\'').unwrap_or(stripped.len());
(&stripped[..end], 1 + end + 1)
} else if let Some(stripped) = after.strip_prefix('"') {
let end = stripped.find('"').unwrap_or(stripped.len());
(&stripped[..end], 1 + end + 1)
} else {
let end = after
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(after.len());
(&after[..end], end)
};
let rest = &after[label_consumed..];
let rest_start = after_start + label_consumed;
let newline_pos = rest.find('\n').unwrap_or(rest.len());
let body_start_in_text = rest_start + newline_pos + 1;
let body = &text[body_start_in_text..];
let mut line_start = 0;
let mut end_line_start = body.len();
let mut indent = String::new();
loop {
if line_start >= body.len() {
break;
}
let line_end = body[line_start..]
.find('\n')
.map(|p| line_start + p)
.unwrap_or(body.len());
let line = &body[line_start..line_end];
let trimmed = line.trim_start_matches([' ', '\t']);
if trimmed == label
|| (trimmed.starts_with(label)
&& trimmed[label.len()..]
.trim_start_matches([';', ',', ')'])
.trim()
.is_empty())
{
end_line_start = line_start;
let indent_len = line.len() - trimmed.len();
indent = line[..indent_len].to_string();
break;
}
line_start = if line_end < body.len() {
line_end + 1
} else {
body.len()
};
}
let content = &body[..end_line_start];
let content = content.strip_suffix('\n').unwrap_or(content);
let content = content.strip_suffix('\r').unwrap_or(content);
let body_end_in_text = body_start_in_text + content.len();
(label, body_start_in_text, body_end_in_text, indent)
}
fn parse_int_no_alloc(bytes: &[u8], base: i64) -> Option<i64> {
let mut value: i64 = 0;
for &b in bytes {
if b == b'_' {
continue;
}
let digit = match base {
16 => match b {
b'0'..=b'9' => (b - b'0') as i64,
b'a'..=b'f' => (b - b'a') as i64 + 10,
b'A'..=b'F' => (b - b'A') as i64 + 10,
_ => continue,
},
2 => match b {
b'0'..=b'1' => (b - b'0') as i64,
_ => continue,
},
8 => match b {
b'0'..=b'7' => (b - b'0') as i64,
_ => continue,
},
_ => match b {
b'0'..=b'9' => (b - b'0') as i64,
_ => continue,
},
};
value = value.checked_mul(base).and_then(|v| v.checked_add(digit))?;
}
Some(value)
}
fn parse_int_as_float(bytes: &[u8], base: f64) -> f64 {
let mut value: f64 = 0.0;
for &b in bytes {
if b == b'_' {
continue;
}
let digit: f64 = match base as u32 {
16 => match b {
b'0'..=b'9' => (b - b'0') as f64,
b'a'..=b'f' => (b - b'a') as f64 + 10.0,
b'A'..=b'F' => (b - b'A') as f64 + 10.0,
_ => continue,
},
2 => match b {
b'0'..=b'1' => (b - b'0') as f64,
_ => continue,
},
_ => match b {
b'0'..=b'7' => (b - b'0') as f64,
_ => continue,
},
};
value = value * base + digit;
}
value
}
fn parse_float_no_alloc(text: &str) -> f64 {
let mut buf = [0u8; 256];
let mut len = 0;
for &b in text.as_bytes() {
if b != b'_' && len < buf.len() {
buf[len] = b;
len += 1;
}
}
std::str::from_utf8(&buf[..len])
.ok()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0)
}