use std::collections::{HashMap, HashSet};
use crate::ast::{
BinaryOp, Consumer, CountCmp, Expr, Follow, LogicalOp, OpNode, OrderSpec, Query, RelOp,
SelectItem, Subquery, UnaryOp, Where,
};
use crate::errors::{OqxError, Result};
use crate::lexer::{TokType, Token, lex_string, lex_template};
use crate::value::Value;
const CONSUMERS: [&str; 6] = ["collect", "exists", "none", "count", "first", "single"];
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Clause {
Select,
From,
Where,
Follow,
OrderBy,
Limit,
Offset,
}
const CLAUSE_ORDER: [Clause; 7] = [
Clause::Select,
Clause::From,
Clause::Where,
Clause::Follow,
Clause::OrderBy,
Clause::Limit,
Clause::Offset,
];
impl Clause {
fn as_str(self) -> &'static str {
match self {
Clause::Select => "select",
Clause::From => "from",
Clause::Where => "where",
Clause::Follow => "follow",
Clause::OrderBy => "order by",
Clause::Limit => "limit",
Clause::Offset => "offset",
}
}
}
fn clause_order_sentence() -> String {
CLAUSE_ORDER
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(", ")
}
const CLAUSE_WORDS: [&str; 18] = [
"collect", "exists", "none", "count", "first", "single", "order", "by", "asc", "desc",
"follow", "distinct", "frontier", "depth", "in", "values", "limit", "offset",
];
const RELOPS: [&str; 6] = ["==", "!=", "<", "<=", ">", ">="];
const CMP_OPS: [&str; 6] = ["==", "!=", "<", "<=", ">", ">="];
const ADD_OPS: [&str; 2] = ["+", "-"];
const MUL_OPS: [&str; 3] = ["*", "/", "%"];
const LITERAL_WORDS: [&str; 3] = ["true", "false", "null"];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BodyCtx {
Top,
Block {
op: Consumer,
lifts_allowed: bool,
},
}
impl BodyCtx {
fn lifts_allowed(self) -> bool {
matches!(
self,
BodyCtx::Block {
lifts_allowed: true,
..
}
)
}
}
pub fn parse_template<S: AsRef<str>>(fragments: &[S], values: usize) -> Result<Query> {
Parser::new(lex_template(fragments, values)?).parse_query()
}
pub fn parse_string(src: &str) -> Result<Query> {
Parser::new(lex_string(src)?).parse_query()
}
fn number_value(text: &str) -> f64 {
text.parse().unwrap_or(f64::NAN)
}
fn is_integer(v: f64) -> bool {
v.is_finite() && v.fract() == 0.0
}
#[derive(Default)]
struct BodyClauses {
froms: Vec<Expr>,
r#where: Option<Where>,
select: Vec<SelectItem>,
order_by: Option<Vec<OrderSpec>>,
follow: Option<Follow>,
distinct: bool,
values: bool,
limit: Option<Expr>,
offset: Option<Expr>,
}
struct Parser {
tokens: Vec<Token>,
pos: usize,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
debug_assert!(tokens.last().is_some_and(|t| t.kind == TokType::Eof));
Self { tokens, pos: 0 }
}
fn peek(&self) -> &Token {
&self.tokens[self.pos.min(self.tokens.len() - 1)]
}
fn peek_at(&self, n: usize) -> Option<&Token> {
self.tokens.get(self.pos + n)
}
fn next(&mut self) -> Token {
let t = self.peek().clone();
self.pos += 1;
t
}
fn at(&self, kind: TokType) -> bool {
self.peek().kind == kind
}
fn at_word(&self, kind: TokType, value: &str) -> bool {
let t = self.peek();
t.kind == kind && t.value == value
}
fn at_op(&self, value: &str) -> bool {
self.at_word(TokType::Op, value)
}
fn fail<T>(&self, msg: impl AsRef<str>) -> Result<T> {
Err(OqxError::parse(format!(
"{} (at offset {})",
msg.as_ref(),
self.peek().pos
)))
}
fn parse_query(&mut self) -> Result<Query> {
if let Some(directive) = self.try_op(false)? {
if self.at(TokType::Eof) {
let sub = directive.sub;
return Ok(Query {
source: directive.receiver,
from: sub.from,
r#where: sub.r#where,
select: sub.select,
order_by: sub.order_by,
consumer: directive.op,
follow: sub.follow,
distinct: directive.distinct,
values: sub.values,
limit: sub.limit,
offset: sub.offset,
});
}
return self.fail(format!(
"unexpected {} after the top-level directive",
self.tok_desc()
));
}
let mut body = self.parse_body(BodyCtx::Top)?;
if !self.at(TokType::Eof) {
return self.fail(format!(
"unexpected {} after the query — nothing may follow the last clause",
self.tok_desc()
));
}
if body.froms.is_empty() {
return self.fail(
"a query must name its source with `from <collection>` (or be `<collection> <consumer> { … }`)",
);
}
let source = body.froms.remove(0);
Ok(Query {
source,
from: body.froms,
r#where: body.r#where,
select: body.select,
order_by: body.order_by,
consumer: Consumer::Collect,
follow: body.follow,
distinct: body.distinct,
values: body.values,
limit: body.limit,
offset: body.offset,
})
}
fn tok_desc(&self) -> String {
let t = self.peek();
if t.kind == TokType::Eof {
"end of query".to_string()
} else if t.value.is_empty() {
format!("'{}'", t.kind.as_str())
} else {
format!("'{}'", t.value)
}
}
fn parse_body(&mut self, ctx: BodyCtx) -> Result<BodyClauses> {
let mut body = BodyClauses::default();
let mut stage: Option<Clause> = None;
while !self.at(TokType::Eof) && !self.at(TokType::RBrace) {
if self.at_word(TokType::Kw, "select") {
self.enter(&mut stage, Clause::Select)?;
self.next();
if self.at_word(TokType::Ident, "distinct") {
self.next();
body.distinct = true;
}
let (items, values) = self.parse_projection(ctx)?;
body.select = items;
body.values = values;
continue;
}
if self.at_word(TokType::Kw, "from") {
self.enter(&mut stage, Clause::From)?;
self.next();
let e = self.parse_value_expr()?;
body.froms.push(e);
continue;
}
if self.at_word(TokType::Kw, "where") {
self.enter(&mut stage, Clause::Where)?;
self.next();
body.r#where = Some(self.parse_where()?);
continue;
}
if self.at_follow() {
self.enter(&mut stage, Clause::Follow)?;
body.follow = Some(self.parse_follow()?);
continue;
}
if self.at_order_by() {
self.enter(&mut stage, Clause::OrderBy)?;
self.next(); self.next(); body.order_by = Some(self.parse_order_specs()?);
continue;
}
if self.at_bound() {
let is_limit = self.peek().value == "limit";
let clause = if is_limit {
Clause::Limit
} else {
Clause::Offset
};
self.enter(&mut stage, clause)?;
self.next();
if ctx == BodyCtx::Top && self.at(TokType::Caret) {
return self.fail(format!(
"`{} ^…` at the top level has no enclosing scope — a top-level bound is a number literal or a binding; inside a block `^name` reads the enclosing row",
clause.as_str()
));
}
let e = self.parse_postfix(None)?;
if is_limit {
body.limit = Some(e);
} else {
body.offset = Some(e);
}
continue;
}
if stage.is_none()
&& (self.at(TokType::Ident)
|| self.at(TokType::Binding)
|| self.at(TokType::Caret)
|| self.can_start_value())
{
self.enter(&mut stage, Clause::Select)?;
let (items, values) = self.parse_projection(ctx)?;
body.select = items;
body.values = values;
continue;
}
return self.fail_unexpected_in_body(stage, ctx);
}
body.r#where = self.inline_aliases(&body.select, body.r#where.take())?;
Ok(body)
}
fn enter(&self, stage: &mut Option<Clause>, clause: Clause) -> Result<()> {
if *stage == Some(clause) {
return self.fail(format!("duplicate `{}` clause", clause.as_str()));
}
if stage.is_some_and(|last| clause < last) {
let last = stage.expect("checked above");
return self.fail(format!(
"`{}` must come before `{}` — OQX clause order is {}",
clause.as_str(),
last.as_str(),
clause_order_sentence()
));
}
*stage = Some(clause);
Ok(())
}
fn fail_unexpected_in_body<T>(&self, stage: Option<Clause>, ctx: BodyCtx) -> Result<T> {
let t = self.peek();
let first_remaining = stage.map_or(0, |s| s as usize + 1);
let remaining = CLAUSE_ORDER[first_remaining..]
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join("/");
let consumer_word = t.kind == TokType::Ident && CONSUMERS.contains(&t.value.as_str());
let v = &t.value;
if let Some(last) = stage {
let last = last.as_str();
let is_word = matches!(
t.kind,
TokType::Ident | TokType::Kw | TokType::Binding | TokType::Caret
);
if !is_word && t.kind != TokType::Comma {
return match ctx {
BodyCtx::Top => self.fail(format!(
"unexpected {} after the query — nothing may follow the last clause (expected {remaining} or the end of the query)",
self.tok_desc()
)),
BodyCtx::Block { op, .. } => self.fail(format!(
"unexpected {} in the {} {{ … }} block — expected {remaining} or '}}' to close the block",
self.tok_desc(),
op.as_str()
)),
};
}
if t.kind == TokType::Ident {
match v.as_str() {
"order" => {
return self.fail(format!(
"unexpected 'order' after `{last}` — an ordering is written `order by <expr> [asc|desc]`"
));
}
"follow" => {
return self.fail(format!(
"unexpected 'follow' after `{last}` — `follow` needs a relation: `follow <relation>` or `follow distinct <relation>`"
));
}
"limit" | "offset" => {
return self.fail(format!(
"unexpected '{v}' after `{last}` — a bound is a non-negative number literal, a binding, or (inside a block) an outer reference `^name`"
));
}
_ => {}
}
}
}
match stage {
Some(Clause::From) if consumer_word => self.fail(format!(
"unexpected `{v}` after `from` — a whole-query consumer is written `<collection> {v} {{ … }}`; to project a field named {v} write `select {v} from …`; a predicate needs `where`"
)),
Some(Clause::From) => self.fail(format!(
"unexpected {} after `from` — a predicate needs `where` (there is no implicit where), and a projection goes before `from` (`select … from …`); expected {remaining}",
self.tok_desc()
)),
Some(Clause::Select) if consumer_word => self.fail(format!(
"unexpected `{v}` after a projection — a consumer test is a predicate: write `where <relation> {v} {{ … }}` — a predicate is never implicit; a nested block in a projection needs a name (`name: <relation> collect {{ … }}`)"
)),
None => self.fail(format!(
"unexpected {} — expected a projection or {remaining}",
self.tok_desc()
)),
Some(last) => self.fail(format!(
"unexpected {} after `{}` — expected {remaining}",
self.tok_desc(),
last.as_str()
)),
}
}
fn at_bound(&self) -> bool {
let t = self.peek();
if t.kind != TokType::Ident || (t.value != "limit" && t.value != "offset") {
return false;
}
self.peek_at(1).is_some_and(|nx| {
matches!(nx.kind, TokType::Number | TokType::Binding | TokType::Caret)
})
}
fn inline_aliases(
&self,
select: &[SelectItem],
r#where: Option<Where>,
) -> Result<Option<Where>> {
let Some(w) = r#where else { return Ok(None) };
if select.is_empty() {
return Ok(Some(w));
}
let mut aliases: HashMap<&str, &SelectItem> = HashMap::new();
for it in select {
match it {
SelectItem::Collect { name, .. } => {
aliases.insert(name, it);
}
SelectItem::Field { name, lift: 0, .. } if !name.is_empty() => {
aliases.insert(name, it);
}
SelectItem::Field { .. } => {}
}
}
if aliases.is_empty() {
return Ok(Some(w));
}
let mut resolving: Vec<String> = Vec::new();
self.walk_where(&aliases, &mut resolving, w).map(Some)
}
fn walk_where(
&self,
aliases: &HashMap<&str, &SelectItem>,
resolving: &mut Vec<String>,
w: Where,
) -> Result<Where> {
Ok(match w {
Where::And { parts } => Where::And {
parts: parts
.into_iter()
.map(|p| self.walk_where(aliases, resolving, p))
.collect::<Result<_>>()?,
},
Where::Or { parts } => Where::Or {
parts: parts
.into_iter()
.map(|p| self.walk_where(aliases, resolving, p))
.collect::<Result<_>>()?,
},
Where::Not { expr } => Where::Not {
expr: Box::new(self.walk_where(aliases, resolving, *expr)?),
},
Where::Scalar { expr } => {
if let Expr::Ident { name } = &expr {
if let Some(SelectItem::Collect { op, .. }) = aliases.get(name.as_str()) {
return Ok(Where::Op(op.clone()));
}
}
Where::Scalar {
expr: self.subst(aliases, resolving, expr)?,
}
}
Where::Op(op) => {
let mut op = *op;
op.receiver = self.subst(aliases, resolving, op.receiver)?;
Where::Op(Box::new(op))
}
})
}
fn subst(
&self,
aliases: &HashMap<&str, &SelectItem>,
resolving: &mut Vec<String>,
e: Expr,
) -> Result<Expr> {
let subst_box =
|this: &Self, resolving: &mut Vec<String>, b: Box<Expr>| -> Result<Box<Expr>> {
this.subst(aliases, resolving, *b).map(Box::new)
};
Ok(match e {
Expr::Ident { name } => {
let Some(a) = aliases.get(name.as_str()) else {
return Ok(Expr::Ident { name });
};
if resolving.last() == Some(&name) {
return Ok(Expr::Ident { name });
}
if let Some(i) = resolving.iter().position(|r| *r == name) {
let cycle = resolving[i..]
.iter()
.map(String::as_str)
.chain(std::iter::once(name.as_str()))
.collect::<Vec<_>>()
.join(" → ");
return self.fail(format!(
"select aliases form a cycle: {cycle} — an alias used in `where` cannot depend on itself"
));
}
match a {
SelectItem::Collect { op, .. } => {
return self.fail(format!(
"select alias '{name}' is a {} {{ … }} block — in `where` it can only stand alone as a non-empty test, not inside an expression",
op.op.as_str()
));
}
SelectItem::Field { expr, .. } => {
resolving.push(name);
let out = self.subst(aliases, resolving, expr.clone())?;
resolving.pop();
out
}
}
}
Expr::Member { recv, name } => Expr::Member {
recv: subst_box(self, resolving, recv)?,
name,
},
Expr::Index { recv, index } => Expr::Index {
recv: subst_box(self, resolving, recv)?,
index: subst_box(self, resolving, index)?,
},
Expr::Call { recv, name, args } => Expr::Call {
recv: match recv {
Some(r) => Some(subst_box(self, resolving, r)?),
None => None,
},
name,
args: args
.into_iter()
.map(|a| self.subst(aliases, resolving, a))
.collect::<Result<_>>()?,
},
Expr::Unary { op, expr } => Expr::Unary {
op,
expr: subst_box(self, resolving, expr)?,
},
Expr::Binary { op, left, right } => Expr::Binary {
op,
left: subst_box(self, resolving, left)?,
right: subst_box(self, resolving, right)?,
},
Expr::Logical { op, left, right } => Expr::Logical {
op,
left: subst_box(self, resolving, left)?,
right: subst_box(self, resolving, right)?,
},
Expr::In { left, right } => Expr::In {
left: subst_box(self, resolving, left)?,
right: subst_box(self, resolving, right)?,
},
Expr::Range {
lo,
hi,
exclusive_end,
} => Expr::Range {
lo: match lo {
Some(b) => Some(subst_box(self, resolving, b)?),
None => None,
},
hi: match hi {
Some(b) => Some(subst_box(self, resolving, b)?),
None => None,
},
exclusive_end,
},
other @ (Expr::Lit(_) | Expr::Binding { .. } | Expr::Outer { .. }) => other,
})
}
fn at_order_by(&self) -> bool {
self.at_word(TokType::Ident, "order")
&& self
.peek_at(1)
.is_some_and(|nx| nx.kind == TokType::Ident && nx.value == "by")
}
fn at_follow(&self) -> bool {
self.at_word(TokType::Ident, "follow")
&& self.peek_at(1).is_some_and(|nx| {
matches!(nx.kind, TokType::Ident | TokType::Binding | TokType::Caret)
})
}
fn parse_follow(&mut self) -> Result<Follow> {
self.next(); let mut distinct = false;
if self.at_word(TokType::Ident, "distinct") {
self.next();
distinct = true;
if !matches!(
self.peek().kind,
TokType::Ident | TokType::Binding | TokType::Caret
) {
return self.fail(
"expected a relation after `follow distinct` (`follow distinct <relation>`)",
);
}
}
if self.at(TokType::Caret) {
return self.fail(
"`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there",
);
}
let receiver = self.parse_receiver()?;
let mut follow = Follow {
receiver,
distinct,
r#where: None,
frontier: None,
depth: None,
by: None,
};
if !self.at(TokType::LBrace) {
return Ok(follow);
}
self.next(); while !self.at(TokType::Eof) && !self.at(TokType::RBrace) {
if self.at_word(TokType::Kw, "where") {
if follow.r#where.is_some() {
return self.fail("duplicate `where` in follow clause");
}
self.next();
follow.r#where = Some(self.parse_value_expr()?);
} else if self.at_word(TokType::Ident, "frontier") {
if follow.frontier.is_some() {
return self.fail("duplicate `frontier` in follow clause");
}
self.next();
follow.frontier = Some(self.parse_value_expr()?);
} else if self.at_word(TokType::Ident, "by") {
if follow.by.is_some() {
return self.fail("duplicate `by` in follow clause");
}
self.next();
follow.by = Some(self.parse_value_expr()?);
} else if self.at_word(TokType::Ident, "depth") {
if follow.depth.is_some() {
return self.fail("duplicate `depth` in follow clause");
}
self.next();
if !self.at(TokType::Number) {
return self.fail("expected an integer after `depth`");
}
let v = number_value(&self.next().value);
if !is_integer(v) || !(1.0..=8.0).contains(&v) {
return self.fail("follow depth must be an integer between 1 and 8");
}
follow.depth = Some(v as u32);
} else {
return self.fail(format!(
"unexpected {} in follow block — expected where/frontier/depth/by",
self.tok_desc()
));
}
}
if !self.at(TokType::RBrace) {
return self.fail("expected '}' to close the follow block");
}
self.next();
Ok(follow)
}
fn parse_receiver(&mut self) -> Result<Expr> {
if self.at(TokType::Binding) {
return Ok(Expr::Binding {
index: binding_index(&self.next()),
});
}
let levels = self.parse_carets();
if !self.at(TokType::Ident) {
return self.fail("expected a collection navigation (a property/relation name)");
}
if LITERAL_WORDS.contains(&self.peek().value.as_str()) {
return self.fail(format!(
"`{}` is a literal, not a collection",
self.peek().value
));
}
let head = self.next();
Ok(self.parse_nav_from(head, levels)?.0)
}
fn parse_carets(&mut self) -> usize {
let mut levels = 0;
while self.at(TokType::Caret) {
self.next();
levels += 1;
}
levels
}
fn parse_nav_from(&mut self, head: Token, levels: usize) -> Result<(Expr, String)> {
let mut expr = if levels > 0 {
Expr::Outer {
levels,
name: head.value.clone(),
}
} else {
Expr::Ident {
name: head.value.clone(),
}
};
let mut name = head.value;
if levels == 0 && self.at(TokType::LParen) {
expr = Expr::Call {
recv: None,
name: name.clone(),
args: self.parse_args()?,
};
}
while self.at(TokType::Dot) {
self.next();
if !self.at(TokType::Ident) {
return self.fail("expected a property name after '.'");
}
name = self.next().value;
expr = Expr::Member {
recv: Box::new(expr),
name: name.clone(),
};
}
Ok((expr, name))
}
fn parse_projection(&mut self, ctx: BodyCtx) -> Result<(Vec<SelectItem>, bool)> {
let mut items = vec![self.parse_select_item(ctx)?];
while self.at(TokType::Comma) {
self.next();
items.push(self.parse_select_item(ctx)?);
}
let mut values = false;
if self.at_word(TokType::Ident, "values") {
self.next();
values = true;
if items.len() != 1 {
return self.fail(format!(
"`values` projects exactly one expression (got {})",
items.len()
));
}
if matches!(&items[0], SelectItem::Field { lift, .. } if *lift > 0) {
return self.fail("a lift (^name: …) cannot be combined with `values`");
}
} else {
let mut seen: HashSet<String> = HashSet::new();
for it in &items {
if it.name().is_empty() {
return self.fail(
"a leading expression is a projection (select): an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `where …` — a predicate is never implicit",
);
}
let lift = match it {
SelectItem::Field { lift, .. } => *lift,
SelectItem::Collect { .. } => 0,
};
let key = format!("{}{}", "^".repeat(lift), it.name());
if !seen.insert(key) {
return self.fail(format!(
"duplicate projection name '{}' — each projected item needs its own name (alias one: `other: expr`)",
it.name()
));
}
}
}
Ok((items, values))
}
fn parse_select_item(&mut self, ctx: BodyCtx) -> Result<SelectItem> {
let lift = self.parse_carets();
if !self.at(TokType::Ident) && !self.can_start_value() {
return self.fail("expected a projection name");
}
if lift > 0 && !ctx.lifts_allowed() {
let what = if self.at(TokType::Ident) {
format!("^{}", self.peek().value)
} else {
"^name".to_string()
};
return self.fail(format!(
"a lift ({what}) binds a value into the enclosing row and is only valid in a `collect {{ … }}` in where position (`where <relation> collect {{ {what}: … }}`)"
));
}
if self.at(TokType::Ident) && self.peek_at(1).is_some_and(|t| t.kind == TokType::Colon) {
let name = self.next().value;
self.next(); if let Some(op) = self.try_op(false)? {
if !matches!(
op.op,
Consumer::Collect | Consumer::First | Consumer::Single
) {
return self.fail(format!(
"projection '{name}' must use collect/first/single, not {} (exists/none/count are where-position tests)",
op.op.as_str()
));
}
if lift > 0 {
return self.fail(format!(
"a lift (^{name}) value must be a scalar expression, not {} {{ … }}",
op.op.as_str()
));
}
return Ok(SelectItem::Collect {
name,
op: Box::new(op),
});
}
let expr = self.parse_value_expr()?;
return Ok(SelectItem::Field { name, expr, lift });
}
let expr = self.parse_value_expr()?;
let name = nav_key(&expr).unwrap_or("").to_string();
Ok(SelectItem::Field { name, expr, lift })
}
fn parse_order_specs(&mut self) -> Result<Vec<OrderSpec>> {
let mut specs = vec![self.parse_order_spec()?];
while self.at(TokType::Comma) {
self.next();
specs.push(self.parse_order_spec()?);
}
Ok(specs)
}
fn parse_order_spec(&mut self) -> Result<OrderSpec> {
let expr = self.parse_value_expr()?;
let mut desc = false;
if self.at_word(TokType::Ident, "asc") {
self.next();
} else if self.at_word(TokType::Ident, "desc") {
self.next();
desc = true;
}
Ok(OrderSpec { expr, desc })
}
fn parse_where(&mut self) -> Result<Where> {
self.parse_where_or()
}
fn parse_where_or(&mut self) -> Result<Where> {
let left = self.parse_where_and()?;
if !self.at_op("||") {
return Ok(left);
}
let mut parts = vec![left];
while self.at_op("||") {
self.next();
parts.push(self.parse_where_and()?);
}
Ok(Where::Or { parts })
}
fn parse_where_and(&mut self) -> Result<Where> {
let left = self.parse_where_primary()?;
if !self.at_op("&&") {
return Ok(left);
}
let mut parts = vec![left];
while self.at_op("&&") {
self.next();
parts.push(self.parse_where_primary()?);
}
Ok(Where::And { parts })
}
fn parse_where_primary(&mut self) -> Result<Where> {
let start = self.pos;
let mut nots = 0;
while self.at_op("!") {
self.next();
nots += 1;
}
if self.at(TokType::LParen) {
self.next();
let inner = self.parse_where()?;
if !self.at(TokType::RParen) {
return self.fail("expected ')' to close a grouped where expression");
}
self.next();
if self.at_scalar_continuation() {
let group = self.where_to_expr(inner)?;
let mut e = self.parse_postfix(Some(group))?;
for _ in 0..nots {
e = Expr::Unary {
op: UnaryOp::Not,
expr: Box::new(e),
};
}
let expr = self.parse_cmp(Some(e))?;
return Ok(Where::Scalar { expr });
}
return Ok(wrap_not(inner, nots));
}
if let Some(op) = self.try_op(true)? {
let op = self.finish_where_op(op)?;
return Ok(wrap_not(Where::Op(Box::new(op)), nots));
}
self.pos = start;
let expr = self.parse_cmp(None)?;
Ok(scalar_leaf(expr))
}
fn at_scalar_continuation(&self) -> bool {
let t = self.peek();
match t.kind {
TokType::Op => {
let v = t.value.as_str();
CMP_OPS.contains(&v) || ADD_OPS.contains(&v) || MUL_OPS.contains(&v)
}
TokType::Dot | TokType::Range => true,
TokType::Ident => t.value == "in",
_ => false,
}
}
fn where_to_expr(&self, w: Where) -> Result<Expr> {
Ok(match w {
Where::Scalar { expr } => expr,
Where::Not { expr } => Expr::Unary {
op: UnaryOp::Not,
expr: Box::new(self.where_to_expr(*expr)?),
},
Where::And { parts } => self.fold_logical(LogicalOp::And, parts)?,
Where::Or { parts } => self.fold_logical(LogicalOp::Or, parts)?,
Where::Op(op) => {
let hint = if op.op == Consumer::Count {
format!(
" — write `<relation> count {{ … }} {} N` without the parentheses",
self.peek().value
)
} else {
String::new()
};
return self.fail(format!(
"a consumer test ({} {{ … }}) is a predicate, not a value, so it cannot be compared or operated on{hint}",
op.op.as_str()
));
}
})
}
fn fold_logical(&self, op: LogicalOp, parts: Vec<Where>) -> Result<Expr> {
let mut iter = parts.into_iter();
let first = iter
.next()
.expect("an And/Or node always has at least one part");
let mut acc = self.where_to_expr(first)?;
for p in iter {
acc = Expr::Logical {
op,
left: Box::new(acc),
right: Box::new(self.where_to_expr(p)?),
};
}
Ok(acc)
}
fn finish_where_op(&mut self, mut op: OpNode) -> Result<OpNode> {
if matches!(op.op, Consumer::First | Consumer::Single) {
return self.fail(format!(
"{} {{ … }} is a select-position lookup; in where use exists {{ … }} / none {{ … }} or count {{ … }} <op> N",
op.op.as_str()
));
}
if op.op == Consumer::Collect {
let all_lift = !op.sub.select.is_empty()
&& op
.sub
.select
.iter()
.all(|s| matches!(s, SelectItem::Field { lift, .. } if *lift > 0));
if !all_lift {
return self.fail(
"collect { … } in where must project only ^lift values (else use exists/count)",
);
}
}
if self.peek().kind == TokType::Op && RELOPS.contains(&self.peek().value.as_str()) {
if op.op != Consumer::Count {
return self.fail(format!(
"only count {{ … }} is comparable; '{} {{ … }} <op> N' is not valid",
op.op.as_str()
));
}
let relop = self.next().value;
if !self.at(TokType::Number) {
return self.fail(format!("expected an integer after 'count {{ … }} {relop}'"));
}
let v = number_value(&self.next().value);
if !is_integer(v) {
return self.fail("count comparison takes an integer");
}
let op_enum = RelOp::from_word(&relop).expect("RELOPS membership was checked");
op.count_cmp = Some(CountCmp {
op: op_enum,
value: v,
});
}
Ok(op)
}
fn try_op(&mut self, in_where: bool) -> Result<Option<OpNode>> {
let start = self.pos;
let receiver = if self.at(TokType::Binding) {
Expr::Binding {
index: binding_index(&self.next()),
}
} else if self.at(TokType::Ident) || self.at(TokType::Caret) {
let levels = self.parse_carets();
if !self.at(TokType::Ident) {
self.pos = start;
return Ok(None);
}
let head = self.next();
self.parse_nav_from(head, levels)?.0
} else {
return Ok(None);
};
if self.at(TokType::Ident) && CONSUMERS.contains(&self.peek().value.as_str()) {
let after = self.peek_at(1);
let op_then_brace = after.is_some_and(|a| a.kind == TokType::LBrace);
let op_distinct_brace = after
.is_some_and(|a| a.kind == TokType::Ident && a.value == "distinct")
&& self.peek_at(2).is_some_and(|a| a.kind == TokType::LBrace);
if op_then_brace || op_distinct_brace {
if let Expr::Ident { name } = &receiver
&& LITERAL_WORDS.contains(&name.as_str())
{
return self.fail(format!("`{name}` is a literal, not a collection"));
}
let op = Consumer::from_word(&self.next().value)
.expect("CONSUMERS membership was checked");
let mut distinct = false;
if self.at_word(TokType::Ident, "distinct") {
self.next();
distinct = true;
}
self.next(); let ctx = BodyCtx::Block {
op,
lifts_allowed: in_where && op == Consumer::Collect,
};
let (sub, body_distinct) = self.parse_subquery(ctx)?;
if !self.at(TokType::RBrace) {
return self.fail(format!(
"expected '}}' to close the {} {{ … }} block",
op.as_str()
));
}
self.next();
return Ok(Some(OpNode {
receiver,
op,
sub,
count_cmp: None,
distinct: distinct || body_distinct,
}));
}
}
self.pos = start;
Ok(None)
}
fn parse_subquery(&mut self, ctx: BodyCtx) -> Result<(Subquery, bool)> {
let body = self.parse_body(ctx)?;
Ok((
Subquery {
from: body.froms,
r#where: body.r#where,
select: body.select,
order_by: body.order_by,
follow: body.follow,
values: body.values,
limit: body.limit,
offset: body.offset,
},
body.distinct,
))
}
fn parse_value_expr(&mut self) -> Result<Expr> {
self.parse_or()
}
fn parse_or(&mut self) -> Result<Expr> {
let mut left = self.parse_and()?;
while self.at_op("||") {
self.next();
let right = self.parse_and()?;
left = Expr::Logical {
op: LogicalOp::Or,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn parse_and(&mut self) -> Result<Expr> {
let mut left = self.parse_cmp(None)?;
while self.at_op("&&") {
self.next();
let right = self.parse_cmp(None)?;
left = Expr::Logical {
op: LogicalOp::And,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn parse_cmp(&mut self, left: Option<Expr>) -> Result<Expr> {
let lhs = self.parse_range(left)?;
if !self.at_cmp() {
return Ok(lhs);
}
let first = self.next().value;
let rhs = self.parse_range(None)?;
let result = if first == "in" {
Expr::In {
left: Box::new(lhs),
right: Box::new(rhs),
}
} else {
Expr::Binary {
op: BinaryOp::from_word(&first).expect("CMP_OPS membership was checked"),
left: Box::new(lhs),
right: Box::new(rhs),
}
};
if self.at_cmp() {
return self.fail(format!(
"comparisons do not chain: `a {first} b {} c` — write two comparisons joined with `&&`",
self.peek().value
));
}
Ok(result)
}
fn at_cmp(&self) -> bool {
let t = self.peek();
(t.kind == TokType::Op && CMP_OPS.contains(&t.value.as_str()))
|| self.at_word(TokType::Ident, "in")
}
fn parse_range(&mut self, left: Option<Expr>) -> Result<Expr> {
if left.is_none() && self.at(TokType::Range) {
let exclusive_end = self.next().value == "...";
if !self.can_start_value() {
return self.fail("a range needs at least one bound: `lo..hi`, `lo..`, or `..hi`");
}
let hi = self.parse_add(None)?;
return Ok(Expr::Range {
lo: None,
hi: Some(Box::new(hi)),
exclusive_end,
});
}
let lo = self.parse_add(left)?;
if self.at(TokType::Range) {
let exclusive_end = self.next().value == "...";
let hi = if self.can_start_value() {
Some(Box::new(self.parse_add(None)?))
} else {
None
};
return Ok(Expr::Range {
lo: Some(Box::new(lo)),
hi,
exclusive_end,
});
}
Ok(lo)
}
fn can_start_value(&self) -> bool {
let t = self.peek();
match t.kind {
TokType::Ident => !CLAUSE_WORDS.contains(&t.value.as_str()),
TokType::Number
| TokType::Str
| TokType::Binding
| TokType::LParen
| TokType::Caret => true,
TokType::Op => t.value == "-" || t.value == "!",
_ => false,
}
}
fn parse_add(&mut self, left: Option<Expr>) -> Result<Expr> {
let mut left = self.parse_mul(left)?;
while self.peek().kind == TokType::Op && ADD_OPS.contains(&self.peek().value.as_str()) {
let op =
BinaryOp::from_word(&self.next().value).expect("ADD_OPS membership was checked");
let right = self.parse_mul(None)?;
left = Expr::Binary {
op,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn parse_mul(&mut self, left: Option<Expr>) -> Result<Expr> {
let mut left = self.parse_unary(left)?;
while self.peek().kind == TokType::Op && MUL_OPS.contains(&self.peek().value.as_str()) {
let op =
BinaryOp::from_word(&self.next().value).expect("MUL_OPS membership was checked");
let right = self.parse_unary(None)?;
left = Expr::Binary {
op,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn parse_unary(&mut self, left: Option<Expr>) -> Result<Expr> {
if left.is_some() {
return self.parse_postfix(left);
}
if self.at_op("!") {
self.next();
return Ok(Expr::Unary {
op: UnaryOp::Not,
expr: Box::new(self.parse_unary(None)?),
});
}
if self.at_op("-") {
self.next();
return Ok(Expr::Unary {
op: UnaryOp::Neg,
expr: Box::new(self.parse_unary(None)?),
});
}
self.parse_postfix(None)
}
fn parse_postfix(&mut self, left: Option<Expr>) -> Result<Expr> {
let bare_head = left.is_none();
let mut expr = match left {
Some(e) => e,
None => self.parse_primary()?,
};
loop {
if self.at(TokType::Dot) {
self.next();
if !self.at(TokType::Ident) {
return self.fail("expected a property name after '.'");
}
let name = self.next().value;
if self.at(TokType::LParen) {
let args = self.parse_args()?;
expr = Expr::Call {
recv: Some(Box::new(expr)),
name,
args,
};
} else {
expr = Expr::Member {
recv: Box::new(expr),
name,
};
}
} else if self.at(TokType::LParen) && bare_head && matches!(expr, Expr::Ident { .. }) {
let Expr::Ident { name } = expr else {
unreachable!()
};
let args = self.parse_args()?;
expr = Expr::Call {
recv: None,
name,
args,
};
} else {
break;
}
}
Ok(expr)
}
fn parse_args(&mut self) -> Result<Vec<Expr>> {
self.next(); let mut args = Vec::new();
if !self.at(TokType::RParen) {
args.push(self.parse_value_expr()?);
while self.at(TokType::Comma) {
self.next();
args.push(self.parse_value_expr()?);
}
}
if !self.at(TokType::RParen) {
return self.fail("expected ')' to close call arguments");
}
self.next();
Ok(args)
}
fn parse_primary(&mut self) -> Result<Expr> {
let t = self.peek().clone();
match t.kind {
TokType::Caret => {
let levels = self.parse_carets();
if !self.at(TokType::Ident) {
return self.fail("expected an identifier after '^' (an outer reference)");
}
Ok(Expr::Outer {
levels,
name: self.next().value,
})
}
TokType::Number => {
self.next();
Ok(Expr::Lit(Value::Number(number_value(&t.value))))
}
TokType::Str => {
self.next();
Ok(Expr::Lit(Value::Str(t.value)))
}
TokType::Binding => {
self.next();
Ok(Expr::Binding {
index: binding_index(&t),
})
}
TokType::LParen => {
self.next();
let e = self.parse_value_expr()?;
if !self.at(TokType::RParen) {
return self.fail("expected ')'");
}
self.next();
Ok(e)
}
TokType::Ident => {
self.next();
Ok(match t.value.as_str() {
"true" => Expr::Lit(Value::Bool(true)),
"false" => Expr::Lit(Value::Bool(false)),
"null" => Expr::Lit(Value::Null),
_ => Expr::Ident { name: t.value },
})
}
_ => self.fail(format!("unexpected {} — expected a value", self.tok_desc())),
}
}
}
fn binding_index(t: &Token) -> usize {
t.index.expect("a binding token always carries its index")
}
fn wrap_not(mut w: Where, n: usize) -> Where {
for _ in 0..n {
w = Where::Not { expr: Box::new(w) };
}
w
}
fn scalar_leaf(e: Expr) -> Where {
match e {
Expr::Unary {
op: UnaryOp::Not,
expr,
} => Where::Not {
expr: Box::new(scalar_leaf(*expr)),
},
expr => Where::Scalar { expr },
}
}
fn nav_key(e: &Expr) -> Option<&str> {
match e {
Expr::Ident { name } | Expr::Outer { name, .. } | Expr::Member { name, .. } => Some(name),
_ => None,
}
}