use crate::error::{QuarbError, Result};
use crate::syntax::ast::visit::{self, Visitor as _};
use crate::syntax::ast::{
Anchor, Arg, ArithOp, Axis, Bound, Branch, Budget, CmpOp, Command, ConcatMode, End, FnCall,
Group, Hole, IndexSpec, InterpSeg, LabelAlt, LabelClause, Matcher, Operand, PatSeg, PathElem,
PredExpr, Predicate, ProjShape, Projection, PushBody, Quant, Query, Reach, RefProp, RegRef,
ScopePath, Step, TableRow, TraitClause, hole_name,
};
use crate::syntax::expand::{Answer, Expander, Form, MacroBinding, RegUsage};
use crate::syntax::lexer::{self, Token};
use crate::value::ColumnType;
use crate::value::Value;
use crate::value::{AND_WORDS, NOT_WORDS, OR_WORDS, is_bool_word};
use globset::Glob;
use regex::Regex;
use std::collections::HashMap;
pub fn parse_scope_path(text: &str) -> Result<ScopePath> {
let toks = crate::syntax::lexer::lex(text)?;
let expander = crate::graph::reflect::Reflector::new(None);
let mut p = Parser::new(&toks, Defs::default(), &expander);
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, &crate::graph::reflect::Reflector::new(None))
}
pub fn parse_with_data(tokens: &[Token], defs: Defs, expander: &dyn Expander) -> Result<Query> {
parse_with_data_spanned(tokens, None, defs, expander)
}
pub fn parse_with_data_spanned(
tokens: &[Token],
spans: Option<&[(usize, usize)]>,
defs: Defs,
expander: &dyn Expander,
) -> Result<Query> {
let mut p = Parser::new(tokens, defs, expander);
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(crate) fn parse_commands_text(text: &str) -> Result<Vec<Command>> {
let text = text.trim();
let text = if text.starts_with('|') || text.starts_with("@|") {
text.to_string()
} else {
format!("| {text}")
};
let tokens = lexer::lex(&text)?;
let expander = crate::graph::reflect::Reflector::new(None);
let mut p = Parser::new(&tokens, Defs::default(), &expander);
let mut commands = Vec::new();
p.pipeline_items(&mut commands, PipeMode::Nav)?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"'{text}' holds more than commands"
)));
}
Ok(commands)
}
pub(crate) fn parse_elems_text(text: &str) -> Result<(Vec<PathElem>, Option<Projection>)> {
let tokens = lexer::lex(text.trim())?;
let expander = crate::graph::reflect::Reflector::new(None);
let mut p = Parser::new(&tokens, Defs::default(), &expander);
let b = p.branch_tail(Anchor::Current, Vec::new(), None)?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"'{text}' holds more than a walk"
)));
}
Ok((b.steps, b.projection))
}
pub(crate) fn parse_record_items_text(text: &str) -> Result<Vec<Arg>> {
let tokens = lexer::lex(&format!("%({})", text.trim()))?;
let expander = crate::graph::reflect::Reflector::new(None);
let mut p = Parser::new(&tokens, Defs::default(), &expander);
p.pos += 1;
let call = p.record_args()?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"'{text}' holds more than record items"
)));
}
Ok(call.args)
}
pub fn parse_first_steps(
tokens: &[Token],
defs: Defs,
expander: &dyn Expander,
) -> Result<Vec<String>> {
let mut p = Parser::new(tokens, defs, expander);
let q = p.parse()?;
let steps = p.first_steps.into_inner();
if steps.is_empty() {
return Ok(vec![crate::syntax::unparse::unparse(&q)]);
}
Ok(steps)
}
pub fn parse_defs(tokens: &[Token]) -> Result<Defs> {
let expander = crate::graph::reflect::Reflector::new(None);
let mut p = Parser::new(tokens, Defs::default(), &expander);
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)
}
pub fn defs_only(tokens: &[Token]) -> Result<bool> {
let expander = crate::graph::reflect::Reflector::new(None);
let mut p = Parser::new(tokens, Defs::default(), &expander);
while p.at_def() || p.at_macro() {
if p.at_def() {
p.parse_def()?;
} else {
p.parse_macro()?;
}
}
Ok(p.peek().is_none())
}
#[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,
uses_driver: bool,
unit_operands: Vec<(String, String)>,
body: DefBody,
}
fn reg_ref(rest: &str) -> Result<RegRef> {
if rest.is_empty() {
return Ok(RegRef::Top);
}
if rest == "." {
return Ok(RegRef::Back(2));
}
if let Some(d) = rest.strip_prefix('-') {
if let Ok(n) = d.parse::<usize>()
&& n >= 1
&& d.bytes().all(|c| c.is_ascii_digit())
{
return Ok(RegRef::Back(n));
}
} else if rest.bytes().all(|c| c.is_ascii_digit()) {
if let Ok(n) = rest.parse::<usize>()
&& n >= 1
{
return Ok(RegRef::Index(n));
}
} else if rest
.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_')
&& rest
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
return Ok(RegRef::Named(rest.to_string()));
}
Err(QuarbError::parse(format!(
"'$.{rest}' is no capsa read: '$.' is the latest value regula, '$.N' the N-th, \
'$.-N' the N-th back from the latest, '$..' the penultimate, '$.name' the latest \
under a name"
)))
}
const CURSOR_PROJECTION: &str = "'_' is the cursor: the position's property is '_:position::k' or '$$_::k'; the driver of a join is '$$0::k'";
const DRIVER_PLACEMENT: &str = "'$$0' names the driver of a correlation inside its \
join condition; elsewhere the invoking position is \
'$$__' and the position '$$_'";
#[derive(Debug, Clone)]
enum DefBody {
Query(Query),
Pipeline(Vec<Command>),
Predicates(Vec<Predicate>),
Macro(Query),
}
fn command_pipe(command: &Command) -> &'static str {
match command {
Command::Agg(_)
| Command::Select(_)
| Command::AggPush { .. }
| Command::AggRecord { .. }
| Command::AggExpr(_)
| Command::AggRecall(_) => "@|",
_ => "|",
}
}
struct CaptureRec {
mac: String,
reg: String,
pushes: usize,
recalls: usize,
}
enum Split {
Texts(Vec<String>),
Forms(Vec<Form>),
}
fn predicate_form(f: &Form) -> bool {
match f {
Form::Predicate(_) | Form::PredExpr(_) => true,
Form::Pipeline(cmds) => {
!cmds.is_empty()
&& cmds
.iter()
.all(|c| matches!(c, Command::Filter(_) | Command::Select(_)))
}
Form::Command(Command::Filter(_) | Command::Select(_)) => true,
_ => false,
}
}
fn as_predicates(f: Form) -> Vec<Form> {
match f {
Form::Pipeline(cmds) => cmds
.into_iter()
.map(|c| match c {
Command::Filter(e) => Form::Predicate(Predicate::Expr(e)),
Command::Select(p) => Form::Predicate(p),
_ => unreachable!("checked by predicate_form"),
})
.collect(),
Form::Command(Command::Filter(e)) => vec![Form::Predicate(Predicate::Expr(e))],
Form::Command(Command::Select(p)) => vec![Form::Predicate(p)],
other => vec![other],
}
}
fn attach_predicate(name: &str, pred: Predicate, steps: &mut [PathElem]) -> Result<()> {
match steps.last_mut() {
Some(PathElem::Step(s)) => {
s.predicates.push(pred);
Ok(())
}
Some(PathElem::Group(g)) => {
if !matches!(pred, Predicate::Expr(_)) {
return Err(QuarbError::parse(
"a group takes expression predicates only \
(positional selection has no order across \
repetition tiers)",
));
}
g.predicates.push(pred);
Ok(())
}
Some(
PathElem::Mark(_) | PathElem::Push { .. } | PathElem::Hole(_) | PathElem::Param(_),
) => Err(QuarbError::parse(format!(
"the predicate '&{name}' answered refines a hop or a \
group; a mark or a push takes no predicates"
))),
None => Err(QuarbError::parse(format!(
"the predicate '&{name}' answered refines the step \
before it; nothing precedes it here — walk first \
('/div&{name}')"
))),
}
}
fn leftover_param(q: &Query) -> Option<String> {
leftover_in_commands(&q.pipeline)
.or_else(|| q.correlations.iter().find_map(leftover_param))
.or_else(|| {
q.branches
.iter()
.find_map(|b| b.steps.iter().find_map(lp_in_elem))
})
}
fn leftover_in_commands(cmds: &[Command]) -> Option<String> {
cmds.iter().find_map(lp_in_command)
}
fn refuse_leftover(name: &str, p: Option<String>) -> Result<()> {
match p {
Some(p) => Err(QuarbError::parse(format!(
"parameter '${p}' of '&{name}' stands where a path or a command \
continues; it takes a path (a projection, steps) or a quarq: \
'&{name}(…; q(| …))'"
))),
None => Ok(()),
}
}
fn lp_in_command(c: &Command) -> Option<String> {
match c {
Command::Param { name, .. } => Some(name.clone()),
Command::Hole { hole, .. } => lp_in_operand(&hole.expr),
Command::Func(call)
| Command::Agg(call)
| Command::RecordWith(call)
| Command::RecordPush { call, .. }
| Command::AggRecord { call, .. } => call.args.iter().find_map(|a| match a {
Arg::Expr(e) | Arg::Opt { value: Some(e), .. } => lp_in_operand(e),
Arg::Lit(Value::Quarq(q)) => leftover_in_commands(&q.body),
_ => None,
}),
Command::Expr(e)
| Command::AggExpr(e)
| Command::ExprPush { expr: e, .. }
| Command::AggPush { body: e, .. } => lp_in_operand(e),
Command::Subcontext { body, .. } => leftover_param(body),
Command::Map(inner)
| Command::Repeat { command: inner, .. }
| Command::Attach { command: inner, .. } => lp_in_command(inner),
Command::Nav(b) => b.steps.iter().find_map(lp_in_elem),
Command::Select(p) => lp_in_predicate(p),
_ => None,
}
}
fn lp_in_predicate(p: &Predicate) -> Option<String> {
let Predicate::RangeForm(a, b) = p else {
return None;
};
[a, b].into_iter().find_map(|b| match b {
Bound::Param(p) => Some(p.clone()),
_ => None,
})
}
fn lp_in_elem(e: &PathElem) -> Option<String> {
match e {
PathElem::Param(p) => Some(p.clone()),
PathElem::Hole(h) => lp_in_operand(&h.expr),
PathElem::Push {
body: PushBody::Query(q),
..
} => leftover_param(q),
PathElem::Push {
body: PushBody::Expr(e),
..
} => lp_in_operand(e),
PathElem::Group(g) => g
.alts
.iter()
.flatten()
.find_map(lp_in_elem)
.or_else(|| g.predicates.iter().find_map(lp_in_predicate)),
PathElem::Step(s) => s.predicates.iter().find_map(lp_in_predicate),
_ => None,
}
}
fn lp_in_operand(o: &Operand) -> Option<String> {
match o {
Operand::Lit(Value::Quarq(q)) => leftover_in_commands(&q.body)
.or_else(|| q.holes.iter().find_map(|h| lp_in_operand(&h.expr))),
Operand::Hole(h) => lp_in_operand(&h.expr),
Operand::Piped { expr, commands } => {
lp_in_operand(expr).or_else(|| leftover_in_commands(commands))
}
Operand::Rel { steps, .. } | Operand::Ctx { steps, .. } => {
steps.iter().find_map(lp_in_elem)
}
Operand::Arith { left, right, .. }
| Operand::Concat { left, right, .. }
| Operand::Coalesce { left, right } => lp_in_operand(left).or_else(|| lp_in_operand(right)),
Operand::Neg(i)
| Operand::Desc(i)
| Operand::Gathered(i)
| Operand::Spread(i)
| Operand::Invoking(i)
| Operand::Position(i)
| Operand::Driver(i) => lp_in_operand(i),
Operand::Through { base, .. }
| Operand::Field { base, .. }
| Operand::ValueMeta { base, .. } => lp_in_operand(base),
Operand::Projected { base, .. } => lp_in_operand(base),
Operand::Typed { inner, .. } => lp_in_operand(inner),
Operand::List(items) => items.iter().find_map(lp_in_operand),
Operand::Row(items) => items.iter().find_map(|(_, e)| lp_in_operand(e)),
Operand::Cond { then, other, .. } => lp_in_operand(then).or_else(|| lp_in_operand(other)),
Operand::Group(e) => lp_in_pred(e),
_ => None,
}
}
fn lp_in_pred(e: &PredExpr) -> Option<String> {
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => lp_in_pred(a).or_else(|| lp_in_pred(b)),
PredExpr::Not(a) => lp_in_pred(a),
PredExpr::Compare(l, _, r) => lp_in_operand(l).or_else(|| lp_in_operand(r)),
PredExpr::Chain(os, _) => os.iter().find_map(lp_in_operand),
PredExpr::Truthy(o) => lp_in_operand(o),
}
}
fn rename_in_commands(commands: &mut [Command], f: &mut dyn FnMut(&str) -> String) {
for c in commands.iter_mut() {
rename_in_command(c, f);
}
}
fn rename_in_query(q: &mut Query, f: &mut dyn FnMut(&str) -> String) {
for corr in &mut q.correlations {
rename_in_query(corr, f);
}
for b in &mut q.branches {
rename_in_branch(b, f);
}
rename_in_commands(&mut q.pipeline, f);
}
fn rename_in_branch(b: &mut Branch, f: &mut dyn FnMut(&str) -> String) {
if let Anchor::Id(op) = &mut b.anchor {
rename_in_operand(op, f);
}
for e in &mut b.steps {
rename_in_elem(e, f);
}
}
fn rename_reg(r: &mut RegRef, f: &mut dyn FnMut(&str) -> String) {
if let RegRef::Named(n) = r
&& crate::syntax::ast::name_hole(n).is_none()
{
*n = f(n);
}
}
fn rename_name(n: &mut Option<String>, f: &mut dyn FnMut(&str) -> String) {
if let Some(n) = n
&& crate::syntax::ast::name_hole(n).is_none()
{
*n = f(n);
}
}
fn rename_in_command(c: &mut Command, f: &mut dyn FnMut(&str) -> String) {
match c {
Command::Push(name) => rename_name(name, f),
Command::SessionPush(name) => {
if crate::syntax::ast::name_hole(name).is_none() {
*name = f(name);
}
}
Command::Subcontext { name, body } => {
rename_name(name, f);
rename_in_query(body, f);
}
Command::ExprPush { name, expr } | Command::AggPush { name, body: expr } => {
rename_name(name, f);
rename_in_operand(expr, f);
}
Command::RecordPush { name, call, .. } => {
rename_name(name, f);
rename_in_args(&mut call.args, f);
}
Command::Attach { name, command } => {
rename_name(name, f);
rename_in_command(command, f);
}
Command::Func(call)
| Command::Agg(call)
| Command::RecordWith(call)
| Command::AggRecord { call, .. } => rename_in_args(&mut call.args, f),
Command::Expr(e) | Command::AggExpr(e) => rename_in_operand(e, f),
Command::Recall(r) | Command::Splice(r) | Command::AggRecall(r) => rename_reg(r, f),
Command::Select(Predicate::Expr(e)) | Command::Filter(e) => rename_in_pred(e, f),
Command::Nav(b) => rename_in_branch(b, f),
Command::Map(inner) | Command::Repeat { command: inner, .. } => rename_in_command(inner, f),
Command::Hole { hole, .. } => rename_in_operand(&mut hole.expr, f),
Command::Select(_)
| Command::Param { .. }
| Command::Spread { .. }
| Command::FieldsPush => {}
}
}
fn rename_in_args(args: &mut [Arg], f: &mut dyn FnMut(&str) -> String) {
for a in args.iter_mut() {
match a {
Arg::Expr(e) | Arg::Opt { value: Some(e), .. } => rename_in_operand(e, f),
Arg::Pairs { keys, values } => {
rename_in_operand(keys, f);
rename_in_operand(values, f);
}
Arg::Hole(h) => rename_in_operand(&mut h.expr, f),
Arg::Lit(Value::Quarq(q)) => {
let mut op = Operand::Lit(Value::Quarq(q.clone()));
rename_in_operand(&mut op, f);
if let Operand::Lit(v) = op {
*a = Arg::Lit(v);
}
}
_ => {}
}
}
}
fn rename_in_elem(e: &mut PathElem, f: &mut dyn FnMut(&str) -> String) {
match e {
PathElem::Mark(name) => rename_name(name, f),
PathElem::Push { name, body } => {
rename_name(name, f);
match body {
PushBody::Query(q) => rename_in_query(q, f),
PushBody::Expr(e) => rename_in_operand(e, f),
}
}
PathElem::Step(s) => {
for p in &mut s.predicates {
if let Predicate::Expr(e) = p {
rename_in_pred(e, f);
}
}
}
PathElem::Group(g) => {
for alt in &mut g.alts {
for e in alt {
rename_in_elem(e, f);
}
}
for p in &mut g.predicates {
if let Predicate::Expr(e) = p {
rename_in_pred(e, f);
}
}
}
PathElem::Hole(h) => rename_in_operand(&mut h.expr, f),
PathElem::Param(_) => {}
}
}
fn rename_in_pred(e: &mut PredExpr, f: &mut dyn FnMut(&str) -> String) {
match e {
PredExpr::Or(a, b) | PredExpr::And(a, b) => {
rename_in_pred(a, f);
rename_in_pred(b, f);
}
PredExpr::Not(a) => rename_in_pred(a, f),
PredExpr::Compare(l, _, r) => {
rename_in_operand(l, f);
rename_in_operand(r, f);
}
PredExpr::Chain(os, _) => {
for o in os {
rename_in_operand(o, f);
}
}
PredExpr::Truthy(o) => rename_in_operand(o, f),
}
}
fn rename_in_operand(o: &mut Operand, f: &mut dyn FnMut(&str) -> String) {
match o {
Operand::Recall(r) => rename_reg(r, f),
Operand::PeerReg { at, reg } => {
if let Some(at) = at {
rename_in_operand(at, f);
}
rename_reg(reg, f);
}
Operand::Lit(Value::Quarq(q)) => {
let mut body = (*q.body).clone();
rename_in_commands(&mut body, f);
let mut holes = (*q.holes).clone();
for h in &mut holes {
rename_in_operand(&mut h.expr, f);
}
*o = Operand::Lit(if holes.is_empty() {
quarq_literal(body)
} else {
Value::quarq_template(body, holes)
});
}
Operand::Hole(h) => rename_in_operand(&mut h.expr, f),
Operand::Rel { steps, anchor, .. } => {
if let Anchor::Id(op) = anchor {
rename_in_operand(op, f);
}
for e in steps {
rename_in_elem(e, f);
}
}
Operand::Ctx { steps, .. } => {
for e in steps {
rename_in_elem(e, f);
}
}
Operand::Arith { left, right, .. }
| Operand::Concat { left, right, .. }
| Operand::Coalesce { left, right } => {
rename_in_operand(left, f);
rename_in_operand(right, f);
}
Operand::Row(items) => {
for (_, i) in items {
rename_in_operand(i, f);
}
}
Operand::Neg(i)
| Operand::Desc(i)
| Operand::Gathered(i)
| Operand::Spread(i)
| Operand::Invoking(i)
| Operand::Position(i)
| Operand::Driver(i)
| Operand::Typed { inner: i, .. } => rename_in_operand(i, f),
Operand::Through { base, .. }
| Operand::Field { base, .. }
| Operand::ValueMeta { base, .. }
| Operand::Index { base, .. } => rename_in_operand(base, f),
Operand::Projected { base, shape } => {
rename_in_operand(base, f);
match shape {
ProjShape::Value(v) => rename_in_operand(v, f),
ProjShape::Columns { items, index } => {
for (_, item) in items {
rename_in_operand(item, f);
}
if let Some(i) = index {
rename_in_operand(i, f);
}
}
}
}
Operand::LabelIndex { base, labels } => {
rename_in_operand(base, f);
for l in labels {
rename_in_operand(l, f);
}
}
Operand::List(items) => {
for i in items {
rename_in_operand(i, f);
}
}
Operand::Range { from, to, step } => {
rename_in_operand(from, f);
rename_in_operand(to, f);
if let Some(s) = step {
rename_in_operand(s, f);
}
}
Operand::Piped { expr, commands } => {
rename_in_operand(expr, f);
rename_in_commands(commands, f);
}
Operand::Cond { cond, then, other } => {
rename_in_pred(cond, f);
rename_in_operand(then, f);
rename_in_operand(other, f);
}
Operand::Group(e) => rename_in_pred(e, f),
Operand::Match {
scrutinee,
arms,
other,
} => {
rename_in_operand(scrutinee, f);
for (t, _, r) in arms {
rename_in_operand(t, f);
rename_in_operand(r, f);
}
rename_in_operand(other, f);
}
Operand::Interp(segs) => {
for seg in segs {
match seg {
InterpSeg::Text(_) => {}
InterpSeg::Expr(e) | InterpSeg::Strict(e, _) => rename_in_operand(e, f),
InterpSeg::Default(e, d) => {
rename_in_operand(e, f);
rename_in_operand(d, f);
}
}
}
}
_ => {}
}
}
fn invites_of(expander: &dyn Expander, args: &[Operand]) -> std::collections::BTreeSet<String> {
let mut s = std::collections::BTreeSet::new();
for a in args {
s.extend(expander.operand_invites(a));
}
s
}
struct Parser<'a> {
toks: &'a [Token],
pos: usize,
defs: Defs,
def_params: Vec<String>,
expander: &'a dyn Expander,
pattern_depth: usize,
predicate_depth: usize,
correlation_depth: usize,
in_def_body: bool,
body_uses_driver: bool,
body_unit_operands: Vec<(String, String)>,
nest_depth: usize,
subquery_depth: usize,
arg_depth: usize,
in_option_value: bool,
keyed_args: bool,
captures: std::cell::RefCell<Vec<CaptureRec>>,
first_steps: std::cell::RefCell<Vec<String>>,
template_holes: Option<Vec<Hole>>,
in_macro_body: bool,
expansions: std::cell::Cell<usize>,
}
const MAX_NEST: usize = 56;
#[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),
budget: None,
})
&& 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 { .. } | PathElem::Hole(_) | PathElem::Param(_) => false,
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum PipeMode {
Nav,
Scalar,
}
pub(crate) fn command_mode_out(command: &Command, cur: PipeMode) -> PipeMode {
match command {
Command::SessionPush(_) => cur,
Command::Param { .. } => cur,
Command::Hole { .. } => cur,
Command::Func(call) if call.name == "session" && !call.args.is_empty() => cur,
Command::Func(call) if call.name == "graft" => PipeMode::Nav,
Command::Agg(call) if call.name == "collapse" => cur,
Command::Nav(b) => {
if b.projection.is_some() {
PipeMode::Scalar
} else {
PipeMode::Nav
}
}
Command::Push(_)
| Command::ExprPush { .. }
| Command::RecordPush { .. }
| Command::FieldsPush
| Command::Subcontext { .. } => PipeMode::Nav,
Command::Filter(_) | Command::Select(_) | Command::AggPush { .. } => cur,
Command::Splice(_) => PipeMode::Nav,
Command::Func(_)
| Command::Expr(_)
| Command::Agg(_)
| Command::AggRecord { .. }
| Command::AggExpr(_)
| Command::AggRecall(_)
| Command::Recall(_)
| Command::RecordWith(_)
| Command::Spread { .. }
| Command::Map(_) => PipeMode::Scalar,
Command::Repeat { command, .. } | Command::Attach { command, .. } => {
command_mode_out(command, cur)
}
}
}
fn attached_ref(body: &str) -> Result<RegRef> {
match body {
"%." => Ok(RegRef::Record),
"." | ".." => Err(QuarbError::parse(format!(
"':{body}' is retired: a node carries attached regulae, read by name (X:.name) or all at once (X:%.)"
))),
b if b.starts_with(['@', '%']) || b.starts_with(".-") => Err(QuarbError::parse(format!(
"':{b}' is retired: a node carries attached regulae, read by name (X:.name) or all at once (X:%.)"
))),
b if b[1..].chars().all(|c| c.is_ascii_digit()) => Err(QuarbError::parse(format!(
"':{b}' is retired: attached regulae are named, read as X:.name (or all at once, X:%.)"
))),
b => Ok(RegRef::Named(b[1..].to_string())),
}
}
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, expander: &'a dyn Expander) -> Parser<'a> {
Parser {
toks,
pos: 0,
defs,
def_params: Vec::new(),
expander,
pattern_depth: 0,
predicate_depth: 0,
correlation_depth: 0,
in_def_body: false,
body_uses_driver: false,
body_unit_operands: Vec::new(),
nest_depth: 0,
subquery_depth: 0,
arg_depth: 0,
in_option_value: false,
keyed_args: false,
captures: std::cell::RefCell::new(Vec::new()),
first_steps: std::cell::RefCell::new(Vec::new()),
template_holes: None,
in_macro_body: false,
expansions: std::cell::Cell::new(0),
}
}
fn child<'t>(&self, toks: &'t [Token], defs: Defs) -> Result<Parser<'t>>
where
'a: 't,
{
Ok(Parser {
nest_depth: self.sub_depth()?,
in_def_body: self.in_def_body,
..Parser::new(toks, defs, self.expander)
})
}
}
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(i) = query.pipeline.iter().position(
|c| matches!(c, Command::Func(call) | Command::Agg(call) if call.name == "copy"),
) && i + 1 < query.pipeline.len()
{
return Err(QuarbError::parse(
"copy stands last: it copies the output exactly as it would have printed; to copy a part, push it and recall it ('| .r(…) | … | $.r | copy')",
));
}
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}' stands apart from its path: a core field glues to the step \
('/x:{text}'; ':fields' lists them), a property is '::{text}'"
)));
}
if self.pos > 0
&& let Some(Token::Name {
text: prev,
quoted: false,
..
}) = self.toks.get(self.pos - 1)
&& let Token::Name {
text: word,
quoted: false,
..
} = tok
&& is_word(word)
&& matches!(
literal_value(prev, false, &|e| self.expander.unit_scale(e)),
Value::Int(_) | Value::Float(_)
)
{
return Err(QuarbError::parse(format!(
"'{prev} {word}' is no expression: a literal head continues only with an \
operator — write {prev}_{word} for the quantity, \"{prev} {word}\" for the text"
)));
}
return Err(QuarbError::parse(format!(
"unexpected trailing input at token {tok:?}"
)));
}
validate_correlation_refs(&query)?;
validate_keyed_commands(&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
};
self.correlation_depth += 1;
let entry = self.parse_query();
self.correlation_depth -= 1;
let mut entry = entry?;
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 = command_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<Command>,
correlations: &mut Vec<Query>,
join_at: &mut usize,
) -> Result<()> {
if branches.is_empty()
&& self.subquery_depth == 0
&& matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "session")
&& matches!(
self.toks.get(self.pos + 1),
None | Some(Token::LParen | Token::Pipe)
)
{
self.pos += 1;
let mut args: Vec<Arg> = Vec::new();
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
loop {
if matches!(self.peek(), Some(Token::RParen)) {
self.pos += 1;
break;
}
let Some(name) = self.option_item() else {
return Err(QuarbError::parse(
"'session(…)' takes its settings as options: 'session(--pretty=always)', 'session(--pretty=structures)', 'session(--pretty=off)', 'session(--precision=n)'",
));
};
match (self.peek(), self.toks.get(self.pos + 1)) {
(Some(Token::DashDash), Some(Token::Name { glued: true, .. })) => {
self.pos += 2
}
_ => self.pos += 1,
}
let value = if matches!(self.peek(), Some(Token::OptEq)) {
self.pos += 1;
match self.peek() {
Some(Token::Name { text, .. }) => {
let text = text.clone();
let v = match literal_value(&text, false, &|e| {
self.expander.unit_scale(e)
}) {
v @ Value::Int(_) => v,
_ => Value::Str(text),
};
self.pos += 1;
Some(Operand::Lit(v))
}
_ => {
return Err(QuarbError::parse(format!(
"'session({name}=…)' takes a word or a number"
)));
}
}
} else {
None
};
args.push(Arg::Opt { name, value });
match self.peek() {
Some(Token::Comma | Token::Semi) => self.pos += 1,
Some(Token::RParen) => {}
_ => {
return Err(QuarbError::parse(
"'session(…)' separates its settings with ';'",
));
}
}
}
}
branches.push(Branch {
steps: Vec::new(),
projection: None,
anchor: Anchor::Root,
});
pipeline.push(Command::Func(FnCall {
headless: false,
name: "session".into(),
args,
}));
return Ok(());
}
if branches.is_empty()
&& self.subquery_depth == 0
&& (matches!(self.peek(), Some(Token::Eq)) || self.value_only_head())
{
if matches!(self.peek(), Some(Token::Eq)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::Pipe)) {
return Err(QuarbError::parse(
"the head's expression may be left out, not replaced by a \
pipe: write '= .f(0) | …' (a leading pipe is a fragment)",
));
}
if self.command_head_ahead() {
let command = self.pipe_item(PipeMode::Nav)?;
let command = self.quantified_push(command)?;
branches.push(Branch {
steps: Vec::new(),
projection: None,
anchor: Anchor::Root,
});
pipeline.push(command);
return Ok(());
}
}
let left = self.additive()?;
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 | …'",
));
}
let mut expr = self.expr_head_from(left)?;
head_projections(&mut expr);
branches.push(Branch {
steps: Vec::new(),
projection: None,
anchor: Anchor::Root,
});
pipeline.push(Command::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)
) {
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 {
let b = self.branch()?;
if branches.is_empty() && matches!(self.peek(), Some(Token::ProjectColon)) {
self.pos += 1;
let shape = self.project_shape()?;
let base = Operand::Rel {
steps: b.steps,
projection: b.projection,
anchor: b.anchor,
};
branches.push(Branch {
steps: Vec::new(),
projection: None,
anchor: Anchor::Root,
});
pipeline.push(Command::Expr(Operand::Projected {
base: Box::new(base),
shape,
}));
return Ok(());
}
branches.push(b);
}
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::Tilde
| Token::TildeTilde
| Token::LParen
| Token::ColonColon
| Token::ColonColonColon
| 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<Command>, 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 = command_mode_out(s, mode);
}
continue;
}
if let Some(Token::Expansion(text)) = self.peek() {
let text = text.clone();
self.pos += 1;
pipeline.push(Command::Hole {
hole: self.expansion_hole(&text)?,
agg: false,
});
continue;
}
if let Some(name) = self.lone_param_ahead() {
self.pos += 2;
pipeline.push(Command::Param { name, agg: false });
continue;
}
let command = self.pipe_item(mode)?;
let command = self.quantified_push(command)?;
mode = command_mode_out(&command, mode);
pipeline.push(command);
}
Some(Token::Dollar) if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe)) => {
self.pos += 2;
let command = Command::Map(Box::new(self.map_command()?));
mode = command_mode_out(&command, mode);
pipeline.push(command);
}
Some(Token::Dollar)
if matches!(
self.toks.get(self.pos + 1),
Some(Token::Name { text, quoted: false, .. }) if self.def_params.iter().any(|p| p == text)
) =>
{
let Some(Token::Name { text, .. }) = self.toks.get(self.pos + 1) else {
unreachable!("matched");
};
let command = Command::Param {
name: text.clone(),
agg: false,
};
self.pos += 2;
pipeline.push(command);
}
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 = command_mode_out(s, mode);
}
continue;
}
if let Some(Token::Expansion(text)) = self.peek() {
let text = text.clone();
self.pos += 1;
pipeline.push(Command::Hole {
hole: self.expansion_hole(&text)?,
agg: true,
});
continue;
}
if let Some(name) = self.lone_param_ahead() {
self.pos += 2;
pipeline.push(Command::Param { name, agg: true });
continue;
}
if self.nav_command_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(_, _)
| Predicate::RangeForm(_, _)
| Predicate::Positions(_)
| Predicate::Computed(_)) => {
pipeline.push(Command::Select(pred));
continue;
}
Predicate::Expr(_) => {
return Err(QuarbError::parse(
"a condition filters per capsa; write '| [cond]' \
('@| [n]' selects positionally)",
));
}
}
}
if self.expr_head_ahead() {
let command = Command::AggExpr(self.additive()?);
mode = command_mode_out(&command, mode);
pipeline.push(command);
continue;
}
if let Some(command) = self.agg_sigil_command()? {
mode = command_mode_out(&command, mode);
pipeline.push(command);
continue;
}
let call = self.func_call()?;
let command = agg_command(call)?;
mode = command_mode_out(&command, mode);
pipeline.push(command);
}
_ => break,
}
}
Ok(())
}
fn push_rest(&mut self, name: Option<String>) -> Result<Command> {
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(Command::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(Command::RecordPush {
name,
call,
enriched,
});
}
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::Expansion(_)))
&& matches!(self.toks.get(self.pos + 1), Some(Token::RParen))
{
let expr = self.cond_expr()?;
self.expect(Token::RParen, "')' to close the push")?;
return Ok(Command::ExprPush { name, expr });
}
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(Command::Subcontext {
name,
body: Box::new(body),
});
}
self.pos = save;
let expr = self.cond_expr()?;
let commands = self.inline_pipe_tail()?;
let expr = if commands.is_empty() {
expr
} else {
Operand::Piped {
expr: Box::new(expr),
commands,
}
};
self.expect(Token::RParen, "')' to close a subcontext")?;
Ok(Command::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(Command::Push(name))
}
}
fn pipe_item(&mut self, mode: PipeMode) -> Result<Command> {
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(Command::Nav(b));
}
self.pos = save;
return Ok(Command::Expr(self.additive()?));
}
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& let Some(op) = arith_word(text)
&& !matches!(
self.toks.get(self.pos + 1),
None | Some(Token::Pipe | Token::RParen)
)
&& !(matches!(self.toks.get(self.pos + 1), Some(Token::At | Token::Dollar))
&& matches!(self.toks.get(self.pos + 2), Some(Token::Pipe)))
{
self.pos += 1;
let left = match op {
ArithOp::Add | ArithOp::Sub => {
let right = self.multiplicative()?;
Operand::Arith {
op,
left: Box::new(Operand::Topic),
right: Box::new(right),
}
}
_ => {
let right = self.unary()?;
self.multiplicative_from(Operand::Arith {
op,
left: Box::new(Operand::Topic),
right: Box::new(right),
})?
}
};
return Ok(Command::Expr(self.additive_from(left)?));
}
if let Some(mode) = self.concat_word_ahead() {
let left = self.concat_rest(mode, Operand::Topic)?;
return Ok(Command::Expr(self.additive_from(left)?));
}
if matches!(self.peek(), Some(Token::Coalesce)) {
let cond = self.coalesce_tail(PredExpr::Truthy(Operand::Topic))?;
return Ok(Command::Expr(self.cond_expr_from(cond)?));
}
if self.cmp_ahead() {
let first = self.pred_primary_from(Operand::Topic)?;
let first = self.pred_and_from(first)?;
let cond = self.pred_or_from(first)?;
let cond = self.coalesce_tail(cond)?;
return Ok(Command::Expr(self.cond_expr_from(cond)?));
}
if self.nav_command_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(Command::Nav(b));
}
self.pos = save;
} else {
let save = self.pos;
let b = self.branch()?;
if !self.expr_continues() {
return Ok(Command::Nav(b));
}
self.pos = save;
return Ok(Command::Expr(self.additive()?));
}
}
}
}
match self.peek() {
Some(Token::Dollar) => Ok(Command::Expr(self.expr_head()?)),
Some(Token::Expansion(_)) => Ok(Command::Expr(self.expr_head()?)),
Some(Token::Driver | Token::Position | Token::Invoking) => {
Ok(Command::Expr(self.expr_head()?))
}
Some(Token::Field | Token::PercentPlus | Token::Capsa(_)) => {
Ok(Command::Expr(self.expr_head()?))
}
Some(
Token::Topics
| Token::Positions
| Token::Matrix { .. }
| Token::NodeMatrix { .. }
| Token::CtxTable
| Token::Cursor,
) => Ok(Command::Expr(self.expr_head()?)),
Some(Token::At)
if matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
|| (matches!(self.toks.get(self.pos + 1), Some(Token::Percent))
&& matches!(self.toks.get(self.pos + 2), Some(Token::LParen))) =>
{
Ok(Command::Expr(self.additive()?))
}
Some(Token::AtAt) => {
self.pos += 1;
self.expect_dot("@@")?;
Ok(Command::Recall(RegRef::NodeWhole))
}
Some(Token::At) => {
if matches!(
self.toks.get(self.pos + 1),
Some(Token::Name { text, quoted: false, .. }) if text == "-"
) {
return Ok(Command::Expr(self.additive()?));
}
self.pos += 1;
if matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "*")
{
return Err(QuarbError::parse(
"'@*' is retired: the context is '@_' (the topics), '@@_' \
(the positions; '@@_::prop' maps over them), '#.' / '#.*' (the \
table of the named / of every value regula) or '##.' / '##.*' \
(of the node regulae)",
));
}
if !matches!(self.peek(), Some(Token::Name { text, .. }) if text == ".") {
return Ok(Command::Func(FnCall {
headless: false,
name: "@".into(),
args: Vec::new(),
}));
}
self.expect_dot("@")?;
Ok(Command::Recall(RegRef::Whole))
}
Some(Token::Percent) if self.projector_items_ahead() => {
let v = self.unary()?;
Ok(Command::Expr(Operand::Projected {
base: Box::new(Operand::Topic),
shape: ProjShape::Value(Box::new(v)),
}))
}
Some(Token::Hash) => {
if matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
|| self.table_count_ahead()
{
return self.table_command();
}
self.pos += 1;
Ok(Command::Func(FnCall {
headless: false,
name: "#".into(),
args: Vec::new(),
}))
}
Some(Token::Percent) => {
let head = self.pos;
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(Command::RecordWith(call));
}
return Ok(Command::Recall(match self.view_dots("%%")? {
1 => RegRef::NodeRecord,
_ => RegRef::NodeFullRecord,
}));
}
if matches!(self.peek(), Some(Token::LParen)) {
let call = self.record_args()?;
if self.expr_continues() {
self.pos = head;
return Ok(Command::Expr(self.additive()?));
}
validate_record(&call)?;
return Ok(Command::Func(call));
}
if matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == ",")
{
self.pos += 1;
return Ok(Command::Recall(RegRef::SessionRecord));
}
if !self.at_view_dots() {
return Ok(Command::Func(FnCall {
headless: false,
name: "%".into(),
args: Vec::new(),
}));
}
Ok(Command::Recall(match self.view_dots("%")? {
1 => RegRef::Record,
_ => RegRef::FullRecord,
}))
}
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(Command::Spread { outer })
}
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with(',') => {
let name = text[1..].to_string();
self.pos += 1;
if name.is_empty()
|| !name
.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_')
{
return Err(QuarbError::parse(
"the session push names its value: '| ,df' files the topic under 'df' for the session",
));
}
if matches!(self.peek(), Some(Token::LParen)) {
return Err(QuarbError::parse(format!(
"the session push takes no body: '| ,{name}' files the topic; compute it first ('| expr | ,{name}')"
)));
}
Ok(Command::SessionPush(name))
}
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::Expansion(t)) = self.peek()
{
let t = t.clone();
self.pos += 1;
name = Some(self.name_hole(&t)?);
}
if name.is_none()
&& matches!(self.peek(), Some(Token::Colon { spaced: false }))
&& matches!(self.toks.get(self.pos + 1), Some(Token::Percent))
{
self.pos += 2;
if matches!(self.peek(), Some(Token::LParen | Token::Percent)) {
return Err(QuarbError::parse(
"the attaching fields push is bare: '.:%' attaches every field of the topic record; a record is attached as '.:name(%(…))'",
));
}
return Ok(Command::Attach {
name: None,
command: Box::new(Command::FieldsPush),
});
}
if name.is_none() && matches!(self.peek(), Some(Token::Field)) {
self.pos += 1;
let Some(Token::Name {
text: n,
quoted: false,
..
}) = self.peek()
else {
return Err(QuarbError::parse(
"an attaching push takes a name: '.:name(body)' attaches the body's value to the node, '.:name' the topic",
));
};
let n = n.clone();
self.pos += 1;
if !n.chars().next().is_some_and(|c| c.is_alphabetic()) {
return Err(QuarbError::parse(format!(
"an attaching push's name starts with a letter ('.:name'), not '{n}'"
)));
}
if matches!(self.peek(), Some(Token::Percent)) {
return Err(QuarbError::parse(
"an attaching push takes a body or the topic: '.:name(body)', '.:name'; a record is attached as '.:name(%(…))'",
));
}
let inner = self.push_rest(Some(n.clone()))?;
return Ok(Command::Attach {
name: Some(n),
command: Box::new(inner),
});
}
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))"
)));
}
self.push_rest(name)
}
Some(Token::Subst {
pattern,
replacement,
mods,
}) => {
let flags = super::lexer::subst_flags(mods);
Regex::new(&format!("{flags}{pattern}"))
.map_err(|e| QuarbError::parse(format!("bad substitution pattern: {e}")))?;
let call = FnCall {
headless: false,
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(Command::Func(call))
}
Some(Token::LBracket)
if matches!(self.toks.get(self.pos + 1), Some(Token::LBracket)) =>
{
let labels = self.label_brackets()?;
return Ok(Command::Expr(Operand::LabelIndex {
base: Box::new(Operand::Topic),
labels,
}));
}
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::RangeForm(_, _) => {
return Err(QuarbError::parse(
"a fragment parameter stands for a slice bound of a step \
('/row[..$n]'), not of a value's index",
));
}
Predicate::Index(n) => {
return Ok(Command::Func(FnCall {
headless: false,
name: "[]".into(),
args: vec![Arg::Lit(Value::Int(n))],
}));
}
Predicate::Range(a, b) => {
return Ok(Command::Func(FnCall {
headless: false,
name: "[]".into(),
args: vec![Arg::Range(a, b)],
}));
}
Predicate::Positions(parts) => {
return Ok(Command::Func(FnCall {
headless: false,
name: "[]".into(),
args: parts
.into_iter()
.map(|p| match p {
Predicate::Index(n) => Arg::Lit(Value::Int(n)),
Predicate::Range(a, b) => Arg::Range(a, b),
_ => unreachable!("a position list holds positions and ranges"),
})
.collect(),
}));
}
Predicate::Computed(op) => {
return Ok(Command::Func(FnCall {
headless: false,
name: "[]".into(),
args: vec![Arg::Expr(op)],
}));
}
};
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(_, _)
| Predicate::RangeForm(_, _)
| Predicate::Positions(_)
| Predicate::Computed(_) => return Err(positional()),
}
}
Ok(Command::Filter(e))
}
Some(Token::ColonColon | Token::ColonColonColon | Token::LParen | Token::Interp(_)) => {
Ok(Command::Expr(self.expr_head()?))
}
Some(Token::Name {
text,
quoted: false,
..
}) if !matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& self.literal_word(text) =>
{
Ok(Command::Expr(self.expr_head()?))
}
Some(Token::Name { quoted: true, .. }) => Ok(Command::Expr(self.expr_head()?)),
Some(Token::Name {
text,
quoted: false,
..
}) if text == "q" && matches!(self.toks.get(self.pos + 1), Some(Token::LParen)) => {
Ok(Command::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]",
));
}
self.check_unit_call(&call)?;
func_command(call)
}
}
}
fn nav_command_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
| Token::Tilde
| Token::TildeTilde
)
};
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)
if matches!(self.toks.get(self.pos + 1), Some(Token::Expansion(_)))
&& matches!(self.toks.get(self.pos + 2), Some(Token::RParen)) =>
{
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::ProjectColon) => true,
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 view_dots(&mut self, sigil: &str) -> Result<usize> {
match self.peek() {
Some(Token::Name { text, .. }) if text == "." => {
self.pos += 1;
Ok(1)
}
Some(Token::Name { text, .. }) if text == ".*" => {
self.pos += 1;
Ok(2)
}
Some(Token::Name { text, .. }) if text == ".." => Err(QuarbError::parse(format!(
"'{sigil}..' is retired: the full view is '{sigil}.*' (the doubled dot is the penultimate regula, '$..')"
))),
_ => Err(QuarbError::parse(format!("expected '.' after '{sigil}'"))),
}
}
fn at_view_dots(&self) -> bool {
matches!(self.peek(), Some(Token::Name { text, .. }) if text == "." || text == ".*" || text == "..")
}
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 outer_body = std::mem::replace(&mut self.in_def_body, true);
let outer_uses = std::mem::replace(&mut self.body_uses_driver, false);
let outer_units = std::mem::take(&mut self.body_unit_operands);
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 commands = Vec::new();
self.pipeline_items(&mut commands, PipeMode::Nav)?;
if commands.is_empty() {
return Err(QuarbError::parse(format!(
"fragment '&{name}' has an empty body"
)));
}
DefBody::Pipeline(commands)
} 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 command; \
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();
let uses_driver = self.body_uses_driver;
let unit_operands = std::mem::replace(&mut self.body_unit_operands, outer_units);
self.in_def_body = outer_body;
self.body_uses_driver = outer_uses;
self.expect(Token::Semi, "';' to end the definition")?;
self.defs.entries.push((
name,
Def {
params,
rest: None,
data_aware: false,
uses_driver,
unit_operands,
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();
self.in_macro_body = true;
let body = self.parse_query();
self.in_macro_body = false;
let body = body?;
self.def_params.clear();
self.expect(Token::Semi, "';' to end the definition")?;
self.defs.entries.push((
name,
Def {
params,
rest,
data_aware,
uses_driver: false,
unit_operands: Vec::new(),
body: DefBody::Macro(body),
},
));
Ok(())
}
fn record_captures(
&self,
name: &str,
invited: &std::collections::BTreeSet<String>,
usage: 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 = self.expander.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 rename_binders(
&self,
invited: &std::collections::BTreeSet<String>,
apply: &mut dyn FnMut(&mut dyn FnMut(&str) -> String),
) {
let n = self.expansions.get() + 1;
self.expansions.set(n);
apply(
&mut |name: &str| match crate::syntax::ast::template_bare(name) {
Some(bare) if invited.contains(bare) => bare.to_string(),
Some(bare) => crate::syntax::ast::fresh_name(bare, n),
None => name.to_string(),
},
);
}
fn join_text(&self, name: &str, values: Vec<String>) -> Result<String> {
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 join_path_text(&self, name: &str, values: Vec<String>) -> Result<String> {
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()
)));
}
self.join_text(name, values)
}
fn expand_macro_answers(
&self,
name: &str,
def: &Def,
args: Vec<Operand>,
) -> Result<Vec<Answer>> {
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::syntax::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,
},
);
resolve_range_forms_query(&mut body, name, &forms)?;
refuse_leftover(name, leftover_param(&body))?;
revalidate_records_query(&body)?;
let mut bindings: Vec<(String, MacroBinding)> = def
.params
.iter()
.zip(&args)
.map(|(p, a)| (p.clone(), MacroBinding::One(a.clone())))
.collect();
if let Some(rest) = &def.rest {
bindings.push((rest.clone(), MacroBinding::Rest(args[n..].to_vec())));
}
self.expander.expand(name, &body, &bindings, def.data_aware)
}
fn split_answers(&self, name: &str, answers: Vec<Answer>) -> Result<Split> {
let forms = answers
.iter()
.filter(|a| matches!(a, Answer::Form(_)))
.count();
if forms == 0 {
return Ok(Split::Texts(
answers
.into_iter()
.map(|a| match a {
Answer::Text(t) => t,
Answer::Form(_) => unreachable!("counted"),
})
.collect(),
));
}
if forms != answers.len() {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered {} form(s) and {} text(s); an \
expansion is structure or text, not both",
forms,
answers.len() - forms
)));
}
Ok(Split::Forms(
answers
.into_iter()
.map(|a| match a {
Answer::Form(f) => f,
Answer::Text(_) => unreachable!("counted"),
})
.collect(),
))
}
fn graft_query(&self, name: &str, forms: Vec<Form>) -> Result<Query> {
let mut out = Query {
correlations: Vec::new(),
branches: Vec::new(),
pipeline: Vec::new(),
join_at: 0,
outer: false,
};
let mut steps: Vec<PathElem> = Vec::new();
let mut projection: Option<Projection> = None;
let mut whole = 0usize;
let flush =
|steps: &mut Vec<PathElem>, projection: &mut Option<Projection>, out: &mut Query| {
if !steps.is_empty() || projection.is_some() {
out.branches.push(Branch {
steps: std::mem::take(steps),
projection: projection.take(),
anchor: Anchor::Current,
});
}
};
let text = |fs: &[Form]| fs.iter().map(Form::text).collect::<Vec<_>>().join(" ");
let texts = text(&forms);
let pipeline_refusal = || {
QuarbError::expansion(format!(
"macro '&{name}' expanded to a pipeline fragment ('{texts}'); \
at path position it must expand to navigation steps"
))
};
for f in forms {
match f {
Form::Elem(e) => {
if projection.is_some() {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered a step after a projection \
('{texts}'); a projection ends the branch"
)));
}
steps.push(e);
}
Form::Predicate(p) => {
attach_predicate(name, p, &mut steps)?;
}
Form::PredExpr(e) => {
attach_predicate(name, Predicate::Expr(e), &mut steps)?;
}
Form::Projection(p) => {
if projection.is_some() {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered two projections ('{texts}'); \
a branch ends in one"
)));
}
projection = Some(p);
}
Form::Branch(b) => {
flush(&mut steps, &mut projection, &mut out);
whole += 1;
out.branches.push(b);
}
Form::Query(q) => {
flush(&mut steps, &mut projection, &mut out);
whole += 1;
if !q.correlations.is_empty() {
out.join_at = out.pipeline.len() + q.join_at;
}
out.correlations.extend(q.correlations);
out.branches.extend(q.branches);
out.pipeline.extend(q.pipeline);
}
Form::Pipeline(cmds) => {
let mut cmds = cmds.into_iter();
match cmds.next() {
Some(Command::Nav(b)) => {
flush(&mut steps, &mut projection, &mut out);
whole += 1;
out.branches.push(b);
out.pipeline.extend(cmds);
}
Some(Command::Expr(Operand::Rel {
steps: rs,
projection: rp,
anchor,
})) => {
flush(&mut steps, &mut projection, &mut out);
whole += 1;
out.branches.push(Branch {
steps: rs,
projection: rp,
anchor,
});
out.pipeline.extend(cmds);
}
_ => return Err(pipeline_refusal()),
}
}
Form::Command(Command::Nav(b)) => {
flush(&mut steps, &mut projection, &mut out);
whole += 1;
out.branches.push(b);
}
Form::Command(_) => return Err(pipeline_refusal()),
Form::Operand(Operand::Rel {
steps: rs,
projection: rp,
anchor,
})
| Form::Arg(Arg::Expr(Operand::Rel {
steps: rs,
projection: rp,
anchor,
})) => {
flush(&mut steps, &mut projection, &mut out);
whole += 1;
out.branches.push(Branch {
steps: rs,
projection: rp,
anchor,
});
}
other => {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered {} at a path position \
('{}'); navigation stands here — invoke it in \
operand position, or answer a path",
other.kind(),
other.text()
)));
}
}
}
flush(&mut steps, &mut projection, &mut out);
if whole >= 2 {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to {whole} branch-shaped values; \
they do not join silently — answer one group, \
'@| q((${{$_}}))', or a single branch"
)));
}
if out.branches.is_empty() {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to nothing"
)));
}
self.first_steps
.borrow_mut()
.push(crate::syntax::unparse::unparse(&out));
Ok(out)
}
fn graft_commands(&self, name: &str, forms: Vec<Form>, pipe: &str) -> Result<Vec<Command>> {
let texts = forms.iter().map(Form::text).collect::<Vec<_>>().join(" ");
let mut out: Vec<Command> = Vec::new();
for f in forms {
match f {
Form::Command(c) => out.push(c),
Form::Pipeline(cmds) => out.extend(cmds),
Form::Query(q) if q.correlations.is_empty() => {
match q.branches.len() {
0 => {}
1 => out.push(Command::Nav(q.branches.into_iter().next().expect("one"))),
_ => {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered a union after a pipe \
('{texts}'); a hop walks one path per thread"
)));
}
}
out.extend(q.pipeline);
}
Form::Branch(b) => out.push(Command::Nav(b)),
Form::Projection(p) => out.push(Command::Nav(Branch {
steps: Vec::new(),
projection: Some(p),
anchor: Anchor::Current,
})),
Form::Operand(Operand::Rel {
steps,
projection,
anchor,
})
| Form::Arg(Arg::Expr(Operand::Rel {
steps,
projection,
anchor,
})) => out.push(Command::Nav(Branch {
steps,
projection,
anchor,
})),
Form::Operand(op) | Form::Arg(Arg::Expr(op)) => out.push(Command::Expr(op)),
Form::Arg(Arg::Lit(v)) => out.push(Command::Expr(Operand::Lit(v))),
Form::Predicate(Predicate::Expr(e)) | Form::PredExpr(e) => {
if pipe != "|" {
return Err(QuarbError::expansion(format!(
"'@|' aggregates the whole context; the predicate \
'&{name}' answered filters per capsa — invoke it \
with '|'"
)));
}
out.push(Command::Filter(e));
}
Form::Predicate(p) => out.push(Command::Select(p)),
other => {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered {} after a pipe ('{}'); a \
walk continues at path position — or open the \
template with the pipe: 'q(| …)'",
other.kind(),
other.text()
)));
}
}
}
let Some(first) = out.first() else {
return Err(QuarbError::expansion(format!(
"macro '&{name}' expanded to nothing"
)));
};
let first = command_pipe(first);
if first != pipe {
return Err(QuarbError::parse(format!(
"macro '&{name}' expanded to a '{first}' pipeline \
('{texts}') but was invoked with '{pipe}'"
)));
}
self.first_steps.borrow_mut().push(
out.iter()
.map(crate::syntax::unparse::command)
.collect::<Vec<_>>()
.join(" "),
);
Ok(out)
}
fn graft_operand(&self, name: &str, forms: Vec<Form>) -> Result<Operand> {
if forms.len() != 1 {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered {} values in operand position; \
an operand is one value",
forms.len()
)));
}
let form = forms.into_iter().next().expect("one");
let branch_query = |b: Branch| Query {
correlations: Vec::new(),
branches: vec![b],
pipeline: Vec::new(),
join_at: 0,
outer: false,
};
let op = match form {
Form::Operand(op) | Form::Arg(Arg::Expr(op)) => op,
Form::Arg(Arg::Lit(v)) => Operand::Lit(v),
Form::Predicate(Predicate::Expr(e)) | Form::PredExpr(e) => Operand::Group(Box::new(e)),
Form::Predicate(_) => {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered a positional predicate in \
operand position; a condition operand needs an expression"
)));
}
Form::Branch(b) => self.operand_from_query(name, branch_query(b))?,
Form::Query(q) => self.operand_from_query(name, q)?,
Form::Elem(e) => self.operand_from_query(
name,
branch_query(Branch {
steps: vec![e],
projection: None,
anchor: Anchor::Current,
}),
)?,
Form::Projection(p) => Operand::Rel {
steps: Vec::new(),
projection: Some(p),
anchor: Anchor::Current,
},
Form::Pipeline(mut cmds) => match cmds.as_slice() {
[Command::Expr(_)] => match cmds.pop() {
Some(Command::Expr(op)) => op,
_ => unreachable!("matched"),
},
[Command::Nav(_)] => match cmds.pop() {
Some(Command::Nav(b)) => self.operand_from_query(name, branch_query(b))?,
_ => unreachable!("matched"),
},
_ => Operand::Piped {
expr: Box::new(Operand::Seed),
commands: cmds,
},
},
Form::Command(Command::Expr(op)) => op,
Form::Command(Command::Nav(b)) => self.operand_from_query(name, branch_query(b))?,
Form::Command(c) => Operand::Piped {
expr: Box::new(Operand::Seed),
commands: vec![c],
},
other => {
return Err(QuarbError::expansion(format!(
"macro '&{name}' answered {} in operand position ('{}')",
other.kind(),
other.text()
)));
}
};
self.first_steps
.borrow_mut()
.push(crate::syntax::unparse::operand_text(&op));
Ok(op)
}
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,
budget: None,
}
} else {
Quant {
min: 0,
max: None,
budget: 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(&mut self, name: &str, def: &Def, bang: bool) -> Result<()> {
if def.uses_driver && self.correlation_depth == 0 {
if self.in_def_body {
self.body_uses_driver = true;
} else {
return Err(QuarbError::parse(format!(
"in expansion of '&{name}': {DRIVER_PLACEMENT}"
)));
}
}
if !def.unit_operands.is_empty() {
match self.expander.unit_names() {
Some(names) => {
for (command, operand) in &def.unit_operands {
if let Some(why) = unit_operand_refusal(command, operand, &names) {
return Err(QuarbError::parse(format!(
"in expansion of '&{name}': {why}"
)));
}
}
}
None if self.in_def_body => {
self.body_unit_operands
.extend(def.unit_operands.iter().cloned());
}
None => {}
}
}
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 check_unit_call(&mut self, call: &FnCall) -> Result<()> {
if !matches!(call.name.as_str(), "convert" | "unit" | "magnitude") {
return Ok(());
}
let Some(Arg::Lit(Value::Str(operand))) = call.args.first() else {
return Ok(());
};
if operand.is_empty() {
return Ok(());
}
match self.expander.unit_names() {
Some(names) => match unit_operand_refusal(&call.name, operand, &names) {
Some(why) => Err(QuarbError::parse(why)),
None => Ok(()),
},
None if self.in_def_body => {
self.body_unit_operands
.push((call.name.clone(), operand.clone()));
Ok(())
}
None => 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,
},
);
resolve_range_forms_query(&mut q, &name, &map)?;
refuse_leftover(&name, leftover_param(&q))?;
revalidate_records_query(&q)?;
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(self.expander, &args);
let answers = self.expand_macro_answers(&name, &def, args)?;
let q = match self.split_answers(&name, answers)? {
Split::Texts(texts) => {
let text = self.join_path_text(&name, texts)?;
self.parse_expansion_query(&name, &text)?
}
Split::Forms(forms) => {
let mut q = self.graft_query(&name, forms)?;
self.rename_binders(&invited, &mut |f| rename_in_query(&mut q, f));
q
}
};
self.record_captures(&name, &invited, self.expander.usage_of_query(&q));
q
}
};
Ok((name, q, quant))
}
fn invoke_pipeline_fragment(
&mut self,
pipe: &'static str,
pipeline: &mut Vec<Command>,
) -> Result<()> {
if matches!(self.toks.get(self.pos + 1), Some(Token::Dollar)) {
if pipe != "|" {
return Err(QuarbError::parse(
"a held quarq runs per capsa: splice it with '| &$.p'",
));
}
self.pos += 1;
pipeline.push(Command::Splice(self.splice_register()?));
return Ok(());
}
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(commands) => {
let first = command_pipe(commands.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,
};
let mut commands = commands;
subst_commands(&mut commands, &subst);
resolve_range_forms_commands(&mut commands, &name, &map)?;
refuse_leftover(&name, leftover_in_commands(&commands))?;
revalidate_records_commands(&commands)?;
pipeline.extend(commands);
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(Command::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(self.expander, &args);
let answers = self.expand_macro_answers(&name, &def, args)?;
let commands = match self.split_answers(&name, answers)? {
Split::Forms(forms) => {
let mut commands = self.graft_commands(&name, forms, pipe)?;
self.rename_binders(&invited, &mut |f| {
rename_in_commands(&mut commands, f)
});
commands
}
Split::Texts(texts) => {
let text = self.join_text(&name, texts)?;
self.reparse_commands(&name, &text, pipe)?
}
};
self.record_captures(&name, &invited, self.expander.usage_of_commands(&commands));
pipeline.extend(commands);
Ok(())
}
}
}
fn reparse_commands(&self, name: &str, text: &str, pipe: &str) -> Result<Vec<Command>> {
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 commands"
)));
}
};
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 commands = Vec::new();
p.pipeline_items(&mut commands, PipeMode::Nav)
.map_err(wrap)?;
if p.pos != tokens.len() {
return Err(QuarbError::parse(format!(
"macro '&{name}' expanded to text with trailing \
content ('{text}')"
)));
}
Ok(commands)
}
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(_)
| Token::ProjectColon
) {
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 matches!(tok, Token::Dollar)
&& self.pattern_depth == 0
&& self.template_holes.is_some()
&& let Some(Token::Name {
text,
quoted: false,
..
}) = self.toks.get(self.pos + 1)
&& self.def_params.iter().any(|p| p == text)
{
steps.push(PathElem::Param(text.clone()));
self.pos += 2;
continue;
}
if is_projection_start(tok) {
if self.is_resolution_ahead() {
steps.push(self.path_elem()?);
continue;
}
break;
}
if matches!(tok, Token::Field) {
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 command, 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,
},
);
resolve_range_forms_query(&mut q, &name, &map)?;
refuse_leftover(&name, leftover_param(&q))?;
revalidate_records_query(&q)?;
q
}
DefBody::Macro(_) => {
let invited = invites_of(self.expander, &args);
let answers = self.expand_macro_answers(&name, &def, args)?;
let q = match self.split_answers(&name, answers)? {
Split::Texts(texts) => {
let text = self.join_path_text(&name, texts)?;
self.parse_expansion_query(&name, &text)?
}
Split::Forms(forms) if forms.iter().all(predicate_form) => {
let forms: Vec<Form> = forms.into_iter().flat_map(as_predicates).collect();
if quant.is_some() {
return Err(QuarbError::parse(format!(
"the predicate '&{name}' answered refines what \
precedes it; a quantifier cannot ride it"
)));
}
let mut preds: Vec<Predicate> = forms
.into_iter()
.map(|f| match f {
Form::Predicate(p) => p,
Form::PredExpr(e) => Predicate::Expr(e),
_ => unreachable!("filtered"),
})
.collect();
self.rename_binders(&invited, &mut |f| {
for p in preds.iter_mut() {
if let Predicate::Expr(e) = p {
rename_in_pred(e, f);
}
}
});
self.first_steps.borrow_mut().push(
preds
.iter()
.map(crate::syntax::unparse::predicate)
.collect::<String>(),
);
self.attach_predicate_fragment(&name, preds, &HashMap::new(), 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);
}
Split::Forms(forms) => {
let mut q = self.graft_query(&name, forms)?;
self.rename_binders(&invited, &mut |f| rename_in_query(&mut q, f));
q
}
};
self.record_captures(&name, &invited, self.expander.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 \
command, 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),
budget: None,
},
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),
budget: None,
}),
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::Tilde
| Token::TildeTilde
| Token::LParen
| Token::Amp
| Token::ColonColon
| Token::ColonColonColon
)
);
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<()> {
for p in &mut preds {
resolve_range_form(p, name, map)?;
}
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 { .. } | PathElem::Hole(_) | PathElem::Param(_),
) => 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.correlation_depth = self.correlation_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::syntax::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::Tilde
| Token::TildeTilde
| Token::ColonColon
| Token::ColonColonColon
)
)
};
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 == ".." {
Some((Anchor::MarkBack(2), 3))
} else if let Some(n) = text
.strip_prefix(".-")
.or_else(|| text.strip_prefix('-'))
.and_then(|d| d.parse::<usize>().ok())
.filter(|&n| n >= 1)
{
Some((Anchor::MarkBack(n), 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 list_items(&mut self, names: bool) -> Result<(Vec<Operand>, bool)> {
let mut items = Vec::new();
let mut bare = true;
loop {
if matches!(self.peek(), Some(Token::RParen)) {
self.pos += 1;
break;
}
if let Some(opt) = self.option_item() {
return Err(QuarbError::parse(format!(
"'@(…)' takes no option ('{opt}' given); its items are values — a hop as \
an item is written '@((--x))' or '@(--x;)'"
)));
}
let word = match (self.peek(), self.toks.get(self.pos + 1)) {
(
Some(Token::Name {
text,
quoted: false,
..
}),
Some(Token::Comma | Token::Semi | Token::RParen),
) if text.parse::<f64>().is_err() => Some(text.clone()),
_ => None,
};
let item = match word {
Some(w) if names => {
self.pos += 1;
Operand::Lit(Value::Str(w))
}
_ => {
bare = false;
self.additive()?
}
};
items.push(item);
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)",
));
}
}
}
let bare = bare && !items.is_empty();
Ok((items, bare))
}
fn table_literal(&mut self) -> Result<Operand> {
self.expect(Token::LParen, "'(' after '#'")?;
let mut header: Option<Vec<String>> = None;
let mut rows: Vec<TableRow> = Vec::new();
let mut held: Option<Vec<Operand>> = None;
let mut first = true;
let header_of = |items: Vec<Operand>| -> Result<Vec<String>> {
let mut names = Vec::with_capacity(items.len());
for item in items {
match item {
Operand::Lit(Value::Str(s)) => names.push(s),
Operand::Lit(v @ (Value::Int(_) | Value::Float(_))) => {
names.push(v.to_string())
}
_ => {
return Err(QuarbError::parse(
"a table's header is a list of names, bare or quoted (#(@(name; age); …)); a value computed there names nothing",
));
}
}
}
Ok(names)
};
loop {
if matches!(self.peek(), Some(Token::RParen)) {
self.pos += 1;
break;
}
match (self.peek(), self.toks.get(self.pos + 1)) {
(Some(Token::At), Some(Token::LParen)) => {
if !first && held.is_none() && !matches!(rows.last(), Some(TableRow::List(_))) {
return Err(QuarbError::parse(
"a list after a record row: a table's rows are all records (a frame, its header the list standing first) or all lists (a matrix, #(@(1; 2); @(3; 4)))",
));
}
self.pos += 2;
let (items, _) = self.list_items(true)?;
if first {
held = Some(items);
} else {
if let Some(h) = held.take() {
rows.push(TableRow::List(h));
}
rows.push(TableRow::List(items));
}
}
(Some(Token::Percent), Some(Token::LParen)) => {
if matches!(rows.last(), Some(TableRow::List(_))) {
return Err(QuarbError::parse(
"a record after a matrix row: a table's rows are all lists (a matrix) or all records (a frame, its header the list standing first)",
));
}
if let Some(h) = held.take() {
header = Some(header_of(h)?);
}
self.pos += 2;
rows.push(TableRow::Record(self.record_row()?));
}
_ if self.option_item().is_some() => {
let opt = self.option_item().expect("checked");
return Err(QuarbError::parse(format!(
"'#(…)' takes its option alone, and '{opt}' stands beside an item: the \
rows of a reshape come through the pipe ('| #(--width=n)'); a \
literal's rows are written in it, without an option"
)));
}
_ => {
return Err(QuarbError::parse(
"a table holds record rows %(…) after an optional header list of names, #(@(name; age); %(\"Braund\"; 22)), or list rows for a matrix, #(@(1; 2); @(3; 4))",
));
}
}
first = false;
match self.peek() {
Some(Token::Comma | Token::Semi) => self.pos += 1,
Some(Token::RParen) => {}
_ => {
return Err(QuarbError::parse(
"a table separates its rows with ';': #(@(1; 2); @(3; 4))",
));
}
}
}
if let Some(h) = held.take() {
if !h.is_empty() && h.iter().all(|i| matches!(i, Operand::Lit(Value::Str(_)))) {
let names: Vec<String> = h
.iter()
.map(|i| match i {
Operand::Lit(Value::Str(s)) => s.clone(),
_ => String::new(),
})
.collect();
return Err(QuarbError::parse(format!(
"'#(@({0}))' alone is retired: a header of no rows; write the rows after it, or build the table from data with '#(--header=@({1}))'",
names.join("; "),
names
.iter()
.map(|n| format!(":{n}"))
.collect::<Vec<_>>()
.join("; ")
)));
}
rows.push(TableRow::List(h));
}
Ok(Operand::Table { header, rows })
}
fn table_count_ahead(&self) -> bool {
matches!(self.toks.get(self.pos + 1), Some(Token::Dollar))
&& matches!(self.toks.get(self.pos + 2), Some(Token::Name { text, quoted: false, .. }) if text.parse::<i64>().is_ok())
}
fn retired_table_count(&self) -> Option<QuarbError> {
if self.table_count_ahead()
&& let Some(Token::Name { text, .. }) = self.toks.get(self.pos + 2)
{
return Some(QuarbError::parse(format!(
"'#({text})' is retired: a number is no row; write '#(--width={text})' \
('-w={text}') to lay the list in rows of {text}"
)));
}
None
}
fn table_option_ahead(&self) -> bool {
matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& match self.toks.get(self.pos + 2) {
Some(Token::DashDash) => true,
Some(Token::Name {
text,
quoted: false,
..
}) => lexer::is_option_word(text),
_ => false,
}
}
fn option_item(&self) -> Option<String> {
if !self.option_ahead() || self.trailing_separator_ahead() {
return None;
}
Some(match (self.peek(), self.toks.get(self.pos + 1)) {
(
Some(Token::DashDash),
Some(Token::Name {
text,
quoted: false,
glued: true,
}),
) => format!("--{text}"),
(Some(Token::DashDash), _) => "--".to_string(),
(Some(Token::Name { text, .. }), _) => text.clone(),
_ => "-".to_string(),
})
}
fn trailing_separator_ahead(&self) -> bool {
let mut depth = 0usize;
let mut i = self.pos;
while let Some(t) = self.toks.get(i) {
match t {
Token::LParen | Token::LBracket | Token::MarkOpen | Token::IdAnchorOpen => {
depth += 1
}
Token::RParen | Token::RBracket => {
if depth == 0 {
return false;
}
depth -= 1;
}
Token::Semi | Token::Comma if depth == 0 => {
return matches!(self.toks.get(i + 1), Some(Token::RParen));
}
_ => {}
}
i += 1;
}
false
}
fn table_options(&mut self) -> Result<FnCall> {
self.pos += 2; let mut opts: Vec<(String, Option<Operand>)> = Vec::new();
loop {
if !opts.is_empty() && matches!(self.peek(), Some(Token::Semi | Token::Comma)) {
self.pos += 1;
}
let name = match (self.peek(), self.toks.get(self.pos + 1)) {
(Some(Token::RParen), _) => {
self.pos += 1;
break;
}
(
Some(Token::DashDash),
Some(Token::Name {
text,
quoted: false,
glued: true,
}),
) => {
let t = format!("--{text}");
self.pos += 2;
t
}
(
Some(Token::Name {
text,
quoted: false,
..
}),
_,
) if text.starts_with('-') => {
let t = text.clone();
self.pos += 1;
t
}
_ => {
let opt = opts
.last()
.map_or("the option".to_string(), |(n, _)| format!("'{n}'"));
return Err(QuarbError::parse(format!(
"'#(…)' takes its option alone, and an item stands beside {opt}: the \
rows of a reshape come through the pipe ('| #(--width=n)'); a \
literal's rows are written in it, without an option"
)));
}
};
let value = if matches!(self.peek(), Some(Token::OptEq)) {
self.pos += 1;
if matches!(name.as_str(), "--name" | "-n")
&& let Some(Token::Name { text, .. }) = self.peek()
{
let text = text.clone();
self.pos += 1;
Some(Operand::Lit(Value::Str(text)))
} else if matches!(self.peek(), Some(Token::At))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
{
self.pos += 2;
let (items, _) = self.list_items(true)?;
Some(Operand::List(items))
} else if matches!(self.peek(), Some(Token::Percent))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
{
self.pos += 2;
Some(Operand::Row(self.record_row()?))
} else if matches!(self.peek(), Some(Token::LParen))
&& matches!(name.as_str(), "--index" | "-i")
{
Some(self.projector_list()?)
} else {
Some(self.unary()?)
}
} else {
None
};
opts.push((name, value));
}
enum Header {
Names(Vec<Value>),
Computed(Operand),
}
let mut width: Option<i64> = None;
let mut header: Option<Header> = None;
let mut index: Option<Operand> = None;
let mut header_name: Option<String> = None;
for (name, value) in opts {
match name.as_str() {
"--name" | "-n" => {
header_name = Some(match value {
Some(Operand::Lit(Value::Str(s))) if !s.is_empty() => s,
Some(Operand::Lit(v @ (Value::Int(_) | Value::Float(_)))) => v.to_string(),
_ => {
return Err(QuarbError::parse(format!(
"'#({name}=word)' names the header, a word bare or quoted"
)));
}
});
}
"--index" | "-i" => {
let Some(mut op) = value else {
return Err(QuarbError::parse(format!(
"'#({name}=:col)' names the index column"
)));
};
head_projections(&mut op);
match &op {
Operand::Lit(Value::Projection { .. }) | Operand::Lit(Value::List(_)) => {
index = Some(op)
}
Operand::Lit(Value::Str(s)) => {
index = Some(Operand::Lit(Value::projection(1, s.clone())))
}
Operand::Lit(v @ (Value::Int(_) | Value::Float(_))) => {
index = Some(Operand::Lit(Value::projection(1, v.to_string())))
}
_ => index = Some(op),
}
}
"--width" | "-w" => {
width = Some(match value {
Some(Operand::Lit(Value::Int(n))) => n,
_ => {
return Err(QuarbError::parse(format!(
"'#({name}=n)' takes the column count, a number"
)));
}
});
}
"--header" | "-h" => {
if let Some(Operand::Row(fields)) = &value {
let spelled: Vec<String> = fields
.iter()
.map(|(k, op)| {
let word = match op {
Operand::Lit(Value::Str(w)) => w.clone(),
_ => "type".to_string(),
};
format!(":{}<{word}>", k.clone().unwrap_or_else(|| "name".into()))
})
.collect();
return Err(QuarbError::parse(format!(
"'#({name}=%(…))' is retired: the header is a list of typed projectors, \
'#({name}=@({}))'",
spelled.join("; ")
)));
}
let items = match value {
Some(Operand::List(items)) => items,
Some(Operand::Lit(Value::List(l))) => {
l.iter().cloned().map(Operand::Lit).collect()
}
Some(
op @ (Operand::Group(_)
| Operand::Recall(_)
| Operand::Field { .. }
| Operand::Rel { .. }
| Operand::Param(_)
| Operand::Piped { .. }
| Operand::Index { .. }
| Operand::PeerReg { .. }
| Operand::Topic),
) => {
header = Some(Header::Computed(op));
continue;
}
_ => {
return Err(QuarbError::parse(format!(
"'#({name}=…)' takes the column names as a list, \
'#({name}=@(a; b; c))', or the typed header as a record, \
'#({name}=%(a = \"int\"; b = \"text\"))'"
)));
}
};
let mut names = Vec::with_capacity(items.len());
for mut item in items {
head_projections(&mut item);
match item {
Operand::Lit(Value::Str(s)) => names.push(Value::Str(s)),
Operand::Lit(v @ (Value::Int(_) | Value::Float(_))) => {
names.push(Value::Str(v.to_string()))
}
Operand::Lit(p @ Value::Projection { .. }) => names.push(p),
_ => {
return Err(QuarbError::parse(
"a header is a list of names, bare or quoted, or of projectors \
('#(--header=@(name; age))', '#(--header=@(:zip<text>; :amount<float>))'); \
a value computed there names nothing",
));
}
}
}
header = Some(Header::Names(names));
}
other => {
return Err(QuarbError::parse(format!(
"'#(…)' takes '--width=n' ('-w=n'), '--header=list' ('-h=list'), \
'--index=:col' ('-i=:col') or '--name=word' ('-n=word'); '{other}' given"
)));
}
}
}
let arg = match (width, header) {
(Some(_), Some(_)) => {
return Err(QuarbError::parse(
"'#(…)' takes one of '--width=n', which lays the list in rows of n, and \
'--header=list', which names the columns (their count the width); both given",
));
}
(Some(n), None) => Some(Arg::Lit(Value::Int(n))),
(None, Some(Header::Names(h))) => Some(Arg::Lit(Value::list(h))),
(None, Some(Header::Computed(op))) => Some(Arg::Expr(op)),
(None, None) if index.is_some() || header_name.is_some() => None,
(None, None) => {
return Err(QuarbError::parse(
"'#(…)' names no option: '#(--width=n)' ('-w=n') lays the list in rows \
of n, '#(--header=list)' ('-h=list') names its columns, '#(--index=:col)' \
('-i=:col') names the index, '#(--name=word)' names the header",
));
}
};
let mut args: Vec<Arg> = arg.into_iter().collect();
if let Some(op) = index {
args.push(Arg::Opt {
name: "--index".into(),
value: Some(op),
});
}
if let Some(n) = header_name {
args.push(Arg::Opt {
name: "--name".into(),
value: Some(Operand::Lit(Value::Str(n))),
});
}
Ok(FnCall {
headless: false,
name: "#".into(),
args,
})
}
fn expr_head_ahead(&self) -> bool {
match self.peek() {
Some(Token::LParen) => true,
Some(Token::Name {
text,
quoted: false,
..
}) => {
(!matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& self.literal_word(text))
|| (text == "q" && matches!(self.toks.get(self.pos + 1), Some(Token::LParen)))
}
Some(Token::Name { quoted: true, .. } | Token::Interp(_)) => true,
Some(Token::At) => {
matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
|| (matches!(self.toks.get(self.pos + 1), Some(Token::Percent))
&& matches!(self.toks.get(self.pos + 2), Some(Token::LParen)))
}
Some(Token::Hash) => {
matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& !self.table_option_ahead()
}
_ => false,
}
}
fn quantified_push(&mut self, command: Command) -> Result<Command> {
let Some(&Token::Quant { min, max }) = self.peek() else {
return Ok(command);
};
if !matches!(
command,
Command::Push(_)
| Command::ExprPush { .. }
| Command::Subcontext { .. }
| Command::RecordPush { .. }
| Command::FieldsPush
| Command::Attach { .. }
) {
return Err(QuarbError::parse(
"a quantifier repeats a push: write it after '.name(body)' (or '.', '.%', '.name%(…)', '.:name(body)')",
));
}
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;
Ok(Command::Repeat {
command: Box::new(command),
min,
max,
})
}
fn command_head_ahead(&self) -> bool {
let next_paren = matches!(self.toks.get(self.pos + 1), Some(Token::LParen));
let env_call = matches!(
(self.toks.get(self.pos + 1), self.toks.get(self.pos + 2)),
(
Some(Token::Field | Token::Colon { spaced: false }),
Some(Token::Name {
quoted: false,
glued: true,
..
})
)
);
match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) => {
!self.literal_word(text)
&& !env_call
&& (text.starts_with('.')
|| (!next_paren
&& !text.starts_with('-')
&& crate::syntax::registry::spec(text).is_some()))
}
_ => false,
}
}
fn value_only_head(&self) -> bool {
let next_paren = matches!(self.toks.get(self.pos + 1), Some(Token::LParen));
match self.peek() {
Some(Token::Name { quoted: true, .. }) | Some(Token::Interp(_)) => true,
Some(Token::Name {
text,
quoted: false,
..
}) => {
(text == "q" && next_paren)
|| (!next_paren && !text.starts_with('-') && self.literal_word(text))
}
Some(Token::At) => {
next_paren
|| (matches!(self.toks.get(self.pos + 1), Some(Token::Percent))
&& matches!(self.toks.get(self.pos + 2), Some(Token::LParen)))
}
Some(Token::Percent | Token::Hash) => next_paren,
Some(Token::Dollar) => {
matches!(self.toks.get(self.pos + 1), Some(Token::Name { text, .. }) if text.starts_with(','))
}
_ => false,
}
}
fn literal_word(&self, text: &str) -> bool {
!matches!(
literal_value(text, false, &|e| self.expander.unit_scale(e)),
Value::Str(_)
)
}
fn table_command(&mut self) -> Result<Command> {
if let Some(err) = self.retired_table_count() {
return Err(err);
}
if !self.table_option_ahead() {
if self.projector_items_ahead() {
self.pos += 2;
let shape = self.shape_columns()?;
return Ok(Command::Expr(Operand::Projected {
base: Box::new(Operand::Topic),
shape,
}));
}
let expr = self.additive()?;
if let Operand::Table {
header: Some(h),
rows,
} = &expr
&& rows.is_empty()
{
let names = h.join("; ");
return Err(QuarbError::parse(format!(
"'#(@({names}))' alone is retired: a header written as an item has its \
rows beside it; write '#(--header=@({names}))' ('-h=…') to name the \
columns of the list topic"
)));
}
return Ok(Command::Expr(expr));
}
let call = self.table_options()?;
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& arith_word(text).is_some()
{
let left = Operand::Piped {
expr: Box::new(Operand::Topic),
commands: vec![Command::Func(call)],
};
let left = self.multiplicative_from(left)?;
return Ok(Command::Expr(self.additive_from(left)?));
}
Ok(Command::Func(call))
}
fn projector_items_ahead(&self) -> bool {
matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& matches!(
self.toks.get(self.pos + 2),
Some(Token::Field | Token::ColonColon)
)
}
fn record_row(&mut self) -> Result<Vec<(Option<String>, Operand)>> {
let mut items: Vec<(Option<String>, Operand)> = Vec::new();
loop {
if matches!(self.peek(), Some(Token::RParen)) {
self.pos += 1;
break;
}
let name = match (self.peek(), self.toks.get(self.pos + 1)) {
(Some(Token::Name { text, quoted, .. }), Some(Token::Eq)) => {
let key = if *quoted {
format!("\u{0}{text}")
} else {
text.clone()
};
self.pos += 2;
Some(key)
}
_ => None,
};
let typed = if name.is_none() {
self.typed_key()?
} else {
None
};
let mut item = self.additive()?;
if let (Some(key), Operand::Typed { inner, typing }) = (&name, &item) {
return Err(typed_value_refusal(key, inner, typing.clone()));
}
let name = match typed {
Some((key, typing)) => {
item = Operand::Typed {
inner: Box::new(item),
typing,
};
Some(key)
}
None => name,
};
if name.is_none()
&& let Some(Token::Name {
text,
quoted: false,
glued: true,
}) = self.peek()
&& text == "..."
{
self.pos += 1;
item = Operand::Spread(Box::new(item));
}
items.push((name, item));
match self.peek() {
Some(Token::Comma | Token::Semi) => self.pos += 1,
Some(Token::RParen) => {}
_ => {
return Err(QuarbError::parse(
"a table row separates its items with ';': %(\"Braund\"; 22)",
));
}
}
}
Ok(items)
}
fn shape_columns(&mut self) -> Result<ProjShape> {
let mut items: Vec<(Option<String>, Operand)> = Vec::new();
let mut index: Option<Box<Operand>> = None;
loop {
if matches!(self.peek(), Some(Token::RParen)) {
self.pos += 1;
break;
}
if let Some(opt) = self.option_item() {
if !matches!(opt.as_str(), "--index" | "-i") {
return Err(QuarbError::parse(format!(
"the table shape takes one option, '--index=:col' ('-i=:col'); '{opt}' given"
)));
}
self.pos += if opt.starts_with("--") { 2 } else { 1 };
if !matches!(self.peek(), Some(Token::OptEq)) {
return Err(QuarbError::parse(
"'--index' names the index column: '#(:A; :B; -i=:A)'",
));
}
self.pos += 1;
let mut op = if matches!(self.peek(), Some(Token::LParen)) {
self.projector_list()?
} else {
self.unary()?
};
head_projections(&mut op);
index = Some(Box::new(op));
} else {
let name = match (self.peek(), self.toks.get(self.pos + 1)) {
(Some(Token::Name { text, .. }), Some(Token::Eq)) => {
let key = text.clone();
self.pos += 2;
Some(key)
}
_ => None,
};
let mut item = self.additive()?;
if name.is_none()
&& let Some(Token::Name {
text,
quoted: false,
glued: true,
}) = self.peek()
&& text == "..."
{
self.pos += 1;
item = Operand::Spread(Box::new(item));
}
items.push((name, item));
}
match self.peek() {
Some(Token::Comma | Token::Semi) => self.pos += 1,
Some(Token::RParen) => {}
_ => {
return Err(QuarbError::parse(
"the table shape separates its items with ';': t:#(:A; :B)",
));
}
}
}
Ok(ProjShape::Columns { items, index })
}
fn record_array(&mut self) -> Result<Operand> {
let Operand::Table { header, rows } = self.table_literal()? else {
unreachable!("the table literal parses to a table operand")
};
let Some(header) = header else {
return Err(QuarbError::parse(
"the record array opens with its header list: @%(@(name; age); %(\"Austen\"; 41))",
));
};
let rows = rows
.into_iter()
.map(|row| match row {
TableRow::Record(items) => items,
TableRow::List(items) => items.into_iter().map(|e| (None, e)).collect(),
})
.collect();
Ok(Operand::RecordArray { header, rows })
}
fn index_spec(&mut self) -> Result<IndexSpec> {
let start = self.pos;
let ends =
|p: &Self| matches!(p.peek(), Some(Token::RBracket | Token::Semi | Token::Comma));
if matches!(self.peek(), Some(Token::At | Token::Dollar))
&& let Ok(op) = self.unary()
&& node_free(&op)
&& ends(self)
{
return Ok(IndexSpec::Expr(Box::new(op)));
}
self.pos = start;
let (text, quoted) = match self.peek() {
Some(Token::Name { text, quoted, .. }) => (text.clone(), *quoted),
_ => return self.index_condition(start),
};
self.pos += 1;
if quoted && ends(self) {
return Ok(IndexSpec::Name(text));
}
if quoted {
return self.index_condition(start);
}
let closes = matches!(
self.peek(),
Some(Token::RBracket | Token::Semi | Token::Comma)
);
if closes {
if let Ok(n) = text.parse::<i64>() {
return Ok(IndexSpec::At(n));
}
if let Some((a, b)) = text.split_once("..") {
let end = |s: &str| -> Result<Option<i64>> {
if s.is_empty() {
Ok(None)
} else {
s.parse::<i64>().map(Some).map_err(|_| {
QuarbError::parse(format!("'{text}' is not a range of positions"))
})
}
};
return Ok(IndexSpec::Range(end(a)?, end(b)?));
}
}
self.index_condition(start)
}
fn index_condition(&mut self, start: usize) -> Result<IndexSpec> {
self.pos = start;
let expr = self.pred_or().map_err(|e| {
QuarbError::parse(format!(
"brackets on a value hold an index — a position ([2]), a range ([2..4], [..3]), a column name ([\"age\"]) — or a condition on its rows or items ([:A > 0], [$_ > 2]): {e}"
))
})?;
if !matches!(
self.peek(),
Some(Token::RBracket | Token::Semi | Token::Comma)
) {
return Err(QuarbError::parse(
"brackets on a value close after one index or condition: ']'",
));
}
Ok(IndexSpec::Cond(Box::new(expr)))
}
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, true)?;
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::RParen)) {
break;
}
} else {
break;
}
}
}
self.expect(Token::RParen, "')' to close the record")?;
Ok(FnCall {
headless: false,
name: "rec".to_string(),
args,
})
}
fn func_call(&mut self) -> Result<FnCall> {
if matches!(self.peek(), Some(Token::Hash))
&& (matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
|| self.table_count_ahead())
{
return match self.table_command()? {
Command::Func(call) => Ok(call),
_ => Err(QuarbError::parse(
"after '@|' the table sigil takes an option: '@| #(--width=n)' lays the stream in rows of n, '@| #(--header=list)' names its columns",
)),
};
}
if let Some(sigil @ (Token::At | Token::Percent | Token::Hash)) = self.peek()
&& !matches!(
self.toks.get(self.pos + 1),
Some(Token::LParen | Token::Percent)
)
&& !matches!(self.toks.get(self.pos + 1), Some(Token::Name { text, .. }) if text == ".")
{
let name = match sigil {
Token::At => "@",
Token::Percent => "%",
_ => "#",
};
self.pos += 1;
return Ok(FnCall {
headless: false,
name: name.to_string(),
args: Vec::new(),
});
}
let name = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse("expected a command name after '|'"));
}
};
let name = match (self.peek(), self.toks.get(self.pos + 1)) {
(
Some(Token::Field | Token::Colon { spaced: false }),
Some(Token::Name {
text,
quoted: false,
glued: true,
}),
) if is_word(&name) => {
let full = format!("{name}:{text}");
self.pos += 2;
full
}
_ => name,
};
self.call_with_name(name)
}
fn call_with_name(&mut self, name: String) -> Result<FnCall> {
let environment = crate::syntax::registry::split_env(&name).is_some();
if !environment && let Some(pointer) = retired_command(&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);
let saved_depth = self.arg_depth;
if !convention {
self.arg_depth += 1;
}
let saved_keyed = self.keyed_args;
self.keyed_args = crate::syntax::registry::known_keyed(&name);
let parsed = self.func_args(&name, convention, &mut args);
self.keyed_args = saved_keyed;
self.arg_depth = saved_depth;
parsed?;
self.expect(Token::RParen, "')' to close the command's operands")?;
}
let name = crate::syntax::registry::canonical(&name).to_string();
self.finish_call(name, args)
}
fn unit_operand(&mut self) -> Option<String> {
let start = self.pos;
let mut text = match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) if !text.starts_with('-') => text.clone(),
_ => return None,
};
if !matches!(
self.toks.get(self.pos + 1),
Some(Token::Slash | Token::Caret)
) {
return None;
}
self.pos += 1;
loop {
match self.peek() {
Some(Token::Slash) => text.push('/'),
Some(Token::Caret) => text.push('^'),
Some(Token::Comma | Token::Semi | Token::RParen) => return Some(text),
_ => {
self.pos = start;
return None;
}
}
self.pos += 1;
match self.peek() {
Some(Token::Name {
text: word,
quoted: false,
..
}) => text.push_str(word),
_ => {
self.pos = start;
return None;
}
}
self.pos += 1;
}
}
fn func_args(&mut self, name: &str, convention: bool, args: &mut Vec<Arg>) -> Result<()> {
{
if !matches!(self.peek(), Some(Token::RParen)) {
loop {
if (name == "edges" || name.starts_with("graph:"))
&& args.is_empty()
&& let Some(Token::DashDash) = self.peek()
&& let Some(matcher) = match self.toks.get(self.pos + 1) {
Some(Token::Name {
text,
quoted: false,
glued: true,
}) if text == "*" => Some(Matcher::Any),
Some(Token::Name {
text,
quoted: false,
glued: true,
}) if !matches!(
text.as_str(),
"weight"
| "damping"
| "iterations"
| "rounds"
| "from"
| "teleport"
| "normalized"
) =>
{
Some(Matcher::Name(text.clone()))
}
_ => None,
}
{
self.pos += 2;
let traits = self.group_traits()?;
let predicates = self.group_predicates()?;
args.push(Arg::Expr(Operand::Rel {
steps: vec![PathElem::Step(Step {
axis: Axis::BothLink,
matcher,
labels: Vec::new(),
traits,
predicates,
leaf: false,
})],
projection: None,
anchor: Anchor::Current,
}));
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::RParen)) {
break;
}
continue;
}
break;
}
if matches!(self.peek(), Some(Token::Semi)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::Semi)) {
self.pos += 1;
}
if matches!(self.peek(), Some(Token::RParen)) {
break;
}
self.option_segment(name, args)?;
break;
}
if self.option_ahead() && !self.trailing_separator_ahead() {
self.option_segment(name, args)?;
break;
}
if convention {
self.record_item(args, name != "group")?;
} 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 if name == "tree"
&& matches!(self.peek(), Some(Token::Percent))
&& matches!(
self.toks.get(self.pos + 1),
Some(Token::Comma | Token::Semi | Token::RParen)
)
{
return Err(QuarbError::parse(
"'%' is retired as tree's operand: write --record or -r for the properties record",
));
} else if name == "pivot"
&& args.is_empty()
&& matches!(self.peek(), Some(Token::LParen))
{
match self.projector_list()? {
Operand::Lit(v) => args.push(Arg::Lit(v)),
other => args.push(Arg::Expr(other)),
}
} else if name == "map" && args.is_empty() {
if matches!(self.peek(), Some(Token::Pipe)) {
self.pos += 1;
}
let saved_depth = self.arg_depth;
self.arg_depth = 0;
if matches!(self.peek(), Some(Token::Lt { spaced: false, .. }))
&& let Some(typing) = self.typing_brackets()?
{
self.arg_depth = saved_depth;
args.push(Arg::Expr(Operand::Typed {
inner: Box::new(Operand::Topic),
typing,
}));
} else {
let parsed = (|| {
let mut commands = Vec::new();
if matches!(self.peek(), Some(Token::Amp)) {
self.invoke_inline_fragment("|", &mut commands)?;
} else {
commands.push(self.inline_command()?);
}
commands.extend(self.inline_pipe_tail()?);
Ok::<_, QuarbError>(commands)
})();
self.arg_depth = saved_depth;
let commands = parsed?;
args.push(Arg::Expr(Operand::Piped {
expr: Box::new(Operand::Seed),
commands,
}));
}
} else if matches!(
name,
"convert" | "unit" | "magnitude" | "exchange" | "dimension"
) && let Some(unit) = self.unit_operand()
{
args.push(Arg::Lit(Value::Str(unit)));
} else {
args.push(self.func_arg()?);
}
if matches!(self.peek(), Some(Token::Comma | Token::Semi)) {
self.pos += 1;
if matches!(self.peek(), Some(Token::RParen)) {
break;
}
} else {
break;
}
}
}
}
Ok(())
}
fn option_ahead(&self) -> bool {
match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) => {
lexer::is_option_word(text)
|| (text == "-" && matches!(self.toks.get(self.pos + 1), Some(Token::OptEq)))
}
Some(Token::DashDash) => matches!(
self.toks.get(self.pos + 1),
Some(Token::Name {
quoted: false,
glued: true,
..
}) | Some(Token::OptEq)
),
_ => false,
}
}
fn option_segment(&mut self, command: &str, args: &mut Vec<Arg>) -> Result<()> {
let saved_depth = self.arg_depth;
self.arg_depth += 1;
let parsed = self.option_items(command, args);
self.arg_depth = saved_depth;
parsed
}
fn option_items(&mut self, command: &str, args: &mut Vec<Arg>) -> Result<()> {
loop {
let name = match self.peek() {
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with('-') => {
let t = text.clone();
self.pos += 1;
let letters: Vec<char> = t[1..].chars().collect();
if letters.len() >= 2
&& letters.iter().all(|c| c.is_alphabetic())
&& crate::syntax::registry::split_env(command).is_none()
&& let Some(opts) = crate::syntax::man::options_of(command)
{
for (i, c) in letters.iter().enumerate() {
let short = format!("-{c}");
let Some(spec) = opts.iter().find(|o| o.answers_to(&short)) else {
let shorts: Vec<&str> =
opts.iter().filter_map(|o| o.short.as_deref()).collect();
return Err(QuarbError::parse(format!(
"'{short}' in '{t}' is no switch of '{command}' (its short switches: {})",
if shorts.is_empty() {
"none".to_string()
} else {
shorts.join(" ")
}
)));
};
if spec.value.is_some() && i + 1 != letters.len() {
return Err(QuarbError::parse(format!(
"'{short}' in '{t}' takes a value and stands last in a bundle: '{short}={}' after the switches",
spec.value.as_deref().unwrap_or("value")
)));
}
}
for c in &letters[..letters.len() - 1] {
args.push(Arg::Opt {
name: format!("-{c}"),
value: None,
});
}
format!("-{}", letters[letters.len() - 1])
} else if letters.len() >= 2
&& letters.iter().all(|c| c.is_alphabetic())
&& crate::syntax::registry::split_env(command).is_none()
{
let takes_of = crate::syntax::registry::takes_of(command);
for c in &letters {
let short = format!("-{c}");
if !(takes_of && short == "-o") {
return Err(QuarbError::parse(format!(
"'{short}' in '{t}' is no switch of '{command}' (its short switches: {})",
if takes_of { "-o" } else { "none" }
)));
}
}
for c in &letters[..letters.len() - 1] {
args.push(Arg::Opt {
name: format!("-{c}"),
value: None,
});
}
format!("-{}", letters[letters.len() - 1])
} else {
t
}
}
Some(Token::DashDash) => {
self.pos += 1;
match self.peek() {
Some(Token::Name {
text,
quoted: false,
glued: true,
}) => {
let t = format!("--{text}");
self.pos += 1;
t
}
_ => "--".to_string(),
}
}
Some(Token::RParen) => return Ok(()),
Some(Token::Comma | Token::Semi) => {
return Err(QuarbError::parse(format!(
"the options of '{command}' stand last: write the operands before the \
option segment, '{command}(operand; …; -opt --opt=value)'"
)));
}
_ => {
return Err(QuarbError::parse(format!(
"in the option segment of '{command}' every token is an option spelled \
as in the shell ('-i', '--name=value'); an operand stands before the segment"
)));
}
};
let value = if matches!(self.peek(), Some(Token::OptEq)) {
self.pos += 1;
if name == "-" {
return Err(QuarbError::parse(format!(
"the void option '-=' is retired: write ';;' before the option \
segment of '{command}' ('{command}(a;; -9)')"
)));
}
if command == "map"
&& name == "--to"
&& matches!(self.peek(), Some(Token::LBracket))
{
self.pos += 1;
let mut coords = vec![self.index_spec()?];
while matches!(self.peek(), Some(Token::Semi | Token::Comma)) {
self.pos += 1;
coords.push(self.index_spec()?);
}
self.expect(Token::RBracket, "']' to close the index of --to")?;
let (rows, cols) = match coords.len() {
1 => (coords.pop().unwrap(), None),
2 => {
let c = coords.pop().unwrap();
(coords.pop().unwrap(), Some(c))
}
_ => (IndexSpec::Coords(coords), None),
};
let mut o = Operand::Index {
base: Box::new(Operand::Topic),
rows,
cols,
};
if let (Some(Token::Field), Some(Token::Name { text, .. })) =
(self.peek(), self.toks.get(self.pos + 1))
{
let name = text.clone();
self.pos += 2;
o = Operand::Field {
base: Box::new(o),
name,
};
}
Some(o)
} else if command == "map"
&& name == "--to"
&& matches!(self.peek(), Some(Token::LParen))
{
self.pos += 1;
let mut items = Vec::new();
loop {
items.push(self.unary()?);
match self.peek() {
Some(Token::Semi | Token::Comma) => self.pos += 1,
_ => break,
}
}
self.expect(Token::RParen, "')' to close the column list of --to")?;
Some(Operand::List(items))
} else {
let outer = self.in_option_value;
self.in_option_value = true;
let v = self.cond_expr();
self.in_option_value = outer;
Some(v?)
}
} else {
None
};
if name == "-" {
return Err(QuarbError::parse(format!(
"a lone '-' in the option segment of '{command}' is no option; a numeric \
option follows ';;' ('{command}(a;; -9)')"
)));
}
args.push(Arg::Opt { name, value });
}
}
fn finish_call(&mut self, name: String, mut args: Vec<Arg>) -> Result<FnCall> {
let mut by_row = false;
if crate::syntax::registry::split_env(&name).is_none()
&& let Some(opts) = crate::syntax::man::options_of(&name)
{
let signature = crate::syntax::man::synopsis(&name).unwrap_or_default();
let mut present: Vec<&str> = Vec::new();
for arg in &mut args {
let Arg::Opt { name: opt, value } = arg else {
continue;
};
let Some(spec) = opts.iter().find(|o| o.answers_to(opt)) else {
return Err(QuarbError::parse(format!(
"'{name}' has no option '{opt}'; the signature: {signature}"
)));
};
match (&spec.value, value.is_some()) {
(Some(placeholder), false) => {
return Err(QuarbError::parse(format!(
"option '{opt}' of '{name}' takes a value ('{opt}={placeholder}'); the signature: {signature}"
)));
}
(None, true) => {
return Err(QuarbError::parse(format!(
"option '{opt}' of '{name}' is a switch and takes no value; the signature: {signature}"
)));
}
_ => {}
}
if let Some(long) = &spec.long {
*opt = long.clone();
present.push(long.as_str());
} else if let Some(short) = &spec.short {
present.push(short.as_str());
}
}
if let Some(missing) = opts.iter().find(|o| {
o.required
&& !present
.iter()
.any(|p| o.long.as_deref() == Some(p) || o.short.as_deref() == Some(p))
}) {
return Err(QuarbError::parse(format!(
"'{name}' needs {}; the signature: {signature}",
missing.spelled()
)));
}
by_row = true;
}
let takes_of = crate::syntax::registry::takes_of(&name);
for arg in args.iter().filter(|_| !by_row) {
if let Arg::Opt { name: opt, value } = arg
&& takes_of
&& matches!(opt.as_str(), "--of" | "-o")
{
if value.is_none() {
return Err(QuarbError::parse(format!(
"'{name}({opt}=expr)' takes the expression to reduce: '{name}(--of=:n)'"
)));
}
continue;
}
if let Arg::Opt { name: opt, .. } = arg
&& crate::syntax::registry::split_env(&name).is_none()
{
return Err(QuarbError::parse(if takes_of {
format!(
"'{name}' takes '--of=expr' ('-o=expr'), the expression to reduce; '{opt}' given"
)
} else {
format!(
"'{name}' takes no options; its parentheses hold operands ('{opt}' given)"
)
}));
}
}
if !matches!(
name.as_str(),
"window" | "substr" | "frequencies" | "top" | "bottom" | "first" | "last"
) && 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,
headless: false,
};
validate_stat_args(&call)?;
Ok(call)
}
fn record_item(&mut self, args: &mut Vec<Arg>, record: bool) -> Result<()> {
if record && let Some(opt) = self.option_item() {
if matches!(opt.as_str(), "--keys" | "--values") {
self.pos += 2;
if !matches!(self.peek(), Some(Token::OptEq)) {
return Err(QuarbError::parse(format!(
"'{opt}' takes a type word: '%({opt}=int)'"
)));
}
self.pos += 1;
let word = match self.bump() {
Some(Token::Name {
text,
quoted: false,
..
}) => text.clone(),
_ => {
return Err(QuarbError::parse(format!(
"'{opt}' takes a type word: '%({opt}=int)'"
)));
}
};
if ColumnType::from_word(&word).is_none() {
return Err(QuarbError::parse(format!(
"'{opt}={word}': '{word}' is no type; the types are {}",
ColumnType::words().join(", ")
)));
}
args.push(Arg::Opt {
name: opt,
value: Some(Operand::Lit(Value::Str(word))),
});
return Ok(());
}
return Err(QuarbError::parse(format!(
"'%(…)' takes no option ('{opt}' given); its items are fields — a hop as a \
field's value is written '%(k = (--x))'"
)));
}
if let Some(Token::Name { text, quoted, .. }) = self.peek()
&& matches!(self.toks.get(self.pos + 1), Some(Token::Eq))
{
let key = text.clone();
let quoted = *quoted;
self.pos += 2;
let typed_key = (record && !quoted)
.then(|| literal_value(&key, false, &|e| self.expander.unit_scale(e)))
.map(|v| match v {
Value::Str(s) => crate::temporal::parse_iso(&s)
.map(|(secs, nanos, offset_min)| Value::Instant {
secs,
nanos,
offset_min,
calendar: Default::default(),
})
.unwrap_or(Value::Str(s)),
other => other,
})
.filter(|v| !matches!(v, Value::Str(_)));
if let Some(k) = typed_key {
args.push(Arg::Key(k));
let value = self.func_arg()?;
if let Arg::Expr(Operand::Typed { inner, typing }) = &value {
return Err(typed_value_refusal(&key, inner, typing.clone()));
}
args.push(value);
return Ok(());
}
args.push(Arg::Lit(Value::Str(key.clone())));
let value = self.func_arg()?;
if let Arg::Expr(Operand::Typed { inner, typing }) = &value {
return Err(typed_value_refusal(&key, inner, typing.clone()));
}
args.push(value);
return Ok(());
}
if record && let Some((key, typing)) = self.typed_key()? {
args.push(Arg::Lit(Value::Str(key)));
let value = match self.func_arg()? {
Arg::Expr(e) => e,
Arg::Lit(v) => Operand::Lit(v),
_ => {
return Err(QuarbError::parse(
"a declared field takes a value on the right of '=': %(price<float> = ::price)",
));
}
};
args.push(Arg::Expr(Operand::Typed {
inner: Box::new(value),
typing,
}));
return Ok(());
}
if record
&& matches!(self.peek(), Some(Token::At))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
&& let Some(close) = self.matching_paren(self.pos + 1)
&& matches!(self.toks.get(close + 1), Some(Token::Eq))
{
self.pos += 2;
let (items, _) = self.list_items(true)?;
self.pos += 1; let values = match self.func_arg()? {
Arg::Expr(e) => e,
Arg::Lit(Value::List(items)) => {
Operand::List(items.iter().cloned().map(Operand::Lit).collect())
}
Arg::Lit(v) => Operand::Lit(v),
_ => {
return Err(QuarbError::parse(
"the pairwise field takes a list of values on the right of '=': %(@(a; b) = @(1; 2))",
));
}
};
args.push(Arg::Pairs {
keys: Operand::List(items),
values,
});
return Ok(());
}
if record {
let save = self.pos;
if let Ok(Arg::Expr(op)) = self.func_arg()
&& let Some(Token::Name {
text,
quoted: false,
glued: true,
}) = self.peek()
&& text == "..."
{
self.pos += 1;
args.push(Arg::Expr(Operand::Spread(Box::new(op))));
return Ok(());
}
self.pos = save;
}
if record {
let save = self.pos;
if let Ok(keys) = self.additive()
&& matches!(self.peek(), Some(Token::Eq))
{
self.pos += 1;
let values = match self.func_arg()? {
Arg::Expr(e) => e,
Arg::Lit(Value::List(items)) => {
Operand::List(items.iter().cloned().map(Operand::Lit).collect())
}
Arg::Lit(v) => Operand::Lit(v),
_ => {
return Err(QuarbError::parse(
"the pairwise field takes a list of values on the right of '=': %(@(a; b) = @(1; 2))",
));
}
};
args.push(Arg::Pairs { keys, values });
return Ok(());
}
self.pos = save;
}
let arg = self.func_arg()?;
if record && let Arg::Lit(Value::Str(name)) = &arg {
return Err(QuarbError::parse(format!(
"a record field needs a name: a leading string no longer names \
the field after it (retired); write the key, e.g. \
%(name = \"{name}\"; age = 22)"
)));
}
args.push(arg);
Ok(())
}
fn matching_paren(&self, open: usize) -> Option<usize> {
let mut depth = 0usize;
for (i, t) in self.toks.iter().enumerate().skip(open) {
match t {
Token::LParen => depth += 1,
Token::RParen => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
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::Hole(h) => Ok(Arg::Hole(h)),
Operand::Lit(Value::Quarq(q)) if q.is_template() => {
Ok(Arg::Expr(Operand::Lit(Value::Quarq(q))))
}
Operand::Lit(v) => Ok(Arg::Lit(v)),
Operand::List(items) if items.iter().all(|i| matches!(i, Operand::Lit(_))) => {
Ok(Arg::Lit(Value::list(
items
.into_iter()
.map(|i| match i {
Operand::Lit(v) => v,
_ => unreachable!(),
})
.collect(),
)))
}
expr => Ok(Arg::Expr(expr)),
}
}
fn projection(&mut self) -> Result<Option<Projection>> {
let proj = match self.peek() {
Some(Token::Field) => {
self.pos += 1;
Projection::CoreMeta(canonical_core_field(
self.require_projection_name("the core field `:`")?,
))
}
Some(Token::ColonColon) => {
self.pos += 1;
if let Some(Token::Expansion(t)) = self.peek() {
let t = t.clone();
self.pos += 1;
Projection::Property(Some(self.name_hole(&t)?))
} else {
Projection::Property(self.opt_projection_name())
}
}
Some(Token::ColonColonColon) => {
let value_side = self.pos == 0
|| !matches!(
self.toks.get(self.pos - 1),
Some(Token::Name { .. } | Token::RBracket | Token::Caret)
);
self.pos += 1;
let key = self.require_projection_name("adapter metadata `:::`")?;
adapter_key(key, value_side)?
}
_ => 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::Expansion(text)) = self.peek() {
let text = text.clone();
self.pos += 1;
return Ok(PathElem::Hole(self.expansion_hole(&text)?));
}
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)) {
if self.name_algebra_ahead() {
let labels = self.name_selection(&axis)?;
let mut step = self.finish_step(axis, Matcher::Any)?;
step.labels = labels;
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(),
}));
}
return Ok(PathElem::Step(step));
}
if matches!(self.toks.get(self.pos + 1), Some(Token::Name { text, quoted: false, .. }) if text == "*")
&& matches!(self.toks.get(self.pos + 2), Some(Token::RParen))
&& !matches!(self.toks.get(self.pos + 3), Some(Token::Quant { .. }))
{
self.pos += 3;
let step = self.finish_step(axis, Matcher::Any)?;
return Ok(PathElem::Step(step));
}
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
| Axis::Any
) {
return Err(QuarbError::parse(
"a quantifier attaches to a single-hop operator or a \
parenthesized group",
));
}
let hop = Step {
axis,
matcher: Matcher::Dot,
labels: Vec::new(),
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),
budget: None,
});
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(_) | PathElem::Hole(_) | PathElem::Param(_)
)
}) {
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::Budget { floor, ceiling }) => {
let budget = Budget {
floor: floor.clone(),
ceiling: ceiling.clone(),
};
self.pos += 1;
Ok(Some(Quant {
min: 0,
max: None,
budget: Some(budget),
}))
}
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,
budget: None,
}))
}
Some(Token::Name {
text,
quoted: false,
glued: true,
}) if text == "+" => {
self.pos += 1;
Ok(Some(Quant {
min: 1,
max: None,
budget: None,
}))
}
Some(Token::Name {
text,
quoted: false,
glued: true,
}) if text == "*" => {
self.pos += 1;
Ok(Some(Quant {
min: 0,
max: None,
budget: 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 param_ahead = self.pattern_depth == 0
&& self.template_holes.is_some()
&& matches!(
self.toks.get(self.pos + 1),
Some(Token::Name { text, quoted: false, .. }) if self.def_params.iter().any(|p| p == text)
);
let leaf = if matches!(self.peek(), Some(Token::Dollar)) && !map_pipe_ahead && !param_ahead
{
self.pos += 1;
true
} else {
false
};
Ok(Step {
axis,
matcher,
labels: Vec::new(),
traits,
predicates,
leaf,
})
}
fn name_algebra_ahead(&self) -> bool {
let mut depth = 0usize;
let mut bracket = 0usize;
let mut angle = 0usize;
let mut i = self.pos;
while let Some(t) = self.toks.get(i) {
match t {
Token::LBracket => bracket += 1,
Token::RBracket => bracket = bracket.saturating_sub(1),
_ if bracket > 0 => {}
Token::Lt { .. } => angle += 1,
Token::Gt { .. } => angle = angle.saturating_sub(1),
_ if angle > 0 => {}
Token::LParen => depth += 1,
Token::RParen => {
depth -= 1;
if depth == 0 {
return false;
}
}
Token::AmpAmp | Token::PipePipe => return true,
Token::Bang
if matches!(
self.toks.get(i - 1),
Some(Token::LParen | Token::AmpAmp | Token::PipePipe | Token::Pipe)
) =>
{
return true;
}
Token::Name {
text,
quoted: false,
..
} if (AND_WORDS.contains(&text.as_str())
|| OR_WORDS.contains(&text.as_str())
|| NOT_WORDS.contains(&text.as_str()))
&& matches!(
self.toks.get(i + 1),
Some(Token::Name { .. } | Token::LParen | Token::Bang)
) =>
{
return true;
}
_ => {}
}
i += 1;
}
false
}
fn name_selection(&mut self, axis: &Axis) -> Result<Vec<LabelClause>> {
self.pos += 1; let what = if axis.is_link() {
"a label selection holds label names joined by '&&', '||' and '!': ->(a && !b)"
} else {
"a name algebra holds designators — names, globs and regex literals — \
joined by '&&', '||' and a leading '!': /(*.rs && !test_*)"
};
let Some(expr) = self.label_or() else {
return Err(QuarbError::parse(what));
};
if !matches!(self.peek(), Some(Token::RParen)) {
return Err(match self.peek() {
Some(Token::Name { .. } | Token::Regex(_)) => QuarbError::parse(format!(
"two designators stand joined by '&&' or '||', spaced: {what}"
)),
_ => QuarbError::parse(format!("expected ')' closing the parenthesis: {what}")),
});
}
self.pos += 1;
let clauses = trait_cnf(expr)?;
clauses
.into_iter()
.map(|c| {
let alts = c
.alts
.into_iter()
.map(|alt| {
let (neg, name) = match alt.strip_prefix('!') {
Some(n) => (true, n),
None => (false, alt.as_str()),
};
let matcher = if let Some(pat) = name.strip_prefix(REGEX_MARK) {
Regex::new(pat).map(Matcher::Regex).map_err(|e| {
QuarbError::parse(format!("bad regex '/{pat}/': {e}"))
})?
} else if let Some(text) = name.strip_prefix(QUOTED_MARK) {
Matcher::Name(text.to_string())
} else {
matcher_for(name, false)?
};
Ok(LabelAlt { neg, matcher })
})
.collect::<Result<Vec<_>>>()?;
Ok(LabelClause { alts })
})
.collect()
}
fn label_or(&mut self) -> Option<TExpr> {
let mut left = self.label_and()?;
while matches!(self.peek(), Some(Token::PipePipe | Token::Pipe))
|| self.eat_word_if_followed(OR_WORDS)
{
if matches!(self.peek(), Some(Token::PipePipe | Token::Pipe)) {
self.pos += 1;
}
let right = self.label_and()?;
left = TExpr::Or(Box::new(left), Box::new(right));
}
Some(left)
}
fn label_and(&mut self) -> Option<TExpr> {
let mut left = self.label_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.label_not()?;
left = TExpr::And(Box::new(left), Box::new(right));
}
Some(left)
}
fn label_not(&mut self) -> Option<TExpr> {
if matches!(self.peek(), Some(Token::Bang)) {
self.pos += 1;
return Some(TExpr::Not(Box::new(self.label_not()?)));
}
if self.eat_word_if_followed(NOT_WORDS) {
return Some(TExpr::Not(Box::new(self.label_not()?)));
}
match self.peek() {
Some(Token::Name { text, quoted, .. }) => {
let name = if *quoted {
format!("{QUOTED_MARK}{text}")
} else {
text.clone()
};
self.pos += 1;
Some(TExpr::Has(name))
}
Some(Token::Regex(pat)) => {
let pat = format!("{REGEX_MARK}{pat}");
self.pos += 1;
Some(TExpr::Has(pat))
}
Some(Token::LParen) => {
self.pos += 1;
let inner = self.label_or()?;
if !matches!(self.peek(), Some(Token::RParen)) {
return None;
}
self.pos += 1;
Some(inner)
}
_ => None,
}
}
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::Tilde
| Token::TildeTilde
| 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));
}
}
}
if !self.def_params.is_empty()
&& let Some(pred) = self.range_form()
{
return Ok(pred);
}
if let Some(parts) = self.position_list()? {
return Ok(Predicate::Positions(parts));
}
let expr = self.pred_or()?;
self.expect(
Token::RBracket,
"']' to close a predicate (')' for the '(?' spelling)",
)?;
if let PredExpr::Or(a, b) | PredExpr::And(a, b) = &expr
&& let (
PredExpr::Truthy(Operand::Lit(Value::Int(x))),
PredExpr::Truthy(Operand::Lit(Value::Int(y))),
) = (&**a, &**b)
{
return Err(QuarbError::parse(format!(
"positions are listed with ';': write '[{x}; {y}]'"
)));
}
if let PredExpr::Truthy(op) = &expr
&& node_free(op)
{
return Ok(Predicate::Computed(op.clone()));
}
Ok(Predicate::Expr(expr))
}
fn range_form(&mut self) -> Option<Predicate> {
fn name_at(p: &Parser<'_>, i: usize) -> Option<String> {
match p.toks.get(i) {
Some(Token::Name {
text,
quoted: false,
..
}) => Some(text.clone()),
_ => None,
}
}
let is_param = |p: &Self, n: &str| p.def_params.iter().any(|d| d == n);
let int_bound = |t: &str| -> Option<Bound> {
if t.is_empty() {
Some(Bound::Open)
} else {
t.parse::<i64>().ok().map(Bound::Int)
}
};
let at = |p: &Self, i: usize, t: &Token| p.toks.get(i) == Some(t);
if let Some(text) = name_at(self, self.pos)
&& let Some(a) = text.strip_suffix("..")
&& let Some(start) = int_bound(a)
&& at(self, self.pos + 1, &Token::Dollar)
&& let Some(p) = name_at(self, self.pos + 2)
&& is_param(self, &p)
&& at(self, self.pos + 3, &Token::RBracket)
{
self.pos += 4;
return Some(Predicate::RangeForm(start, Bound::Param(p)));
}
if at(self, self.pos, &Token::Dollar)
&& let Some(text) = name_at(self, self.pos + 1)
&& let Some((p, b)) = text.split_once("..")
&& is_param(self, p)
{
let p = p.to_string();
if b.is_empty()
&& at(self, self.pos + 2, &Token::Dollar)
&& let Some(q) = name_at(self, self.pos + 3)
&& is_param(self, &q)
&& at(self, self.pos + 4, &Token::RBracket)
{
self.pos += 5;
return Some(Predicate::RangeForm(Bound::Param(p), Bound::Param(q)));
}
if let Some(end) = int_bound(b)
&& at(self, self.pos + 2, &Token::RBracket)
{
self.pos += 3;
return Some(Predicate::RangeForm(Bound::Param(p), end));
}
}
None
}
fn position_list(&mut self) -> Result<Option<Vec<Predicate>>> {
let start = self.pos;
let mut parts = Vec::new();
loop {
let Some(Token::Name {
text,
quoted: false,
..
}) = self.toks.get(self.pos)
else {
break;
};
let part = if let Ok(n) = text.parse::<i64>() {
Predicate::Index(n)
} else 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()) {
Predicate::Range(start, end)
} else {
break;
}
} else {
break;
};
parts.push(part);
self.pos += 1;
match self.toks.get(self.pos) {
Some(Token::Semi) => self.pos += 1,
Some(Token::RBracket) if parts.len() >= 2 => {
self.pos += 1;
return Ok(Some(parts));
}
_ => break,
}
}
self.pos = start;
Ok(None)
}
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 first = self.pred_and()?;
self.pred_or_from(first)
}
fn pred_or_from(&mut self, first: PredExpr) -> Result<PredExpr> {
let mut left = first;
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 first = self.pred_not()?;
self.pred_and_from(first)
}
fn pred_and_from(&mut self, first: PredExpr) -> Result<PredExpr> {
let mut left = first;
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()?;
self.pred_primary_from(left)
}
fn pred_primary_from(&mut self, left: Operand) -> Result<PredExpr> {
let opens = self.pos > 0
&& matches!(
self.toks.get(self.pos - 1),
Some(Token::Pipe | Token::LParen)
);
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 !opens
&& 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::syntax::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()?;
let mut operands = vec![left, right];
let mut ops = vec![op];
while let Some(next) = self.cmp_op() {
ops.push(next);
operands.push(self.additive()?);
}
if ops.len() == 1 {
let right = operands.pop().unwrap();
let left = operands.pop().unwrap();
return Ok(PredExpr::Compare(left, op, right));
}
Ok(PredExpr::Chain(operands, ops))
} 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 left = self.multiplicative()?;
self.additive_from(left)
}
fn additive_from(&mut self, mut left: Operand) -> Result<Operand> {
loop {
if let Some(mode) = self.concat_word_ahead() {
left = self.concat_rest(mode, left)?;
continue;
}
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,
Some(Token::Desc) => {
return Err(QuarbError::parse(
"a binary minus is spaced on both sides: write 'a - b' (a glued '-::key' is the descending sort key)",
));
}
_ => {
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 concat_word_ahead(&self) -> Option<ConcatMode> {
let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
else {
return None;
};
match text.as_str() {
"++" if matches!(self.toks.get(self.pos + 1), Some(Token::Bang)) => {
Some(ConcatMode::Overwrite)
}
"++" => Some(ConcatMode::Merge),
"+@+" => Some(ConcatMode::Item),
_ => None,
}
}
fn concat_rest(&mut self, mode: ConcatMode, left: Operand) -> Result<Operand> {
self.pos += if mode == ConcatMode::Overwrite { 2 } else { 1 };
let right = if matches!(self.peek(), Some(Token::Percent))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
{
self.pos += 2;
Operand::Row(self.record_row()?)
} else {
self.multiplicative()?
};
Ok(Operand::Concat {
mode,
left: Box::new(left),
right: Box::new(right),
})
}
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('-'), rest) if numeric(rest) && self.in_option_value => return Ok(()),
(Some(c @ ('+' | '-' | '*')), rest) if numeric(rest) => c,
(_, _) if numeric(text) => {
match crate::syntax::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 left = self.unary()?;
self.multiplicative_from(left)
}
fn multiplicative_from(&mut self, mut left: Operand) -> Result<Operand> {
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,
Some(Token::Name {
text,
quoted: false,
..
}) if text == "x" => ArithOp::MatMul,
_ => 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 node_read(&mut self, o: Operand) -> Result<Operand> {
let Some(Token::Capsa(body)) = self.peek() else {
return Ok(o);
};
let body = body.clone();
let reg = attached_ref(&body)?;
let bare = |o: &Operand| {
matches!(
o,
Operand::Rel {
projection: None,
..
} | Operand::Ctx {
projection: None,
..
} | Operand::Field { .. }
)
};
let ok = match &o {
Operand::Invoking(inner) | Operand::Position(inner) | Operand::Driver(inner) => {
bare(inner)
}
other => bare(other),
};
if !ok {
return Err(QuarbError::parse(format!(
"':{body}' reads what is attached to a node: write it after a node path (<-link:{body}, \\/*:{body}), an edge's end ($-:target:{body}), a node-valued field (:source:{body}), or alone for the current node (:{body})"
)));
}
self.pos += 1;
Ok(Operand::PeerReg {
at: Some(Box::new(o)),
reg,
})
}
fn unary_inner(&mut self) -> Result<Operand> {
let mut o = if matches!(self.peek(), Some(Token::Field)) {
Operand::Topic
} else if let Some(Token::Capsa(body)) = self.peek() {
let reg = attached_ref(&body.clone())?;
self.pos += 1;
Operand::PeerReg { at: None, reg }
} else {
self.unary_primary()?
};
if matches!(self.peek(), Some(Token::Resolve))
&& matches!(
o,
Operand::Recall(_) | Operand::Field { .. } | Operand::Index { .. }
)
&& !matches!(
self.toks.get(self.pos + 1),
Some(Token::Name { glued: true, .. })
)
{
self.pos += 1;
if let Operand::Rel {
steps, projection, ..
} = self.rel_operand_continue(Vec::new())?
{
o = Operand::Rel {
steps,
projection,
anchor: Anchor::Id(Box::new(o)),
};
}
}
let mut o = self.node_read(o)?;
loop {
match self.peek() {
Some(Token::Capsa(_)) => {
o = self.node_read(o)?;
}
Some(Token::ProjectColon) => {
self.pos += 1;
let shape = self.project_shape()?;
o = Operand::Projected {
base: Box::new(o),
shape,
};
}
Some(Token::ColonColon) => {
self.pos += 1;
let name = self.opt_projection_name();
o = Operand::Through {
base: Box::new(o),
projection: Projection::Property(name),
};
}
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: projection @ None,
..
} = &mut o
{
*projection = Some(Projection::CoreMeta(canonical_core_field(name)));
continue;
}
if name.starts_with(['%', '@']) {
o = Operand::ValueMeta {
base: Box::new(o),
key: name,
};
continue;
}
o = Operand::Field {
base: Box::new(o),
name,
};
}
Some(Token::LBracket)
if matches!(self.toks.get(self.pos + 1), Some(Token::LBracket))
&& !matches!(
o,
Operand::Rel { .. } | Operand::Ctx { .. } | Operand::Pattern(_)
) =>
{
let labels = self.label_brackets()?;
o = Operand::LabelIndex {
base: Box::new(o),
labels,
};
}
Some(Token::LBracket)
if !matches!(
o,
Operand::Rel { .. } | Operand::Ctx { .. } | Operand::Pattern(_)
) =>
{
self.pos += 1;
let mut coords = vec![self.index_spec()?];
while matches!(self.peek(), Some(Token::Semi | Token::Comma)) {
self.pos += 1;
coords.push(self.index_spec()?);
}
self.expect(Token::RBracket, "']' to close the index")?;
let (rows, cols) = match coords.len() {
1 => (coords.pop().unwrap(), None),
2 => {
let c = coords.pop().unwrap();
(coords.pop().unwrap(), Some(c))
}
_ => (IndexSpec::Coords(coords), None),
};
o = Operand::Index {
base: Box::new(o),
rows,
cols,
};
}
_ => break,
}
}
while matches!(self.peek(), Some(Token::ColonColonColon)) {
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.toks.get(self.pos + 1)
&& matches!(
text.as_str(),
"fields"
| "keys"
| "header"
| "type"
| "dim"
| "nrows"
| "ncols"
| "index"
| "name"
)
{
let key = text.clone();
self.pos += 2;
o = Operand::Piped {
expr: Box::new(o),
commands: vec![Command::Expr(Operand::Rel {
steps: Vec::new(),
projection: Some(Projection::AdapterMeta(key)),
anchor: Anchor::Current,
})],
};
continue;
}
if !matches!(
o,
Operand::Recall(_)
| Operand::Topic
| Operand::Field { .. }
| Operand::ValueMeta { .. }
| Operand::Piped { .. }
| Operand::PeerReg { .. }
) {
return Err(QuarbError::parse(
"':::key' after a value is retired: its provenance reads as ':%provenance' (the record; ':%provenance:source' for one key), ':@provenance', ':%origin' or ':@origin'",
));
}
self.pos += 1;
let key = self.require_projection_name("value provenance `:::`")?;
return Err(QuarbError::parse(format!(
"':::{key}' after a value is retired: its provenance reads as ':%provenance:{key}' (the record), ':@provenance', ':%origin' or ':@origin'"
)));
}
if is_bare_projection(&o)
&& let Some(typing) = self.typing_brackets()?
{
o = Operand::Typed {
inner: Box::new(o),
typing,
};
}
Ok(o)
}
fn projector_list(&mut self) -> Result<Operand> {
self.pos += 1;
let mut items = Vec::new();
loop {
let mut item = self.unary()?;
head_projections(&mut item);
items.push(item);
match self.peek() {
Some(Token::Semi | Token::Comma) => self.pos += 1,
_ => break,
}
}
self.expect(Token::RParen, "')' to close the list of index columns")?;
if items.iter().all(|i| matches!(i, Operand::Lit(_))) {
let values = items
.into_iter()
.map(|i| match i {
Operand::Lit(v) => v,
_ => unreachable!(),
})
.collect();
return Ok(Operand::Lit(Value::list(values)));
}
Ok(Operand::List(items))
}
fn label_brackets(&mut self) -> Result<Vec<Operand>> {
self.pos += 2;
let mut labels = Vec::new();
loop {
if matches!(self.peek(), Some(Token::RBracket)) {
break;
}
let bare = matches!(self.peek(), Some(Token::Name { quoted: false, .. }));
let label = self.additive()?;
labels.push(if bare {
label_range(label, &|e| self.expander.unit_scale(e))
} else {
label
});
match self.peek() {
Some(Token::Semi | Token::Comma) => self.pos += 1,
Some(Token::RBracket) => {}
_ => {
return Err(QuarbError::parse(
"the double bracket holds labels apart by ';': t[[x; y]], a range t[[a..b]]",
));
}
}
}
if labels.is_empty() {
return Err(QuarbError::parse(
"the double bracket names a label: t[[2013-01-02]], r[[\"a\"]]",
));
}
self.pos += 1;
if !matches!(self.peek(), Some(Token::RBracket)) {
return Err(QuarbError::parse("the double bracket closes with ']]'"));
}
self.pos += 1;
Ok(labels)
}
fn typing_brackets(&mut self) -> Result<Option<crate::value::Typing>> {
use crate::value::Typing;
if !matches!(self.peek(), Some(Token::Lt { spaced: false, .. })) {
return Ok(None);
}
let start = self.pos;
self.pos += 1;
let word = |t: Option<&Token>| match t {
Some(Token::Name {
text,
quoted: false,
..
}) => Some(text.clone()),
_ => None,
};
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
let mut words = Vec::new();
loop {
let Some(w) = word(self.peek()) else {
return Err(QuarbError::parse(
"a tuple type lists its types in parentheses: '<(int; text)>'",
));
};
self.pos += 1;
words.push(w);
match self.peek() {
Some(Token::Semi | Token::Comma) => self.pos += 1,
_ => break,
}
}
self.expect(Token::RParen, "')' to close the tuple type")?;
let spelled = format!("({})", words.join("; "));
let ty = ColumnType::from_word(&spelled).ok_or_else(|| {
QuarbError::parse(format!(
"'<{spelled}>': a tuple's types are {}",
ColumnType::words().join(", ")
))
})?;
if !matches!(self.peek(), Some(Token::Gt { .. })) {
return Err(QuarbError::parse(
"the trait brackets of a projector close with '>'",
));
}
self.pos += 1;
return Ok(Some(Typing::Declared(ty)));
}
let Some(first) = word(self.peek()) else {
self.pos = start;
return Ok(None);
};
self.pos += 1;
let typing = if first == "*" {
let reported = if matches!(self.peek(), Some(Token::Semi | Token::Comma)) {
self.pos += 1;
let Some(w) = word(self.peek()) else {
return Err(QuarbError::parse(
"'<*; type>' reports the inferred type after the star: '<*; float>'",
));
};
self.pos += 1;
Some(ColumnType::from_word(&w).ok_or_else(|| {
QuarbError::parse(format!(
"'<*; {w}>': '{w}' is no type; the types are {}",
ColumnType::words().join(", ")
))
})?)
} else {
None
};
Typing::Star(reported)
} else {
match ColumnType::from_word(&first) {
Some(t) => Typing::Declared(t),
None => {
if matches!(self.peek(), Some(Token::Gt { .. })) {
return Err(QuarbError::parse(format!(
"'<{first}>' on a projector declares its type, and '{first}' is none; the types are {}; '<*>' declares nothing",
ColumnType::words().join(", ")
)));
}
self.pos = start;
return Ok(None);
}
}
};
if !matches!(self.peek(), Some(Token::Gt { .. })) {
return Err(QuarbError::parse(format!(
"the trait brackets of a projector close with '>': '{}'",
typing.spelling()
)));
}
self.pos += 1;
Ok(Some(typing))
}
fn typed_key(&mut self) -> Result<Option<(String, crate::value::Typing)>> {
let start = self.pos;
let Some(Token::Name { text, .. }) = self.peek() else {
return Ok(None);
};
if !matches!(
self.toks.get(self.pos + 1),
Some(Token::Lt { spaced: false, .. })
) {
return Ok(None);
}
let key = text.clone();
self.pos += 1;
let typing = match self.typing_brackets() {
Ok(Some(t)) if matches!(self.peek(), Some(Token::Eq)) => t,
Ok(_) => {
self.pos = start;
return Ok(None);
}
Err(e) => {
if self.toks[start + 1..self.pos.max(start + 1)]
.iter()
.any(|t| matches!(t, Token::Gt { .. }))
|| matches!(self.toks.get(self.pos), Some(Token::Gt { .. }))
{
return Err(e);
}
self.pos = start;
return Ok(None);
}
};
self.pos += 1; Ok(Some((key, typing)))
}
fn unary_primary(&mut self) -> Result<Operand> {
if let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& let Some((from, rest)) = range_head(text)
{
self.pos += 1;
return self.range_rest(Operand::Lit(Value::Int(from)), rest);
}
if matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == "x")
&& self.toks.get(self.pos + 1).is_none_or(|t| {
!matches!(
t,
Token::RParen | Token::Semi | Token::Comma | Token::RBracket | Token::Pipe
)
})
{
return Err(QuarbError::parse(
"'x' is the matrix product and stands between two operands; the string is written quoted (\"x\")",
));
}
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::Desc)) {
self.pos += 1;
return Ok(Operand::Desc(Box::new(self.unary()?)));
}
if self.keyed_args
&& self.pos > 0
&& matches!(
self.toks.get(self.pos - 1),
Some(Token::LParen | Token::Semi | Token::Comma)
)
&& let (
Some(Token::ArrowIn { rounded: true }),
Some(Token::Name {
text,
quoted: false,
glued: true,
}),
) = (self.toks.get(self.pos), self.toks.get(self.pos + 1))
{
let name = text.clone();
self.pos += 2;
return Ok(Operand::Desc(Box::new(Operand::Field {
base: Box::new(Operand::Topic),
name,
})));
}
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;
if matches!(self.peek(), Some(Token::LBracket)) {
let saved_depth = self.arg_depth;
self.arg_depth = 0;
let parsed = (|| {
let mut commands = vec![self.pipe_item(PipeMode::Nav)?];
commands.extend(self.inline_pipe_tail()?);
Ok::<_, QuarbError>(commands)
})();
self.arg_depth = saved_depth;
let commands = parsed?;
self.expect(Token::RParen, "')' to close a group")?;
return Ok(Operand::Piped {
expr: Box::new(Operand::Seed),
commands,
});
}
let saved_depth = self.arg_depth;
self.arg_depth = 0;
let parsed = (|| {
let inner = self.cond_expr()?;
let commands = self.inline_pipe_tail()?;
Ok::<_, QuarbError>((inner, commands))
})();
self.arg_depth = saved_depth;
let (inner, commands) = parsed?;
self.expect(Token::RParen, "')' to close a group")?;
if commands.is_empty()
&& let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& let Some(rest) = text.strip_prefix("..")
&& rest
.split("..")
.all(|p| p.is_empty() || p.parse::<i64>().is_ok())
{
let rest: Vec<String> = rest.split("..").map(str::to_string).collect();
self.pos += 1;
return self.range_rest(inner, rest);
}
return Ok(if commands.is_empty() {
inner
} else {
Operand::Piped {
expr: Box::new(inner),
commands,
}
});
}
self.operand()
}
fn range_rest(&mut self, from: Operand, mut parts: Vec<String>) -> Result<Operand> {
let mut bounds: Vec<Operand> = Vec::new();
let mut i = 0;
while i < parts.len() {
let p = parts[i].clone();
i += 1;
if p.is_empty() {
if !matches!(self.peek(), Some(Token::LParen)) {
return Err(QuarbError::parse(
"a range's bound is an integer or a parenthesized expression: 1..5, 1..(n + 1)",
));
}
self.pos += 1;
let e = self.additive()?;
self.expect(Token::RParen, "')' to close the range's bound")?;
bounds.push(e);
if i == parts.len()
&& bounds.len() == 1
&& let Some(Token::Name {
text,
quoted: false,
..
}) = self.peek()
&& let Some(rest) = text.strip_prefix("..")
&& (rest.is_empty() || rest.parse::<i64>().is_ok())
{
parts.push(rest.to_string());
self.pos += 1;
}
} else {
let n: i64 = p.parse().map_err(|_| {
QuarbError::parse(format!("a range's bound is an integer: '{p}' is not one"))
})?;
bounds.push(Operand::Lit(Value::Int(n)));
}
}
let mut bounds = bounds.into_iter();
let to = bounds
.next()
.ok_or_else(|| QuarbError::parse("a range needs its end: 1..5"))?;
let step = bounds.next().map(Box::new);
if bounds.next().is_some() {
return Err(QuarbError::parse(
"a range has at most three parts: start..end..step",
));
}
Ok(Operand::Range {
from: Box::new(from),
to: Box::new(to),
step,
})
}
fn inline_pipe_tail(&mut self) -> Result<Vec<Command>> {
let mut commands = Vec::new();
let mut mode = PipeMode::Scalar;
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 commands)?;
mode = PipeMode::Scalar;
}
Some(Token::Pipe) => {
self.pos += 1;
let command = self.inline_command_in(mode)?;
mode = command_mode_out(&command, mode);
commands.push(command);
}
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 commands)?;
}
Some(Token::At) if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe)) => {
self.pos += 2;
commands.push(self.inline_agg_command()?);
}
Some(Token::Dollar) if matches!(self.toks.get(self.pos + 1), Some(Token::Pipe)) => {
self.pos += 2;
commands.push(Command::Map(Box::new(self.map_command()?)));
}
_ => break,
}
}
Ok(commands)
}
fn headless_body(&mut self, name: String) -> Result<Operand> {
if crate::syntax::registry::split_env(&name).is_none()
&& crate::syntax::registry::spec(&name).is_none()
&& retired_command(&name).is_none()
{
return Err(QuarbError::parse(format!(
"'{name}' is not a command; a bare word in a body names one: \
quote a string \"{name}\""
)));
}
let mut call = if crate::syntax::registry::split_env(&name).is_some()
&& matches!(self.peek(), Some(Token::LParen))
{
self.call_with_name(name)?
} else {
FnCall {
name,
args: Vec::new(),
headless: false,
}
};
call.headless = true;
let mut commands = vec![func_command(call)?];
commands.extend(self.inline_pipe_tail()?);
Ok(Operand::Piped {
expr: Box::new(Operand::Seed),
commands,
})
}
fn coalesce_tail(&mut self, left: PredExpr) -> Result<PredExpr> {
if !matches!(self.peek(), Some(Token::Coalesce)) {
return Ok(left);
}
self.pos += 1;
let right = self.pred_or()?;
let right = self.coalesce_tail(right)?;
let as_operand = |p: PredExpr| match p {
PredExpr::Truthy(op) => op,
other => Operand::Group(Box::new(other)),
};
Ok(PredExpr::Truthy(Operand::Coalesce {
left: Box::new(as_operand(left)),
right: Box::new(as_operand(right)),
}))
}
fn cond_expr(&mut self) -> Result<Operand> {
let cond = self.pred_or()?;
let cond = self.coalesce_tail(cond)?;
self.cond_expr_from(cond)
}
fn cond_expr_from(&mut self, cond: PredExpr) -> Result<Operand> {
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,
commands: &mut Vec<Command>,
) -> Result<()> {
let before = commands.len();
self.invoke_pipeline_fragment(pipe, commands)?;
for command in &commands[before..] {
if matches!(
command,
Command::Push(_)
| Command::ExprPush { .. }
| Command::RecordPush { .. }
| Command::FieldsPush
| Command::Subcontext { .. }
| Command::Attach { .. }
| Command::AggPush { .. }
| Command::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 command instead)",
));
}
}
Ok(())
}
fn inline_command(&mut self) -> Result<Command> {
self.inline_command_in(PipeMode::Scalar)
}
fn inline_command_in(&mut self, mode: PipeMode) -> Result<Command> {
let command = self.pipe_item(mode)?;
match &command {
Command::Push(_)
| Command::ExprPush { .. }
| Command::RecordPush { .. }
| Command::FieldsPush
| Command::Subcontext { .. }
| Command::Attach { .. } => Err(QuarbError::parse(
"a pipe inside an expression transforms a value; \
pushes belong to real capsae (use a command)",
)),
_ => Ok(command),
}
}
fn map_command(&mut self) -> Result<Command> {
if self.nav_command_ahead() {
return Err(QuarbError::parse(
"navigation doesn't ride the map pipe; '$|' transforms \
the elements of a list topic — write '| /path' as its \
own command",
));
}
if matches!(self.peek(), Some(Token::LBracket)) {
let pred = self.predicate()?;
return Ok(match pred {
Predicate::Expr(e) => Command::Filter(e),
positional => Command::Select(positional),
});
}
self.inline_command()
}
fn inline_agg_command(&mut self) -> Result<Command> {
if matches!(self.peek(), Some(Token::LBracket)) {
return match self.predicate()? {
pred @ (Predicate::Index(_)
| Predicate::Range(_, _)
| Predicate::RangeForm(_, _)
| Predicate::Positions(_)
| Predicate::Computed(_)) => Ok(Command::Select(pred)),
Predicate::Expr(_) => Err(QuarbError::parse(
"a condition filters per capsa; write '| [cond]' \
('@| [n]' selects positionally)",
)),
};
}
match self.agg_sigil_command()? {
Some(Command::AggPush { .. }) => {
return Err(QuarbError::parse(
"a pipe inside an expression transforms a value; \
pushes belong to real capsae (use a command)",
));
}
Some(command) => return Ok(command),
None => {}
}
let call = self.func_call()?;
agg_command(call)
}
fn agg_sigil_command(&mut self) -> Result<Option<Command>> {
match self.peek() {
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(body)'); quote any other \
('@| .\"{n}\"(body)')"
)));
}
if !matches!(self.peek(), Some(Token::LParen)) {
return Err(QuarbError::parse(
"the aggregate push takes a body: '@| .name(body)' files the body's \
value on every capsa (a plain '| .name' files each topic)",
));
}
self.pos += 1;
let expr = self.cond_expr()?;
let commands = self.inline_pipe_tail()?;
let body = if commands.is_empty() {
expr
} else {
Operand::Piped {
expr: Box::new(expr),
commands,
}
};
self.expect(Token::RParen, "')' to close the aggregate push's body")?;
Ok(Some(Command::AggPush { name, body }))
}
Some(Token::Percent) => {
let head = self.pos;
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)) {
let call = self.record_args()?;
if !enriched && self.expr_continues() {
self.pos = head;
return Ok(Some(Command::AggExpr(self.additive()?)));
}
validate_record(&call)?;
return Ok(Some(Command::AggRecord { call, enriched }));
}
if !enriched
&& matches!(self.peek(), Some(Token::Name { text, quoted: false, .. }) if text == ",")
{
self.pos += 1;
return Ok(Some(Command::AggRecall(RegRef::SessionRecord)));
}
if !enriched && !self.at_view_dots() {
return Ok(Some(Command::Agg(FnCall {
headless: false,
name: "%".into(),
args: Vec::new(),
})));
}
let dots = self.view_dots(if enriched { "%%" } else { "%" })?;
Ok(Some(Command::AggRecall(match (enriched, dots) {
(false, 1) => RegRef::Record,
(false, _) => RegRef::FullRecord,
(true, 1) => RegRef::NodeRecord,
(true, _) => RegRef::NodeFullRecord,
})))
}
Some(Token::Dollar) => {
let save = self.pos;
match self.additive()? {
Operand::Recall(r) => Ok(Some(Command::AggRecall(r))),
_ => {
self.pos = save;
Err(QuarbError::parse(
"after '@|' a register accessor reads once ('@| $.name', '@| %.'); \
an expression over it belongs to '|' or to a body",
))
}
}
}
Some(Token::At) if matches!(self.toks.get(self.pos + 1), Some(Token::Name { text, .. }) if text == ".") =>
{
self.pos += 2;
Ok(Some(Command::AggRecall(RegRef::Whole)))
}
Some(Token::AtAt) if matches!(self.toks.get(self.pos + 1), Some(Token::Name { text, .. }) if text == ".") =>
{
self.pos += 2;
Ok(Some(Command::AggRecall(RegRef::NodeWhole)))
}
_ => Ok(None),
}
}
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::Tilde
| Token::TildeTilde
| Token::LParen
)
) {
steps.push(self.path_elem()?);
continue;
}
break;
}
let projection = self.projection()?;
Ok(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
})
}
fn lone_param_ahead(&self) -> Option<String> {
if self.template_holes.is_none() || !matches!(self.peek(), Some(Token::Dollar)) {
return None;
}
let Some(Token::Name {
text,
quoted: false,
..
}) = self.toks.get(self.pos + 1)
else {
return None;
};
if !self.def_params.iter().any(|p| p == text) {
return None;
}
match self.toks.get(self.pos + 2) {
None | Some(Token::Pipe | Token::RParen | Token::At | Token::Semi) => {
Some(text.clone())
}
_ => None,
}
}
fn expansion_hole(&mut self, text: &str) -> Result<Hole> {
if self.template_holes.is_none() {
return Err(QuarbError::parse(format!(
"an expansion '${{{text}}}' stands inside a quarq template 'q(…)' or \
inside a string; here write the value itself"
)));
}
let expr = match self.parse_hole(text)? {
InterpSeg::Expr(e) => e,
_ => {
return Err(QuarbError::parse(format!(
"in '${{{text}}}': a template expansion holds one value \
expression; ':?' and ':-' belong to the interpolated string"
)));
}
};
let holes = self.template_holes.as_mut().expect("checked above");
let hole = Hole {
text: text.to_string(),
expr: Box::new(expr),
index: holes.len(),
};
holes.push(hole.clone());
Ok(hole)
}
fn name_hole(&mut self, text: &str) -> Result<String> {
let hole = self.expansion_hole(text)?;
Ok(hole_name(hole.index, text))
}
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, false)?;
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, true)?)
});
}
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 commands = 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 commands)
.map_err(context)?;
}
Some(Token::Pipe) => {
p.pos += 1;
commands.push(p.inline_command().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 commands)
.map_err(context)?;
}
Some(Token::At) if matches!(p.toks.get(p.pos + 1), Some(Token::Pipe)) => {
p.pos += 2;
commands.push(p.inline_agg_command().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 commands.is_empty() {
expr
} else {
Operand::Piped {
expr: Box::new(expr),
commands,
}
}))
}
fn hole_expr(&mut self, part: &str, whole: &str, word: bool) -> 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();
p.arg_depth = usize::from(word);
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::DashDash))
&& matches!(
self.toks.get(self.pos + 1),
Some(Token::Name {
quoted: false,
glued: true,
..
})
)
&& matches!(self.toks.get(self.pos + 2), Some(Token::OptEq))
{
break;
}
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::Tilde
| Token::TildeTilde
| 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::Tilde
| Token::TildeTilde
| 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::Tilde
| Token::TildeTilde
| 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
| Token::Tilde
| Token::TildeTilde,
) => 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::Tilde
| Token::TildeTilde
| 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) => {
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 text == "q" && matches!(self.toks.get(self.pos + 1), Some(Token::LParen)) => {
self.pos += 2;
let piped = matches!(self.peek(), Some(Token::Pipe));
if piped {
self.pos += 1;
}
let saved_depth = self.arg_depth;
let saved_keyed = self.keyed_args;
self.arg_depth = 0;
self.keyed_args = false;
let saved_sub = self.subquery_depth;
self.subquery_depth = 0;
let saved_holes = self.template_holes.replace(Vec::new());
let parsed = (|| {
let mut commands = Vec::new();
if piped && let Some(Token::Expansion(text)) = self.peek() {
let text = text.clone();
self.pos += 1;
commands.push(Command::Hole {
hole: self.expansion_hole(&text)?,
agg: false,
});
} else if matches!(self.peek(), Some(Token::Amp))
&& matches!(
self.peek_invocation_body(),
Some(DefBody::Query(_) | DefBody::Macro(_))
)
{
let (name, q, quant) = self.invoke_query_fragment()?;
if quant.is_some() || !q.correlations.is_empty() || q.branches.len() != 1 {
return Err(QuarbError::parse(format!(
"'&{name}' heads a quarq; one branch stands \
there, without a quantifier or a correlation"
)));
}
commands.push(Command::Nav(q.branches.into_iter().next().expect("one")));
commands.extend(q.pipeline);
} else if matches!(self.peek(), Some(Token::Amp)) {
self.invoke_pipeline_fragment("|", &mut commands)?;
} else if piped
&& matches!(self.peek(), Some(Token::Dollar))
&& let Some(Token::Name {
text,
quoted: false,
..
}) = self.toks.get(self.pos + 1)
&& self.def_params.iter().any(|p| p == text)
&& matches!(
self.toks.get(self.pos + 2),
Some(Token::RParen | Token::Pipe | Token::At)
)
{
commands.push(Command::Param {
name: text.clone(),
agg: false,
});
self.pos += 2;
} else {
commands.push(self.pipe_item(PipeMode::Nav)?);
}
let mode = commands
.iter()
.fold(PipeMode::Nav, |m, c| command_mode_out(c, m));
self.pipeline_items(&mut commands, mode)?;
Ok::<_, QuarbError>(commands)
})();
self.arg_depth = saved_depth;
self.keyed_args = saved_keyed;
self.subquery_depth = saved_sub;
let holes =
std::mem::replace(&mut self.template_holes, saved_holes).unwrap_or_default();
let mut commands = parsed?;
self.expect(Token::RParen, "')' to close the quarq")?;
if self.in_macro_body {
rename_in_commands(&mut commands, &mut |n: &str| {
crate::syntax::ast::template_name(n)
});
}
Ok(Operand::Lit(if holes.is_empty() {
quarq_literal(commands)
} else {
Value::quarq_template(commands, holes)
}))
}
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_command(&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::Key(_))
| Some(Arg::Named { .. })
| Some(Arg::Opt { .. })
| Some(Arg::Pairs { .. })
| Some(Arg::Hole(_)) => {
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 command_call = FnCall {
headless: false,
name: call.name,
args: args.collect(),
};
let first = if crate::syntax::registry::takes_of(&command_call.name) {
Operand::Gathered(Box::new(first))
} else {
first
};
self.check_unit_call(&command_call)?;
let command = func_command_at(command_call, true)?;
Ok(Operand::Piped {
expr: Box::new(first),
commands: vec![command],
})
}
Some(Token::Name { text, quoted, .. }) => {
let value = literal_value(text, *quoted, &|e| self.expander.unit_scale(e));
let prefixed_call = self.arg_depth > 0
&& matches!(
(
self.toks.get(self.pos + 1),
self.toks.get(self.pos + 2),
self.toks.get(self.pos + 3)
),
(
Some(Token::Field | Token::Colon { spaced: false }),
Some(Token::Name {
quoted: false,
glued: true,
..
}),
Some(Token::LParen)
)
);
if !*quoted
&& (self.arg_depth == 0 || prefixed_call)
&& is_word(text)
&& matches!(value, Value::Str(_))
{
let mut name = text.clone();
self.pos += 1;
if let (
Some(Token::Field | Token::Colon { spaced: false }),
Some(Token::Name {
text: cmd,
quoted: false,
glued: true,
}),
) = (self.peek(), self.toks.get(self.pos + 1))
{
name = format!("{name}:{cmd}");
self.pos += 2;
}
return self.headless_body(name);
}
self.pos += 1;
Ok(Operand::Lit(value))
}
Some(Token::Expansion(text)) => {
let text = text.clone();
self.pos += 1;
Ok(Operand::Hole(self.expansion_hole(&text)?))
}
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::Name { text, quoted: false, .. }) if text == ",") =>
{
self.pos += 2;
Ok(Operand::Recall(RegRef::SessionRecord))
}
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),
commands: vec![Command::Func(call)],
})
}
Some(Token::Hash) => {
if let Some(err) = self.retired_table_count() {
return Err(err);
}
if self.table_option_ahead() {
let call = self.table_options()?;
return Ok(Operand::Piped {
expr: Box::new(Operand::Topic),
commands: vec![Command::Func(call)],
});
}
self.pos += 1;
return self.table_literal();
}
Some(Token::Topics) => {
self.pos += 1;
Ok(Operand::Topics)
}
Some(Token::Positions) => {
self.pos += 1;
let projection = self.projection()?;
Ok(Operand::Positions { projection })
}
Some(Token::Matrix { full }) => {
let full = *full;
self.pos += 1;
Ok(Operand::Matrix { node: false, full })
}
Some(Token::NodeMatrix { full }) => {
let full = *full;
self.pos += 1;
Ok(Operand::Matrix { node: true, full })
}
Some(Token::CtxTable) => {
self.pos += 1;
Ok(Operand::CtxTable)
}
Some(Token::AtAt) if matches!(self.toks.get(self.pos + 1), Some(Token::Name { text, quoted: false, .. }) if text == ".") =>
{
self.pos += 2;
Ok(Operand::Recall(RegRef::NodeWhole))
}
Some(Token::At) => {
self.pos += 1;
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
let (items, _) = self.list_items(false)?;
return Ok(Operand::List(items));
}
if matches!(self.peek(), Some(Token::Percent))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
{
self.pos += 1;
return self.record_array();
}
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 == "*" => Err(QuarbError::parse(
"'@*' is retired: the context is '@_' (the topics), '@@_' \
(the positions; '@@_::prop' maps over them), '#*' (the \
table of the value regulae) or '##*' (of the node regulae)",
)),
_ => Err(QuarbError::parse(
"expected '-' (arrived edges), '.' (register), '_' (the \
topics) or '@_' (the positions) after '@' in an operand",
)),
}
}
Some(Token::Position) => {
self.pos += 1;
return self.position();
}
Some(Token::Cursor) => {
self.pos += 1;
if matches!(
self.peek(),
Some(Token::ColonColon | Token::ColonColonColon)
) {
return Err(QuarbError::parse(CURSOR_PROJECTION));
}
if matches!(self.peek(), Some(Token::Field)) {
match self.toks.get(self.pos + 1) {
Some(Token::Name {
text,
quoted: false,
..
}) if text == "position" => {
self.pos += 2;
return self.position();
}
Some(Token::Name {
text,
quoted: false,
..
}) if text == "topic" => {
self.pos += 2;
return Ok(Operand::Topic);
}
_ => {}
}
}
return Ok(Operand::Cursor);
}
Some(Token::Invoking) => {
self.pos += 1;
return self.invoking_position();
}
Some(Token::Driver) => {
self.pos += 1;
return self.driver();
}
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.driver();
}
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 position, '$$__' the \
invoking position, '$$0' the driver, '$$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;
return Ok(Operand::Recall(reg_ref(&rest)?));
}
Some(Token::Name {
text,
quoted: false,
..
}) if text.starts_with(',') => {
let rest = text[1..].to_string();
self.pos += 1;
if rest.is_empty() {
return Err(QuarbError::parse(
"the session read names its value: '$,df' (the capsa's register is '$.df')",
));
}
return Ok(Operand::Recall(RegRef::Session(rest)));
}
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 let Some(Projection::CoreMeta(k)) = &projection
&& let Some(end) = match k.as_str() {
"source" => Some(End::Source),
"target" => Some(End::Target),
_ => None,
}
{
let anchor = Anchor::EdgeEnd(end);
return Ok(match self.rel_operand_continue(Vec::new())? {
Operand::Rel {
steps, projection, ..
} => Operand::Rel {
steps,
projection,
anchor,
},
_ => Operand::Rel {
steps: Vec::new(),
projection: None,
anchor,
},
});
}
match &projection {
Some(Projection::CoreMeta(k))
if matches!(k.as_str(), "round" | "hops" | "total") => {}
Some(Projection::CoreMeta(k)) => {
return Err(QuarbError::parse(format!(
"an edge carries plain properties ($-::{k}), its ends ':source' and ':target', and the facts ':round', ':hops' and ':total'; ':{k}' is none of them"
)));
}
Some(Projection::AdapterMeta(_)) => {
return Err(QuarbError::parse(
"an edge carries plain properties ($-::prop), its ends ':source', ':target', and the facts ':round', ':hops', ':total'",
));
}
_ => {}
}
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(),
None => Err(QuarbError::parse(
"the query ends where a value is expected: a binary operator takes a right \
side (12 x @(1; 2)); a quantity literal is written 12_h, a text \"12 h\"",
)),
other => Err(QuarbError::parse(format!(
"expected a value or path in a predicate, found {other:?}"
))),
}
}
fn splice_register(&mut self) -> Result<RegRef> {
match self.operand()? {
Operand::Recall(r) => Ok(r),
_ => Err(QuarbError::parse(
"'&' before a register splices the quarq it holds: &$.p, &$.",
)),
}
}
fn splice_operand_fragment(&mut self) -> Result<Operand> {
if matches!(self.toks.get(self.pos + 1), Some(Token::Dollar)) {
self.pos += 1;
let r = self.splice_register()?;
return Ok(Operand::Piped {
expr: Box::new(Operand::Seed),
commands: vec![Command::Splice(r)],
});
}
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,
},
);
resolve_range_forms_query(&mut q, &name, &map)?;
refuse_leftover(&name, leftover_param(&q))?;
revalidate_records_query(&q)?;
self.operand_from_query(&name, q)
}
DefBody::Macro(_) => {
let invited = invites_of(self.expander, &args);
let answers = self.expand_macro_answers(&name, &def, args)?;
let op = match self.split_answers(&name, answers)? {
Split::Forms(forms) => {
let mut op = self.graft_operand(&name, forms)?;
self.rename_binders(&invited, &mut |f| rename_in_operand(&mut op, f));
op
}
Split::Texts(texts) => {
let text = self.join_path_text(&name, texts)?;
self.reparse_operand(&name, &text)?
}
};
self.record_captures(&name, &invited, self.expander.usage_of_operand(&op));
Ok(op)
}
}
}
fn reparse_operand(&self, name: &str, text: &str) -> Result<Operand> {
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}')"
)));
}
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),
budget: None,
},
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 command in &q.pipeline {
if matches!(
command,
Command::Push(_)
| Command::ExprPush { .. }
| Command::RecordPush { .. }
| Command::FieldsPush
| Command::Subcontext { .. }
| Command::Attach { .. }
| Command::AggPush { .. }
| Command::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),
commands: q.pipeline,
})
}
fn project_shape(&mut self) -> Result<ProjShape> {
let saved_depth = self.arg_depth;
self.arg_depth = 0;
let shape = if matches!(self.peek(), Some(Token::Hash))
&& matches!(self.toks.get(self.pos + 1), Some(Token::LParen))
{
self.pos += 2;
self.shape_columns()
} else {
self.unary().map(|mut v| {
fold_typed(&mut v);
ProjShape::Value(Box::new(v))
})
};
self.arg_depth = saved_depth;
shape
}
fn expr_head(&mut self) -> Result<Operand> {
let left = self.additive()?;
self.expr_head_from(left)
}
fn expr_head_from(&mut self, left: Operand) -> Result<Operand> {
if self.cmp_ahead()
|| matches!(
self.peek(),
Some(
Token::Coalesce
| Token::AmpAmp
| Token::PipePipe
| Token::Question { .. }
| Token::QuestionEq
)
)
{
let first = self.pred_primary_from(left)?;
let first = self.pred_and_from(first)?;
let cond = self.pred_or_from(first)?;
let cond = self.coalesce_tail(cond)?;
return self.cond_expr_from(cond);
}
Ok(left)
}
fn cmp_ahead(&mut self) -> bool {
let save = self.pos;
let ahead = self.cmp_op().is_some();
self.pos = save;
ahead
}
fn cmp_op(&mut self) -> Option<CmpOp> {
let opens = self.pos > 0
&& matches!(
self.toks.get(self.pos - 1),
Some(Token::Pipe | Token::LParen)
);
let op = match self.peek()? {
Token::Eq => CmpOp::Eq,
Token::Ne => CmpOp::Ne,
Token::Lt {
spaced: true,
before,
} if *before || opens => CmpOp::Lt,
Token::Le => CmpOp::Le,
Token::Gt {
spaced: true,
before,
} if *before || opens => CmpOp::Gt,
Token::Ge => CmpOp::Ge,
Token::MatchEq => CmpOp::Match,
Token::NotMatchEq => CmpOp::NotMatch,
Token::Contains => CmpOp::Contains,
Token::Within => CmpOp::Within,
Token::Encloses => CmpOp::Encloses,
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 cursor_tail(&mut self) -> Result<Box<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(Box::new(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
}))
}
fn invoking_position(&mut self) -> Result<Operand> {
Ok(Operand::Invoking(self.cursor_tail()?))
}
fn position(&mut self) -> Result<Operand> {
Ok(Operand::Position(self.cursor_tail()?))
}
fn driver(&mut self) -> Result<Operand> {
if self.correlation_depth == 0 {
if !self.in_def_body {
return Err(QuarbError::parse(DRIVER_PLACEMENT));
}
self.body_uses_driver = true;
}
Ok(Operand::Driver(self.cursor_tail()?))
}
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;
loop {
match (self.peek(), self.toks.get(self.pos + 1)) {
(
Some(Token::Field | Token::Colon { spaced: false }),
Some(Token::Name { text, .. }),
) => {
let text = text.clone();
self.pos += 2;
name.push(':');
name.push_str(&text);
}
(Some(Token::Capsa(c)), _) if c == "." => {
self.pos += 1;
name.push_str(":.");
}
_ => break,
}
}
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::Tilde) => Axis::Any,
Some(Token::TildeTilde) => Axis::AnyDeep(self.reach()),
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}' stands apart from its step: a core field glues to it \
('/x:{name}'; ':fields' lists them), a 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::Expansion(text)) => {
let text = text.clone();
Ok(Matcher::Name(self.name_hole(&text)?))
}
Some(Token::Regex(pat)) => Regex::new(pat)
.map(Matcher::Regex)
.map_err(|e| QuarbError::parse(format!("bad regex '~({pat})': {e}"))),
Some(Token::Pipe) | None => Err(QuarbError::parse(
"a navigation operator must be followed by a name; the root alone is the \
anchor '^' ('^ | …')",
)),
_ => Err(QuarbError::parse(
"a navigation operator must be followed by a name",
)),
}
}
}
fn range_head(text: &str) -> Option<(i64, Vec<String>)> {
let (first, rest) = text.split_once("..")?;
let from: i64 = first.parse().ok()?;
let parts: Vec<String> = rest.split("..").map(str::to_string).collect();
if parts.len() > 2
|| parts
.iter()
.any(|p| !p.is_empty() && p.parse::<i64>().is_err())
{
return None;
}
Some((from, parts))
}
fn arith_word(text: &str) -> Option<ArithOp> {
Some(match text {
"+" => ArithOp::Add,
"-" => ArithOp::Sub,
"*" => ArithOp::Mul,
"div" => ArithOp::Div,
"idiv" => ArithOp::IDiv,
"mod" => ArithOp::Mod,
"x" => ArithOp::MatMul,
_ => return None,
})
}
const REGEX_MARK: char = '\u{1}';
const QUOTED_MARK: char = '\u{2}';
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_command(call: FnCall) -> Result<Command> {
if crate::syntax::registry::split_env(&call.name).is_some() {
return Ok(Command::Agg(call));
}
let Some(spec) = crate::syntax::registry::spec(&call.name).filter(|s| s.agg.is_some()) else {
return Err(QuarbError::unsupported(format!(
"aggregate command '{}'",
call.name
)));
};
if call.name == "ungroup" && !call.args.is_empty() {
return Err(QuarbError::parse("'ungroup' takes no arguments"));
}
check_operand_bound(&call, false, true)?;
let shaped = if crate::syntax::registry::takes_of(&call.name) {
call.without_of()
} else {
call.clone()
};
match spec.check {
crate::syntax::registry::Check::Command => validate_window_shift(&shaped)?,
crate::syntax::registry::Check::Keyed => validate_keyed(&shaped)?,
_ => {}
}
Ok(Command::Agg(call))
}
fn unit_operand_refusal(command: &str, operand: &str, names: &[String]) -> Option<String> {
crate::value::quantity::unit_operand_refusal(operand, names)
.map(|why| format!("{command}({operand}): {why}"))
}
fn check_operand_bound(call: &FnCall, nested: bool, agg: bool) -> Result<()> {
if !call.name.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Ok(());
}
let Some(max) = crate::syntax::man::operand_bound(&call.name) else {
return Ok(());
};
if crate::syntax::man::options_of(&call.name)
.is_some_and(|o| o.iter().any(|s| s.long.as_deref() == Some("--sample")))
&& call
.args
.iter()
.any(|a| matches!(a, Arg::Lit(Value::Str(s)) if s == "sample"))
{
return Err(QuarbError::parse(format!(
"'sample' is an option: write {0}(--sample)",
call.name
)));
}
let spec = crate::syntax::registry::spec(&call.name);
let reducing =
spec.is_some_and(|s| matches!(s.agg, Some(crate::syntax::registry::Agg::Reducing)));
let keyed_agg =
spec.is_some_and(|s| matches!(s.agg, Some(crate::syntax::registry::Agg::Keyed)));
let places_input = !keyed_agg
&& call
.args
.iter()
.any(|a| matches!(a, Arg::Expr(Operand::Topic)));
let is_operand = |a: &Arg| !matches!(a, Arg::Opt { .. } | Arg::Named { .. });
let written = call.args.iter().filter(|a| is_operand(a)).count();
if written <= max && !(reducing && places_input && !nested) {
return Ok(());
}
let signature = crate::syntax::man::synopsis(&call.name).unwrap_or_default();
let plural = |n: usize| if n == 1 { "" } else { "s" };
if !nested && places_input {
let stripped = FnCall {
headless: false,
name: call.name.clone(),
args: call
.args
.iter()
.filter(|a| !matches!(a, Arg::Expr(Operand::Topic)))
.cloned()
.collect(),
};
let command = if agg {
Command::Agg(stripped)
} else {
Command::Func(stripped)
};
return Err(QuarbError::parse(format!(
"{} reads its input from the pipe: write {}",
call.name,
crate::syntax::unparse::command(&command)
)));
}
let takes = match max {
0 => "no operand".to_string(),
n => format!("{n} operand{} at most", plural(n)),
};
Err(QuarbError::parse(if nested {
format!(
"'{}' inside an expression takes its input and {takes} after it, {written} given; the signature: {signature}",
call.name
)
} else {
format!(
"'{}' takes {takes}, {written} given; the signature: {signature}",
call.name
)
}))
}
fn canonical_core_field(name: String) -> String {
match name.as_str() {
"properties" => "props".to_string(),
"metadata" => "meta".to_string(),
_ => name,
}
}
fn func_command(call: FnCall) -> Result<Command> {
func_command_at(call, false)
}
fn retired_word_pointer(call: &FnCall) -> Option<String> {
let word = call.args.iter().find_map(|a| match a {
Arg::Lit(Value::Str(s)) => Some(s.as_str()),
_ => None,
})?;
match (call.name.as_str(), word) {
("dfmt", "iso") => Some("'iso' is a flag: write dfmt(--iso)".to_string()),
("scale", "binary") => Some("'binary' is a flag: write scale(--binary)".to_string()),
_ => None,
}
}
fn duration_takes_one_end(call: &FnCall) -> Option<String> {
if !matches!(call.name.as_str(), "duration" | "td") {
return None;
}
let has = |which: &str| {
call.args
.iter()
.any(|a| matches!(a, Arg::Opt { name, .. } if name == which))
};
(has("--from") && has("--to")).then(|| {
format!(
"{} takes --from or --to, not both: the topic is the other instant",
call.name
)
})
}
fn func_command_at(call: FnCall, nested: bool) -> Result<Command> {
use crate::syntax::registry::{Agg, Check};
if crate::syntax::registry::split_env(&call.name).is_some() {
return Ok(Command::Func(call));
}
if let Some(pointer) = retired_word_pointer(&call).or_else(|| duration_takes_one_end(&call)) {
return Err(QuarbError::parse(pointer));
}
check_operand_bound(&call, nested, false)?;
let spec = crate::syntax::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_command(&call.name) {
return Err(QuarbError::parse(pointer));
}
let err = QuarbError::unsupported(format!("pipeline command '{}'", call.name));
let digits: String = call
.name
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
let unit = &call.name[digits.len()..];
if !digits.is_empty()
&& digits.starts_with(|c: char| c.is_ascii_digit())
&& !unit.is_empty()
&& unit.chars().all(char::is_alphabetic)
&& !matches!(
literal_value(&format!("{digits}_{unit}"), false, &|_| None),
Value::Str(_)
)
{
return Err(err.with_hint(format!(
" (a bare word after a pipe names a command; the quantity literal is written \
{digits}_{unit}, the text \"{}\")",
call.name
)));
}
return Err(if spec.is_some_and(|s| s.agg == Some(Agg::Context)) {
err.with_hint(format!(" ('{}' uses '@|')", call.name))
} else {
err
});
}
let shaped = if crate::syntax::registry::takes_of(&call.name) {
call.without_of()
} else {
call.clone()
};
match spec.map(|s| s.check) {
Some(Check::Keyed) => validate_keyed(&shaped)?,
_ => validate_record(&shaped)?,
}
Ok(Command::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::value::encoding::is_decodable(&v.to_string())
|| crate::value::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 => "*=",
CmpOp::Within => "<<=",
CmpOp::Encloses => ">>=",
}
}
pub(crate) fn retired_command(name: &str) -> Option<String> {
match name {
"rec" | "record" => Some(format!("'{name}(…)' is retired: the record is '%(…)'")),
"chars" => Some("'chars' is retired: write 'cc'".into()),
"types" => Some("'types' is retired: the type of a table's columns is 'type'".into()),
"table" => Some("'table' is retired: the table is '@| pretty'".into()),
"pagerank" => Some(
"'@| pagerank' is retired: the graph library ranks the context, '@| graph:pagerank(->link)' (a float per node; '@| nodes' for the table)"
.into(),
),
"project" => Some(
"'project(…)' is retired: the projection operator — 'v:%(…)', 't:#(:A; :B)', 'v:$.p' — and '| %' for a node's record"
.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: 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::Key(_) => {
if i + 1 >= call.args.len() {
return Err(QuarbError::parse(format!(
"{what} has a trailing field name with no value"
)));
}
i += 2;
}
Arg::Opt { name, .. } if matches!(name.as_str(), "--keys" | "--values") => {
i += 1;
}
Arg::Expr(Operand::Spread(_)) => {
i += 1;
}
Arg::Hole(_) => {
i += 1;
}
Arg::Expr(Operand::Param(_)) => {
i += 1;
}
Arg::Expr(e) => {
let Some(name) = crate::syntax::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;
}
Arg::Pairs { keys, .. } => {
if let Operand::List(items) = keys {
for item in items {
if let Operand::Lit(v) = item {
check(&v.to_string())?;
}
}
}
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");
let positional: Vec<&Arg> = call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.collect();
match positional.as_slice() {
[] => return Ok(()),
[Arg::Lit(v)] | [_, Arg::Lit(v)] if atergo(v) => {
return Err(QuarbError::parse(
"'atergo' is a flag: write sort(--atergo) or sort(-a), sort(ru-RU; -a)",
));
}
[Arg::Lit(v)] => return locale(v),
_ => {
return Err(QuarbError::parse(
"sort takes a locale identifier, then the flags --atergo (-a) and \
--desc (-d): sort, sort(ru-RU), sort(-a), sort(ru-RU; -a); 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(_)) | Arg::Range(_, _))
)
{
return Err(QuarbError::parse(format!(
"{} takes a count or a rank range first: {}(3; ::key), {}(11..20; ::key), {}(-10; ::key)",
call.name, call.name, call.name, call.name
)));
}
let mut keys = args.filter(|a| !matches!(a, Arg::Opt { .. })).peekable();
if keys.peek().is_none()
|| keys.any(|a| matches!(a, Arg::Lit(v) if !matches!(v, Value::Quarq(_))))
{
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)",
)),
},
"ngrams" => match call.args.as_slice() {
[Arg::Lit(Value::Int(n))] if *n >= 1 => Ok(()),
_ => Err(QuarbError::parse(
"ngrams takes the size of the window: ngrams(2)",
)),
},
"bigrams" | "trigrams" => {
if call.args.is_empty() {
Ok(())
} else {
Err(QuarbError::parse(format!(
"'{}' takes no arguments; a window of another size is ngrams(n)",
call.name
)))
}
}
"skipgrams" => match call.args.as_slice() {
[Arg::Lit(Value::Int(n)), Arg::Lit(Value::Int(k))] if *n >= 1 && *k >= 0 => Ok(()),
_ => Err(QuarbError::parse(
"skipgrams takes the window size and the skips allowed: skipgrams(2; 1)",
)),
},
"frequencies" => match call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.collect::<Vec<_>>()
.as_slice()
{
[] => Ok(()),
[Arg::Lit(Value::Int(k))] if *k != 0 => Ok(()),
[Arg::Range(_, _)] => Ok(()),
_ => Err(QuarbError::parse(
"frequencies takes nothing, how many to keep, or a rank range: \
frequencies(10), frequencies(11..20), frequencies(-10)",
)),
},
"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" | "pretty" => {
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::Lit(Value::Str(_))] => Ok(()),
_ => Err(QuarbError::parse(
"number takes a locale: number, number(fr); an outline is numbered by outline(…; --style=\"1.\")",
)),
},
"timeline" => {
if call
.args
.iter()
.any(|a| matches!(a, Arg::Lit(Value::Str(w)) if w == "forward"))
{
return Err(QuarbError::parse(
"'forward' is a flag: write timeline(expr; --forward), timeline(expr; anchor; --forward)",
));
}
let args: Vec<&Arg> = call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.collect();
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, then \
the flag --forward: timeline($_), timeline($_; $.d), \
timeline($_; $.d; --forward)",
))
}
}
"tree" => {
for a in &call.args {
if let Arg::Lit(Value::Str(s)) = a {
let switch = match s.as_str() {
"kind" => Some("--kind or -k"),
"traits" => Some("--traits or -t"),
"props" => Some("--props or -p"),
"%" | "record" => Some("--record or -r"),
_ => None,
};
return Err(QuarbError::parse(match switch {
Some(sw) => format!("'{s}' is a string here: write {sw}"),
None => format!(
"tree takes column expressions, then the options -d=n (--depth=n), \
--kind, --traits, --props, --record: tree(-d=2 -t), tree(::mtime; -d=0 -r) \
('{s}' is a string here)"
),
}));
}
if let Arg::Lit(Value::Int(n)) = a {
return Err(QuarbError::parse(format!(
"tree's depth is an option: write tree(-d={n}) or tree(--depth={n})"
)));
}
if let Arg::Opt { name, value } = a
&& (name == "-d" || name == "--depth")
&& !matches!(value, Some(Operand::Lit(Value::Int(n))) if *n >= 0)
{
return Err(QuarbError::parse(
"tree's depth is a count: tree(-d=2), tree(--depth=0)",
));
}
}
let ok = call
.args
.iter()
.all(|a| matches!(a, Arg::Expr(_) | Arg::Opt { .. }));
if ok {
Ok(())
} else {
Err(QuarbError::parse(
"tree takes column expressions, then the options -d=n (--depth=n), \
--kind (-k), --traits (-t), --props (-p), --record (-r): tree(-d=2 -t), \
tree(::mtime; -d=0 -r), tree((/* @| count); -d=0 -t)",
))
}
}
"outline" => {
let positional: Vec<&Arg> = call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.collect();
if let Some(Arg::Opt { value, .. }) = call
.args
.iter()
.find(|a| matches!(a, Arg::Opt { name, .. } if name == "--style"))
&& !matches!(value, Some(Operand::Lit(Value::Str(s))) if s == "1.")
{
return Err(QuarbError::parse(
"outline's numbering style is \"1.\": outline(:depth; ::lemma; --style=\"1.\")",
));
}
match positional.as_slice() {
[Arg::Expr(_), Arg::Expr(_), Arg::Lit(Value::Str(_))] => Err(QuarbError::parse(
"outline's numbering style is an option: outline(:depth; ::lemma; --style=\"1.\")",
)),
[] | [_] | [_, _] => Ok(()),
_ => Err(QuarbError::parse(
"outline takes a level and an optional text, then the numbering style as an \
option: outline(:depth; ::lemma), outline(:depth; --style=\"1.\")",
)),
}
}
"zscore" => {
let operands: Vec<&Arg> = call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.collect();
if operands
.iter()
.any(|a| matches!(a, Arg::Lit(Value::Str(s)) if s == "sample"))
{
return Err(QuarbError::parse(
"'sample' is an option: write zscore(--sample), zscore(key; --sample)",
));
}
let ok = match operands.as_slice() {
[] => true,
[Arg::Expr(_)] => true,
_ => false,
};
if ok {
Ok(())
} else {
Err(QuarbError::parse(
"zscore takes a partition key and the switch --sample: \
zscore, zscore($.w), zscore(--sample), zscore($.w; --sample)",
))
}
}
_ => Ok(()),
}
}
fn validate_stat_args(call: &FnCall) -> Result<()> {
match call.name.as_str() {
"std" | "var" | "stddev" | "variance" => {
let operands: Vec<&Arg> = call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.collect();
let rest = operands.as_slice();
if rest
.iter()
.any(|a| matches!(a, Arg::Lit(Value::Str(s)) if s == "sample"))
{
return Err(QuarbError::parse(format!(
"'sample' is an option: write {0}(--sample)",
call.name
)));
}
if rest.is_empty() {
Ok(())
} else {
Err(QuarbError::parse(format!(
"{0} takes no operand, the switch --sample and --of=expr: {0}, {0}(--sample), {0}(--sample --of=:x)",
call.name
)))
}
}
"copy" => {
if call.args.is_empty() {
Ok(())
} else {
Err(QuarbError::parse(
"copy takes no operand and no option: it copies the output exactly as it would have printed ('… @| json | copy')",
))
}
}
"encoding" => match call.args.as_slice() {
[Arg::Lit(Value::Str(label))] => {
if crate::value::encoding::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 is_word(text: &str) -> bool {
let mut chars = text.chars();
chars.next().is_some_and(|c| c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || c == '_')
}
pub(crate) fn literal_value(
text: &str,
quoted: bool,
scale: &dyn Fn(&str) -> Option<(f64, String)>,
) -> Value {
if quoted {
return Value::Str(text.to_string());
}
if crate::syntax::lexer::is_quantity_head(text) {
if let Some((value, base, wv, wu)) =
crate::value::quantity::parse_unit_text_with(text, scale)
{
return Value::Quantity {
value,
base,
written: Some((wv, wu)),
};
}
let glued = text.replacen('_', "", 1);
if let Some((secs, nanos)) = Value::Str(glued.clone())
.durational_reading()
.or_else(|| crate::temporal::span_from_units(&glued, scale))
{
return Value::seconds(secs, nanos);
}
return Value::Str(text.to_string());
}
match text {
"true" => return Value::Bool(true),
"false" => return Value::Bool(false),
"null" => return Value::Null,
"nan" => return Value::Float(f64::NAN),
"inf" => return Value::Float(f64::INFINITY),
"-inf" => return Value::Float(f64::NEG_INFINITY),
_ => {}
}
if text.len() >= 10
&& text.as_bytes()[4] == b'-'
&& let Some((secs, nanos, offset_min)) = crate::temporal::parse_iso(text)
{
return Value::Instant {
secs,
nanos,
offset_min,
calendar: Default::default(),
};
}
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 adapter_key(key: String, value_side: bool) -> Result<Projection> {
if crate::syntax::ast::CORE_KEYS.contains(&key.as_str())
&& !(value_side && matches!(key.as_str(), "index" | "name"))
{
return Err(QuarbError::parse(format!(
"':::{key}' is the adapter metadata, and '{key}' is a core property: write ':{key}' (the ladder counts from the core: ':name', '::p', ':::k')"
)));
}
Ok(Projection::AdapterMeta(key))
}
fn is_projection_start(tok: &Token) -> bool {
matches!(tok, Token::ColonColon | Token::ColonColonColon)
}
pub(crate) 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);
}
let mut trailing: Vec<Command> = Vec::new();
for b in &mut q.branches {
if let Anchor::Id(op) = &mut b.anchor {
subst_operand(op, map);
}
if let Some(cmds) = subst_branch(b, map) {
trailing.extend(cmds);
}
}
if !trailing.is_empty() {
trailing.extend(std::mem::take(&mut q.pipeline));
q.pipeline = trailing;
}
subst_commands(&mut q.pipeline, map);
}
fn subst_branch(b: &mut Branch, map: &Subst<'_>) -> Option<Vec<Command>> {
let mut out: Vec<PathElem> = Vec::with_capacity(b.steps.len());
let mut trailing: Option<Vec<Command>> = None;
for mut elem in std::mem::take(&mut b.steps) {
if let PathElem::Param(p) = &elem {
match map.outer.get(p) {
Some(Operand::Rel {
steps,
projection,
anchor: Anchor::Current,
}) => {
out.extend(steps.iter().cloned());
if projection.is_some() {
b.projection = projection.clone();
}
continue;
}
Some(Operand::Lit(Value::Quarq(q))) => {
trailing
.get_or_insert_with(Vec::new)
.extend(q.body.iter().cloned());
continue;
}
_ => {}
}
}
subst_elem(&mut elem, map);
out.push(elem);
}
b.steps = out;
trailing
}
fn subst_quarq(q: &crate::value::Quarq, map: &Subst<'_>) -> Value {
let mut body = (*q.body).clone();
subst_commands(&mut body, map);
let mut holes = (*q.holes).clone();
for h in &mut holes {
subst_operand(&mut h.expr, map);
}
if holes.is_empty() {
quarq_literal(body)
} else {
Value::quarq_template(body, holes)
}
}
fn subst_commands(commands: &mut Vec<Command>, map: &Subst<'_>) {
let mut out: Vec<Command> = Vec::with_capacity(commands.len());
for mut command in std::mem::take(commands) {
if let Command::Nav(b) = &mut command
&& let Some(cmds) = subst_branch(b, map)
{
subst_command(&mut command, map);
out.push(command);
out.extend(cmds);
continue;
}
if let Command::Param { name, agg } = &command
&& let Some(Operand::Lit(Value::Quarq(q))) = map.outer.get(name)
{
if *agg {
match q.body.as_slice() {
[Command::Func(call)] => out.push(Command::Agg(call.clone())),
[Command::Expr(e)] => out.push(Command::AggExpr(e.clone())),
[Command::Agg(_) | Command::AggExpr(_) | Command::AggPush { .. }] => {
out.extend(q.body.iter().cloned())
}
_ => out.push(command),
}
} else {
out.extend(q.body.iter().cloned());
}
continue;
}
subst_command(&mut command, map);
out.push(command);
}
*commands = out;
}
fn subst_elem(elem: &mut PathElem, map: &Subst<'_>) {
match elem {
PathElem::Param(_) => {}
PathElem::Hole(h) => subst_operand(&mut h.expr, map),
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_command(command: &mut Command, map: &Subst<'_>) {
match command {
Command::SessionPush(_) => {}
Command::Hole { hole, .. } => subst_operand(&mut hole.expr, map),
Command::Func(call) | Command::Agg(call) => {
for arg in &mut call.args {
match arg {
Arg::Expr(e) => {
let was_param = matches!(e, Operand::Param(_));
subst_operand(e, map);
if was_param && let Some(folded) = literal_arg(e) {
*arg = folded;
}
}
Arg::Opt { value: Some(e), .. } => subst_operand(e, map),
Arg::Hole(h) => subst_operand(&mut h.expr, map),
Arg::Lit(Value::Quarq(q)) => *arg = Arg::Lit(subst_quarq(q, map)),
_ => {}
}
}
}
Command::RecordWith(call) | Command::RecordPush { call, .. } => {
for arg in &mut call.args {
match arg {
Arg::Expr(e) | Arg::Opt { value: Some(e), .. } => subst_operand(e, map),
Arg::Hole(h) => subst_operand(&mut h.expr, map),
Arg::Lit(Value::Quarq(q)) => *arg = Arg::Lit(subst_quarq(q, map)),
_ => {}
}
}
}
Command::Expr(e)
| Command::AggExpr(e)
| Command::ExprPush { expr: e, .. }
| Command::AggPush { body: e, .. } => subst_operand(e, map),
Command::AggRecord { call, .. } => {
for arg in &mut call.args {
match arg {
Arg::Expr(e) | Arg::Opt { value: Some(e), .. } => subst_operand(e, map),
Arg::Hole(h) => subst_operand(&mut h.expr, map),
Arg::Lit(Value::Quarq(q)) => *arg = Arg::Lit(subst_quarq(q, map)),
_ => {}
}
}
}
Command::Nav(b) => {
if let Anchor::Id(op) = &mut b.anchor {
subst_operand(op, map);
}
subst_branch(b, map);
}
Command::Subcontext { body, .. } => subst_query(body, map),
Command::Filter(e) => subst_pred_expr(e, map),
Command::Select(Predicate::Expr(e)) => subst_pred_expr(e, map),
Command::Map(inner)
| Command::Repeat { command: inner, .. }
| Command::Attach { command: inner, .. } => subst_command(inner, map),
Command::Select(_)
| Command::Push(_)
| Command::FieldsPush
| Command::Recall(_)
| Command::Splice(_)
| Command::Param { .. }
| Command::AggRecall(_)
| Command::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::Chain(operands, _) => operands.iter_mut().for_each(|o| subst_operand(o, map)),
PredExpr::Truthy(o) => subst_operand(o, map),
}
}
fn literal_arg(op: &Operand) -> Option<Arg> {
match op {
Operand::Lit(
v @ (Value::Int(_) | Value::Float(_) | Value::Str(_) | Value::Bool(_) | Value::Null),
) => Some(Arg::Lit(v.clone())),
Operand::Range {
from,
to,
step: None,
} => match (&**from, &**to) {
(Operand::Lit(Value::Int(a)), Operand::Lit(Value::Int(b))) => {
Some(Arg::Range(Some(*a), Some(*b)))
}
_ => None,
},
_ => None,
}
}
fn resolve_range_forms_query(
q: &mut Query,
name: &str,
map: &HashMap<String, Operand>,
) -> Result<()> {
for corr in &mut q.correlations {
resolve_range_forms_query(corr, name, map)?;
}
for b in &mut q.branches {
resolve_range_forms_elems(&mut b.steps, name, map)?;
}
resolve_range_forms_commands(&mut q.pipeline, name, map)
}
fn resolve_range_forms_elems(
elems: &mut [PathElem],
name: &str,
map: &HashMap<String, Operand>,
) -> Result<()> {
for elem in elems.iter_mut() {
match elem {
PathElem::Step(step) => {
for p in &mut step.predicates {
resolve_range_form(p, name, map)?;
}
}
PathElem::Group(group) => {
for alt in &mut group.alts {
resolve_range_forms_elems(alt, name, map)?;
}
for p in &mut group.predicates {
resolve_range_form(p, name, map)?;
}
}
PathElem::Push {
body: PushBody::Query(q),
..
} => resolve_range_forms_query(q, name, map)?,
_ => {}
}
}
Ok(())
}
fn resolve_range_forms_commands(
cmds: &mut [Command],
name: &str,
map: &HashMap<String, Operand>,
) -> Result<()> {
for c in cmds.iter_mut() {
resolve_range_forms_command(c, name, map)?;
}
Ok(())
}
fn resolve_range_forms_command(
c: &mut Command,
name: &str,
map: &HashMap<String, Operand>,
) -> Result<()> {
match c {
Command::Select(p) => resolve_range_form(p, name, map),
Command::Nav(b) => resolve_range_forms_elems(&mut b.steps, name, map),
Command::Subcontext { body, .. } => resolve_range_forms_query(body, name, map),
Command::Map(inner)
| Command::Repeat { command: inner, .. }
| Command::Attach { command: inner, .. } => resolve_range_forms_command(inner, name, map),
_ => Ok(()),
}
}
fn resolve_range_form(p: &mut Predicate, name: &str, map: &HashMap<String, Operand>) -> Result<()> {
let Predicate::RangeForm(a, b) = p else {
return Ok(());
};
let bound = |b: &Bound| -> Result<Option<i64>> {
match b {
Bound::Open => Ok(None),
Bound::Int(n) => Ok(Some(*n)),
Bound::Param(param) => match map.get(param) {
Some(Operand::Lit(Value::Int(n))) => Ok(Some(*n)),
Some(other) => Err(QuarbError::parse(format!(
"parameter '${param}' of '&{name}' stands where a slice bound \
stands; it takes an integer, not '{}'",
crate::syntax::unparse::operand_text(other)
))),
None => Err(QuarbError::parse(format!(
"parameter '${param}' of '&{name}' is not bound"
))),
},
}
};
let (from, to) = (bound(a)?, bound(b)?);
*p = Predicate::Range(from, to);
Ok(())
}
struct Revalidate(Option<QuarbError>);
impl visit::Visitor for Revalidate {
fn call(&mut self, c: &FnCall) {
if self.0.is_none() {
let checked = if c.name == "group" {
validate_record_convention(c, "group")
} else {
validate_record(c)
};
if let Err(e) = checked {
self.0 = Some(e);
}
}
visit::walk_call(self, c);
}
}
fn revalidate_records_query(q: &Query) -> Result<()> {
let mut v = Revalidate(None);
v.query(q);
v.0.map_or(Ok(()), Err)
}
fn revalidate_records_commands(cmds: &[Command]) -> Result<()> {
let mut v = Revalidate(None);
for c in cmds {
v.command(c);
}
v.0.map_or(Ok(()), Err)
}
fn subst_operand(o: &mut Operand, map: &Subst<'_>) {
match o {
Operand::Pattern(_) => {}
Operand::Lit(Value::Quarq(q)) => {
*o = Operand::Lit(subst_quarq(q, map));
}
Operand::Hole(h) => subst_operand(&mut h.expr, map),
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, .. } | Operand::Concat { left, right, .. } => {
subst_operand(left, map);
subst_operand(right, map);
}
Operand::Row(items) => {
for (_, i) in items {
subst_operand(i, map);
}
}
Operand::Neg(inner)
| Operand::Desc(inner)
| Operand::Gathered(inner)
| Operand::Spread(inner) => subst_operand(inner, map),
Operand::Through { base, .. } => subst_operand(base, map),
Operand::Projected { base, shape } => {
subst_operand(base, map);
match shape {
ProjShape::Value(v) => subst_operand(v, map),
ProjShape::Columns { items, index } => {
for (_, item) in items {
subst_operand(item, map);
}
if let Some(i) = index {
subst_operand(i, map);
}
}
}
}
Operand::Typed { inner, .. } => subst_operand(inner, map),
Operand::LabelIndex { base, labels } => {
subst_operand(base, map);
for l in labels {
subst_operand(l, map);
}
}
Operand::Field { base, .. }
| Operand::ValueMeta { base, .. }
| Operand::Index { base, .. } => subst_operand(base, map),
Operand::Coalesce { left, right } => {
subst_operand(left, map);
subst_operand(right, map);
}
Operand::PeerReg { at, .. } => {
if let Some(at) = at {
subst_operand(at, map)
}
}
Operand::List(items) => {
for item in items {
subst_operand(item, map);
}
}
Operand::RecordArray { rows, .. } => {
for row in rows {
for (_, i) in row {
subst_operand(i, map);
}
}
}
Operand::Range { from, to, step } => {
subst_operand(from, map);
subst_operand(to, map);
if let Some(s) = step {
subst_operand(s, map);
}
}
Operand::Table { rows, .. } => {
for row in rows {
match row {
TableRow::List(items) => {
for i in items {
subst_operand(i, map);
}
}
TableRow::Record(items) => {
for (_, i) in items {
subst_operand(i, map);
}
}
}
}
}
Operand::NamedCaptures => {}
Operand::Group(e) => subst_pred_expr(e, map),
Operand::Invoking(inner) | Operand::Position(inner) | Operand::Driver(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, commands } => {
subst_operand(expr, map);
subst_commands(commands, 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::Cursor
| Operand::Seed
| Operand::Ordinal
| Operand::Edge { .. }
| Operand::Edges { .. }
| Operand::Topics
| Operand::Positions { .. }
| Operand::Matrix { .. }
| Operand::CtxTable
| 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_command(s: &Command) -> usize {
let mut v = MaxCtx(0);
v.command(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);
}
}
pub(crate) fn quarq_literal(commands: Vec<Command>) -> Value {
use crate::syntax::ast::{Anchor, Projection};
let projector = match commands.as_slice() {
[Command::Expr(Operand::Projected { base, shape })]
if matches!(**base, Operand::Topic)
&& let ProjShape::Value(v) = shape
&& let Operand::Piped { expr, commands } = &**v
&& matches!(**expr, Operand::Topic)
&& let [Command::Func(call)] = commands.as_slice()
&& matches!(call.name.as_str(), "rec" | "record") =>
{
fold_projector_record(call)
}
[Command::Expr(op)] => {
let mut folded = op.clone();
head_projections(&mut folded);
fold_closed(&folded)
}
[Command::Nav(b)] if b.steps.is_empty() && matches!(b.anchor, Anchor::Current) => {
match &b.projection {
Some(Projection::CoreMeta(k)) => Some(Value::projection(1, k.clone())),
Some(Projection::Property(Some(k))) => Some(Value::projection(2, k.clone())),
Some(Projection::AdapterMeta(k)) => Some(Value::projection(3, k.clone())),
_ => None,
}
}
[Command::Func(call)] if matches!(call.name.as_str(), "rec" | "record") => {
fold_projector_record(call)
}
_ => None,
}
.filter(Value::holds_projector);
Value::quarq(commands, projector)
}
fn fold_projector_record(call: &FnCall) -> Option<Value> {
let mut args = call.args.clone();
for a in &mut args {
if let Arg::Expr(e) = a {
head_projections(e);
}
}
fold_record(&args)
}
fn fold_closed(op: &Operand) -> Option<Value> {
match op {
Operand::Lit(v) => Some(v.clone()),
Operand::List(items) => Some(Value::list(
items.iter().map(fold_closed).collect::<Option<Vec<_>>>()?,
)),
Operand::Piped { expr, commands }
if matches!(**expr, Operand::Topic | Operand::Seed)
&& let [Command::Func(call)] = commands.as_slice()
&& matches!(call.name.as_str(), "rec" | "record") =>
{
fold_record(&call.args)
}
_ => None,
}
}
fn fold_record(args: &[Arg]) -> Option<Value> {
let mut fields = Vec::new();
let mut args = args.iter();
while let Some(arg) = args.next() {
match arg {
Arg::Lit(Value::Str(name)) => {
let value = match args.next()? {
Arg::Expr(e) => fold_closed(e)?,
Arg::Lit(v) => v.clone(),
_ => return None,
};
fields.push((name.clone(), value));
}
Arg::Expr(e) => {
let name = crate::syntax::ast::auto_field_name(e)?.to_string();
fields.push((name, fold_closed(e)?));
}
_ => return None,
}
}
Some(Value::Record(fields.into()))
}
fn head_projections(op: &mut Operand) {
use crate::syntax::ast::{Anchor, Projection, TableRow};
if let Operand::Typed { inner, typing } = op {
let typing = typing.clone();
head_projections(inner);
if let Operand::Lit(Value::Projection { ladder, key, .. }) = inner.as_ref() {
*op = Operand::Lit(Value::typed_projection(*ladder, key.clone(), Some(typing)));
}
return;
}
let spelled = match op {
Operand::Field { base, name } if matches!(**base, Operand::Topic) => {
Some(Value::projection(1, name.clone()))
}
Operand::Rel {
steps,
projection: Some(p),
anchor: Anchor::Current,
} if steps.is_empty() => match p {
Projection::CoreMeta(k) => Some(Value::projection(1, k.clone())),
Projection::Property(Some(k)) => Some(Value::projection(2, k.clone())),
Projection::AdapterMeta(k) => Some(Value::projection(3, k.clone())),
Projection::Property(None) => None,
},
_ => None,
};
if let Some(v) = spelled {
*op = Operand::Lit(v);
return;
}
match op {
Operand::List(items) => items.iter_mut().for_each(head_projections),
Operand::Table { rows, .. } => {
for row in rows {
match row {
TableRow::List(items) => items.iter_mut().for_each(head_projections),
TableRow::Record(items) => {
items.iter_mut().for_each(|(_, o)| head_projections(o))
}
}
}
}
Operand::Piped { expr, commands } if matches!(**expr, Operand::Topic | Operand::Seed) => {
for command in commands {
match command {
Command::Func(call) => {
for a in &mut call.args {
if let Arg::Expr(e) = a {
head_projections(e);
}
}
}
Command::Expr(e) => head_projections(e),
_ => {}
}
}
}
Operand::Projected { base, .. } => head_projections(base),
Operand::Index { base, .. } => head_projections(base),
Operand::Spread(inner) | Operand::Neg(inner) | Operand::Desc(inner) => {
head_projections(inner)
}
Operand::Arith { left, right, .. }
| Operand::Concat { left, right, .. }
| Operand::Coalesce { left, right } => {
head_projections(left);
head_projections(right);
}
_ => {}
}
}
pub(crate) fn node_free(op: &Operand) -> bool {
match op {
Operand::Lit(_) | Operand::Recall(_) => true,
Operand::List(items) => items.iter().all(node_free),
Operand::Arith { left, right, .. } => node_free(left) && node_free(right),
Operand::Neg(inner) | Operand::Desc(inner) => node_free(inner),
_ => false,
}
}
fn validate_keyed_commands(q: &Query) -> Result<()> {
let mut grouped = false;
for command in &q.pipeline {
match command {
Command::Agg(call) if matches!(call.name.as_str(), "group" | "window") => {
grouped = true
}
Command::Func(call) if crate::syntax::registry::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_commands(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 = $$0::x]'",
));
}
}
}
for st in &q.pipeline {
let m = max_ctx_command(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(_, _)
| Predicate::Positions(_)
| Predicate::Computed(_)
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_command(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::Field { base, .. } if matches!(**base, Operand::Topic) => {}
Operand::Invoking(_)
| Operand::Driver(_)
| Operand::Recall(_)
| Operand::Topic
| Operand::Ordinal
| Operand::Capture(_)
| Operand::NamedCaptures => self.0 = true,
Operand::Rel {
anchor:
Anchor::Mark(_)
| Anchor::MarkIndex(_)
| Anchor::MarkTop
| Anchor::MarkBack(_)
| Anchor::MarksAll
| Anchor::MarksNamed(_)
| Anchor::EdgeEnd(_),
..
} => self.0 = true,
_ => visit::walk_operand(self, o),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_arrow_opens_a_navigation_command() {
let first = |q: &str| {
let p = parse(&lexer::lex(q).unwrap()).unwrap().pipeline;
match &p[0] {
Command::Nav(_) => "nav",
Command::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 defs_only_classifies_cells() {
let only = "# it's a note\ndef &a: /x ;\nmacro &m: ^ | \"/y\" ;";
assert_eq!(crate::defs_only(only).unwrap(), true);
let preamble = "macro &pivot($c): /x | $c @| unique ;\n^ | &pivot(::k) | %.";
assert_eq!(crate::defs_only(preamble).unwrap(), false);
let bad = "def &a: /x[ ;";
assert!(crate::defs_only(bad).is_err());
assert_eq!(crate::defs_only("# only").unwrap(), true);
}
#[test]
fn fragment_bodies_cover_chains_and_inline_pipes() {
for q in [
"def &j: /a/* <=> /b/*[::x = $$0::x]; &j @| count",
"def &j: /a/* <=> /b/*[::x = $$0::x]; &j | [!$$1::x]",
"def &j: /a/* <=> /b/*[::x = $$0::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 = $$0::x]; /d/* <=> &j",
"chains are flat",
),
(
"def &j: /a/* <=> /b/*[::x = $$0::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 = $$0::x]",
"/a/* <=>? /b/*[::x = $$0::x] | [!$$1::x]",
"/a/* <=> /b/*[::x = $$0::x] <=> /c/*[::y = $$1::y] | %($$1::n; $$2::m)",
"/a/* <=> /b/*[$$0/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[$$0::x]/c/*", "final step"),
(
"/a/* <=> /b/*[::x = $$0::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 options_close_a_simple_commands_parentheses() {
let refuse = |q: &str| parse(&lexer::lex(q).unwrap()).unwrap_err().to_string();
let e = refuse("/x | upper(-i)");
assert!(e.contains("'upper' has no option '-i'"), "{e}");
let e = refuse("/x | ::n @| sort(::name; --locale=ru)");
assert!(e.contains("has no option '--locale'"), "{e}");
let e = refuse("/x | upper(--casefold -i)");
assert!(e.contains("has no option '--casefold'"), "{e}");
let e = refuse("/x | upper(-T=4 -9)");
assert!(e.contains("no option '-T'"), "{e}");
let e = refuse("/x | upper(;; -9)");
assert!(e.contains("no option '-9'"), "{e}");
let e = refuse("/x | upper(-= -9)");
assert!(e.contains("'-=' is retired"), "{e}");
parse(&lexer::lex("/x | ::n | round(2;;)").unwrap()).unwrap();
let e = refuse("/x | upper(a; -n=10; b)");
assert!(e.contains("stand last"), "{e}");
parse(&lexer::lex("/x | ::n | round(-2)").unwrap()).unwrap();
parse(&lexer::lex("/x | ::n | substr(-3..)").unwrap()).unwrap();
let call = |args: Vec<Arg>| FnCall {
headless: false,
name: "f".into(),
args,
};
let opt = |n: &str, v: Option<Operand>| Arg::Opt {
name: n.into(),
value: v,
};
assert_eq!(
crate::syntax::unparse::fn_call_text(&call(vec![
Arg::Lit(Value::Str("a".into())),
opt("-n", Some(Operand::Lit(Value::Int(10)))),
opt("--locale", Some(Operand::Lit(Value::Str("ru".into())))),
opt("-i", None),
])),
"f(\"a\"; -n=10 --locale=ru -i)"
);
assert_eq!(
crate::syntax::unparse::fn_call_text(&call(vec![opt("-9", None)])),
"f(;; -9)"
);
}
#[test]
fn a_prefixed_command_stands_headless_in_a_body() {
let toks = lexer::lex("/w | %(lemma = morph:lemma; pos = morph:pos(--lang=ru))").unwrap();
let q = parse(&toks).unwrap();
let dbg = format!("{q:?}");
assert!(
dbg.contains("name: \"morph:lemma\"") && dbg.contains("name: \"morph:pos\""),
"{dbg}"
);
assert!(dbg.contains("Seed"), "{dbg}");
}
#[test]
fn bare_inf_and_nan_are_float_literals() {
let toks = lexer::lex("/x[::status = inf]").unwrap();
let q = parse(&toks).unwrap();
let dbg = format!("{q:?}");
assert!(dbg.contains("Lit(Float(inf))"), "got {dbg}");
let toks = lexer::lex("/x[::status = nan]").unwrap();
let dbg = format!("{:?}", parse(&toks).unwrap());
assert!(dbg.contains("Lit(Float(NaN))"), "got {dbg}");
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, Command::Map(_))),
"expected a map command, 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_literal_is_retired() {
let err = lexer::lex("macro &m: ^ | `echo hi`; &m").unwrap_err();
assert!(
err.to_string().contains("sh=shell:"),
"expected the retirement pointer, 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 BARE_BOOL_WORDS: &[(&str, &str)] = &[("and", "&&"), ("or", "||"), ("not", "!")];
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 '*'",
)),
}
}
fn typed_value_refusal(key: &str, inner: &Operand, typing: crate::value::Typing) -> QuarbError {
let value = crate::syntax::unparse::operand(inner);
let cast = match typing.declared() {
Some(t) => format!(", or cast the value, '{key} = {}({value})'", t.word()),
None => String::new(),
};
QuarbError::parse(format!(
"'{value}{}' right of '=' declares nothing: a trait describes a field, never a value; declare the key, '{key}{} = {value}'{cast}",
typing.spelling(),
typing.spelling()
))
}
fn label_range(label: Operand, scale: &dyn Fn(&str) -> Option<(f64, String)>) -> Operand {
let Operand::Lit(Value::Str(text)) = &label else {
return label;
};
let instant = |t: &str| {
crate::temporal::parse_iso(t).map(|(secs, nanos, offset_min)| Value::Instant {
secs,
nanos,
offset_min,
calendar: Default::default(),
})
};
let Some((a, b)) = text.split_once("..") else {
return match instant(text) {
Some(v) => Operand::Lit(v),
None => label,
};
};
if a.is_empty() || b.is_empty() || b.contains("..") {
return label;
}
let bound = |t: &str| match literal_value(t, false, scale) {
Value::Str(s) => instant(&s).unwrap_or(Value::Str(s)),
v => v,
};
Operand::Range {
from: Box::new(Operand::Lit(bound(a))),
to: Box::new(Operand::Lit(bound(b))),
step: None,
}
}
fn is_bare_projection(o: &Operand) -> bool {
use crate::syntax::ast::{Anchor, Projection};
match o {
Operand::Field { base, .. } => matches!(**base, Operand::Topic),
Operand::Rel {
steps,
projection: Some(p),
anchor: Anchor::Current,
} => steps.is_empty() && !matches!(p, Projection::Property(None)),
_ => false,
}
}
fn fold_typed(op: &mut Operand) {
use crate::syntax::ast::TableRow;
match op {
Operand::Typed { .. } => head_projections(op),
Operand::List(items) => items.iter_mut().for_each(fold_typed),
Operand::Table { rows, .. } => {
for row in rows {
match row {
TableRow::List(items) => items.iter_mut().for_each(fold_typed),
TableRow::Record(items) => items.iter_mut().for_each(|(_, o)| fold_typed(o)),
}
}
}
Operand::Piped { expr, commands } if matches!(**expr, Operand::Topic | Operand::Seed) => {
for command in commands {
if let Command::Func(call) = command {
for a in &mut call.args {
if let Arg::Expr(e) = a {
fold_typed(e);
}
}
}
}
}
_ => {}
}
}