use crate::ast::{Ast, Constraint, Operator, Value};
use crate::error::{ParseError, ParseErrorAt};
use crate::parsing::span::Span;
use crate::parsing::token::{Spanned, Token};
use std::string::ToString;
pub fn parse_tokens(tokens: &[Spanned], source: &str) -> Result<Ast, ParseError> {
let mut p = GrammarParser::new(tokens, source);
let ast = p.parse_or_expr()?;
if let Some(tok) = p.peek() {
return Err(p.error_at(&tok.span, format!("unexpected token {:?}", tok.token)));
}
Ok(ast)
}
struct GrammarParser<'a> {
tokens: &'a [Spanned],
pos: usize,
source: &'a str,
}
impl<'a> GrammarParser<'a> {
fn new(tokens: &'a [Spanned], source: &'a str) -> Self {
GrammarParser {
tokens,
pos: 0,
source,
}
}
fn peek(&self) -> Option<&Spanned> {
self.tokens.get(self.pos)
}
fn advance(&mut self) -> Option<&Spanned> {
let t = self.tokens.get(self.pos)?;
self.pos += 1;
Some(t)
}
fn peek_is_and_sep(&self) -> bool {
match self.peek().map(|t| &t.token) {
Some(Token::Semi) => true,
Some(Token::Word(w)) if w == "and" || w == "AND" => true,
_ => false,
}
}
fn peek_is_or_sep(&self) -> bool {
match self.peek().map(|t| &t.token) {
Some(Token::Comma) => true,
Some(Token::Word(w)) if w == "or" || w == "OR" => true,
_ => false,
}
}
fn error_at(&self, span: &Span, message: String) -> ParseError {
let (line, col) = span.line_col(self.source);
ParseError::At(ParseErrorAt {
line,
col,
span_len: span.len().max(1),
snippet: span.source_line(self.source).to_string(),
message,
})
}
fn error_eof(&self, message: &str) -> ParseError {
let pos = self.source.len();
self.error_at(&Span::new(pos, pos), message.to_string())
}
fn parse_or_expr(&mut self) -> Result<Ast, ParseError> {
let first = self.parse_and_expr()?;
let mut children = vec![first];
while self.peek_is_or_sep() {
self.advance(); children.push(self.parse_and_expr()?);
}
if children.len() == 1 {
Ok(children.into_iter().next().unwrap())
} else {
Ok(Ast::Or(children))
}
}
fn parse_and_expr(&mut self) -> Result<Ast, ParseError> {
let first = self.parse_atom()?;
let mut children = vec![first];
while self.peek_is_and_sep() {
self.advance(); children.push(self.parse_atom()?);
}
if children.len() == 1 {
Ok(children.into_iter().next().unwrap())
} else {
Ok(Ast::And(children))
}
}
fn parse_atom(&mut self) -> Result<Ast, ParseError> {
match self.peek() {
Some(t) if t.token == Token::LParen => {
self.advance(); let inner = self.parse_or_expr()?;
match self.peek() {
Some(t) if t.token == Token::RParen => {
self.advance();
}
Some(t) => {
let span = t.span;
return Err(self.error_at(&span, "expected ')'".to_string()));
}
None => {
return Err(
self.error_eof("expected ')' to close parenthesized expression")
);
}
}
Ok(inner)
}
_ => self.parse_constraint(),
}
}
fn parse_constraint(&mut self) -> Result<Ast, ParseError> {
let field = self.expect_selector()?;
let op = self.expect_op()?;
let argument = self.parse_argument()?;
Ok(Ast::Constraint(Constraint {
field,
operator: op,
value: argument,
}))
}
fn expect_selector(&mut self) -> Result<String, ParseError> {
match self.peek() {
Some(t) => match &t.token {
Token::Word(w) => {
let w = w.clone();
self.advance();
Ok(w)
}
other => {
let span = t.span;
Err(self.error_at(&span, format!("expected a field name, got {:?}", other)))
}
},
None => Err(self.error_eof("expected a field name")),
}
}
fn expect_op(&mut self) -> Result<Operator, ParseError> {
match self.peek() {
Some(t) => match &t.token {
Token::Op(op) => {
let op = op.clone();
self.advance();
Ok(op)
}
other => {
let span = t.span;
Err(self.error_at(&span, format!("expected an operator, got {:?}", other)))
}
},
None => Err(self.error_eof("expected an operator")),
}
}
fn parse_argument(&mut self) -> Result<Value, ParseError> {
match self.peek() {
Some(t) if t.token == Token::LParen => {
self.advance(); let first = self.parse_value()?;
let mut values = vec![first];
while matches!(self.peek().map(|t| &t.token), Some(Token::Comma)) {
self.advance(); values.push(self.parse_value()?);
}
match self.peek() {
Some(t) if t.token == Token::RParen => {
self.advance();
}
Some(t) => {
let span = t.span;
return Err(
self.error_at(&span, "expected ')' to close argument list".to_string())
);
}
None => return Err(self.error_eof("expected ')' to close argument list")),
}
Ok(Value::List(values))
}
_ => Ok(self.parse_value()?),
}
}
fn parse_value(&mut self) -> Result<Value, ParseError> {
match self.peek() {
Some(t) => match &t.token {
Token::Null => {
self.advance();
Ok(Value::Null)
}
Token::Bool(b) => {
let b = *b;
self.advance();
Ok(Value::Bool(b))
}
Token::Integer(i) => {
let i = *i;
self.advance();
Ok(Value::Int(i))
}
Token::Float(f) => {
let f = *f;
self.advance();
Ok(Value::Float(f))
}
Token::QuotedStr(s) => {
let s = s.clone();
self.advance();
Ok(Value::String(s))
}
Token::Date(s) => {
let s = s.clone();
self.advance();
Ok(Value::Date(s))
}
Token::DateTime(s) => {
let s = s.clone();
self.advance();
Ok(Value::DateTime(s))
}
Token::Word(w) => {
let w = w.clone();
self.advance();
Ok(Value::String(w))
}
other => {
let span = t.span;
Err(self.error_at(&span, format!("expected a value, got {:?}", other)))
}
},
None => Err(self.error_eof("expected a value")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::lexer::tokenize;
fn parse(source: &str) -> Result<Ast, ParseError> {
let tokens = tokenize(source).unwrap();
parse_tokens(&tokens, source)
}
#[test]
fn simple_eq() {
let ast = parse("name==Alice").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "name".into(),
operator: Operator::Eq,
value: Value::String("Alice".into()),
})
);
}
#[test]
fn and_two_constraints() {
let ast = parse("a==1;b>2").unwrap();
assert!(matches!(ast, Ast::And(ref v) if v.len() == 2));
let ast = parse("a==1 and b>2").unwrap();
assert!(matches!(ast, Ast::And(ref v) if v.len() == 2));
let ast = parse("a==1 AND b>2").unwrap();
assert!(matches!(ast, Ast::And(ref v) if v.len() == 2));
}
#[test]
fn or_two_constraints() {
let ast = parse("a==1,b==2").unwrap();
assert!(matches!(ast, Ast::Or(ref v) if v.len() == 2));
let ast = parse("a==1 or b==2").unwrap();
assert!(matches!(ast, Ast::Or(ref v) if v.len() == 2));
let ast = parse("a==1 OR b==2").unwrap();
assert!(matches!(ast, Ast::Or(ref v) if v.len() == 2));
}
#[test]
fn in_list() {
let ast = parse("role=in=(admin,user)").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "role".into(),
operator: Operator::In,
value: Value::List(vec![
Value::String("admin".into()),
Value::String("user".into()),
]),
})
);
}
#[test]
fn between_two_numbers() {
let ast = parse("age=between=(18,65)").unwrap();
assert!(matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Between,
value: Value::List(_),
..
})
));
}
#[test]
fn nested_parens() {
let ast = parse("(a==1,b==2);c==3").unwrap();
if let Ast::And(children) = ast {
assert!(matches!(children[0], Ast::Or(_)));
} else {
panic!("expected And at top level");
}
}
#[test]
fn quoted_string_value() {
let ast = parse(r#"name=="John Doe""#).unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "name".into(),
operator: Operator::Eq,
value: Value::String("John Doe".into()),
})
);
}
#[test]
fn bool_value() {
let ast = parse("active==true").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "active".into(),
operator: Operator::Eq,
value: Value::Bool(true),
})
);
}
#[test]
fn number_value() {
let ast = parse("price==42.5").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "price".into(),
operator: Operator::Eq,
value: Value::Float(42.5),
})
);
}
#[test]
fn error_missing_operator() {
let err = parse("name");
assert!(matches!(err, Err(ParseError::At(_))));
}
#[test]
fn error_unexpected_trailing_token() {
let err = parse("name==Alice)");
assert!(matches!(err, Err(ParseError::At(_))));
}
#[test]
fn error_missing_closing_paren() {
let err = parse("(a==1;b==2");
assert!(matches!(err, Err(ParseError::At(_))));
}
#[test]
fn error_message_contains_snippet() {
let err = parse("name").unwrap_err();
if let ParseError::At(e) = err {
assert!(!e.snippet.is_empty());
assert!(e.col >= 1);
} else {
panic!("expected ParseError::At");
}
}
#[test]
fn null_keyword_as_scalar_argument() {
let ast = parse("name==null").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "name".into(),
operator: Operator::Eq,
value: Value::Null,
})
);
}
#[test]
fn null_keyword_inside_list() {
let ast = parse("name=in=(null,foo)").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "name".into(),
operator: Operator::In,
value: Value::List(vec![Value::Null, Value::String("foo".into())]),
})
);
}
#[test]
fn empty_input() {
assert!(matches!(parse(""), Err(ParseError::At(_))));
}
#[test]
fn whitespace_only() {
assert!(matches!(parse(" "), Err(ParseError::At(_))));
}
#[test]
fn missing_value_after_operator() {
assert!(matches!(parse("name=="), Err(ParseError::At(_))));
}
#[test]
fn empty_argument_list() {
assert!(matches!(parse("name=in=()"), Err(ParseError::At(_))));
}
#[test]
fn double_comma_in_list() {
assert!(matches!(parse("name=in=(a,,b)"), Err(ParseError::At(_))));
}
#[test]
fn trailing_and_separator() {
assert!(matches!(parse("a==1;"), Err(ParseError::At(_))));
}
#[test]
fn trailing_or_separator() {
assert!(matches!(parse("a==1,"), Err(ParseError::At(_))));
}
#[test]
fn operator_where_field_expected() {
assert!(matches!(parse("==foo"), Err(ParseError::At(_))));
}
#[test]
fn word_where_operator_expected() {
assert!(matches!(parse("name foo"), Err(ParseError::At(_))));
}
#[test]
fn unclosed_nested_paren() {
assert!(matches!(parse("((a==1)"), Err(ParseError::At(_))));
}
#[test]
fn non_rparen_token_after_subexpr() {
assert!(matches!(parse("(a==1 b==2"), Err(ParseError::At(_))));
}
#[test]
fn non_rparen_token_closes_argument_list() {
assert!(matches!(parse("name=in=(a;b)"), Err(ParseError::At(_))));
}
#[test]
fn eof_inside_argument_list() {
assert!(matches!(parse("name=in=(a,b"), Err(ParseError::At(_))));
}
#[test]
fn error_col_at_eof() {
let err = parse("age==").unwrap_err();
if let ParseError::At(e) = err {
assert_eq!(e.line, 1);
assert_eq!(e.col, 6);
} else {
panic!("expected ParseError::At");
}
}
#[test]
fn unicode_field_name_and_bare_value() {
let ast = parse("prénom==Élodie").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "prénom".into(),
operator: Operator::Eq,
value: Value::String("Élodie".into()),
})
);
}
#[test]
fn cjk_in_quoted_value() {
let ast = parse(r#"description=="日本語テスト""#).unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "description".into(),
operator: Operator::Eq,
value: Value::String("日本語テスト".into()),
})
);
}
#[test]
fn emoji_in_quoted_list() {
let ast = parse(r#"tag=in=("🚀","🌟","💡")"#).unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "tag".into(),
operator: Operator::In,
value: Value::List(vec![
Value::String("🚀".into()),
Value::String("🌟".into()),
Value::String("💡".into()),
]),
})
);
}
#[test]
fn unicode_in_and_expr() {
let ast = parse("prénom==André;ville==Montréal").unwrap();
assert!(matches!(ast, Ast::And(ref v) if v.len() == 2));
}
#[test]
fn error_snippet_contains_unicode_line() {
let err = parse("日本語").unwrap_err();
if let ParseError::At(e) = err {
assert_eq!(e.snippet, "日本語");
} else {
panic!("expected ParseError::At");
}
}
#[test]
fn date_scalar_argument() {
let ast = parse("created_at==2024-01-15").unwrap();
assert!(matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Eq,
value: Value::Date(_),
..
})
));
}
#[test]
fn datetime_scalar_argument() {
let ast = parse("created_at==2024-01-15T10:30:00Z").unwrap();
assert!(matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Eq,
value: Value::DateTime(_),
..
})
));
}
#[test]
fn date_in_between_list() {
let ast = parse("created_at=between=(2024-01-01,2024-12-31)").unwrap();
assert!(matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Between,
value: Value::List(_),
..
})
));
if let Ast::Constraint(c) = ast
&& let Value::List(values) = c.value
{
assert!(matches!(values[0], Value::Date(_)));
assert!(matches!(values[1], Value::Date(_)));
}
}
#[test]
fn date_comparison_operators() {
let ast = parse("start_date>=2024-01-01").unwrap();
assert!(matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Gte,
..
})
));
let ast = parse("end_date<2024-12-31").unwrap();
assert!(matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Lt,
..
})
));
}
}