use crate::adapter::AstAdapter;
use crate::ast::visit::{self, Visitor as _};
use crate::ast::{
Anchor, Arg, ArithOp, Axis, Branch, CmpOp, FnCall, Group, InterpSeg, Matcher, Operand, PatSeg,
PathElem, PredExpr, Predicate, Projection, PushBody, Quant, Query, Reach, RefProp, RegRef,
ScopePath, Stage, Step, TraitClause,
};
use crate::error::{QuarbError, Result};
use crate::lexer::{self, Token};
use crate::value::Value;
use globset::Glob;
use regex::Regex;
use std::collections::HashMap;
pub fn parse_scope_path(text: &str) -> Result<ScopePath> {
let toks = crate::lexer::lex(text)?;
let mut p = Parser::new(&toks, Defs::default(), None);
let sp = p.scope_path()?;
if p.pos < toks.len() {
return Err(QuarbError::parse(format!(
"a scope path ends with its last step: '{text}'"
)));
}
sp.ok_or_else(|| QuarbError::parse(format!("not a scope path: '{text}'")))
}
pub fn parse(tokens: &[Token]) -> Result<Query> {
parse_with_defs(tokens, Defs::default())
}
pub fn parse_with_defs(tokens: &[Token], defs: Defs) -> Result<Query> {
parse_with_data(tokens, defs, None)
}
pub fn parse_with_data(
tokens: &[Token],
defs: Defs,
data: Option<&dyn AstAdapter>,
) -> Result<Query> {
parse_with_data_spanned(tokens, None, defs, data)
}
pub fn parse_with_data_spanned(
tokens: &[Token],
spans: Option<&[(usize, usize)]>,
defs: Defs,
data: Option<&dyn AstAdapter>,
) -> Result<Query> {
let mut p = Parser::new(tokens, defs, data);
let out = p.parse();
match (out, spans) {
(Err(e), Some(spans)) if e.kind() == crate::error::Kind::Parse && e.offset().is_none() => {
let at = p.pos.min(spans.len().saturating_sub(1));
match spans.get(at) {
Some(&(start, _)) => Err(e.at(start)),
None => Err(e),
}
}
(out, _) => out,
}
}
pub fn parse_first_steps(
tokens: &[Token],
defs: Defs,
data: Option<&dyn AstAdapter>,
) -> Result<Vec<String>> {
let mut p = Parser::new(tokens, defs, data);
p.parse()?;
Ok(p.first_steps.into_inner())
}
pub fn parse_defs(tokens: &[Token]) -> Result<Defs> {
let mut p = Parser::new(tokens, Defs::default(), None);
while p.at_def() || p.at_macro() {
if p.at_def() {
p.parse_def()?;
} else {
p.parse_macro()?;
}
}
if let Some(tok) = p.peek() {
return Err(QuarbError::parse(format!(
"a definitions file holds only 'def' and 'macro' statements; unexpected {tok:?}"
)));
}
Ok(p.defs)
}
#[derive(Debug, Clone, Default)]
pub struct Defs {
entries: Vec<(String, Def)>,
}
impl Defs {
fn get(&self, name: &str) -> Option<&Def> {
self.entries.iter().find(|(n, _)| n == name).map(|(_, d)| d)
}
fn before(&self, name: &str) -> Defs {
let end = self
.entries
.iter()
.position(|(n, _)| n == name)
.unwrap_or(self.entries.len());
Defs {
entries: self.entries[..end].to_vec(),
}
}
}
#[derive(Debug, Clone)]
struct Def {
params: Vec<String>,
rest: Option<String>,
data_aware: bool,
body: DefBody,
}
#[derive(Debug, Clone)]
enum DefBody {
Query(Query),
Pipeline(Vec<Stage>),
Predicates(Vec<Predicate>),
Macro(Query),
}
fn stage_pipe(stage: &Stage) -> &'static str {
match stage {
Stage::Agg(_) | Stage::Select(_) => "@|",
_ => "|",
}
}
struct CaptureRec {
mac: String,
reg: String,
pushes: usize,
recalls: usize,
}
fn invites_of(args: &[Operand]) -> std::collections::BTreeSet<String> {
let mut s = std::collections::BTreeSet::new();
for a in args {
s.extend(crate::reflect::operand_invites(a));
}
s
}
struct Parser<'a> {
toks: &'a [Token],
pos: usize,
defs: Defs,
def_params: Vec<String>,
data: Option<&'a dyn AstAdapter>,
pattern_depth: usize,
predicate_depth: usize,
nest_depth: usize,
subquery_depth: usize,
captures: std::cell::RefCell<Vec<CaptureRec>>,
first_steps: std::cell::RefCell<Vec<String>>,
}
const MAX_NEST: usize = 64;
#[derive(Clone, Copy, PartialEq, Eq)]
enum SplicePos {
MidPath,
GroupAlt,
}
fn splices_as_path(q: &Query) -> bool {
q.correlations.is_empty()
&& q.pipeline.is_empty()
&& q.branches.iter().all(|b| b.anchor == Anchor::Current)
&& (q.branches.len() == 1 || q.branches.iter().all(|b| b.projection.is_none()))
}
fn group_wrap(
elems: Vec<PathElem>,
quant: Quant,
predicates: Vec<Predicate>,
reach: Reach,
) -> Vec<PathElem> {
if elems.len() == 1
&& let Some(PathElem::Group(g)) = elems.first()
&& g.quant
== (Quant {
min: 1,
max: Some(1),
})
&& g.predicates.is_empty()
&& g.reach == Reach::All
{
let Some(PathElem::Group(mut g)) = elems.into_iter().next() else {
unreachable!("shape checked above");
};
g.quant = quant;
g.predicates = predicates;
g.reach = reach;
return vec![PathElem::Group(g)];
}
vec![PathElem::Group(Group {
alts: vec![elems],
quant,
predicates,
traits: Vec::new(),
reach,
})]
}
fn walks_reverse_resolve(elems: &[PathElem]) -> bool {
elems.iter().any(|e| match e {
PathElem::Step(s) => matches!(
s.axis,
Axis::ReverseResolve { scope: None, .. } | Axis::ValueMatch { scope: None, .. }
),
PathElem::Group(g) => g.alts.iter().any(|a| walks_reverse_resolve(a)),
PathElem::Mark(_) | PathElem::Push { .. } => false,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PipeMode {
Nav,
Scalar,
}
fn stage_mode_out(stage: &Stage, cur: PipeMode) -> PipeMode {
match stage {
Stage::Nav(b) => {
if b.projection.is_some() {
PipeMode::Scalar
} else {
PipeMode::Nav
}
}
Stage::Push(_)
| Stage::ExprPush { .. }
| Stage::RecordPush { .. }
| Stage::FieldsPush
| Stage::Subcontext { .. } => PipeMode::Nav,
Stage::Filter(_) | Stage::Select(_) => cur,
Stage::Func(_)
| Stage::Expr(_)
| Stage::Agg(_)
| Stage::Recall(_)
| Stage::RecordWith(_)
| Stage::Spread { .. }
| Stage::Map(_) => PipeMode::Scalar,
Stage::Repeat { stage, .. } => stage_mode_out(stage, cur),
}
}
impl<'a> Parser<'a> {
fn peek(&self) -> Option<&Token> {
self.toks.get(self.pos)
}
fn descend(&mut self) -> Result<()> {
self.nest_depth += 1;
if self.nest_depth > MAX_NEST {
return Err(QuarbError::parse(format!(
"query nested more than {MAX_NEST} levels deep"
)));
}
Ok(())
}
fn new(toks: &'a [Token], defs: Defs, data: Option<&'a dyn AstAdapter>) -> Parser<'a> {
Parser {
toks,
pos: 0,
defs,
def_params: Vec::new(),
data,
pattern_depth: 0,
predicate_depth: 0,
nest_depth: 0,
subquery_depth: 0,
captures: std::cell::RefCell::new(Vec::new()),
first_steps: std::cell::RefCell::new(Vec::new()),
}
}
fn child<'t>(&self, toks: &'t [Token], defs: Defs) -> Result<Parser<'t>>
where
'a: 't,
{
Ok(Parser {
nest_depth: self.sub_depth()?,
..Parser::new(toks, defs, self.data)
})
}
}
impl Parser<'_> {
fn sub_depth(&self) -> Result<usize> {
let depth = self.nest_depth + 1;
if depth > MAX_NEST {
return Err(QuarbError::parse(format!(
"query nested more than {MAX_NEST} levels deep"
)));
}
Ok(depth)
}
fn bump(&mut self) -> Option<&Token> {
let t = self.toks.get(self.pos);
if t.is_some() {
self.pos += 1;
}
t
}
fn parse(&mut self) -> Result<Query> {
if self.toks.is_empty() {
return Err(QuarbError::parse("empty query"));
}
while self.at_def() || self.at_macro() {
if self.at_def() {
self.parse_def()?;
} else {
self.parse_macro()?;
}
}
let query = self.parse_chain()?;
if let Some(tok) = self.peek() {
if matches!(tok, Token::Field)
&& let Some(Token::Name { text, .. }) = self.toks.get(self.pos + 1)
{
return Err(QuarbError::parse(format!(
"':{text}' reads a record's field; a node's property is '::{text}'"
)));
}
return Err(QuarbError::parse(format!(
"unexpected trailing input at token {tok:?}"
)));
}
validate_correlation_refs(&query)?;
validate_keyed_stages(&query)?;
self.sweep_captures(&query)?;
Ok(query)
}
fn parse_chain(&mut self) -> Result<Query> {
let mut query = self.parse_query()?;
if matches!(self.peek(), Some(Token::Correlate)) && query.correlations.is_empty() {
query.join_at = query.pipeline.len();
}
while matches!(self.peek(), Some(Token::Correlate)) {
self.pos += 1;
let outer = if matches!(self.peek(), Some(Token::Question { .. })) {
self.pos += 1;
true
} else {
false
};
let mut entry = self.parse_query()?;
if !entry.correlations.is_empty() {
return Err(QuarbError::parse(
"a joined expression cannot itself carry a \
correlation — chains are flat; splice a \
chain-carrying fragment at driver position",
));
}
entry.outer = outer;
query.correlations.push(entry);
}
if let Some(last) = query.correlations.last_mut() {
query.pipeline.append(&mut last.pipeline);
}
Ok(query)
}
fn parse_query(&mut self) -> Result<Query> {
let mut branches = Vec::new();
let mut pipeline = Vec::new();
let mut correlations = Vec::new();
let mut join_at = 0usize;
self.union_element(
&mut branches,
&mut pipeline,
&mut correlations,
&mut join_at,
)?;
while matches!(self.peek(), Some(Token::PipePipe)) || self.peek_word(OR_WORDS) {
if !pipeline.is_empty() || !correlations.is_empty() {
return Err(QuarbError::parse(
"a fragment carrying a pipeline or a correlation must \
stand alone, not in a union",
));
}
self.pos += 1;
self.union_element(
&mut branches,
&mut pipeline,
&mut correlations,
&mut join_at,
)?;
}
let mut mode = if branches.iter().any(|b| b.projection.is_some()) {
PipeMode::Scalar
} else {
PipeMode::Nav
};
for s in &pipeline {
mode = stage_mode_out(s, mode);
}
self.pipeline_items(&mut pipeline, mode)?;
Ok(Query {
correlations,
outer: false,
branches,
pipeline,
join_at,
})
}
fn union_element(
&mut self,
branches: &mut Vec<Branch>,
pipeline: &mut Vec<Stage>,
correlations: &mut Vec<Query>,
join_at: &mut usize,
) -> Result<()> {
if branches.is_empty() && self.subquery_depth == 0 && matches!(self.peek(), Some(Token::Eq))
{
self.pos += 1;
let expr = self.additive()?;
branches.push(Branch {
steps: Vec::new(),
projection: None,
anchor: Anchor::Root,
});
pipeline.push(Stage::Expr(expr));
if matches!(self.peek(), Some(Token::PipePipe)) || self.peek_word(OR_WORDS) {
return Err(QuarbError::parse(
"an expression head stands alone; it cannot join a \
union — pipe from it instead: '= expr | …'",
));
}
return Ok(());
}
if matches!(self.peek(), Some(Token::Amp)) {
let alone = branches.is_empty();
if matches!(self.peek_invocation_body(), Some(DefBody::Predicates(_))) {
branches.push(self.branch()?);
return Ok(());
}
let (name, q, quant) = self.invoke_query_fragment()?;
if quant.is_some() && !splices_as_path(&q) {
return Err(QuarbError::parse(format!(
"fragment '&{name}' carries a pipeline, a correlation, \
or an anchor; a quantifier cannot ride it — quantify \
plain navigation"
)));
}
if splices_as_path(&q)
&& (q.branches.len() == 1 || quant.is_some() || self.splice_refinement_ahead())
{
let (elems, projection) =
self.finish_splice(&name, q.branches, quant, SplicePos::MidPath)?;
branches.push(self.branch_tail(Anchor::Current, elems, projection)?);
return Ok(());
}
if (!q.pipeline.is_empty() || !q.correlations.is_empty()) && !alone {
return Err(QuarbError::parse(
"a fragment carrying a pipeline or a correlation must \
stand alone, not in a union",
));
}
branches.extend(q.branches);
if !q.correlations.is_empty() {
*join_at = pipeline.len() + q.join_at;
}
pipeline.extend(q.pipeline);
correlations.extend(q.correlations);
if matches!(self.peek(), Some(Token::LBracket)) {
return Err(QuarbError::parse(
"a fragment carrying a pipeline, a correlation, or an \
anchor does not take trailing predicates; refine it \
with a pipeline filter: '&name | [cond]'",
));
}
if matches!(
self.peek(),
Some(Token::ColonColon | Token::ColonColonColon | Token::SemiSemiSemi)
) {
return Err(QuarbError::parse(
"a fragment carrying a pipeline, a correlation, or an \
anchor does not take a trailing projection; project \
it through the pipe: '&name | ::key'",
));
}
if matches!(self.peek(), Some(Token::Lt { .. })) {
return Err(QuarbError::parse(
"a fragment does not take a trailing trait selector; \
put the trait inside a definition: \
'def &errs: /entry<error> ;'",
));
}
} else {
branches.push(self.branch()?);
}
Ok(())
}
fn peek_invocation_body(&self) -> Option<&DefBody> {
match self.toks.get(self.pos + 1) {
Some(Token::Name {
text,
quoted: false,
..
}) => self.defs.get(text).map(|d| &d.body),
_ => None,
}
}
fn splice_refinement_ahead(&self) -> bool {
match self.peek() {
Some(
Token::Quant { .. }
| Token::LBracket
| Token::Amp
| Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
| Token::ColonColon
| Token::ColonColonColon
| Token::SemiSemiSemi
| Token::Lt { .. }
| Token::Gt { .. }
| Token::NextSibling
| Token::PrevSibling,
) => true,
Some(Token::Name {
text,
quoted: false,
glued: true,
}) => text == "+" || text == "*",
_ => false,
}
}
fn pipeline_items(&mut self, pipeline: &mut Vec<Stage>, mut mode: PipeMode) -> Result<()> {
loop {
match self.peek() {
Some(Token::Pipe) => {
self.pos += 1;
if matches!(self.peek(), Some(Token::Amp)) {
let before = pipeline.len();
self.invoke_pipeline_fragment("|", pipeline)?;
for s in &pipeline[before..] {
mode = stage_mode_out(s, mode);
}
continue;
}
let mut stage = self.pipe_item(mode)?;
if let Some(&Token::Quant { min, max }) = self.peek() {
if !matches!(
stage,
Stage::Push(_)
| Stage::ExprPush { .. }
| Stage::Subcontext { .. }
| Stage::RecordPush { .. }
| Stage::FieldsPush
) {
return Err(QuarbError::parse(
"a quantifier repeats a push: write it after '.name(body)' (or '.', '.%', '.name%(…)')",
));
}
if max.is_some_and(|n| n < min) {
return Err(QuarbError::parse(format!(
"a quantifier's bounds are ordered: {{{min};{}}}",
max.unwrap()
)));
}
self.pos += 1;
stage = Stage::Repeat {
stage: Box::new(stage),
min,
max,
};
}
mode = stage_mode_out(&stage, mode);
pipeline.push(stage);
}
Some(Token::Dollar) if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe)) => {
self.pos += 2;
let stage = Stage::Map(Box::new(self.map_stage()?));
mode = stage_mode_out(&stage, mode);
pipeline.push(stage);
}
Some(Token::At) => {
self.pos += 1;
self.expect(Token::Pipe, "'|' after '@' for aggregation")?;
if matches!(self.peek(), Some(Token::Amp)) {
let before = pipeline.len();
self.invoke_pipeline_fragment("@|", pipeline)?;
for s in &pipeline[before..] {
mode = stage_mode_out(s, mode);
}
continue;
}
if self.nav_stage_ahead() {
return Err(QuarbError::parse(
"navigation is per-thread; write '| /path' \
('@|' aggregates the whole context — hops don't take it)",
));
}
if matches!(self.peek(), Some(Token::LBracket)) {
match self.predicate()? {
pred @ (Predicate::Index(_) | Predicate::Range(_, _)) => {
pipeline.push(Stage::Select(pred));
continue;
}
Predicate::Expr(_) => {
return Err(QuarbError::parse(
"a condition filters per capsa; write '| [cond]' \
('@| [n]' selects positionally)",
));
}
}
}
let call = self.func_call()?;
let stage = agg_stage(call)?;
mode = stage_mode_out(&stage, mode);
pipeline.push(stage);
}
_ => break,
}
}
Ok(())
}
fn pipe_item(&mut self, mode: PipeMode) -> Result<Stage> {
if matches!(mode, PipeMode::Nav)
&& (self.is_resolution_ahead()
|| matches!(self.peek(), Some(Token::Resolve | Token::ReverseResolve)))
{
let save = self.pos;
let b = self.branch()?;
if !self.expr_continues() {
return Ok(Stage::Nav(b));
}
self.pos = save;
return Ok(Stage::Expr(self.additive()?));
}
if self.nav_stage_ahead() {
match mode {
PipeMode::Scalar => {
if !matches!(self.peek(), Some(Token::LParen)) {
return Err(QuarbError::parse(
"cannot navigate in scalar mode — the topic is live \
and a hop would drop it; file it first with '| .' \
(or '| .name') and navigation resumes. \
(Parenthesize the path for a value expression.)",
));
}
}
PipeMode::Nav => {
if matches!(self.peek(), Some(Token::LParen)) && !self.mark_anchor_ahead() {
let save = self.pos;
if let Ok(b) = self.branch()
&& !self.expr_continues()
{
return Ok(Stage::Nav(b));
}
self.pos = save;
} else {
let save = self.pos;
let b = self.branch()?;
if !self.expr_continues() {
return Ok(Stage::Nav(b));
}
self.pos = save;
return Ok(Stage::Expr(self.additive()?));
}
}
}
}
match self.peek() {
Some(Token::Dollar) => Ok(Stage::Expr(self.additive()?)),
Some(Token::Driver) => Ok(Stage::Expr(self.additive()?)),
Some(Token::Field | Token::PercentPlus) => Ok(Stage::Expr(self.additive()?)),
Some(Token::At) if matches!(self.toks.get(self.pos + 1), Some(Token::LParen)) => {
Ok(Stage::Expr(self.additive()?))
}
Some(Token::At) => {
if matches!(
self.toks.get(self.pos + 1),
Some(Token::Name { text, quoted: false, .. }) if text == "-"
) {
return Ok(Stage::Expr(self.additive()?));
}
self.pos += 1;
self.expect_dot("@")?;
Ok(Stage::Recall(RegRef::Whole))
}
Some(Token::Shell(parts)) => {
let parts = parts.clone();
self.pos += 1;
let arg = if let [lexer::InterpPart::Text(t)] = parts.as_slice() {
Arg::Lit(Value::Str(t.clone()))
} else {
let mut segs = Vec::new();
for part in parts {
match part {
lexer::InterpPart::Text(t) => segs.push(InterpSeg::Text(t)),
lexer::InterpPart::Hole(src) => {
segs.push(self.parse_hole(&src)?);
}
}
}
Arg::Expr(Operand::Interp(segs))
};
Ok(Stage::Func(FnCall {
name: "sh".into(),
args: vec![arg],
}))
}
Some(Token::Percent) => {
self.pos += 1;
if matches!(self.peek(), Some(Token::Percent)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::LParen)) {
let call = self.record_args()?;
if call.args.is_empty() {
return Err(QuarbError::parse(
"an empty %%() adds nothing — the register view is %.",
));
}
validate_record(&call)?;
return Ok(Stage::RecordWith(call));
}
self.expect_dot("%%")?;
return Ok(Stage::Recall(RegRef::FullRecord));
}
if matches!(self.peek(), Some(Token::LParen)) {
let call = self.record_args()?;
validate_record(&call)?;
return Ok(Stage::Func(call));
}
self.expect_dot("%")?;
Ok(Stage::Recall(RegRef::Record))
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "..." => {
self.pos += 1;
let outer = if matches!(self.peek(), Some(Token::Question { .. })) {
self.pos += 1;
true
} else {
false
};
Ok(Stage::Spread { outer })
}
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with('.') => {
let text = text.clone();
self.pos += 1;
let mut name = if text == "." {
None
} else {
Some(text[1..].to_string())
};
if name.is_none()
&& let Some(Token::Name {
text: q,
quoted: true,
glued: true,
}) = self.peek()
{
name = Some(q.clone());
self.pos += 1;
}
if let Some(n) = &name
&& text.len() > 1
&& !n.chars().next().is_some_and(|c| c.is_alphabetic())
{
return Err(QuarbError::parse(format!(
"a push name starts with a letter ('| .name'); quote any other \
('| .\"{n}\"'), and recall a position with $.N or ((.N))"
)));
}
if matches!(self.peek(), Some(Token::Percent)) {
self.pos += 1;
let enriched = if matches!(self.peek(), Some(Token::Percent)) {
self.pos += 1;
true
} else {
false
};
if !matches!(self.peek(), Some(Token::LParen)) {
if !enriched && name.is_none() {
return Ok(Stage::FieldsPush);
}
return Err(QuarbError::parse(
"a record push takes its fields: `.%(…)` or `.name%(…)`; \
the bare `.%` (no name, no parens) spreads the topic record",
));
}
let call = self.record_args()?;
validate_record(&call)?;
return Ok(Stage::RecordPush {
name,
call,
enriched,
});
}
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
let save = self.pos;
self.subquery_depth += 1;
let tried = self.parse_query();
self.subquery_depth -= 1;
if let Ok(body) = tried
&& matches!(self.peek(), Some(Token::RParen))
{
self.pos += 1;
return Ok(Stage::Subcontext {
name,
body: Box::new(body),
});
}
self.pos = save;
let expr = self.cond_expr()?;
let stages = self.inline_pipe_tail()?;
let expr = if stages.is_empty() {
expr
} else {
Operand::Piped {
expr: Box::new(expr),
stages,
}
};
self.expect(Token::RParen, "')' to close a subcontext")?;
Ok(Stage::ExprPush { name, expr })
} else {
if name.as_deref().is_some_and(|n| {
n.len() > 1
&& n.starts_with('_')
&& n[1..].chars().all(|c| c.is_ascii_digit())
}) {
return Err(QuarbError::parse(
"a mark named '_N' is reserved — ((_N)) is the join's N-th node ($$N)",
));
}
Ok(Stage::Push(name))
}
}
Some(Token::Subst {
pattern,
replacement,
mods,
}) => {
let case = if mods.contains('i') { "(?i)" } else { "" };
Regex::new(&format!("{case}{pattern}"))
.map_err(|e| QuarbError::parse(format!("bad substitution pattern: {e}")))?;
let call = FnCall {
name: "s".to_string(),
args: vec![
Arg::Lit(Value::Str(pattern.clone())),
Arg::Lit(Value::Str(replacement.clone())),
Arg::Lit(Value::Str(mods.clone())),
],
};
self.pos += 1;
Ok(Stage::Func(call))
}
Some(Token::LBracket) => {
let positional = || {
QuarbError::parse(
"positional selection is whole-context; write '@| [n]' / \
'@| [a..b]' (a plain '| [cond]' filters per capsa)",
)
};
let mut e = match self.predicate()? {
Predicate::Expr(e) => e,
Predicate::Index(_) | Predicate::Range(_, _) => return Err(positional()),
};
while matches!(self.peek(), Some(Token::LBracket)) {
match self.predicate()? {
Predicate::Expr(next) => e = PredExpr::And(Box::new(e), Box::new(next)),
Predicate::Index(_) | Predicate::Range(_, _) => return Err(positional()),
}
}
Ok(Stage::Filter(e))
}
Some(
Token::ColonColon
| Token::ColonColonColon
| Token::SemiSemiSemi
| Token::LParen
| Token::Interp(_),
) => Ok(Stage::Expr(self.additive()?)),
Some(Token::Name { quoted: true, .. }) => Ok(Stage::Expr(self.additive()?)),
_ => {
let call = self.func_call()?;
if call.name == "now" {
return Err(QuarbError::parse(
"now() is a call operand (the invocation instant); it takes no \
topic — use it in expression position: [::date > now() - 12h]",
));
}
func_stage(call)
}
}
}
fn nav_stage_ahead(&self) -> bool {
let axis = |t: &Token| {
matches!(
t,
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::Gt { .. }
| Token::Lt { .. }
| Token::NextSibling
| Token::PrevSibling
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
)
};
match self.peek() {
Some(t) if axis(t) => true,
Some(Token::Caret) => true,
Some(Token::MarkOpen) => self.mark_anchor_ahead(),
Some(Token::IdAnchorOpen) => true,
Some(Token::LParen) => self.toks.get(self.pos + 1).is_some_and(axis),
_ => false,
}
}
fn expr_continues(&self) -> bool {
match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) => {
matches!(text.as_str(), "+" | "-" | "*" | "div" | "idiv" | "mod")
}
Some(Token::Question { .. }) => true,
Some(Token::Capsa(_)) => true,
_ => false,
}
}
fn expect_dot(&mut self, sigil: &str) -> Result<()> {
match self.peek() {
Some(Token::Name { text, .. }) if text == "." => {
self.pos += 1;
Ok(())
}
_ => Err(QuarbError::parse(format!("expected '.' after '{sigil}'"))),
}
}
fn at_def(&self) -> bool {
matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "def")
&& matches!(self.toks.get(self.pos + 1), Some(Token::Amp))
}
fn parse_def(&mut self) -> Result<()> {
self.pos += 1; self.expect(Token::Amp, "'&' after 'def'")?;
let name = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse("expected a fragment name after 'def &'"));
}
};
if self.defs.get(&name).is_some() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' is already defined"
)));
}
let mut params = Vec::new();
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
loop {
self.expect(Token::Dollar, "'$' before a parameter name")?;
let param = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse("expected a parameter name after '$'"));
}
};
if param == "_"
|| param == "ord"
|| param == "ordinal"
|| param.starts_with('.')
|| param.starts_with('*')
|| !param.chars().next().is_some_and(|c| c.is_alphabetic())
{
return Err(QuarbError::parse(format!(
"parameter name '${param}' collides with a capsa-scope operand"
)));
}
params.push(param);
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
} else {
break;
}
}
self.expect(Token::RParen, "')' to close the parameter list")?;
}
self.expect_separator("':' between the fragment name and its body")?;
self.def_params = params.clone();
let body = if matches!(self.peek(), Some(Token::LBracket)) {
let mut preds = Vec::new();
while matches!(self.peek(), Some(Token::LBracket)) {
preds.push(self.predicate()?);
}
DefBody::Predicates(preds)
} else if matches!(self.peek(), Some(Token::Pipe | Token::At)) {
let mut stages = Vec::new();
self.pipeline_items(&mut stages, PipeMode::Nav)?;
if stages.is_empty() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' has an empty body"
)));
}
DefBody::Pipeline(stages)
} else {
if matches!(self.peek(),
Some(Token::Name { text, quoted: false, .. }) if text.starts_with('.'))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
{
return Err(QuarbError::parse(format!(
"fragment '&{name}' body looks like a pipeline stage; \
a pipeline fragment starts with its pipe: \
'def &{name}: | .push(...) ;'"
)));
}
DefBody::Query(self.parse_chain().map_err(|e| {
QuarbError::parse(format!(
"in fragment '&{name}' body: {e} (a pipeline \
fragment's body starts with its pipe: \
'def &{name}: | ...')"
))
})?)
};
self.def_params.clear();
self.expect(Token::Semi, "';' to end the definition")?;
self.defs.entries.push((
name,
Def {
params,
rest: None,
data_aware: false,
body,
},
));
Ok(())
}
fn at_macro(&self) -> bool {
matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "macro")
&& matches!(self.toks.get(self.pos + 1), Some(Token::Amp))
}
fn parse_macro(&mut self) -> Result<()> {
self.pos += 1; self.expect(Token::Amp, "'&' after 'macro'")?;
let name = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse("expected a macro name after 'macro &'"));
}
};
let data_aware = matches!(self.peek(), Some(Token::Bang));
if data_aware {
self.pos += 1;
}
if self.defs.get(&name).is_some() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' is already defined"
)));
}
let mut params = Vec::new();
let mut rest = None;
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
loop {
let is_rest = match self.peek() {
Some(Token::Dollar) => false,
Some(Token::At) => true,
_ => {
return Err(QuarbError::parse(
"expected '$name' or a trailing '@rest' parameter",
));
}
};
self.pos += 1;
let param = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse(
"expected a parameter name after its sigil",
));
}
};
if param == "_"
|| param == "ord"
|| param == "ordinal"
|| param.starts_with('.')
|| param.starts_with('*')
|| !param.chars().next().is_some_and(|c| c.is_alphabetic())
{
return Err(QuarbError::parse(format!(
"parameter name '{param}' collides with a capsa-scope operand"
)));
}
if data_aware && param == "data" {
return Err(QuarbError::parse(
"a data-aware macro mounts the dataset as '/data'; \
pick another parameter name",
));
}
if is_rest {
rest = Some(param);
break;
}
params.push(param);
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
} else {
break;
}
}
self.expect(Token::RParen, "')' to close the parameter list")?;
}
self.expect_separator("':' between the macro name and its body")?;
if matches!(self.peek(), Some(Token::Pipe | Token::At)) {
return Err(QuarbError::parse(format!(
"a macro body is a query over its expansion arbor; anchor a \
non-navigating body at the root: 'macro &{name}: ^ | ...;'"
)));
}
self.def_params = params.clone();
let body = self.parse_query()?;
self.def_params.clear();
self.expect(Token::Semi, "';' to end the definition")?;
self.defs.entries.push((
name,
Def {
params,
rest,
data_aware,
body: DefBody::Macro(body),
},
));
Ok(())
}
fn record_captures(
&self,
name: &str,
invited: &std::collections::BTreeSet<String>,
usage: crate::reflect::RegUsage,
) {
if usage.pushes.is_empty() {
return;
}
let mut caps = self.captures.borrow_mut();
for (reg, n) in &usage.pushes {
if invited.contains(reg) {
continue;
}
caps.push(CaptureRec {
mac: name.to_string(),
reg: reg.clone(),
pushes: *n,
recalls: usage.recalls.get(reg).copied().unwrap_or(0),
});
}
}
fn sweep_captures(&self, q: &Query) -> Result<()> {
let caps = self.captures.borrow();
if caps.is_empty() {
return Ok(());
}
let global = crate::reflect::usage_of_query(q);
let mut own: std::collections::BTreeMap<&str, (usize, usize, &str)> =
std::collections::BTreeMap::new();
for c in caps.iter() {
let e = own.entry(c.reg.as_str()).or_insert((0, 0, c.mac.as_str()));
e.0 += c.pushes;
e.1 += c.recalls;
}
for (reg, (_p, r, mac)) in own {
let gr = global.recalls.get(reg).copied().unwrap_or(0);
if gr > r {
return Err(QuarbError::parse(format!(
"macro '&{mac}' pushes the register '.{reg}', and the \
surrounding query recalls '$.{reg}' — capture must \
be invited through an argument: pass '$.{reg}' (or \
the bare name) to '&{mac}', or rename the emitted \
push"
)));
}
}
Ok(())
}
fn expand_macro_text(&self, name: &str, def: &Def, args: Vec<Operand>) -> Result<String> {
let values = self.expand_macro_values(name, def, args)?;
let text = values.join(" ");
if text.trim().is_empty() {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to nothing"
)));
}
self.first_steps.borrow_mut().push(text.clone());
Ok(text)
}
fn expand_macro_path_text(&self, name: &str, def: &Def, args: Vec<Operand>) -> Result<String> {
let values = self.expand_macro_values(name, def, args)?;
let nav_start = |v: &str| {
let t = v.trim_start();
["/", "\\", "->", "<-", "--", "(", "^", ">", "<"]
.iter()
.any(|op| t.starts_with(op))
};
if values.len() >= 2 && values.iter().all(|v| nav_start(v)) {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to {} branch-shaped values; \
space-joined they would read as one path — join them \
into a union (@| join(' || ')) or emit a single group",
values.len()
)));
}
let text = values.join(" ");
if text.trim().is_empty() {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to nothing"
)));
}
self.first_steps.borrow_mut().push(text.clone());
Ok(text)
}
fn expand_macro_values(
&self,
name: &str,
def: &Def,
args: Vec<Operand>,
) -> Result<Vec<String>> {
let n = def.params.len();
let arity_ok = match def.rest {
Some(_) => args.len() >= n,
None => args.len() == n,
};
if !arity_ok {
return Err(QuarbError::parse(format!(
"macro '&{name}' takes {}{} argument(s), got {}",
n,
if def.rest.is_some() { "+" } else { "" },
args.len()
)));
}
let mut forms = HashMap::new();
let mut texts = HashMap::new();
for (p, a) in def.params.iter().zip(&args) {
forms.insert(p.clone(), a.clone());
let text = match a {
Operand::Lit(v) => Operand::Lit(v.clone()),
form => Operand::Lit(Value::Str(crate::unparse::operand_text(form))),
};
texts.insert(p.clone(), text);
}
let DefBody::Macro(body) = &def.body else {
unreachable!("checked by caller");
};
let mut body = body.clone();
subst_query(
&mut body,
&Subst {
outer: &forms,
hole: &texts,
},
);
let mut bindings: Vec<(String, crate::reflect::MacroBinding)> = def
.params
.iter()
.zip(&args)
.map(|(p, a)| (p.clone(), crate::reflect::MacroBinding::One(a.clone())))
.collect();
if let Some(rest) = &def.rest {
bindings.push((
rest.clone(),
crate::reflect::MacroBinding::Rest(args[n..].to_vec()),
));
}
let arbor = crate::reflect::expansion_arbor(&bindings);
let allow_shell = crate::exec::run_option(|o| o.allow_shell);
let result = if def.data_aware {
let Some(data) = self.data else {
return Err(QuarbError::parse(format!(
"macro '&{name}!' is data-aware: its expansion reads \
the dataset, so it needs an input (it cannot expand \
from query text alone)"
)));
};
let combined = crate::reflect::ExpansionAdapter::new(arbor, data);
crate::exec::gate_shell(&body, allow_shell)?;
crate::exec::eval(&body, &combined)
} else {
crate::exec::gate_shell(&body, allow_shell)?;
crate::exec::eval(&body, &arbor)
};
let values = match result {
crate::exec::QueryResult::Values(vs) => vs,
crate::exec::QueryResult::Nodes(_) => {
return Err(QuarbError::parse(format!(
"macro '&{name}' must produce query text; its body \
returned nodes (project or interpolate)"
)));
}
};
Ok(values.iter().map(|v| v.to_string()).collect())
}
fn invocation(&mut self) -> Result<(String, Vec<Operand>, bool, Option<Quant>)> {
self.expect(Token::Amp, "'&'")?;
let mut name = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse("expected a fragment name after '&'"));
}
};
let mut quant = None;
if self.defs.get(&name).is_none()
&& let Some(stripped) = name.strip_suffix(['+', '*'])
&& self.defs.get(stripped).is_some()
{
quant = Some(if name.ends_with('+') {
Quant { min: 1, max: None }
} else {
Quant { min: 0, max: None }
});
name = stripped.to_string();
}
let bang = matches!(self.peek(), Some(Token::Bang));
if bang {
self.pos += 1;
}
let mut args = Vec::new();
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
if !matches!(self.peek(), Some(Token::RParen)) {
loop {
args.push(self.cond_expr()?);
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
} else {
break;
}
}
}
self.expect(Token::RParen, "')' to close fragment arguments")?;
}
Ok((name, args, bang, quant))
}
fn check_bang(&self, name: &str, def: &Def, bang: bool) -> Result<()> {
if def.data_aware && !bang {
return Err(QuarbError::parse(format!(
"macro '&{name}' is data-aware (its expansion reads the \
dataset); invoke it as '&{name}!(...)'"
)));
}
if !def.data_aware && bang {
return Err(QuarbError::parse(format!(
"'!' marks data-aware macros; '&{name}' is pure — invoke \
it without the '!'"
)));
}
Ok(())
}
fn bind(
&self,
name: &str,
params: &[String],
args: Vec<Operand>,
) -> Result<HashMap<String, Operand>> {
if args.len() != params.len() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' takes {} argument(s), got {}",
params.len(),
args.len()
)));
}
Ok(params.iter().cloned().zip(args).collect())
}
fn invoke_query_fragment(&mut self) -> Result<(String, Query, Option<Quant>)> {
let (name, args, bang, quant) = self.invocation()?;
let Some(def) = self.defs.get(&name).cloned() else {
return Err(QuarbError::parse(format!("unknown fragment '&{name}'")));
};
self.check_bang(&name, &def, bang)?;
let q = match def.body {
DefBody::Query(mut q) => {
let map = self.bind(&name, &def.params, args)?;
subst_query(
&mut q,
&Subst {
outer: &map,
hole: &map,
},
);
q
}
DefBody::Pipeline(_) => {
return Err(QuarbError::parse(format!(
"'&{name}' is a pipeline fragment; invoke it after a pipe"
)));
}
DefBody::Predicates(_) => {
return Err(QuarbError::parse(format!(
"'&{name}' is a predicate fragment; it refines the \
step before it ('/div&{name}') or reads as a \
condition ('[&{name}]')"
)));
}
DefBody::Macro(_) => {
let invited = invites_of(&args);
let text = self.expand_macro_path_text(&name, &def, args)?;
let wrap = |e: QuarbError| {
QuarbError::expansion(format!("in expansion of '&{name}' ('{text}')"))
.wrapping(e)
};
let tokens = lexer::lex(&text).map_err(wrap)?;
if matches!(tokens.first(), Some(Token::Pipe | Token::At)) {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to a pipeline fragment \
('{text}'); invoke it after a pipe"
)));
}
let q =
parse_with_data(&tokens, self.defs.before(&name), self.data).map_err(wrap)?;
self.record_captures(&name, &invited, crate::reflect::usage_of_query(&q));
q
}
};
Ok((name, q, quant))
}
fn invoke_pipeline_fragment(
&mut self,
pipe: &'static str,
pipeline: &mut Vec<Stage>,
) -> Result<()> {
let (name, args, bang, quant) = self.invocation()?;
let Some(def) = self.defs.get(&name).cloned() else {
return Err(QuarbError::parse(format!("unknown fragment '&{name}'")));
};
if quant.is_some() {
return Err(QuarbError::parse(format!(
"a quantifier doesn't ride the pipe; quantify '&{name}' \
at path position"
)));
}
self.check_bang(&name, &def, bang)?;
match def.body {
DefBody::Pipeline(stages) => {
let first = stage_pipe(stages.first().expect("non-empty checked at def time"));
if first != pipe {
return Err(QuarbError::parse(format!(
"fragment '&{name}' begins with '{first}' but was invoked with '{pipe}'"
)));
}
let map = self.bind(&name, &def.params, args)?;
let subst = Subst {
outer: &map,
hole: &map,
};
for mut stage in stages {
subst_stage(&mut stage, &subst);
pipeline.push(stage);
}
Ok(())
}
DefBody::Query(_) => Err(QuarbError::parse(format!(
"'&{name}' is a query fragment; invoke it at path position"
))),
DefBody::Predicates(preds) => {
if pipe != "|" {
return Err(QuarbError::parse(format!(
"'@|' aggregates the whole context; the predicate \
fragment '&{name}' filters per capsa — invoke it \
with '|'"
)));
}
let map = self.bind(&name, &def.params, args)?;
let subst = Subst {
outer: &map,
hole: &map,
};
for pred in preds {
match pred {
Predicate::Expr(mut e) => {
subst_pred_expr(&mut e, &subst);
pipeline.push(Stage::Filter(e));
}
_ => {
return Err(QuarbError::parse(format!(
"the predicate fragment '&{name}' holds a \
positional predicate; select positionally \
with '@| [n]'"
)));
}
}
}
Ok(())
}
DefBody::Macro(_) => {
let invited = invites_of(&args);
let text = self.expand_macro_text(&name, &def, args)?;
let wrap = |e: QuarbError| {
QuarbError::expansion(format!("in expansion of '&{name}' ('{text}')"))
.wrapping(e)
};
let tokens = lexer::lex(&text).map_err(wrap)?;
let first = match tokens.first() {
Some(Token::Pipe) => "|",
Some(Token::At) => "@|",
_ => {
return Err(QuarbError::parse(format!(
"macro '&{name}' expanded to a query fragment \
('{text}'); invoke it at path position — or \
have the body emit a leading '| ' to splice \
as pipeline stages"
)));
}
};
if first != pipe {
return Err(QuarbError::parse(format!(
"macro '&{name}' expanded to a '{first}' pipeline \
('{text}') but was invoked with '{pipe}'"
)));
}
let mut p = self.child(&tokens, self.defs.before(&name))?;
let mut stages = Vec::new();
p.pipeline_items(&mut stages, PipeMode::Nav).map_err(wrap)?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"macro '&{name}' expanded to text with trailing \
content ('{text}')"
)));
}
self.record_captures(&name, &invited, crate::reflect::usage_of_stages(&stages));
pipeline.extend(stages);
Ok(())
}
}
}
fn branch(&mut self) -> Result<Branch> {
if matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "*")
&& matches!(self.toks.get(self.pos + 1), Some(Token::SlashSlash))
{
self.pos += 1;
let mut elem = self.path_elem()?;
universalize(&mut elem)?;
return self.branch_tail(Anchor::Root, vec![elem], None);
}
let anchor = if matches!(self.peek(), Some(Token::Caret)) {
self.pos += 1;
Anchor::Root
} else {
self.mark_anchor().unwrap_or(Anchor::Current)
};
self.branch_tail(anchor, Vec::new(), None)
}
fn branch_tail(
&mut self,
anchor: Anchor,
mut steps: Vec<PathElem>,
mut projection: Option<Projection>,
) -> Result<Branch> {
while projection.is_none()
&& let Some(tok) = self.peek()
{
if matches!(
tok,
Token::Pipe
| Token::PipePipe
| Token::At
| Token::RParen
| Token::Correlate
| Token::Semi
| Token::Capsa(_)
) {
break;
}
if matches!(tok, Token::Amp) {
projection = self.splice_path_fragment(&mut steps, SplicePos::MidPath)?;
continue;
}
if self.peek_word(OR_WORDS) {
break;
}
if matches!(tok, Token::Dollar)
&& self.pattern_depth == 0
&& matches!(self.toks.get(self.pos + 1), Some(Token::Pipe))
{
break;
}
if is_projection_start(tok) {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
break;
}
steps.push(self.path_elem()?);
}
if projection.is_none() {
projection = self.projection()?;
}
if steps.is_empty() && projection.is_none() && anchor == Anchor::Current {
return Err(QuarbError::parse(
"a query branch needs at least one step or a projection",
));
}
Ok(Branch {
steps,
projection,
anchor,
})
}
fn splice_path_fragment(
&mut self,
steps: &mut Vec<PathElem>,
at: SplicePos,
) -> Result<Option<Projection>> {
let (name, args, bang, quant) = self.invocation()?;
let Some(def) = self.defs.get(&name).cloned() else {
return Err(QuarbError::parse(format!("unknown fragment '&{name}'")));
};
self.check_bang(&name, &def, bang)?;
let q = match def.body {
DefBody::Predicates(preds) => {
if quant.is_some() {
return Err(QuarbError::parse(format!(
"the predicate fragment '&{name}' refines what \
precedes it; a quantifier cannot ride it"
)));
}
let map = self.bind(&name, &def.params, args)?;
self.attach_predicate_fragment(&name, preds, &map, steps)?;
while matches!(self.peek(), Some(Token::LBracket)) {
let p = self.predicate()?;
self.attach_predicate_fragment(&name, vec![p], &HashMap::new(), steps)?;
}
return Ok(None);
}
DefBody::Pipeline(_) => {
return Err(QuarbError::parse(format!(
"fragment '&{name}' carries a pipeline; a pipe leaves \
navigation — invoke it at branch head or as a \
pipeline stage, not inside a walk"
)));
}
DefBody::Query(mut q) => {
let map = self.bind(&name, &def.params, args)?;
subst_query(
&mut q,
&Subst {
outer: &map,
hole: &map,
},
);
q
}
DefBody::Macro(_) => {
let invited = invites_of(&args);
let text = self.expand_macro_path_text(&name, &def, args)?;
let q = self.parse_expansion_query(&name, &text)?;
self.record_captures(&name, &invited, crate::reflect::usage_of_query(&q));
q
}
};
if !q.correlations.is_empty() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' carries a correlation; a chain \
splices at driver position only"
)));
}
if !q.pipeline.is_empty() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' carries a pipeline; a pipe leaves \
navigation — invoke it at branch head or as a pipeline \
stage, not inside a walk"
)));
}
if q.branches.iter().any(|b| b.anchor != Anchor::Current) {
return Err(QuarbError::parse(format!(
"fragment '&{name}' re-anchors; mid-path splicing \
continues the walk — invoke it at branch head"
)));
}
let (elems, projection) = self.finish_splice(&name, q.branches, quant, at)?;
steps.extend(elems);
Ok(projection)
}
fn finish_splice(
&mut self,
name: &str,
branches: Vec<Branch>,
glued_quant: Option<Quant>,
at: SplicePos,
) -> Result<(Vec<PathElem>, Option<Projection>)> {
let (mut elems, projection) = if branches.len() == 1 {
let b = branches.into_iter().next().expect("non-empty");
(b.steps, b.projection)
} else {
if branches.iter().any(|b| b.projection.is_some()) {
return Err(QuarbError::parse(format!(
"fragment '&{name}' is a projected union; it splices \
whole at branch head, not inside a walk"
)));
}
let alts: Vec<Vec<PathElem>> = branches.into_iter().map(|b| b.steps).collect();
(
vec![PathElem::Group(Group {
alts,
quant: Quant {
min: 1,
max: Some(1),
},
predicates: Vec::new(),
traits: Vec::new(),
reach: Reach::All,
})],
None,
)
};
let quant = match glued_quant {
Some(q) => Some(q),
None => self.group_quant()?,
};
if quant.is_some() || matches!(self.peek(), Some(Token::LBracket)) {
if projection.is_some() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' ends in a projection; refinement \
cannot follow it — refine through the pipe instead"
)));
}
let predicates = self.group_predicates()?;
let reach = self.reach();
elems = group_wrap(
elems,
quant.unwrap_or(Quant {
min: 1,
max: Some(1),
}),
predicates,
reach,
);
}
if projection.is_some() {
let continues = matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
| Token::Amp
| Token::ColonColon
| Token::ColonColonColon
| Token::SemiSemiSemi
)
);
if at == SplicePos::GroupAlt {
return Err(QuarbError::parse(format!(
"fragment '&{name}' ends in a projection; a group \
alternative walks on — invoke it where the branch ends"
)));
}
if continues {
return Err(QuarbError::parse(format!(
"fragment '&{name}' ends in a projection; navigation \
cannot continue past it — invoke it where the branch \
ends"
)));
}
}
if matches!(self.peek(), Some(Token::Lt { .. }))
&& matches!(self.toks.get(self.pos + 1), Some(Token::Name { .. }))
&& matches!(self.toks.get(self.pos + 2), Some(Token::Gt { .. }))
&& !matches!(
self.toks.get(self.pos + 3),
Some(Token::Name { .. } | Token::Regex(_))
)
{
return Err(QuarbError::parse(
"a fragment does not take a trailing trait selector; \
put the trait inside a definition: \
'def &errs: /entry<error> ;'",
));
}
Ok((elems, projection))
}
fn attach_predicate_fragment(
&self,
name: &str,
mut preds: Vec<Predicate>,
map: &HashMap<String, Operand>,
steps: &mut [PathElem],
) -> Result<()> {
let subst = Subst {
outer: map,
hole: map,
};
for pred in &mut preds {
if let Predicate::Expr(e) = pred {
subst_pred_expr(e, &subst);
}
}
match steps.last_mut() {
Some(PathElem::Step(s)) => {
s.predicates.extend(preds);
Ok(())
}
Some(PathElem::Group(g)) => {
if preds.iter().any(|p| !matches!(p, Predicate::Expr(_))) {
return Err(QuarbError::parse(
"a group takes expression predicates only \
(positional selection has no order across \
repetition tiers)",
));
}
g.predicates.extend(preds);
Ok(())
}
Some(PathElem::Mark(_) | PathElem::Push { .. }) => Err(QuarbError::parse(format!(
"the predicate fragment '&{name}' refines a hop or a \
group; a mark or a push takes no predicates"
))),
None => Err(QuarbError::parse(format!(
"the predicate fragment '&{name}' refines the step \
before it; nothing precedes it here — walk first \
('/div&{name}')"
))),
}
}
fn parse_expansion_query(&self, name: &str, text: &str) -> Result<Query> {
let wrap = |e: QuarbError| {
QuarbError::expansion(format!("in expansion of '&{name}' ('{text}')")).wrapping(e)
};
let tokens = lexer::lex(text).map_err(wrap)?;
if matches!(tokens.first(), Some(Token::Pipe | Token::At)) {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to a pipeline fragment \
('{text}'); at path position it must expand to \
navigation steps"
)));
}
let mut p = self.child(&tokens, self.defs.before(name))?;
p.pattern_depth = self.pattern_depth;
p.predicate_depth = self.predicate_depth;
p.parse().map_err(wrap)
}
fn mark_anchor_ahead(&self) -> bool {
self.peek_anchor().is_some() || matches!(self.peek(), Some(Token::IdAnchorOpen))
}
fn refuse_single_paren_anchor(&self) -> Result<()> {
if matches!(self.toks.get(self.pos), Some(Token::LParen))
&& let Some((anchor, _)) = self.anchor_shape()
{
let spelled = match anchor {
Anchor::MarksAll => "((@))".to_string(),
Anchor::MarksNamed(n) => format!("((@{n}))"),
Anchor::Id(op) => format!("((!{}))", crate::unparse::operand_text(&op)),
Anchor::Mark(n) => format!("$$.{n}"),
Anchor::MarkIndex(n) => format!("$$.{n}"),
_ => "$$.".to_string(),
};
return Err(QuarbError::parse(format!(
"a mark anchor is a node: write '{spelled}' (single parentheses are the value side)"
)));
}
Ok(())
}
fn peek_anchor(&self) -> Option<(Anchor, usize)> {
if !matches!(self.toks.get(self.pos), Some(Token::MarkOpen)) {
return None;
}
self.anchor_shape()
}
fn anchor_shape(&self) -> Option<(Anchor, usize)> {
let continues = |i: usize| {
matches!(
self.toks.get(i),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::ColonColon
| Token::ColonColonColon
| Token::SemiSemiSemi
)
)
};
let live = matches!(self.toks.get(self.pos), Some(Token::MarkOpen));
let continues = |i: usize| live || continues(i);
let word = |text: &str| {
text.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_')
&& !text.contains('/')
};
match self.toks.get(self.pos + 1) {
Some(Token::At) => match self.toks.get(self.pos + 2) {
Some(Token::RParen) => Some((Anchor::MarksAll, 3)),
Some(Token::Name {
text,
quoted: false,
..
}) if word(text) && matches!(self.toks.get(self.pos + 3), Some(Token::RParen)) => {
Some((Anchor::MarksNamed(text.clone()), 4))
}
_ => None,
},
Some(Token::Name {
text,
quoted: false,
..
}) if matches!(self.toks.get(self.pos + 2), Some(Token::RParen)) => {
if text == "." {
Some((Anchor::MarkTop, 3))
} else if text.chars().all(|c| c.is_ascii_digit()) {
if continues(self.pos + 3) {
text.parse()
.ok()
.filter(|&n| n >= 1)
.map(|n| (Anchor::MarkIndex(n), 3))
} else {
None
}
} else if word(text) && continues(self.pos + 3) {
Some((Anchor::Mark(text.clone()), 3))
} else {
None
}
}
_ => None,
}
}
fn mark_anchor(&mut self) -> Option<Anchor> {
if matches!(self.peek(), Some(Token::IdAnchorOpen)) {
return self.id_anchor().ok();
}
let (anchor, len) = self.peek_anchor()?;
self.pos += len;
Some(anchor)
}
fn id_anchor(&mut self) -> Result<Anchor> {
self.pos += 1;
let op = self.additive()?;
for _ in 0..2 {
if !matches!(self.peek(), Some(Token::RParen)) {
return Err(QuarbError::parse(
"expected '))' to close the node-id anchor '((!…))'",
));
}
self.pos += 1;
}
Ok(Anchor::Id(Box::new(op)))
}
fn record_args(&mut self) -> Result<FnCall> {
self.expect(Token::LParen, "'(' after '%'")?;
let mut args = Vec::new();
if !matches!(self.peek(), Some(Token::RParen)) {
loop {
self.record_item(&mut args)?;
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
} else {
break;
}
}
}
self.expect(Token::RParen, "')' to close the record")?;
Ok(FnCall {
name: "rec".to_string(),
args,
})
}
fn func_call(&mut self) -> Result<FnCall> {
let name = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse("expected a function name after '|'"));
}
};
if let Some(pointer) = retired_function(&name) {
return Err(QuarbError::parse(pointer));
}
let mut args = Vec::new();
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
let convention = is_record_convention(&name);
if !matches!(self.peek(), Some(Token::RParen)) {
loop {
if convention {
self.record_item(&mut args)?;
} else if name == "csv"
&& args.is_empty()
&& let Some(Token::Name {
text, quoted: true, ..
}) = self.peek()
{
let delimiter = match text.as_str() {
"comma" => ",".to_string(),
"semicolon" => ";".to_string(),
"tab" => "\t".to_string(),
"pipe" => "|".to_string(),
t if t.chars().count() == 1 && t.is_ascii() => t.to_string(),
_ => {
return Err(QuarbError::parse(format!(
"csv takes one delimiter first: csv(\";\"), csv(\"tab\") \
— not {text:?}"
)));
}
};
args.push(Arg::Named {
name: "delimiter".into(),
value: Value::Str(delimiter.to_string()),
});
self.pos += 1;
} else {
args.push(self.func_arg()?);
}
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
} else {
break;
}
}
}
self.expect(Token::RParen, "')' to close function arguments")?;
}
if !matches!(name.as_str(), "window" | "substr")
&& args.iter().any(|a| matches!(a, Arg::Range(_, _)))
{
return Err(QuarbError::parse(format!(
"'{name}' takes no range argument ('window(a..b)' does)"
)));
}
let call = FnCall { name, args };
validate_stat_args(&call)?;
Ok(call)
}
fn record_item(&mut self, args: &mut Vec<Arg>) -> Result<()> {
if let Some(Token::Name { text, .. }) = self.peek()
&& matches!(self.toks.get(self.pos + 1), Some(Token::Eq))
{
let key = text.clone();
self.pos += 2;
args.push(Arg::Lit(Value::Str(key)));
}
args.push(self.func_arg()?);
Ok(())
}
fn func_arg(&mut self) -> Result<Arg> {
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& let Some((a, b)) = text.split_once("..")
{
let start = if a.is_empty() { None } else { a.parse().ok() };
let end = if b.is_empty() { None } else { b.parse().ok() };
if (a.is_empty() || start.is_some()) && (b.is_empty() || end.is_some()) {
self.pos += 1;
return Ok(Arg::Range(start, end));
}
}
match self.cond_expr()? {
Operand::Lit(v) => Ok(Arg::Lit(v)),
expr => Ok(Arg::Expr(expr)),
}
}
fn projection(&mut self) -> Result<Option<Projection>> {
let proj = match self.peek() {
Some(Token::ColonColon) => {
self.pos += 1;
Projection::Property(self.opt_projection_name())
}
Some(Token::ColonColonColon) => {
self.pos += 1;
Projection::CoreMeta(self.require_projection_name("core metadata `:::`")?)
}
Some(Token::SemiSemiSemi) => {
self.pos += 1;
Projection::AdapterMeta(self.require_projection_name("adapter metadata `::::`")?)
}
_ => return Ok(None),
};
Ok(Some(proj))
}
fn opt_projection_name(&mut self) -> Option<String> {
if let Some(Token::Name {
text,
quoted,
glued,
}) = self.peek()
{
if !glued {
return None;
}
if !quoted && is_bool_word(text) {
return None;
}
let name = text.clone();
self.pos += 1;
Some(name)
} else {
None
}
}
fn require_projection_name(&mut self, what: &str) -> Result<String> {
self.opt_projection_name()
.ok_or_else(|| QuarbError::parse(format!("{what} needs a key")))
}
fn path_elem(&mut self) -> Result<PathElem> {
self.descend()?;
let r = self.path_elem_inner();
self.nest_depth -= 1;
r
}
fn path_elem_inner(&mut self) -> Result<PathElem> {
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& text.starts_with('.')
&& !text[1..].starts_with('.')
&& (self.pattern_depth == 0
|| !matches!(self.toks.get(self.pos + 1), Some(Token::LParen)))
{
let name = if text == "." {
match self.toks.get(self.pos + 1) {
Some(Token::Name {
text: q,
quoted: true,
glued: true,
}) => {
let q = q.clone();
self.pos += 1;
Some(q)
}
_ => None,
}
} else {
let n = &text[1..];
if !n.chars().next().is_some_and(|c| c.is_alphabetic()) {
return Err(QuarbError::parse(format!(
"a mark name starts with a letter ('.name'); quote any other \
('.\"{n}\"'), and recall a position with ((.N))"
)));
}
Some(n.to_string())
};
self.pos += 1;
return Ok(PathElem::Mark(name));
}
if self.is_resolution_ahead() {
return Ok(PathElem::Step(self.resolution_step()?));
}
if matches!(self.peek(), Some(Token::Resolve | Token::ReverseResolve)) {
return Ok(PathElem::Step(self.bare_resolution_step()?));
}
if matches!(self.peek(), Some(Token::LParen)) {
return Ok(PathElem::Group(self.group(None)?));
}
let axis = self.axis()?;
if matches!(self.peek(), Some(Token::LParen)) {
return Ok(PathElem::Group(self.group(Some(axis))?));
}
if matches!(self.peek(), Some(Token::Quant { .. })) {
if !matches!(
axis,
Axis::Child
| Axis::Parent
| Axis::NextSibling
| Axis::PrevSibling
| Axis::OutLink
| Axis::InLink
) {
return Err(QuarbError::parse(
"a quantifier attaches to a single-hop operator or a \
parenthesized group",
));
}
let hop = Step {
axis,
matcher: Matcher::Dot,
traits: Vec::new(),
predicates: Vec::new(),
leaf: false,
};
let quant = self.group_quant()?.expect("peeked a quantifier");
let predicates = self.group_predicates()?;
return Ok(PathElem::Group(Group {
alts: vec![vec![PathElem::Step(hop)]],
quant,
predicates,
traits: Vec::new(),
reach: self.reach(),
}));
}
let matcher = self.matcher_after(&axis)?;
let step = self.finish_step(axis, matcher)?;
if matches!(self.peek(), Some(Token::Quant { .. })) {
let quant = self.group_quant()?.expect("peeked a quantifier");
let predicates = self.group_predicates()?;
return Ok(PathElem::Group(Group {
alts: vec![vec![PathElem::Step(step)]],
quant,
predicates,
traits: Vec::new(),
reach: self.reach(),
}));
}
Ok(PathElem::Step(step))
}
fn group(&mut self, pending: Option<Axis>) -> Result<Group> {
self.pos += 1; self.pattern_depth += 1;
let mut alts = Vec::new();
let alts_result = loop {
match self.group_alt(&pending) {
Ok(alt) => alts.push(alt),
Err(e) => break Err(e),
}
match self.peek() {
Some(Token::Pipe) => self.pos += 1,
Some(Token::RParen) => {
self.pos += 1;
break Ok(());
}
_ => {
break Err(QuarbError::parse(
"expected '|' or ')' in a path-pattern group",
));
}
}
};
self.pattern_depth -= 1;
alts_result?;
let quant = self.group_quant()?.unwrap_or(Quant {
min: 1,
max: Some(1),
});
let traits = self.group_traits()?;
let predicates = self.group_predicates()?;
let reach = self.reach();
Ok(Group {
alts,
quant,
predicates,
traits,
reach,
})
}
fn group_traits(&mut self) -> Result<Vec<TraitClause>> {
let mut traits = Vec::new();
while let Some(clauses) = self.try_trait()? {
traits.extend(clauses);
}
Ok(traits)
}
fn group_predicates(&mut self) -> Result<Vec<Predicate>> {
let mut predicates = Vec::new();
while matches!(self.peek(), Some(Token::LBracket)) {
match self.predicate()? {
p @ Predicate::Expr(_) => predicates.push(p),
_ => {
return Err(QuarbError::parse(
"a group takes expression predicates only \
(positional selection has no order across \
repetition tiers)",
));
}
}
}
Ok(predicates)
}
fn group_alt(&mut self, pending: &Option<Axis>) -> Result<Vec<PathElem>> {
let mut elems = Vec::new();
if let Some(axis) = pending {
match self.peek() {
Some(Token::Name { .. } | Token::Regex(_)) => {
let matcher = self.matcher_after(&axis)?;
elems.push(PathElem::Step(self.finish_step(axis.clone(), matcher)?));
}
_ => {
return Err(QuarbError::parse(
"the operator before '(' distributes over name \
alternatives; write the strict form '(/p|/div)'",
));
}
}
}
loop {
match self.peek() {
Some(Token::Pipe | Token::RParen) | None => break,
Some(Token::Amp) => {
self.splice_path_fragment(&mut elems, SplicePos::GroupAlt)?;
}
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with('.')
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen)) =>
{
elems.push(self.pattern_push()?);
}
_ => elems.push(self.path_elem()?),
}
}
if !elems
.iter()
.any(|e| matches!(e, PathElem::Step(_) | PathElem::Group(_)))
{
return Err(QuarbError::parse(
"a path-pattern alternative needs at least one hop",
));
}
Ok(elems)
}
fn pattern_push(&mut self) -> Result<PathElem> {
let name = match self.bump() {
Some(Token::Name { text, .. }) => {
let rest = &text[1..];
if rest.is_empty() {
None
} else {
Some(rest.to_string())
}
}
_ => unreachable!("peeked a dot-leading name"),
};
self.pos += 1; let depth = std::mem::take(&mut self.pattern_depth);
let save = self.pos;
self.subquery_depth += 1;
let tried = self.parse_query();
self.subquery_depth -= 1;
let body = if let Ok(q) = tried
&& matches!(self.peek(), Some(Token::RParen))
{
PushBody::Query(Box::new(q))
} else {
self.pos = save;
match self.additive() {
Ok(expr) => PushBody::Expr(expr),
Err(e) => {
self.pattern_depth = depth;
return Err(e);
}
}
};
self.pattern_depth = depth;
self.expect(Token::RParen, "')' to close a pattern push")?;
Ok(PathElem::Push { name, body })
}
fn group_quant(&mut self) -> Result<Option<Quant>> {
match self.peek() {
Some(Token::Quant { min, max }) => {
let (min, max) = (*min, *max);
if max.is_some_and(|n| n < min) {
return Err(QuarbError::parse(format!(
"quantifier {{{min},{}}} has max below min",
max.expect("checked")
)));
}
self.pos += 1;
Ok(Some(Quant { min, max }))
}
Some(Token::Name {
text,
quoted: false,
glued: true,
}) if text == "+" => {
self.pos += 1;
Ok(Some(Quant { min: 1, max: None }))
}
Some(Token::Name {
text,
quoted: false,
glued: true,
}) if text == "*" => {
self.pos += 1;
Ok(Some(Quant { min: 0, max: None }))
}
_ => Ok(None),
}
}
fn finish_step(&mut self, axis: Axis, matcher: Matcher) -> Result<Step> {
let mut traits = Vec::new();
while let Some(clauses) = self.try_trait()? {
traits.extend(clauses);
}
let mut predicates = Vec::new();
while matches!(self.peek(), Some(Token::LBracket)) {
predicates.push(self.predicate()?);
}
let map_pipe_ahead =
self.pattern_depth == 0 && matches!(self.toks.get(self.pos + 1), Some(Token::Pipe));
let leaf = if matches!(self.peek(), Some(Token::Dollar)) && !map_pipe_ahead {
self.pos += 1;
true
} else {
false
};
Ok(Step {
axis,
matcher,
traits,
predicates,
leaf,
})
}
fn is_resolution_ahead(&self) -> bool {
matches!(self.toks.get(self.pos), Some(Token::ColonColon))
&& matches!(self.toks.get(self.pos + 1), Some(Token::Name { .. }))
&& matches!(
self.toks.get(self.pos + 2),
Some(Token::Resolve | Token::ReverseResolve | Token::ValueMatch)
)
}
fn resolution_step(&mut self) -> Result<Step> {
self.pos += 1; let property = match self.bump() {
Some(Token::Name { text, .. }) => text.clone(),
_ => {
return Err(QuarbError::parse(
"expected a property name before '-->' or '=>'",
));
}
};
match self.peek() {
Some(Token::ReverseResolve) => Err(QuarbError::parse(format!(
"a property before '<--' has no meaning: the property is the \
referring nodes' own and goes in the argument — write \
'<--(::{property})', or '<--(/scope/*::{property})' to bound \
the search"
))),
Some(Token::ValueMatch) => {
self.pos += 1;
let (scope, target) = self.match_argument()?;
if scope.is_none() && self.predicate_depth > 0 {
return Err(QuarbError::parse(
"an unscoped value match '=>(::b)' is not allowed inside a \
predicate (it would walk the whole arbor per node); give \
it a scope, '=>(/path/*::b)'",
));
}
self.finish_step(
Axis::ValueMatch {
property,
scope,
target,
},
Matcher::Any,
)
}
_ => self.resolution_tail(Some(property)),
}
}
fn bare_resolution_step(&mut self) -> Result<Step> {
if matches!(self.peek(), Some(Token::ReverseResolve)) {
return self.reverse_step();
}
self.resolution_tail(None)
}
fn resolution_tail(&mut self, property: Option<String>) -> Result<Step> {
self.pos += 1; let hint = self.bare_hint();
self.finish_step(
Axis::Resolve {
property,
label: hint,
},
Matcher::Any,
)
}
fn bare_hint(&mut self) -> Option<String> {
match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) => {
let h = text.clone();
self.pos += 1;
Some(h)
}
_ => None,
}
}
fn reverse_step(&mut self) -> Result<Step> {
self.pos += 1; let hint = self.bare_hint();
let (scope, property) = if matches!(self.peek(), Some(Token::LParen)) {
let (scope, prop) = self.reverse_argument()?;
(scope, prop)
} else {
(None, RefProp::Default)
};
if scope.is_none() && property == RefProp::Any {
return Err(QuarbError::parse(
"'<--(::*)' asks every property of every node: give it a scope, \
'<--(/path/*::*)'",
));
}
if scope.is_none() && self.predicate_depth > 0 {
return Err(QuarbError::parse(
"an unscoped reverse resolution '<--' is not allowed inside a \
predicate (it would walk the whole arbor per node); give it a \
scope, '<--(/path/*::y)', or use an incoming edge '<-'",
));
}
self.finish_step(
Axis::ReverseResolve {
label: hint,
scope,
property,
},
Matcher::Any,
)
}
fn reverse_argument(&mut self) -> Result<(Option<ScopePath>, RefProp)> {
self.pos += 1; let scope = self.scope_path()?;
let property = if matches!(self.peek(), Some(Token::ColonColon)) {
self.pos += 1;
match self.bump() {
Some(Token::Name { text, quoted, .. }) if text == "*" && !quoted => RefProp::Any,
Some(Token::Name { text, .. }) => RefProp::Named(text.clone()),
_ => {
return Err(QuarbError::parse(
"the reverse arrow's argument names a property after '::' \
('<--(::y)', '<--(/scope/*::y)', '<--(/scope/*::*)')",
));
}
}
} else {
RefProp::Default
};
if !matches!(self.bump(), Some(Token::RParen)) {
return Err(QuarbError::parse(
"expected ')' to close the reverse arrow's argument",
));
}
if scope.is_none() && property == RefProp::Default {
return Err(QuarbError::parse(
"'<--()' is empty: write '<--' for the whole arbor, '<--(::y)' \
for a property, or '<--(/scope/*)' for a scope",
));
}
Ok((scope, property))
}
fn match_argument(&mut self) -> Result<(Option<ScopePath>, String)> {
if !matches!(self.bump(), Some(Token::LParen)) {
return Err(QuarbError::parse(
"the value match takes its argument in parentheses: \
'::a=>(/scope/*::b)' or '::a=>(::b)'",
));
}
let scope = self.scope_path()?;
if !matches!(self.bump(), Some(Token::ColonColon)) {
return Err(QuarbError::parse(
"the value match names the property to compare after '::': \
'::a=>(/scope/*::b)'",
));
}
let target = match self.bump() {
Some(Token::Name { text, .. }) if text != "*" => text.clone(),
_ => {
return Err(QuarbError::parse(
"the value match compares one named property ('::b')",
));
}
};
if !matches!(self.bump(), Some(Token::RParen)) {
return Err(QuarbError::parse(
"expected ')' to close the value match's argument",
));
}
Ok((scope, target))
}
fn scope_path(&mut self) -> Result<Option<ScopePath>> {
if matches!(self.peek(), Some(Token::ColonColon)) {
return Ok(None);
}
if matches!(self.peek(), Some(Token::Caret)) {
self.pos += 1;
}
let mut steps = Vec::new();
while matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
)
) {
steps.push(self.path_elem()?);
}
if steps.is_empty() {
return Err(QuarbError::parse(
"the arrow's argument opens with a scope path ('/books/*', '//a') \
or with '::'",
));
}
Ok(Some(ScopePath(steps)))
}
fn predicate(&mut self) -> Result<Predicate> {
let depth = std::mem::take(&mut self.pattern_depth);
self.predicate_depth += 1;
let result = self.predicate_inner();
self.predicate_depth -= 1;
self.pattern_depth = depth;
result
}
fn predicate_inner(&mut self) -> Result<Predicate> {
self.pos += 1; if let (
Some(Token::Name {
text,
quoted: false,
..
}),
Some(Token::RBracket),
) = (self.toks.get(self.pos), self.toks.get(self.pos + 1))
{
if let Ok(n) = text.parse::<i64>() {
self.pos += 2;
return Ok(Predicate::Index(n));
}
let digits = text.strip_prefix('-').unwrap_or(text);
if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(QuarbError::parse(format!(
"positional index [{text}] is out of range"
)));
}
if let Some((a, b)) = text.split_once("..") {
let start = if a.is_empty() { None } else { a.parse().ok() };
let end = if b.is_empty() { None } else { b.parse().ok() };
if (a.is_empty() || start.is_some()) && (b.is_empty() || end.is_some()) {
self.pos += 2;
return Ok(Predicate::Range(start, end));
}
}
}
let expr = self.pred_or()?;
self.expect(
Token::RBracket,
"']' to close a predicate (')' for the '(?' spelling)",
)?;
Ok(Predicate::Expr(expr))
}
fn refuse_bare_bool_word(&self) -> Result<()> {
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& let Some((w, sym)) = BARE_BOOL_WORDS.iter().find(|(w, _)| w == text)
{
return Err(QuarbError::parse(format!(
"the bare word '{w}' is retired: write '{sym}' (or the dotted '.{w}.')"
)));
}
Ok(())
}
fn pred_or(&mut self) -> Result<PredExpr> {
let mut left = self.pred_and()?;
self.refuse_bare_bool_word()?;
while self.eat_word(OR_WORDS)
|| matches!(self.peek(), Some(Token::PipePipe)) && {
self.pos += 1;
true
}
{
let right = self.pred_and()?;
self.refuse_bare_bool_word()?;
left = PredExpr::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn pred_and(&mut self) -> Result<PredExpr> {
let mut left = self.pred_not()?;
while self.eat_word(AND_WORDS)
|| matches!(self.peek(), Some(Token::AmpAmp)) && {
self.pos += 1;
true
}
{
let right = self.pred_not()?;
left = PredExpr::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn pred_not(&mut self) -> Result<PredExpr> {
self.refuse_bare_bool_word()?;
self.descend()?;
let r = self.pred_not_inner();
self.nest_depth -= 1;
r
}
fn pred_not_inner(&mut self) -> Result<PredExpr> {
if matches!(self.peek(), Some(Token::Bang)) {
self.pos += 1;
return Ok(PredExpr::Not(Box::new(self.pred_not()?)));
}
if self.eat_word(NOT_WORDS) {
return Ok(PredExpr::Not(Box::new(self.pred_not()?)));
}
self.pred_primary()
}
fn pred_primary(&mut self) -> Result<PredExpr> {
let left = self.additive()?;
if let Some(t @ (Token::Gt { spaced: false, .. } | Token::Lt { spaced: false, .. })) =
self.peek()
{
let c = if matches!(t, Token::Gt { .. }) {
'>'
} else {
'<'
};
return Err(QuarbError::parse(format!(
"'{c}' glued to what follows is the sibling hop; the comparison \
is spaced: '::x {c} 3'"
)));
}
if let Some(t @ (Token::Gt { before: false, .. } | Token::Lt { before: false, .. })) =
self.peek()
{
let c = if matches!(t, Token::Gt { .. }) {
'>'
} else {
'<'
};
return Err(QuarbError::parse(format!(
"'{c}' is an operator and takes a space on both sides: write ' {c} ' \
(the sibling hop is glued to its name: '{c}name')"
)));
}
if let Some(op) = self.cmp_op() {
match (op, self.peek()) {
(CmpOp::Eq, Some(Token::Regex(_))) => {
return Err(QuarbError::parse(
"'= (/…/)' is retired: the pattern comparison is '== (/…/)'",
));
}
(CmpOp::Ne, Some(Token::Regex(_))) => {
return Err(QuarbError::parse(
"'!= (/…/)' is retired: the pattern comparison is '!== (/…/)'",
));
}
_ => {}
}
if matches!(op, CmpOp::Eq | CmpOp::Ne | CmpOp::Match | CmpOp::NotMatch)
&& let Some(right) =
self.pattern_operand(matches!(op, CmpOp::Match | CmpOp::NotMatch))?
{
if matches!(op, CmpOp::Eq | CmpOp::Ne) {
let eq = if matches!(op, CmpOp::Eq) { "==" } else { "!==" };
return Err(QuarbError::parse(format!(
"a pattern literal after '{}' is retired: the pattern comparison is '{eq}' ('{eq} {}')",
cmp_text(op),
crate::unparse::operand_text(&right)
)));
}
let op = match op {
CmpOp::Match => CmpOp::Eq,
CmpOp::NotMatch => CmpOp::Ne,
op => op,
};
return Ok(PredExpr::Compare(left, op, right));
}
let right = self.additive()?;
Ok(PredExpr::Compare(left, op, right))
} else {
Ok(PredExpr::Truthy(left))
}
}
fn pattern_operand(&mut self, bare: bool) -> Result<Option<Operand>> {
let start = self.pos;
let star = |t: &Token| matches!(t, Token::Name { text, quoted: false, .. } if text == "*");
let is_glued = |t: &Token| match t {
Token::Name { glued, .. } => *glued,
_ => false,
};
let mut segs: Vec<PatSeg> = Vec::new();
loop {
let Some(tok) = self.peek() else { break };
let first = segs.is_empty();
if !first && matches!(tok, Token::Interp(_)) {
return Err(QuarbError::parse(
"a pattern segment must be a literal string — \
an interpolated \"${...}\" hole cannot glob; \
use == with a path for a dynamic pattern",
));
}
if !first && !is_glued(tok) {
break;
}
match tok {
t if star(t) => {
if matches!(segs.last(), Some(PatSeg::Star)) {
break;
}
segs.push(PatSeg::Star);
}
Token::Name {
text, quoted: true, ..
} => {
if matches!(segs.last(), Some(PatSeg::Lit(_))) {
break;
}
segs.push(PatSeg::Lit(text.clone()));
}
_ => break,
}
self.pos += 1;
}
let stars = segs.iter().filter(|s| matches!(s, PatSeg::Star)).count();
let lits = segs.len() - stars;
if stars >= 1 && lits >= 1 {
if let Some(t) = self.peek()
&& is_glued(t)
{
return Err(QuarbError::parse(
"a pattern segment must be a quoted string or the \
glob star",
));
}
return Ok(Some(Operand::Pattern(segs)));
}
if bare && stars == 0 && lits == 1 {
let continues = match self.peek() {
Some(Token::Name {
glued,
text,
quoted: false,
..
}) => *glued || matches!(text.as_str(), "+" | "-" | "*" | "div" | "mod"),
Some(Token::Name {
glued,
quoted: true,
..
}) => *glued,
Some(Token::Interp(_)) => true,
_ => false,
};
if !continues {
return Ok(Some(Operand::Pattern(segs)));
}
}
if stars == 1 && lits == 0 {
return Err(QuarbError::parse(
"a lone '*' is not a pattern — attach it to a string \
(*\"...\"*, \"...\"*), quote it ('*') to compare \
against the literal star, or test existence with the \
bare key",
));
}
self.pos = start;
Ok(None)
}
fn additive(&mut self) -> Result<Operand> {
let mut left = self.multiplicative()?;
loop {
let op = match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) if text == "+" => ArithOp::Add,
Some(Token::Name {
text,
quoted: false,
..
}) if text == "-" => ArithOp::Sub,
_ => {
self.one_sided_arith(&left)?;
break;
}
};
self.pos += 1;
let right = self.multiplicative()?;
left = Operand::Arith {
op,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn one_sided_arith(&self, left: &Operand) -> Result<()> {
let Some(Token::Name {
text,
quoted: false,
glued: false,
}) = self.peek()
else {
return Ok(());
};
let numeric = |t: &str| t.starts_with(|c: char| c.is_ascii_digit() || c == '.');
let mut chars = text.chars();
let op = match (chars.next(), chars.as_str()) {
(Some(c @ ('+' | '-' | '*')), rest) if numeric(rest) => c,
(_, _) if numeric(text) => match crate::unparse::operand_text(left).chars().last() {
Some(c @ ('+' | '-' | '*')) => c,
_ => return Ok(()),
},
_ => return Ok(()),
};
Err(QuarbError::parse(format!(
"'{op}' is an operator and takes a space on both sides: write ' {op} '"
)))
}
fn multiplicative(&mut self) -> Result<Operand> {
let mut left = self.unary()?;
loop {
let op = match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) if text == "*" => ArithOp::Mul,
Some(Token::Name {
text,
quoted: false,
..
}) if text == "div" => ArithOp::Div,
Some(Token::Name {
text,
quoted: false,
..
}) if text == "idiv" => ArithOp::IDiv,
Some(Token::Name {
text,
quoted: false,
..
}) if text == "mod" => ArithOp::Mod,
_ => break,
};
self.pos += 1;
let right = self.unary()?;
left = Operand::Arith {
op,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn unary(&mut self) -> Result<Operand> {
self.descend()?;
let r = self.unary_inner();
self.nest_depth -= 1;
r
}
fn unary_inner(&mut self) -> Result<Operand> {
let mut o = if matches!(self.peek(), Some(Token::Field)) {
Operand::Topic
} else {
self.unary_primary()?
};
if let Some(Token::Capsa(body)) = self.peek() {
let body = body.clone();
let reg = match body.as_str() {
"." => RegRef::Top,
"@." => RegRef::Whole,
"%." => RegRef::Record,
"%%." => RegRef::FullRecord,
b => {
let rest = &b[1..];
match rest.parse::<usize>() {
Ok(n) => RegRef::Index(n),
Err(_) => RegRef::Named(rest.to_string()),
}
}
};
let bare = |o: &Operand| {
matches!(
o,
Operand::Rel {
projection: None,
..
} | Operand::Ctx {
projection: None,
..
}
)
};
let ok = match &o {
Operand::Outer(inner) => bare(inner),
other => bare(other),
};
if !ok {
return Err(QuarbError::parse(format!(
"':{body}' reads the register of the capsa standing at a node: write it after a node path (<-link:{body}, \\/*:{body})"
)));
}
self.pos += 1;
o = Operand::PeerReg {
at: Box::new(o),
reg,
};
}
while matches!(self.peek(), Some(Token::Field)) {
self.pos += 1;
let name = match self.bump() {
Some(Token::Name { text, .. }) => text.clone(),
_ => return Err(QuarbError::parse("expected a field name after ':'")),
};
if let Operand::Rel {
projection: None, ..
} = &o
{
return Err(QuarbError::parse(format!(
"':{name}' reads a record's field; a node's property is '::{name}'"
)));
}
o = Operand::Field {
base: Box::new(o),
name,
};
}
while matches!(self.peek(), Some(Token::ColonColonColon)) {
if !matches!(
o,
Operand::Recall(_)
| Operand::Topic
| Operand::Field { .. }
| Operand::ValueMeta { .. }
| Operand::Piped { .. }
| Operand::PeerReg { .. }
) {
return Err(QuarbError::parse(
"':::key' after a value reads its provenance: write it after a register value ($.name), the topic ($_), or a record field",
));
}
self.pos += 1;
let key = self.require_projection_name("value provenance `:::`")?;
o = Operand::ValueMeta {
base: Box::new(o),
key,
};
}
Ok(o)
}
fn unary_primary(&mut self) -> Result<Operand> {
if matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "-") {
self.pos += 1;
return Ok(Operand::Neg(Box::new(self.unary()?)));
}
if matches!(self.peek(), Some(Token::MarkOpen)) {
return self.operand();
}
if matches!(self.peek(), Some(Token::LParen)) {
self.refuse_single_paren_anchor()?;
let start = self.pos;
if !matches!(self.toks.get(self.pos + 1), Some(Token::Field)) {
match self.rel_from_group() {
Ok(op) => return Ok(op),
Err(_) => self.pos = start,
}
}
self.pos += 1;
let inner = self.cond_expr()?;
let stages = self.inline_pipe_tail()?;
self.expect(Token::RParen, "')' to close a group")?;
return Ok(if stages.is_empty() {
inner
} else {
Operand::Piped {
expr: Box::new(inner),
stages,
}
});
}
self.operand()
}
fn inline_pipe_tail(&mut self) -> Result<Vec<Stage>> {
let mut stages = Vec::new();
loop {
match self.peek() {
Some(Token::Pipe) if matches!(self.toks.get(self.pos + 1), Some(Token::Amp)) => {
self.pos += 1;
self.invoke_inline_fragment("|", &mut stages)?;
}
Some(Token::Pipe) => {
self.pos += 1;
stages.push(self.inline_stage()?);
}
Some(Token::At)
if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe))
&& matches!(self.toks.get(self.pos + 2), Some(Token::Amp)) =>
{
self.pos += 2;
self.invoke_inline_fragment("@|", &mut stages)?;
}
Some(Token::At) if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe)) => {
self.pos += 2;
stages.push(self.inline_agg_stage()?);
}
Some(Token::Dollar) if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe)) => {
self.pos += 2;
stages.push(Stage::Map(Box::new(self.map_stage()?)));
}
_ => break,
}
}
Ok(stages)
}
fn cond_expr(&mut self) -> Result<Operand> {
let cond = self.pred_or()?;
if matches!(self.peek(), Some(Token::QuestionEq)) {
self.pos += 1;
let PredExpr::Truthy(scrutinee) = cond else {
return Err(QuarbError::parse(
"the value match compares a VALUE: '(x ?= k ? r : else)' — a boolean condition belongs to the plain conditional",
));
};
let mut arms = Vec::new();
let other = loop {
let (test, regex) = if let Some(Token::Regex(pat)) = self.peek() {
let pat = pat.clone();
self.pos += 1;
(Operand::Lit(Value::Str(pat)), true)
} else {
(self.additive()?, false)
};
if !matches!(self.peek(), Some(Token::Question { .. })) {
if regex {
return Err(QuarbError::parse(
"a value match needs a final else after the regex arm: '(x ?= ~(pat) ? r : else)'",
));
}
break test;
}
self.binary_marker('?')?;
let result = self.additive()?;
self.expect_else("':' after a value-match arm")?;
arms.push((test, regex, result));
};
if arms.is_empty() {
return Err(QuarbError::parse(
"a value match needs at least one arm: '(x ?= k ? r : else)'",
));
}
return Ok(Operand::Match {
scrutinee: Box::new(scrutinee),
arms,
other: Box::new(other),
});
}
if matches!(self.peek(), Some(Token::Question { .. })) {
self.binary_marker('?')?;
let then = self.cond_expr()?;
self.expect_else("':' between the conditional's branches")?;
let other = self.cond_expr()?;
return Ok(Operand::Cond {
cond: Box::new(cond),
then: Box::new(then),
other: Box::new(other),
});
}
Ok(match cond {
PredExpr::Truthy(op) => op,
other => Operand::Group(Box::new(other)),
})
}
fn invoke_inline_fragment(
&mut self,
pipe: &'static str,
stages: &mut Vec<Stage>,
) -> Result<()> {
let before = stages.len();
self.invoke_pipeline_fragment(pipe, stages)?;
for stage in &stages[before..] {
if matches!(
stage,
Stage::Push(_)
| Stage::ExprPush { .. }
| Stage::RecordPush { .. }
| Stage::FieldsPush
| Stage::Subcontext { .. }
| Stage::Nav(_)
) {
return Err(QuarbError::parse(
"a fragment spliced inside an expression pipe may \
not push or navigate (pushes belong to real \
capsae; use it as a pipeline stage instead)",
));
}
}
Ok(())
}
fn inline_stage(&mut self) -> Result<Stage> {
let stage = self.pipe_item(PipeMode::Scalar)?;
match &stage {
Stage::Push(_)
| Stage::ExprPush { .. }
| Stage::RecordPush { .. }
| Stage::FieldsPush
| Stage::Subcontext { .. } => Err(QuarbError::parse(
"a pipe inside an expression transforms a value; \
pushes belong to real capsae (use a stage)",
)),
_ => Ok(stage),
}
}
fn map_stage(&mut self) -> Result<Stage> {
if self.nav_stage_ahead() {
return Err(QuarbError::parse(
"navigation doesn't ride the map pipe; '$|' transforms \
the elements of a list topic — write '| /path' as its \
own stage",
));
}
if matches!(self.peek(), Some(Token::LBracket)) {
let pred = self.predicate()?;
return Ok(match pred {
Predicate::Expr(e) => Stage::Filter(e),
positional => Stage::Select(positional),
});
}
self.inline_stage()
}
fn inline_agg_stage(&mut self) -> Result<Stage> {
if matches!(self.peek(), Some(Token::LBracket)) {
return match self.predicate()? {
pred @ (Predicate::Index(_) | Predicate::Range(_, _)) => Ok(Stage::Select(pred)),
Predicate::Expr(_) => Err(QuarbError::parse(
"a condition filters per capsa; write '| [cond]' \
('@| [n]' selects positionally)",
)),
};
}
let call = self.func_call()?;
agg_stage(call)
}
fn rel_from_group(&mut self) -> Result<Operand> {
let mut steps = vec![PathElem::Group(self.group(None)?)];
loop {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
if matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
)
) {
steps.push(self.path_elem()?);
continue;
}
break;
}
let projection = self.projection()?;
Ok(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
})
}
fn parse_hole(&mut self, src: &str) -> Result<InterpSeg> {
if let Some((idx, which)) = top_level_bash_op(src) {
if idx == 0 || src[..idx].ends_with(|c: char| c.is_whitespace()) {
return Err(QuarbError::parse(format!(
"in '${{{src}}}': '{which}' glues to its expression \
(`${{expr{which}...}}`)"
)));
}
let expr = self.hole_expr(&src[..idx], src)?;
let tail = &src[idx + 2..];
return Ok(if which == ":?" {
let msg = tail.trim();
InterpSeg::Strict(expr, (!msg.is_empty()).then(|| msg.to_string()))
} else {
InterpSeg::Default(expr, self.hole_expr(tail, src)?)
});
}
let context = |e: QuarbError| QuarbError::parse(format!("in '${{{src}}}': {e}"));
let tokens = lexer::lex(src).map_err(context)?;
let mut p = self.child(&tokens, self.defs.clone())?;
p.def_params = self.def_params.clone();
let expr = p.additive().map_err(context)?;
let mut stages = Vec::new();
loop {
match p.peek() {
Some(Token::Pipe) if matches!(p.toks.get(p.pos + 1), Some(Token::Amp)) => {
p.pos += 1;
p.invoke_inline_fragment("|", &mut stages)
.map_err(context)?;
}
Some(Token::Pipe) => {
p.pos += 1;
stages.push(p.inline_stage().map_err(context)?);
}
Some(Token::At)
if matches!(p.toks.get(p.pos + 1), Some(Token::Pipe))
&& matches!(p.toks.get(p.pos + 2), Some(Token::Amp)) =>
{
p.pos += 2;
p.invoke_inline_fragment("@|", &mut stages)
.map_err(context)?;
}
Some(Token::At) if matches!(p.toks.get(p.pos + 1), Some(Token::Pipe)) => {
p.pos += 2;
stages.push(p.inline_agg_stage().map_err(context)?);
}
_ => break,
}
}
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"in '${{{src}}}': an interpolation hole holds one value \
expression (with an optional pipe tail or glued ':?' / ':-')"
)));
}
Ok(InterpSeg::Expr(if stages.is_empty() {
expr
} else {
Operand::Piped {
expr: Box::new(expr),
stages,
}
}))
}
fn hole_expr(&mut self, part: &str, whole: &str) -> Result<Operand> {
let context = |e: QuarbError| QuarbError::parse(format!("in '${{{whole}}}': {e}"));
let tokens = lexer::lex(part).map_err(context)?;
let mut depth = 0i32;
for t in &tokens {
match t {
Token::LParen => depth += 1,
Token::RParen => depth -= 1,
Token::Pipe if depth == 0 => {
return Err(QuarbError::parse(format!(
"in '${{{whole}}}': a bash operator and a pipe tail \
are one or the other in a hole (parenthesize the \
piped expression)"
)));
}
_ => {}
}
}
let mut p = self.child(&tokens, self.defs.clone())?;
p.def_params = self.def_params.clone();
let expr = p.additive().map_err(context)?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"in '${{{whole}}}': each side of ':?' / ':-' holds one \
value expression"
)));
}
Ok(expr)
}
fn rel_operand_from_step(&mut self) -> Result<Operand> {
self.rel_operand_continue(Vec::new())
}
fn rel_operand_continue(&mut self, mut steps: Vec<PathElem>) -> Result<Operand> {
loop {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
if matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::ReverseResolve
| Token::LParen
)
) {
steps.push(self.path_elem()?);
continue;
}
break;
}
let projection = self.projection()?;
Ok(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
})
}
fn operand(&mut self) -> Result<Operand> {
match self.peek() {
Some(Token::MarkOpen | Token::IdAnchorOpen) if self.mark_anchor_ahead() => {
let anchor = self.mark_anchor().expect("lookahead hit");
let mut steps = Vec::new();
loop {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
if matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
)
) {
steps.push(self.path_elem()?);
continue;
}
break;
}
let projection = self.projection()?;
Ok(Operand::Rel {
steps,
projection,
anchor,
})
}
Some(Token::Caret) | Some(Token::Name { quoted: false, .. })
if matches!(self.peek(), Some(Token::Caret))
|| (matches!(self.peek(), Some(Token::Name { text, .. }) if text == "*")
&& matches!(self.toks.get(self.pos + 1), Some(Token::SlashSlash))) =>
{
let mut steps = Vec::new();
if matches!(self.peek(), Some(Token::Caret)) {
self.pos += 1;
} else {
self.pos += 1;
let mut elem = self.path_elem()?;
universalize(&mut elem)?;
steps.push(elem);
}
loop {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
if matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
)
) {
steps.push(self.path_elem()?);
continue;
}
break;
}
let projection = self.projection()?;
if steps.is_empty() && projection.is_none() {
return Err(QuarbError::parse(
"'^' in operand position starts a root-anchored path; follow it with steps or a projection",
));
}
Ok(Operand::Rel {
steps,
projection,
anchor: Anchor::Root,
})
}
Some(Token::Gt { spaced: false, .. } | Token::Lt { spaced: false, .. }) => {
let first = self.path_elem()?;
self.rel_operand_continue(vec![first])
}
Some(Token::ReverseResolve | Token::Resolve) => {
let first = self.path_elem()?;
self.rel_operand_continue(vec![first])
}
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash,
) => self.rel_operand_from_step(),
Some(Token::ColonColon) if self.is_resolution_ahead() => {
let mut steps = Vec::new();
loop {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
if matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::ArrowOut
| Token::ArrowIn
| Token::DashDash
| Token::LParen
)
) {
steps.push(self.path_elem()?);
continue;
}
break;
}
let projection = self.projection()?;
Ok(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
})
}
Some(Token::ColonColon | Token::ColonColonColon | Token::SemiSemiSemi) => {
let projection = self.projection()?.expect("projection start");
Ok(Operand::Rel {
steps: Vec::new(),
projection: Some(projection),
anchor: Anchor::Current,
})
}
Some(Token::Name {
text,
quoted: false,
..
}) if matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& text.chars().all(|c| c.is_alphanumeric() || c == '_')
&& !text.chars().next().is_some_and(|c| c.is_ascii_digit()) =>
{
let call = self.func_call()?;
if call.name == "now" {
if !call.args.is_empty() {
return Err(QuarbError::parse(
"now() takes no arguments (it is the invocation instant)",
));
}
return Ok(Operand::Now);
}
if let Some(pointer) = retired_function(&call.name) {
return Err(QuarbError::parse(pointer));
}
if matches!(call.name.as_str(), "rec" | "record") {
return Err(QuarbError::parse(format!(
"'{n}(...)' as an operand would ride its first \
field as the topic ('{n}(x, ...)' is \
'(x | {n}(...))'), silently re-keying the \
record; spell the pipe form '(x | {n}(...))' \
explicitly if that is meant",
n = call.name
)));
}
let mut args = call.args.into_iter();
let first = match args.next() {
Some(Arg::Lit(v)) => Operand::Lit(v),
Some(Arg::Expr(e)) => e,
Some(Arg::Range(..)) | Some(Arg::Named { .. }) => {
return Err(QuarbError::parse(format!(
"'{}(...)' as an operand cannot ride a range or an option as its topic",
call.name
)));
}
None => {
return Err(QuarbError::parse(format!(
"a call operand needs a first argument to ride as the topic \
('{0}(x)' is '(x | {0})'); only now() is nullary",
call.name
)));
}
};
let stage_call = FnCall {
name: call.name,
args: args.collect(),
};
let stage = func_stage(stage_call)?;
Ok(Operand::Piped {
expr: Box::new(first),
stages: vec![stage],
})
}
Some(Token::Name { text, quoted, .. }) => {
let value = literal_value(text, *quoted);
self.pos += 1;
Ok(Operand::Lit(value))
}
Some(Token::Interp(parts)) => {
let parts = parts.clone();
self.pos += 1;
let mut segs = Vec::new();
for part in parts {
match part {
lexer::InterpPart::Text(t) => segs.push(InterpSeg::Text(t)),
lexer::InterpPart::Hole(src) => {
segs.push(self.parse_hole(&src)?);
}
}
}
Ok(Operand::Interp(segs))
}
Some(Token::Regex(pat)) => {
let value = Value::Str(pat.clone());
self.pos += 1;
Ok(Operand::Lit(value))
}
Some(Token::PercentPlus) => {
self.pos += 1;
Ok(Operand::NamedCaptures)
}
Some(Token::Percent) if matches!(self.toks.get(self.pos + 1), Some(Token::LParen)) => {
self.pos += 1;
let call = self.record_args()?;
validate_record(&call)?;
Ok(Operand::Piped {
expr: Box::new(Operand::Topic),
stages: vec![Stage::Func(call)],
})
}
Some(Token::At) => {
self.pos += 1;
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
let mut items = Vec::new();
loop {
if matches!(self.peek(), Some(Token::RParen)) {
self.pos += 1;
break;
}
items.push(self.additive()?);
match self.peek() {
Some(Token::Comma | Token::Semi) => self.pos += 1,
Some(Token::RParen) => {}
_ => {
return Err(QuarbError::parse(
"a list literal separates its items with ';': @(a; b)",
));
}
}
}
return Ok(Operand::List(items));
}
match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) if text == "-" => {
self.pos += 1;
let projection = self.projection()?;
if matches!(
projection,
Some(Projection::CoreMeta(_) | Projection::AdapterMeta(_))
) {
return Err(QuarbError::parse(
"an edge carries plain properties only (@-::prop)",
));
}
Ok(Operand::Edges { projection })
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "." => {
self.pos += 1;
Ok(Operand::Recall(RegRef::Whole))
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "*" => {
self.pos += 1;
let projection = self.projection()?;
Ok(Operand::Capsae { projection })
}
_ => Err(QuarbError::parse(
"expected '-' (arrived edges), '.' (register), or '*' \
(the context) after '@' in an operand",
)),
}
}
Some(Token::Driver) => {
self.pos += 1;
return self.served_node();
}
Some(Token::Dollar) => {
self.pos += 1;
let mut joined: Option<usize> = None;
if matches!(self.peek(), Some(Token::Dollar)) {
let digits = match self.toks.get(self.pos + 1) {
Some(Token::Name {
text,
quoted: false,
..
}) if !text.is_empty() && text.chars().all(|c| c.is_ascii_digit()) => {
Some(text.clone())
}
_ => None,
};
match digits {
Some(d) => {
let k = d.parse::<usize>().map_err(|_| {
QuarbError::parse(format!("bad join position '$${d}'"))
})?;
self.pos += 2;
if k == 0 {
return self.served_node();
}
joined = Some(k);
}
None => {
return Err(QuarbError::parse(
"the capsa is shared: '$.name', '$_' and '$ord' read the same register \
inside a body or a join condition; '_' is the served node, '$$N' a \
bound node, '$$.name' a mark",
));
}
}
}
let index = if joined.is_some() {
joined
} else {
match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with('*') => {
let digits = text[1..].to_string();
self.pos += 1;
if digits.is_empty() {
None
} else {
return Err(QuarbError::parse(format!(
"'$*{digits}' is retired: the join's N-th node is '$${digits}'"
)));
}
}
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with('.') => {
let rest = text[1..].to_string();
self.pos += 1;
let r = if rest.is_empty() {
RegRef::Top
} else if let Ok(n) = rest.parse::<usize>() {
RegRef::Index(n)
} else {
RegRef::Named(rest)
};
return Ok(Operand::Recall(r));
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "_" => {
self.pos += 1;
return Ok(Operand::Topic);
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "ord" => {
self.pos += 1;
return Ok(Operand::Ordinal);
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "ordinal" => {
return Err(QuarbError::parse("'$ordinal' is retired: write '$ord'"));
}
Some(Token::Name {
text,
quoted: false,
..
}) if text.chars().all(|c| c.is_ascii_digit()) => {
let n: usize = text.parse().map_err(|_| {
QuarbError::parse(format!("bad capture reference '${text}'"))
})?;
if n == 0 {
return Err(QuarbError::parse(
"capture references are 1-based ('$1')",
));
}
self.pos += 1;
return Ok(Operand::Capture(n));
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "-" => {
self.pos += 1;
let projection = self.projection()?;
if matches!(
projection,
Some(Projection::CoreMeta(_) | Projection::AdapterMeta(_))
) {
return Err(QuarbError::parse(
"an edge carries plain properties only ($-::prop)",
));
}
return Ok(Operand::Edge { projection });
}
Some(Token::Name {
text,
quoted: false,
..
}) if self.def_params.iter().any(|p| p == text) => {
let name = text.clone();
self.pos += 1;
return Ok(Operand::Param(name));
}
_ => {
return Err(QuarbError::parse(
"expected '*N', '.name', '_', '-', or 'ord' after '$' in an operand",
));
}
}
};
let mut steps = Vec::new();
while matches!(
self.peek(),
Some(
Token::Slash
| Token::SlashSlash
| Token::Backslash
| Token::BackslashBackslash
| Token::FollowingSiblings(_)
| Token::PrecedingSiblings(_)
| Token::NextSibling
| Token::PrevSibling
| Token::LParen
)
) {
steps.push(self.path_elem()?);
}
let projection = self.projection()?;
Ok(Operand::Ctx {
index,
steps,
projection,
})
}
Some(Token::Amp) => self.splice_operand_fragment(),
other => Err(QuarbError::parse(format!(
"expected a value or path in a predicate, found {other:?}"
))),
}
}
fn splice_operand_fragment(&mut self) -> Result<Operand> {
let (name, args, bang, quant) = self.invocation()?;
let Some(def) = self.defs.get(&name).cloned() else {
return Err(QuarbError::parse(format!("unknown fragment '&{name}'")));
};
if quant.is_some() {
return Err(QuarbError::parse(format!(
"a quantifier doesn't ride '&{name}' in operand position; \
put the quantified group inside the definition"
)));
}
self.check_bang(&name, &def, bang)?;
match def.body {
DefBody::Predicates(preds) => {
let map = self.bind(&name, &def.params, args)?;
let subst = Subst {
outer: &map,
hole: &map,
};
let mut conj: Option<PredExpr> = None;
for pred in preds {
let Predicate::Expr(mut e) = pred else {
return Err(QuarbError::parse(format!(
"the predicate fragment '&{name}' holds a \
positional predicate; a condition operand \
needs an expression"
)));
};
subst_pred_expr(&mut e, &subst);
conj = Some(match conj {
Some(prev) => PredExpr::And(Box::new(prev), Box::new(e)),
None => e,
});
}
let conj = conj.expect("a predicate body holds at least one predicate");
Ok(Operand::Group(Box::new(conj)))
}
DefBody::Pipeline(_) => Err(QuarbError::parse(format!(
"'&{name}' is a pipeline fragment; invoke it after a pipe"
))),
DefBody::Query(mut q) => {
let map = self.bind(&name, &def.params, args)?;
subst_query(
&mut q,
&Subst {
outer: &map,
hole: &map,
},
);
self.operand_from_query(&name, q)
}
DefBody::Macro(_) => {
let invited = invites_of(&args);
let text = self.expand_macro_path_text(&name, &def, args)?;
let wrap = |e: QuarbError| {
QuarbError::expansion(format!("in expansion of '&{name}' ('{text}')"))
.wrapping(e)
};
let tokens = lexer::lex(&text).map_err(wrap)?;
let mut p = self.child(&tokens, self.defs.before(&name))?;
p.predicate_depth = self.predicate_depth;
let op = p.cond_expr().map_err(wrap)?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"macro '&{name}' expanded to text with trailing \
content ('{text}')"
)));
}
self.record_captures(&name, &invited, crate::reflect::usage_of_operand(&op));
Ok(op)
}
}
}
fn operand_from_query(&self, name: &str, q: Query) -> Result<Operand> {
if !q.correlations.is_empty() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' carries a correlation; a chain \
splices at driver position only"
)));
}
let rel = if q.branches.len() == 1 {
let b = q.branches.into_iter().next().expect("non-empty");
Operand::Rel {
steps: b.steps,
projection: b.projection,
anchor: b.anchor,
}
} else {
if q.branches
.iter()
.any(|b| b.anchor != Anchor::Current || b.projection.is_some())
{
return Err(QuarbError::parse(format!(
"fragment '&{name}' is a projected or anchored union; \
in operand position a union body must be plain \
navigation from the current node"
)));
}
let alts: Vec<Vec<PathElem>> = q.branches.into_iter().map(|b| b.steps).collect();
Operand::Rel {
steps: vec![PathElem::Group(Group {
alts,
quant: Quant {
min: 1,
max: Some(1),
},
predicates: Vec::new(),
traits: Vec::new(),
reach: Reach::All,
})],
projection: None,
anchor: Anchor::Current,
}
};
if self.predicate_depth > 0
&& let Operand::Rel { steps, .. } = &rel
&& walks_reverse_resolve(steps)
{
return Err(QuarbError::parse(format!(
"fragment '&{name}' walks reverse resolution '<--', \
which is not allowed inside a predicate (it would scan \
the whole arbor per node); rewrite as a descending path \
or an incoming edge '<-'"
)));
}
if q.pipeline.is_empty() {
return Ok(rel);
}
for stage in &q.pipeline {
if matches!(
stage,
Stage::Push(_)
| Stage::ExprPush { .. }
| Stage::RecordPush { .. }
| Stage::FieldsPush
| Stage::Subcontext { .. }
| Stage::Nav(_)
) {
return Err(QuarbError::parse(format!(
"fragment '&{name}' carries a pipeline that pushes or \
navigates; an operand pipe transforms a value \
(pushes belong to real capsae)"
)));
}
}
Ok(Operand::Piped {
expr: Box::new(rel),
stages: q.pipeline,
})
}
fn cmp_op(&mut self) -> Option<CmpOp> {
let op = match self.peek()? {
Token::Eq => CmpOp::Eq,
Token::Ne => CmpOp::Ne,
Token::Lt {
spaced: true,
before: true,
} => CmpOp::Lt,
Token::Le => CmpOp::Le,
Token::Gt {
spaced: true,
before: true,
} => CmpOp::Gt,
Token::Ge => CmpOp::Ge,
Token::MatchEq => CmpOp::Match,
Token::NotMatchEq => CmpOp::NotMatch,
Token::Contains => CmpOp::Contains,
Token::Name {
text,
quoted: false,
..
} => cmp_word(text)?,
_ => return None,
};
self.pos += 1;
Some(op)
}
fn expect_separator(&mut self, what: &str) -> Result<()> {
if matches!(self.peek(), Some(Token::Field)) {
self.pos += 1;
return Ok(());
}
if matches!(self.peek(), Some(Token::Colon { .. })) {
self.pos += 1;
return Ok(());
}
Err(QuarbError::parse(format!("expected {what}")))
}
fn binary_marker(&mut self, c: char) -> Result<()> {
let spaced = match self.peek() {
Some(Token::Question { spaced }) if c == '?' => *spaced,
Some(Token::Colon { spaced }) if c == ':' => *spaced,
_ => return Err(QuarbError::parse(format!("expected '{c}'"))),
};
if !spaced {
return Err(QuarbError::parse(format!(
"'{c}' is an operator and takes a space on both sides: write ' {c} '"
)));
}
self.pos += 1;
Ok(())
}
fn expect_else(&mut self, what: &str) -> Result<()> {
match self.peek() {
Some(Token::Colon { .. }) => self.binary_marker(':'),
Some(Token::Field) => Err(QuarbError::parse(
"':' is an operator and takes a space on both sides: write ' : '",
)),
_ => Err(QuarbError::parse(format!("expected {what}"))),
}
}
fn eat_word_if_followed(&mut self, words: &[&str]) -> bool {
let followed = matches!(
self.toks.get(self.pos + 1),
Some(Token::Name { .. } | Token::LParen | Token::Bang)
);
followed && self.eat_word(words)
}
fn served_node(&mut self) -> Result<Operand> {
let mut steps = Vec::new();
while matches!(self.peek(), Some(Token::Slash | Token::SlashSlash)) {
steps.push(self.path_elem()?);
}
let projection = self.projection()?;
Ok(Operand::Outer(Box::new(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
})))
}
fn peek_word(&self, words: &[&str]) -> bool {
matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if words.contains(&text.as_str()))
}
fn eat_word(&mut self, words: &[&str]) -> bool {
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& words.contains(&text.as_str())
{
self.pos += 1;
return true;
}
false
}
fn expect(&mut self, tok: Token, what: &str) -> Result<()> {
if self.peek() == Some(&tok) {
self.pos += 1;
Ok(())
} else {
Err(QuarbError::parse(format!("expected {what}")))
}
}
fn try_trait(&mut self) -> Result<Option<Vec<TraitClause>>> {
if !matches!(self.peek(), Some(Token::Lt { spaced: false, .. })) {
return Ok(None);
}
let start = self.pos;
self.pos += 1; let Some(expr) = self.trait_or() else {
self.pos = start;
return Ok(None);
};
if !matches!(self.peek(), Some(Token::Gt { .. })) {
self.pos = start;
return Ok(None);
}
self.pos += 1;
trait_cnf(expr).map(Some)
}
fn trait_or(&mut self) -> Option<TExpr> {
let mut left = self.trait_and()?;
while matches!(self.peek(), Some(Token::PipePipe)) || self.eat_word_if_followed(OR_WORDS) {
if matches!(self.peek(), Some(Token::PipePipe)) {
self.pos += 1;
}
let right = self.trait_and()?;
left = TExpr::Or(Box::new(left), Box::new(right));
}
Some(left)
}
fn trait_and(&mut self) -> Option<TExpr> {
let mut left = self.trait_not()?;
while matches!(self.peek(), Some(Token::AmpAmp)) || self.eat_word_if_followed(AND_WORDS) {
if matches!(self.peek(), Some(Token::AmpAmp)) {
self.pos += 1;
}
let right = self.trait_not()?;
left = TExpr::And(Box::new(left), Box::new(right));
}
Some(left)
}
fn trait_not(&mut self) -> Option<TExpr> {
if matches!(self.peek(), Some(Token::Bang)) {
self.pos += 1;
return Some(TExpr::Not(Box::new(self.trait_not()?)));
}
if self.eat_word_if_followed(NOT_WORDS) {
return Some(TExpr::Not(Box::new(self.trait_not()?)));
}
match self.peek() {
Some(Token::Name { text, .. }) => {
let mut name = text.clone();
self.pos += 1;
while matches!(self.peek(), Some(Token::Field))
&& matches!(self.toks.get(self.pos + 1), Some(Token::Name { .. }))
{
self.pos += 1;
if let Some(Token::Name { text, .. }) = self.bump() {
name.push(':');
name.push_str(text);
}
}
Some(TExpr::Has(name))
}
Some(Token::LParen) => {
self.pos += 1;
let inner = self.trait_or()?;
if !matches!(self.peek(), Some(Token::RParen)) {
return None;
}
self.pos += 1;
Some(inner)
}
_ => None,
}
}
fn axis(&mut self) -> Result<Axis> {
let axis = match self.bump() {
Some(Token::Slash) => Axis::Child,
Some(Token::SlashSlash) => Axis::Descendant(self.reach()),
Some(Token::Backslash) => Axis::Parent,
Some(Token::BackslashBackslash) => Axis::Ancestor(self.reach()),
Some(Token::Gt { spaced: false, .. }) | Some(Token::NextSibling) => Axis::NextSibling,
Some(Token::Lt { spaced: false, .. }) | Some(Token::PrevSibling) => Axis::PrevSibling,
Some(t @ (Token::Gt { spaced: true, .. } | Token::Lt { spaced: true, .. })) => {
let c = if matches!(t, Token::Gt { .. }) {
'>'
} else {
'<'
};
return Err(QuarbError::parse(format!(
"'{c}' followed by a space is the comparison; the sibling hop \
is glued to its name: '{c}name' ('{c}*' for any)"
)));
}
Some(Token::FollowingSiblings(mark)) => Axis::FollowingSiblings(mark_reach(*mark)),
Some(Token::PrecedingSiblings(mark)) => Axis::PrecedingSiblings(mark_reach(*mark)),
Some(Token::ArrowOut) => Axis::OutLink,
Some(Token::ArrowIn) => Axis::InLink,
Some(Token::DashDash) => Axis::BothLink,
Some(Token::Name { text, .. }) => {
return Err(QuarbError::parse(format!(
"expected a navigation operator before '{text}' \
(queries are root-anchored; start with '/', or open \
with an expression head: '= expr')"
)));
}
Some(Token::Field) => {
let name = match self.peek() {
Some(Token::Name { text, .. }) => text.clone(),
_ => "name".to_string(),
};
return Err(QuarbError::parse(format!(
"':{name}' reads a record's field; a node's property is '::{name}'"
)));
}
_ => {
return Err(QuarbError::parse(
"expected a navigation operator ('/', '//', '\\', …)",
));
}
};
Ok(axis)
}
fn reach(&mut self) -> Reach {
match self.peek() {
Some(Token::Question { .. }) => {
self.pos += 1;
Reach::Proximal
}
Some(Token::Bang) => {
self.pos += 1;
Reach::Distal
}
_ => Reach::All,
}
}
fn matcher_after(&mut self, axis: &Axis) -> Result<Matcher> {
if matches!(axis, Axis::Parent | Axis::Ancestor(_))
&& (!matches!(
self.peek(),
Some(Token::Name { .. } | Token::Regex(_) | Token::Lt { .. })
) || self.peek_word(OR_WORDS))
{
return Ok(Matcher::Any);
}
self.matcher()
}
fn matcher(&mut self) -> Result<Matcher> {
let in_pattern = self.pattern_depth > 0;
if matches!(self.peek(), Some(Token::Lt { .. })) {
return Ok(Matcher::Any);
}
match self.bump() {
Some(Token::Name { text, quoted, .. }) => {
if !*quoted && text == "." && in_pattern {
return Ok(Matcher::Dot);
}
matcher_for(text, *quoted)
}
Some(Token::Regex(pat)) => Regex::new(pat)
.map(Matcher::Regex)
.map_err(|e| QuarbError::parse(format!("bad regex '~({pat})': {e}"))),
_ => Err(QuarbError::parse(
"a navigation operator must be followed by a name",
)),
}
}
}
enum TExpr {
Has(String),
Not(Box<TExpr>),
And(Box<TExpr>, Box<TExpr>),
Or(Box<TExpr>, Box<TExpr>),
}
const MAX_TRAIT_CLAUSES: usize = 512;
fn trait_cnf(e: TExpr) -> Result<Vec<TraitClause>> {
fn nnf(e: TExpr, neg: bool) -> TExpr {
match e {
TExpr::Not(inner) => nnf(*inner, !neg),
TExpr::And(a, b) => {
let (a, b) = (Box::new(nnf(*a, neg)), Box::new(nnf(*b, neg)));
if neg {
TExpr::Or(a, b)
} else {
TExpr::And(a, b)
}
}
TExpr::Or(a, b) => {
let (a, b) = (Box::new(nnf(*a, neg)), Box::new(nnf(*b, neg)));
if neg {
TExpr::And(a, b)
} else {
TExpr::Or(a, b)
}
}
TExpr::Has(n) => {
if neg {
TExpr::Has(format!("!{n}"))
} else {
TExpr::Has(n)
}
}
}
}
fn clauses(e: TExpr) -> Result<Vec<Vec<String>>> {
let out = match e {
TExpr::Has(n) => vec![vec![n]],
TExpr::And(a, b) => {
let mut out = clauses(*a)?;
out.extend(clauses(*b)?);
out
}
TExpr::Or(a, b) => {
let (ca, cb) = (clauses(*a)?, clauses(*b)?);
let mut out = Vec::with_capacity(ca.len().saturating_mul(cb.len()));
for x in &ca {
for y in &cb {
let mut alt = x.clone();
alt.extend(y.iter().cloned());
out.push(alt);
}
}
out
}
TExpr::Not(_) => unreachable!("nnf removed compound negation"),
};
if out.len() > MAX_TRAIT_CLAUSES {
return Err(QuarbError::parse(format!(
"trait filter too complex: its normal form exceeds \
{MAX_TRAIT_CLAUSES} clauses"
)));
}
Ok(out)
}
Ok(clauses(nnf(e, false))?
.into_iter()
.map(|alts| TraitClause { alts })
.collect())
}
fn agg_stage(call: FnCall) -> Result<Stage> {
let Some(spec) = crate::registry::spec(&call.name).filter(|s| s.agg.is_some()) else {
return Err(QuarbError::unsupported(format!(
"aggregate function '{}'",
call.name
)));
};
if call.name == "ungroup" && !call.args.is_empty() {
return Err(QuarbError::parse("'ungroup' takes no arguments"));
}
match spec.check {
crate::registry::Check::Stage => validate_window_shift(&call)?,
crate::registry::Check::Keyed => validate_keyed(&call)?,
_ => {}
}
Ok(Stage::Agg(call))
}
fn func_stage(call: FnCall) -> Result<Stage> {
use crate::registry::{Agg, Check};
let spec = crate::registry::spec(&call.name);
let stands = spec.is_some_and(|s| {
s.scalar
|| matches!(
s.agg,
Some(Agg::Reducing | Agg::Keyed | Agg::Association { .. })
)
});
if !stands {
if let Some(pointer) = retired_function(&call.name) {
return Err(QuarbError::parse(pointer));
}
let err = QuarbError::unsupported(format!("pipeline function '{}'", call.name));
return Err(if spec.is_some_and(|s| s.agg == Some(Agg::Context)) {
err.with_hint(format!(" ('{}' uses '@|')", call.name))
} else {
err
});
}
match spec.map(|s| s.check) {
Some(Check::Keyed) => validate_keyed(&call)?,
_ => validate_record(&call)?,
}
Ok(Stage::Func(call))
}
fn validate_record(call: &FnCall) -> Result<()> {
if call.name == "decode" || call.name == "dec" {
match call.args.as_slice() {
[Arg::Lit(v)]
if crate::encoding::is_decodable(&v.to_string())
|| crate::encoding::is_structured_format(&v.to_string()) =>
{
return Ok(());
}
[Arg::Lit(v)] => {
return Err(QuarbError::parse(format!(
"decode: '{}' is not a decodable format \
(base64, base64url, base32, crockford32, hex, \
json, yaml, toml, xml)",
v
)));
}
_ => {
return Err(QuarbError::parse(
"decode takes one scheme name, e.g. decode(base64)",
));
}
}
}
if !matches!(call.name.as_str(), "record" | "rec") {
return Ok(());
}
validate_record_convention(call, "record")
}
fn cmp_text(op: CmpOp) -> &'static str {
match op {
CmpOp::Eq => "=",
CmpOp::Ne => "!=",
CmpOp::Lt => "<",
CmpOp::Le => "<=",
CmpOp::Gt => ">",
CmpOp::Ge => ">=",
CmpOp::Match => "==",
CmpOp::NotMatch => "!==",
CmpOp::Contains => "*=",
}
}
pub(crate) fn retired_function(name: &str) -> Option<String> {
match name {
"rec" | "record" => Some(format!("'{name}(…)' is retired: the record is '%(…)'")),
"chars" => Some("'chars' is retired: write 'cc'".into()),
_ => None,
}
}
fn is_record_convention(name: &str) -> bool {
matches!(name, "rec" | "record" | "group")
}
fn validate_record_convention(call: &FnCall, what: &str) -> Result<()> {
if call.args.is_empty() {
let noun = if what == "group" { "key" } else { "field" };
return Err(QuarbError::parse(format!(
"{what} needs at least one {noun}, e.g. {what}(::name)"
)));
}
let mut names: Vec<String> = Vec::new();
let mut check = |name: &str| -> Result<()> {
if names.iter().any(|n| n == name) {
return Err(QuarbError::parse(format!(
"{what} names the field '{name}' twice — name one \
explicitly: {what}(\"other-{name}\", ...)"
)));
}
names.push(name.to_string());
Ok(())
};
let mut i = 0;
while i < call.args.len() {
match &call.args[i] {
Arg::Lit(Value::Str(name)) => {
if i + 1 >= call.args.len() {
return Err(QuarbError::parse(format!(
"{what} has a trailing field name with no value"
)));
}
check(name)?;
i += 2;
}
Arg::Expr(e) => {
let Some(name) = crate::ast::auto_field_name(e) else {
return Err(QuarbError::parse(format!(
"a {what} field needs a name: give a computed value a key, \
e.g. %(total = ::price * ::qty)"
)));
};
let name = name.to_string();
check(&name)?;
i += 1;
}
_ => {
return Err(QuarbError::parse(format!(
"a {what} field needs a name: give a computed value a key, \
e.g. %(total = ::price * ::qty)"
)));
}
}
}
Ok(())
}
fn valid_locale_tag(tag: &str) -> bool {
let mut subtags = tag.split('-');
let Some(lang) = subtags.next() else {
return false;
};
(2..=8).contains(&lang.len())
&& lang.bytes().all(|b| b.is_ascii_alphabetic())
&& subtags
.all(|s| (1..=8).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_alphanumeric()))
}
fn validate_keyed(call: &FnCall) -> Result<()> {
if call.name == "group" {
return validate_record_convention(call, "group");
}
if call.name == "sort" {
let locale = |v: &Value| -> Result<()> {
let tag = v.to_string();
if !valid_locale_tag(&tag) {
return Err(QuarbError::parse(format!(
"sort: '{tag}' is not a Unicode locale identifier \
(try ru-RU, de-DE, zh-Hant, ...)"
)));
}
Ok(())
};
let atergo = |v: &Value| matches!(v, Value::Str(s) if s == "atergo");
match call.args.as_slice() {
[] => return Ok(()),
[Arg::Lit(v)] if atergo(v) => return Ok(()),
[Arg::Lit(v)] => return locale(v),
[Arg::Lit(loc), Arg::Lit(mode)] if atergo(mode) && !atergo(loc) => {
return locale(loc);
}
_ => {
return Err(QuarbError::parse(
"sort takes a locale identifier, the word atergo, or both \
with the locale first: sort, sort(ru-RU), sort(atergo), \
sort(ru-RU; atergo); keyed sorting is sort_by",
));
}
}
}
let keyed = matches!(
call.name.as_str(),
"sort_by" | "unique_by" | "min_by" | "max_by" | "top" | "bottom"
);
if !keyed {
return Ok(());
}
let mut args = call.args.iter();
if matches!(call.name.as_str(), "top" | "bottom")
&& !matches!(args.next(), Some(Arg::Lit(Value::Int(n))) if *n >= 0)
{
return Err(QuarbError::parse(format!(
"{} takes a non-negative integer count first: {}(3, ::key)",
call.name, call.name
)));
}
let mut keys = args.peekable();
if keys.peek().is_none() || keys.any(|a| matches!(a, Arg::Lit(_))) {
return Err(QuarbError::parse(format!(
"{} needs value-expression keys, e.g. {}(::age)",
call.name, call.name
)));
}
Ok(())
}
fn validate_window_shift(call: &FnCall) -> Result<()> {
let key_ok = |rest: &[Arg]| matches!(rest, [] | [Arg::Expr(_)]);
match call.name.as_str() {
"window" => match call.args.split_first() {
Some((Arg::Range(a, b), rest)) if key_ok(rest) => {
if let (Some(a), Some(b)) = (a, b)
&& a > b
{
return Err(QuarbError::parse(format!(
"window({a}..{b}) is empty: the range needs start <= end"
)));
}
Ok(())
}
Some((Arg::Lit(Value::Int(n)), rest)) if *n >= 1 && key_ok(rest) => Ok(()),
_ => Err(QuarbError::parse(
"window takes an offset range or a count, then an optional \
partition key: window(-2..0), window(3, ::group)",
)),
},
"shift" => match call.args.split_first() {
Some((Arg::Lit(Value::Int(_)), rest)) if key_ok(rest) => Ok(()),
_ => Err(QuarbError::parse(
"shift takes an integer distance, then an optional partition \
key: shift(1), shift(1, ::group)",
)),
},
"combinations" => match call.args.as_slice() {
[Arg::Lit(Value::Int(n))] if *n >= 1 => Ok(()),
_ => Err(QuarbError::parse(
"combinations takes the size of the subset: combinations(2)",
)),
},
"pairs" | "triples" => {
if call.args.is_empty() {
Ok(())
} else {
Err(QuarbError::parse(format!(
"'{}' takes no arguments; a subset of another size is combinations(n)",
call.name
)))
}
}
"csv" | "table" => {
if call.args.iter().all(|a| {
matches!(a, Arg::Lit(Value::Str(_)))
|| matches!(a, Arg::Named { name, .. } if name == "delimiter")
}) {
Ok(())
} else {
Err(QuarbError::parse(format!(
"{0} takes column names: {0}, {0}(year; title)",
call.name
)))
}
}
"number" => match call.args.as_slice() {
[] | [Arg::Expr(_)] => Ok(()),
_ => Err(QuarbError::parse(
"number takes an optional level expression: number, number(:::depth)",
)),
},
"timeline" => {
let args = match call.args.split_last() {
Some((Arg::Lit(Value::Str(w)), rest)) if w == "forward" => rest,
_ => call.args.as_slice(),
};
if args.len() <= 2 && args.iter().all(|a| !matches!(a, Arg::Range(..))) {
Ok(())
} else {
Err(QuarbError::parse(
"timeline takes the time expression, an optional anchor, and \
the optional word forward: timeline($_), timeline($_; $.d), \
timeline($_; $.d; forward)",
))
}
}
"outline" => match call.args.as_slice() {
[] | [Arg::Expr(_), Arg::Expr(_)] => Ok(()),
[Arg::Expr(_), Arg::Expr(_), Arg::Lit(Value::Str(style))] if style == "1." => Ok(()),
_ => Err(QuarbError::parse(
"outline takes a level and a text, then an optional numbering \
style: outline(:::depth; ::lemma), outline(:::depth; ::lemma; \"1.\")",
)),
},
"zscore" => {
let ok = match call.args.split_first() {
None => true,
Some((Arg::Lit(Value::Str(s)), rest)) if s == "sample" => key_ok(rest),
Some((Arg::Expr(_), [])) => true,
_ => false,
};
if ok {
Ok(())
} else {
Err(QuarbError::parse(
"zscore takes the word sample and/or a partition key: \
zscore, zscore(sample), zscore($.w), zscore(sample; $.w)",
))
}
}
_ => Ok(()),
}
}
fn validate_stat_args(call: &FnCall) -> Result<()> {
match call.name.as_str() {
"std" | "var" | "stddev" | "variance" => {
let rest = match call.args.split_first() {
Some((Arg::Expr(_), rest)) => rest,
_ => &call.args,
};
let ok = match rest {
[] => true,
[Arg::Lit(Value::Str(s))] => s == "sample",
_ => false,
};
if ok {
Ok(())
} else {
Err(QuarbError::parse(format!(
"{0} takes only the word sample: {0}, {0}(sample)",
call.name
)))
}
}
"encoding" => match call.args.as_slice() {
[Arg::Lit(Value::Str(label))] => {
if crate::source::encoding_for_label(label).is_some() {
Ok(())
} else {
Err(QuarbError::parse(format!(
"encoding: unknown label {label:?} (labels are the Encoding \
Standard's: utf-8, windows-1251, koi8-r, iso-8859-2, …)"
)))
}
}
_ => Err(QuarbError::parse(
"encoding takes one label: encoding(windows-1251)",
)),
},
"corr" | "spearman" | "cosine" | "delta" | "dp" | "kappa" | "precision" | "recall"
| "f1" => {
if matches!(call.args.as_slice(), [Arg::Expr(_), Arg::Expr(_)]) {
Ok(())
} else {
Err(QuarbError::parse(format!(
"{0} needs two value-expression keys: {0}(:a; :b), {0}(::x; ::y)",
call.name
)))
}
}
_ => Ok(()),
}
}
fn mark_reach(mark: char) -> Reach {
match mark {
'?' => Reach::Proximal,
'!' => Reach::Distal,
_ => Reach::All,
}
}
fn literal_value(text: &str, quoted: bool) -> Value {
if quoted {
return Value::Str(text.to_string());
}
match text {
"true" => return Value::Bool(true),
"false" => return Value::Bool(false),
"null" => return Value::Null,
_ => {}
}
if let Some(norm) = numeric_text(text) {
if let Ok(n) = norm.parse::<i64>() {
return Value::Int(n);
}
if let Ok(f) = norm.parse::<f64>()
&& f.is_finite()
{
return Value::Float(f);
}
}
if let Ok(n) = text.parse::<i64>() {
return Value::Int(n);
}
if text.starts_with(|c: char| c.is_ascii_digit() || c == '-' || c == '+' || c == '.')
&& let Ok(f) = text.parse::<f64>()
&& f.is_finite()
{
return Value::Float(f);
}
Value::Str(text.to_string())
}
fn numeric_text(text: &str) -> Option<String> {
let body = text.strip_prefix('-').unwrap_or(text);
let mut out = String::with_capacity(text.len());
if text.starts_with('-') {
out.push('-');
}
let mut seen_point = false;
let chars: Vec<char> = body.chars().collect();
if chars.first().is_none_or(|c| !c.is_ascii_digit()) {
return None;
}
for (k, &c) in chars.iter().enumerate() {
match c {
d if d.is_ascii_digit() => out.push(d),
'_' => {
let between = k > 0
&& chars[k - 1].is_ascii_digit()
&& chars.get(k + 1).is_some_and(|n| n.is_ascii_digit());
if !between {
return None;
}
}
'.' | ',' => {
if seen_point || !chars.get(k + 1).is_some_and(|n| n.is_ascii_digit()) {
return None;
}
seen_point = true;
out.push('.');
}
_ => return None,
}
}
Some(out)
}
fn is_projection_start(tok: &Token) -> bool {
matches!(
tok,
Token::ColonColon | Token::ColonColonColon | Token::SemiSemiSemi
)
}
fn matcher_for(text: &str, quoted: bool) -> Result<Matcher> {
if quoted {
return Ok(Matcher::Name(text.to_string()));
}
if text == "*" {
return Ok(Matcher::Any);
}
if text.contains('*') {
let glob =
Glob::new(text).map_err(|e| QuarbError::parse(format!("bad glob '{text}': {e}")))?;
return Ok(Matcher::Glob(glob.compile_matcher()));
}
Ok(Matcher::Name(text.to_string()))
}
struct Subst<'a> {
outer: &'a HashMap<String, Operand>,
hole: &'a HashMap<String, Operand>,
}
fn subst_query(q: &mut Query, map: &Subst<'_>) {
for corr in &mut q.correlations {
subst_query(corr, map);
}
for b in &mut q.branches {
if let Anchor::Id(op) = &mut b.anchor {
subst_operand(op, map);
}
for elem in &mut b.steps {
subst_elem(elem, map);
}
}
for stage in &mut q.pipeline {
subst_stage(stage, map);
}
}
fn subst_elem(elem: &mut PathElem, map: &Subst<'_>) {
match elem {
PathElem::Mark(_) => {}
PathElem::Step(step) => subst_step(step, map),
PathElem::Group(group) => {
for alt in &mut group.alts {
for elem in alt {
subst_elem(elem, map);
}
}
for pred in &mut group.predicates {
if let Predicate::Expr(e) = pred {
subst_pred_expr(e, map);
}
}
}
PathElem::Push { body, .. } => match body {
PushBody::Query(q) => subst_query(q, map),
PushBody::Expr(e) => subst_operand(e, map),
},
}
}
fn subst_step(step: &mut Step, map: &Subst<'_>) {
for pred in &mut step.predicates {
if let Predicate::Expr(e) = pred {
subst_pred_expr(e, map);
}
}
}
fn subst_stage(stage: &mut Stage, map: &Subst<'_>) {
match stage {
Stage::Func(call)
| Stage::Agg(call)
| Stage::RecordWith(call)
| Stage::RecordPush { call, .. } => {
for arg in &mut call.args {
if let Arg::Expr(e) = arg {
subst_operand(e, map);
}
}
}
Stage::Expr(e) | Stage::ExprPush { expr: e, .. } => subst_operand(e, map),
Stage::Nav(b) => {
if let Anchor::Id(op) = &mut b.anchor {
subst_operand(op, map);
}
for elem in &mut b.steps {
subst_elem(elem, map);
}
}
Stage::Subcontext { body, .. } => subst_query(body, map),
Stage::Filter(e) => subst_pred_expr(e, map),
Stage::Select(Predicate::Expr(e)) => subst_pred_expr(e, map),
Stage::Map(inner) | Stage::Repeat { stage: inner, .. } => subst_stage(inner, map),
Stage::Select(_)
| Stage::Push(_)
| Stage::FieldsPush
| Stage::Recall(_)
| Stage::Spread { .. } => {}
}
}
fn subst_pred_expr(e: &mut PredExpr, map: &Subst<'_>) {
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => {
subst_pred_expr(a, map);
subst_pred_expr(b, map);
}
PredExpr::Not(a) => subst_pred_expr(a, map),
PredExpr::Compare(l, _, r) => {
subst_operand(l, map);
subst_operand(r, map);
}
PredExpr::Truthy(o) => subst_operand(o, map),
}
}
fn subst_operand(o: &mut Operand, map: &Subst<'_>) {
match o {
Operand::Pattern(_) => {}
Operand::Match {
scrutinee,
arms,
other,
} => {
subst_operand(scrutinee, map);
for (test, _, result) in arms {
subst_operand(test, map);
subst_operand(result, map);
}
subst_operand(other, map);
}
Operand::Param(name) => {
if let Some(arg) = map.outer.get(name) {
*o = arg.clone();
}
}
Operand::Rel { steps, anchor, .. } => {
if let Anchor::Id(op) = anchor {
subst_operand(op, map);
}
for elem in steps {
subst_elem(elem, map);
}
}
Operand::Ctx { steps, .. } => {
for elem in steps {
subst_elem(elem, map);
}
}
Operand::Arith { left, right, .. } => {
subst_operand(left, map);
subst_operand(right, map);
}
Operand::Neg(inner) => subst_operand(inner, map),
Operand::Field { base, .. } | Operand::ValueMeta { base, .. } => subst_operand(base, map),
Operand::PeerReg { at, .. } => subst_operand(at, map),
Operand::List(items) => {
for item in items {
subst_operand(item, map);
}
}
Operand::NamedCaptures => {}
Operand::Group(e) => subst_pred_expr(e, map),
Operand::Outer(inner) => subst_operand(inner, map),
Operand::Interp(segs) => {
let inside = Subst {
outer: map.hole,
hole: map.hole,
};
for seg in segs {
if let InterpSeg::Expr(e) = seg {
subst_operand(e, &inside);
}
}
}
Operand::Piped { expr, stages } => {
subst_operand(expr, map);
for st in stages.iter_mut() {
subst_stage(st, map);
}
}
Operand::Cond { cond, then, other } => {
subst_pred_expr(cond, map);
subst_operand(then, map);
subst_operand(other, map);
}
Operand::Lit(_)
| Operand::Recall(_)
| Operand::Topic
| Operand::Ordinal
| Operand::Edge { .. }
| Operand::Edges { .. }
| Operand::Capsae { .. }
| Operand::Capture(_)
| Operand::Now => {}
}
}
pub(crate) fn max_ctx_pred_expr(e: &PredExpr) -> usize {
let mut v = MaxCtx(0);
v.pred(e);
v.0
}
fn max_ctx_elem(e: &PathElem) -> usize {
let mut v = MaxCtx(0);
v.elem(e);
v.0
}
fn max_ctx_stage(s: &Stage) -> usize {
let mut v = MaxCtx(0);
v.stage(s);
v.0
}
struct MaxCtx(usize);
impl visit::Visitor for MaxCtx {
fn operand(&mut self, o: &Operand) {
if let Operand::Ctx { index, .. } = o {
self.0 = self.0.max(index.unwrap_or(0));
}
visit::walk_operand(self, o);
}
}
fn validate_keyed_stages(q: &Query) -> Result<()> {
let mut grouped = false;
for stage in &q.pipeline {
match stage {
Stage::Agg(call) if matches!(call.name.as_str(), "group" | "window") => grouped = true,
Stage::Func(call) if crate::stdlib::known_keyed(&call.name) && !grouped => {
return Err(QuarbError::parse(format!(
"'{n}' ranks a context: write '@| {n}(…)' (on the plain pipe it works per group, after '@| group')",
n = call.name
)));
}
_ => {}
}
}
for c in &q.correlations {
validate_keyed_stages(c)?;
}
Ok(())
}
fn validate_correlation_refs(q: &Query) -> Result<()> {
let n = q.correlations.len();
for b in &q.branches {
for elem in &b.steps {
if max_ctx_elem(elem) > 0 {
return Err(QuarbError::parse(
"the first expression drives the join and cannot \
reference '$*k'; join conditions belong on the \
joined expression: 'A <=> B[::x = _::x]'",
));
}
}
}
for st in &q.pipeline {
let m = max_ctx_stage(st);
if m > n {
return Err(QuarbError::parse(format!(
"the pipeline references '$*{m}', but only {n} \
expression(s) are joined"
)));
}
}
for (i, entry) in q.correlations.iter().enumerate() {
let k = i + 1;
for b in &entry.branches {
let last = b.steps.len().saturating_sub(1);
for (j, elem) in b.steps.iter().enumerate() {
let m = max_ctx_elem(elem);
if m == 0 && !elem_mentions_outer(elem) {
continue;
}
if j != last || !matches!(elem, PathElem::Step(_)) {
return Err(QuarbError::parse(
"join conditions belong in the joined \
expression's final step's brackets",
));
}
if m >= k {
return Err(QuarbError::parse(format!(
"joined expression #{k} references '$*{m}'; it may \
only reference expressions joined before it \
('$*1'..'$*{}') — the driver is '$$'",
k - 1
)));
}
}
if let Some(PathElem::Step(s)) = b.steps.last() {
let mut seen_corr = false;
for p in &s.predicates {
match p {
Predicate::Expr(e)
if max_ctx_pred_expr(e) > 0 || pred_mentions_outer(e) =>
{
seen_corr = true;
}
Predicate::Index(_) | Predicate::Range(_, _) if seen_corr => {
return Err(QuarbError::parse(
"positional selection after a join condition \
is ambiguous — select before the join \
condition, or in the pipeline",
));
}
_ => {}
}
}
}
}
for st in &entry.pipeline {
if max_ctx_stage(st) > 0 {
return Err(QuarbError::parse(
"a joined expression's own pipeline shapes its context \
before the join and cannot reference '$*k'; join \
conditions belong in its final step's brackets",
));
}
}
}
Ok(())
}
pub(crate) fn pred_mentions_outer(e: &PredExpr) -> bool {
let mut v = Outer(false);
v.pred(e);
v.0
}
pub(crate) fn elem_mentions_outer(e: &PathElem) -> bool {
let mut v = Outer(false);
v.elem(e);
v.0
}
struct Outer(bool);
impl visit::Visitor for Outer {
fn operand(&mut self, o: &Operand) {
match o {
Operand::Outer(_)
| Operand::Recall(_)
| Operand::Topic
| Operand::Ordinal
| Operand::Capture(_)
| Operand::NamedCaptures => self.0 = true,
Operand::Rel {
anchor:
Anchor::Mark(_)
| Anchor::MarkIndex(_)
| Anchor::MarkTop
| Anchor::MarksAll
| Anchor::MarksNamed(_),
..
} => self.0 = true,
_ => visit::walk_operand(self, o),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_arrow_opens_a_navigation_stage() {
let first = |q: &str| {
let p = parse(&lexer::lex(q).unwrap()).unwrap().pipeline;
match &p[0] {
Stage::Nav(_) => "nav",
Stage::Expr(_) => "expr",
_ => "other",
}
};
assert_eq!(first("/a/* | ::artist_id-->"), "nav");
assert_eq!(first("/a/* | ::artist_id-->::name"), "nav");
assert_eq!(first("/a/* | ::artist_id=>(/b/*::id)"), "nav");
assert_eq!(first("/a/* | ::artist_id=>(/b/*::id) | ::name"), "nav");
assert_eq!(first("/a/* | -->"), "nav");
assert_eq!(first("/a/* | <--(/b/*::artist_id)"), "nav");
assert_eq!(first("/a/* | ::artist_id-->::n * 2"), "expr");
assert_eq!(first("/a/* | (::artist_id--> @| count)"), "expr");
assert_eq!(first("/a/* | ->artist_id"), "nav");
}
#[test]
fn a_bare_sibling_hop_leads_an_operand() {
let tree = |q: &str| format!("{:?}", parse(&lexer::lex(q).unwrap()).unwrap());
assert_eq!(
tree("//p[substr(::; 1..3) = substr(>p::; 1..3)]"),
tree("//p[substr(::; 1..3) = substr(;-p::; 1..3)]")
);
assert_eq!(
tree("/x | %(prev = <y::; next = >y::)"),
tree("/x | %(prev = -;y::; next = ;-y::)")
);
assert_eq!(tree("//p[>p::n = 3]"), tree("//p[;-p::n = 3]"));
assert!(parse(&lexer::lex("//p[::n > 3]").unwrap()).is_ok());
assert!(parse(&lexer::lex("//p[substr(::; 1..3) > 3]").unwrap()).is_ok());
assert!(parse(&lexer::lex("//p[::n > 3 && ::m < 2]").unwrap()).is_ok());
let e = parse(&lexer::lex("//p[::n>3]").unwrap())
.unwrap_err()
.to_string();
assert!(e.contains("spaced"), "{e}");
let e = parse(&lexer::lex("//h2 > p::").unwrap())
.unwrap_err()
.to_string();
assert!(e.contains("glued"), "{e}");
assert!(parse(&lexer::lex("//h2>p::").unwrap()).is_ok());
assert!(parse(&lexer::lex("//*<function>[::n > 1]").unwrap()).is_ok());
}
#[test]
fn adversarial_nesting_is_an_error_not_an_abort() {
let deep = "(".repeat(5_000);
let toks = lexer::lex(&deep).unwrap();
assert!(parse(&toks).is_err());
let bangs = format!("//a[{}::x]", "!".repeat(5_000));
let toks = lexer::lex(&bangs).unwrap();
assert!(parse(&toks).is_err());
let ok = format!("{}::x{}", "(".repeat(20), ")".repeat(20));
let toks = lexer::lex(&format!("//a[{ok}]")).unwrap();
assert!(parse(&toks).is_ok());
}
#[test]
fn trait_cnf_blowup_is_an_error_not_a_hang() {
let pairs: Vec<String> = (0..24).map(|i| format!("(a{i} && b{i})")).collect();
let toks = lexer::lex(&format!("//*<{}>", pairs.join(" || "))).unwrap();
assert!(parse(&toks).is_err());
let toks = lexer::lex("//*<(a && b) || (c && d)>").unwrap();
assert!(parse(&toks).is_ok());
}
#[test]
fn record_field_names_must_be_unique() {
for q in [
"/r/* | %(::name; ::name)",
"/r/* | %(a = ::x; a = ::y)",
"/r/* | %(name = ::x; ::name)",
"/r/* @| group(::k, ::k) | count",
] {
let toks = lexer::lex(q).unwrap();
let err = parse(&toks).expect_err(q).to_string();
assert!(err.contains("twice"), "{q}: {err}");
}
let toks = lexer::lex("/r/* | %(::name; other = ::name)").unwrap();
assert!(parse(&toks).is_ok());
}
#[test]
fn fragment_bodies_cover_chains_and_inline_pipes() {
for q in [
"def &j: /a/* <=> /b/*[::x = _::x]; &j @| count",
"def &j: /a/* <=> /b/*[::x = _::x]; &j | [!$$1::x]",
"def &j: /a/* <=> /b/*[::x = _::x]; &j <=> /c/*[::y = $$1::y]",
"def &g: | trim | upper; /r/* | .u((::name | &g))",
"def &g2: | tp(\"%Y-%m-%d\") | ($_ + 12d); /r/* | .d((::when | &g2))",
] {
let toks = lexer::lex(q).unwrap();
assert!(parse(&toks).is_ok(), "should parse: {q}");
}
for (q, needle) in [
(
"def &j: /a/* <=> /b/*[::x = _::x]; /d/* <=> &j",
"chains are flat",
),
(
"def &j: /a/* <=> /b/*[::x = _::x]; /d/* || &j",
"stand alone",
),
("def &p: .x(::a); /r/*", "starts with its pipe"),
(
"def &p: | .x(::a); /r/* | .u((::name | &p))",
"may not push or navigate",
),
] {
let toks = lexer::lex(q).unwrap();
let err = parse(&toks).expect_err(q).to_string();
assert!(err.contains(needle), "{q}: {err}");
}
}
#[test]
fn full_register_view_and_numeric_push_names() {
for q in [
"/r/* | .(::a) | %%.",
"/r/* | .x(::a) | %. | %%.",
"/r/* | .\"1\"(::a) | %.",
"/r/* | %(a = 1; b = 2) | .%",
] {
let toks = lexer::lex(q).unwrap();
assert!(parse(&toks).is_ok(), "should parse: {q}");
}
let e = parse(&lexer::lex("/r/* | .1(::a) | %.").unwrap())
.unwrap_err()
.to_string();
assert!(e.contains("starts with a letter"), "{e}");
let e = parse(&lexer::lex("/r/* | ._id(::a)").unwrap())
.unwrap_err()
.to_string();
assert!(e.contains("starts with a letter"), "{e}");
}
#[test]
fn driver_first_placement_rules() {
for q in [
"/a/* <=> /b/*[::x = _::x]",
"/a/* <=>? /b/*[::x = _::x] | [!$$1::x]",
"/a/* <=> /b/*[::x = _::x] <=> /c/*[::y = $$1::y] | %($$1::n; $$2::m)",
"/a/* <=> /b/*[_/kid::x = ::x]",
] {
let toks = lexer::lex(q).unwrap();
assert!(parse(&toks).is_ok(), "should parse: {q}");
}
for (q, needle) in [
("/a/*[::x = $$1::x] <=> /b/*", "first expression drives"),
("/a/* <=> /b/*[::x = $$1::x]", "joined expression #1"),
(
"/a/* <=> /b/*[::x = $$2::x] <=> /c/*",
"joined expression #1",
),
("/a/* <=> /b/* | %($$2::n)", "only 1 expression"),
("/a/* <=> /b[_::x]/c/*", "final step"),
(
"/a/* <=> /b/*[::x = _::x][1]",
"positional selection after a join condition",
),
] {
let toks = lexer::lex(q).unwrap();
let err = parse(&toks).expect_err(q).to_string();
assert!(err.contains(needle), "{q}: {err}");
}
}
#[test]
fn bare_conditionals_in_delimited_positions() {
for q in [
"/r/* | .born(::quarter ? 1890 : 0)",
"/r/* | .born(::a ? 1 : ::b ? 2 : 3)",
"/r/* | .port(::e ?= \"C\" ? 1 : \"Q\" ? 2 : 0)",
"/r/* | .kind(/em ? \"has\" : \"none\")",
"/r/* | .y(::quarter ? (::quarter | s/ Q.*$//) * 1 : 0)",
"/r/* | %(age = ::Age ? ::Age * 1 : 0)",
"/r/* @| group(\"src\", ::Age ? \"manifest\" : \"records\") | count",
"def &f($x): /r/*[::a = $x]; &f(::b ? 1 : 2)",
"/r/* | .born((::quarter ? 1890 : 0))",
"/r/* | %(age = (( ::Age ? 1 : 0 )))",
] {
let toks = lexer::lex(q).unwrap();
assert!(parse(&toks).is_ok(), "should parse: {q}");
}
}
#[test]
fn overflowing_positional_index_is_an_error() {
let toks = lexer::lex("/a[9999999999999999999]").unwrap();
assert!(parse(&toks).is_err());
}
#[test]
fn bare_inf_and_nan_are_string_literals() {
let toks = lexer::lex("/x[::status = inf]").unwrap();
let q = parse(&toks).unwrap();
let dbg = format!("{q:?}");
assert!(dbg.contains("Str(\"inf\")"), "got {dbg}");
}
fn last_step(q: &Query) -> &Step {
match q.branches.last().unwrap().steps.last().unwrap() {
PathElem::Step(s) => s,
other => panic!("expected a step, got {other:?}"),
}
}
#[test]
fn map_pipe_after_step_is_not_leaf_anchor() {
let toks = lexer::lex("/data/tags $| upper").unwrap();
let q = parse(&toks).unwrap();
assert!(
q.pipeline.iter().any(|s| matches!(s, Stage::Map(_))),
"expected a map stage, got {:?}",
q.pipeline
);
assert!(
!last_step(&q).leaf,
"the step preceding `$|` must !be leaf-anchored"
);
}
#[test]
fn bare_dollar_still_anchors_leaf() {
let toks = lexer::lex("/data/tags$").unwrap();
let q = parse(&toks).unwrap();
assert!(
last_step(&q).leaf,
"a bare trailing `$` anchors the step to leaves"
);
}
#[test]
fn macro_body_shell_is_gated_without_allow_shell() {
let toks = lexer::lex("macro &m: ^ | `echo hi`; &m").unwrap();
let err = parse(&toks).unwrap_err();
assert!(
err.to_string().contains("allow-shell"),
"expected the shell gate to fire, got: {err}"
);
}
#[test]
fn trait_block_after_axis_is_wildcard_sugar() {
let sugar = parse(&lexer::lex("/<leaf>").unwrap()).unwrap();
let full = parse(&lexer::lex("/*<leaf>").unwrap()).unwrap();
assert_eq!(
format!("{sugar:?}"),
format!("{full:?}"),
"'/<leaf>' must parse identically to '/*<leaf>'"
);
}
#[test]
fn reverse_resolution_refused_inside_predicate() {
assert!(parse(&lexer::lex("//a[::r<--]").unwrap()).is_err());
assert!(parse(&lexer::lex("//a::r<--").unwrap()).is_err());
assert!(parse(&lexer::lex("//a<--(::r)").unwrap()).is_ok());
assert!(parse(&lexer::lex("//a[<--(::r)]").unwrap()).is_err());
assert!(parse(&lexer::lex("//a[<--(/x/*::r)]").unwrap()).is_ok());
assert!(parse(&lexer::lex("//a[<-b]").unwrap()).is_ok());
assert!(parse(&lexer::lex("//g | %(n = (<-b<--(::r) @| count))").unwrap()).is_ok());
assert!(parse(&lexer::lex("//g | %(n = (<--(::r)::x-->::y))").unwrap()).is_ok());
}
}
fn top_level_bash_op(src: &str) -> Option<(usize, &'static str)> {
let chars: Vec<char> = src.chars().collect();
let mut depth = 0i32;
let mut i = 0usize;
let mut byte = 0usize;
while i < chars.len() {
let c = chars[i];
match c {
'\'' | '"' | '`' => {
let q = c;
byte += c.len_utf8();
i += 1;
while i < chars.len() {
let d = chars[i];
if d == '\\' && q != '\'' && i + 1 < chars.len() {
byte += d.len_utf8() + chars[i + 1].len_utf8();
i += 2;
continue;
}
byte += d.len_utf8();
i += 1;
if d == q {
break;
}
}
continue;
}
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
':' if depth == 0 => {
if chars.get(i + 1) == Some(&':') {
while chars.get(i) == Some(&':') {
byte += 1;
i += 1;
}
continue;
}
match chars.get(i + 1) {
Some('?') => return Some((byte, ":?")),
Some('-') => return Some((byte, ":-")),
_ => {}
}
}
_ => {}
}
byte += c.len_utf8();
i += 1;
}
None
}
fn cmp_word(word: &str) -> Option<CmpOp> {
Some(match word {
".lt." | ".inf." | ".μικρότερο." | ".μικροτερο." | ".minor." | ".менее." | ".menor." => {
CmpOp::Lt
}
".le." | ".nonsup." | ".τοπολύ." | ".τοπολυ." | ".nonmaior." | ".неболее."
| ".nomayor." => CmpOp::Le,
".gt." | ".sup." | ".μεγαλύτερο." | ".μεγαλυτερο." | ".maior." | ".более." | ".mayor." => {
CmpOp::Gt
}
".ge."
| ".noninf."
| ".τουλάχιστον."
| ".τουλαχιστον."
| ".nonminor."
| ".неменее."
| ".nomenor." => CmpOp::Ge,
_ => return None,
})
}
const AND_WORDS: &[&str] = &[".and.", ".et.", ".και.", ".и.", ".y."];
const OR_WORDS: &[&str] = &[".or.", ".ou.", ".ή.", ".η.", ".vel.", ".или.", ".o."];
const NOT_WORDS: &[&str] = &[".not.", ".non.", ".όχι.", ".οχι.", ".не.", ".no."];
const BARE_BOOL_WORDS: &[(&str, &str)] = &[("and", "&&"), ("or", "||"), ("not", "!")];
pub(crate) fn is_bool_word(word: &str) -> bool {
AND_WORDS.contains(&word) || OR_WORDS.contains(&word) || NOT_WORDS.contains(&word)
}
fn universalize(elem: &mut PathElem) -> Result<()> {
fn step(s: &mut Step) -> Result<()> {
match s.axis {
Axis::Descendant(Reach::All) => {
s.axis = Axis::Universal;
Ok(())
}
_ => Err(QuarbError::parse(
"'*//' searches the whole arbor and takes no reach ('*//?', '*//!')",
)),
}
}
match elem {
PathElem::Step(s) => step(s),
PathElem::Group(g) => {
for alt in &mut g.alts {
match alt.as_mut_slice() {
[PathElem::Step(s)] => step(s)?,
_ => {
return Err(QuarbError::parse(
"'*//(…)' takes name alternatives: each a single name or pattern",
));
}
}
}
Ok(())
}
_ => Err(QuarbError::parse(
"'*//' is the universal search: follow it with a name, a pattern, or '*'",
)),
}
}