use super::lexer::{Tok, is_known_iter_method};
use super::{CmpOp, Namespace, Value, WhenError, WhenExpr};
use regex::Regex;
pub(super) struct Parser {
tokens: Vec<(Tok, usize)>,
pos: usize,
}
impl Parser {
pub(super) fn new(tokens: Vec<(Tok, usize)>) -> Self {
Self { tokens, pos: 0 }
}
}
impl Parser {
fn peek(&self) -> Option<&Tok> {
self.tokens.get(self.pos).map(|(t, _)| t)
}
fn advance(&mut self) -> Option<&(Tok, usize)> {
let p = self.pos;
self.pos += 1;
self.tokens.get(p)
}
fn pos_here(&self) -> usize {
self.tokens.get(self.pos).map_or_else(
|| self.tokens.last().map_or(0, |(_, p)| *p + 1),
|(_, p)| *p,
)
}
fn err(&self, message: impl Into<String>) -> WhenError {
WhenError::Parse {
pos: self.pos_here(),
message: message.into(),
}
}
pub(super) fn expect_eof(&mut self) -> Result<(), WhenError> {
if self.peek().is_some() {
Err(self.err("unexpected trailing token"))
} else {
Ok(())
}
}
pub(super) fn parse_expr(&mut self) -> Result<WhenExpr, WhenError> {
self.parse_or()
}
fn parse_or(&mut self) -> Result<WhenExpr, WhenError> {
let mut left = self.parse_and()?;
while matches!(self.peek(), Some(Tok::KwOr)) {
self.advance();
let right = self.parse_and()?;
left = WhenExpr::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_and(&mut self) -> Result<WhenExpr, WhenError> {
let mut left = self.parse_not()?;
while matches!(self.peek(), Some(Tok::KwAnd)) {
self.advance();
let right = self.parse_not()?;
left = WhenExpr::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_not(&mut self) -> Result<WhenExpr, WhenError> {
if matches!(self.peek(), Some(Tok::KwNot)) {
self.advance();
let inner = self.parse_cmp()?;
return Ok(WhenExpr::Not(Box::new(inner)));
}
self.parse_cmp()
}
fn parse_cmp(&mut self) -> Result<WhenExpr, WhenError> {
let left = self.parse_primary()?;
let op = match self.peek() {
Some(Tok::Eq2) => Some(CmpOp::Eq),
Some(Tok::Ne) => Some(CmpOp::Ne),
Some(Tok::Lt) => Some(CmpOp::Lt),
Some(Tok::Le) => Some(CmpOp::Le),
Some(Tok::Gt) => Some(CmpOp::Gt),
Some(Tok::Ge) => Some(CmpOp::Ge),
Some(Tok::KwIn) => Some(CmpOp::In),
_ => None,
};
if let Some(op) = op {
self.advance();
let right = self.parse_primary()?;
return Ok(WhenExpr::Cmp {
left: Box::new(left),
op,
right: Box::new(right),
});
}
if matches!(self.peek(), Some(Tok::KwMatches)) {
self.advance();
let pos = self.pos_here();
match self.advance() {
Some((Tok::Str(s), _)) => {
let pattern = Regex::new(s)
.map_err(|e| WhenError::Regex(format!("{e} (at column {pos})")))?;
return Ok(WhenExpr::Matches {
left: Box::new(left),
pattern,
});
}
_ => {
return Err(WhenError::Parse {
pos,
message: "`matches` right-hand side must be a string literal".into(),
});
}
}
}
Ok(left)
}
#[allow(clippy::too_many_lines)] fn parse_primary(&mut self) -> Result<WhenExpr, WhenError> {
let pos = self.pos_here();
match self.advance() {
Some((Tok::Bool(b), _)) => Ok(WhenExpr::Literal(Value::Bool(*b))),
Some((Tok::Null, _)) => Ok(WhenExpr::Literal(Value::Null)),
Some((Tok::Int(n), _)) => Ok(WhenExpr::Literal(Value::Int(*n))),
Some((Tok::Str(s), _)) => Ok(WhenExpr::Literal(Value::String(s.clone()))),
Some((Tok::LParen, _)) => {
let inner = self.parse_expr()?;
match self.advance() {
Some((Tok::RParen, _)) => Ok(inner),
_ => Err(WhenError::Parse {
pos,
message: "expected ')'".into(),
}),
}
}
Some((Tok::LBracket, _)) => {
let mut items = Vec::new();
if !matches!(self.peek(), Some(Tok::RBracket)) {
items.push(self.parse_expr()?);
while matches!(self.peek(), Some(Tok::Comma)) {
self.advance();
items.push(self.parse_expr()?);
}
}
match self.advance() {
Some((Tok::RBracket, _)) => Ok(WhenExpr::List(items)),
_ => Err(WhenError::Parse {
pos,
message: "expected ']'".into(),
}),
}
}
Some((Tok::Ident(name), _)) => {
let name_owned = name.clone();
let ns = match name_owned.as_str() {
"facts" => Namespace::Facts,
"vars" => Namespace::Vars,
"iter" => Namespace::Iter,
other => {
return Err(WhenError::Parse {
pos,
message: format!(
"unknown identifier {other:?}; only `facts.NAME`, \
`vars.NAME`, and `iter.NAME` are allowed"
),
});
}
};
if !matches!(self.advance(), Some((Tok::Dot, _))) {
return Err(WhenError::Parse {
pos,
message: format!("expected '.' after {name_owned:?}"),
});
}
let field_pos = self.pos_here();
let field = match self.advance() {
Some((Tok::Ident(f), _)) => f.clone(),
_ => {
return Err(WhenError::Parse {
pos: field_pos,
message: "expected identifier after '.'".into(),
});
}
};
if matches!(self.peek(), Some(Tok::LParen)) {
self.advance(); if ns != Namespace::Iter {
return Err(WhenError::Parse {
pos: field_pos,
message: format!(
"function-call syntax is only available on `iter` \
(got `{name_owned}.{field}(...)`)"
),
});
}
if !is_known_iter_method(&field) {
return Err(WhenError::Parse {
pos: field_pos,
message: format!(
"unknown iter method {field:?}; the only callable \
method on `iter` is `has_file`"
),
});
}
let mut args = Vec::new();
if !matches!(self.peek(), Some(Tok::RParen)) {
args.push(self.parse_expr()?);
while matches!(self.peek(), Some(Tok::Comma)) {
self.advance();
args.push(self.parse_expr()?);
}
}
match self.advance() {
Some((Tok::RParen, _)) => {}
_ => {
return Err(WhenError::Parse {
pos: field_pos,
message: "expected ')'".into(),
});
}
}
return Ok(WhenExpr::Call {
ns,
method: field,
args,
});
}
Ok(WhenExpr::Ident { ns, name: field })
}
_ => Err(WhenError::Parse {
pos,
message: "expected literal, identifier, '(' or '['".into(),
}),
}
}
}