use crate::ast::{
Arg, ArithOp, Axis, Branch, CmpOp, Group, InterpSeg, Matcher, Operand, PathElem, PredExpr,
Predicate, Projection, PushBody, Query, Reach, RegRef, Stage, Step,
};
use crate::value::Value;
pub fn unparse(query: &Query) -> String {
let mut out = String::new();
for corr in &query.correlations {
out.push_str(&unparse(corr));
out.push_str(if corr.outer { " <=>? " } else { " <=> " });
}
out.push_str(&query_body(query));
out
}
fn query_body(q: &Query) -> String {
let mut out = q
.branches
.iter()
.map(branch)
.collect::<Vec<_>>()
.join(" || ");
for stage_ast in &q.pipeline {
out.push(' ');
out.push_str(&stage(stage_ast));
}
out
}
fn branch(b: &Branch) -> String {
let mut out = String::new();
if b.anchored {
out.push('^');
}
if let Some(m) = &b.mark {
out.push_str(&format!("({m})"));
}
for e in &b.steps {
out.push_str(&elem(e));
}
if let Some(p) = &b.projection {
out.push_str(&projection(p));
}
out
}
fn elem(e: &PathElem) -> String {
match e {
PathElem::Mark(name) => format!(" .{name} "),
PathElem::Step(s) => step(s),
PathElem::Group(g) => group(g),
PathElem::Push { name, body } => {
let lead = if name.is_some() { " ." } else { "." };
let n = name.as_deref().unwrap_or("");
match body {
PushBody::Query(q) => format!("{lead}{n}({})", unparse(q)),
PushBody::Expr(e) => format!("{lead}{n}({})", operand(e)),
}
}
}
}
fn group(g: &Group) -> String {
let alts: Vec<String> = g
.alts
.iter()
.map(|alt| alt.iter().map(elem).collect())
.collect();
let quant = match (g.quant.min, g.quant.max) {
(1, Some(1)) => String::new(),
(1, None) => "+".to_string(),
(0, None) => "*".to_string(),
(m, Some(n)) if m == n => format!("{{{m}}}"),
(m, Some(n)) => format!("{{{m},{n}}}"),
(m, None) => format!("{{{m},}}"),
};
let preds: String = g.predicates.iter().map(predicate).collect();
format!(
"({}){}{}{}",
alts.join("|"),
quant,
preds,
reach_mark(&g.reach)
)
}
fn step(s: &Step) -> String {
let mut out = String::new();
match &s.axis {
Axis::Child => out.push('/'),
Axis::Descendant(Reach::All) => out.push_str("//"),
Axis::Descendant(Reach::Proximal) => out.push_str("//?"),
Axis::Descendant(Reach::Distal) => out.push_str("//!"),
Axis::Parent => out.push('\\'),
Axis::Ancestor(Reach::All) => out.push_str("\\\\"),
Axis::Ancestor(Reach::Proximal) => out.push_str("\\\\?"),
Axis::Ancestor(Reach::Distal) => out.push_str("\\\\!"),
Axis::NextSibling => out.push('>'),
Axis::PrevSibling => out.push('<'),
Axis::FollowingSiblings(r) => out.push_str(&format!(">>{}", reach_mark(r))),
Axis::PrecedingSiblings(r) => out.push_str(&format!("<<{}", reach_mark(r))),
Axis::OutLink => out.push_str("->"),
Axis::InLink => out.push_str("<-"),
Axis::Resolve { property, hint } => {
out.push_str("::");
out.push_str(&name_text(property));
out.push_str("~>");
if let Some(h) = hint {
out.push_str(&name_text(h));
}
return out + &step_suffix(s);
}
Axis::ReverseResolve { property, hint } => {
out.push_str("::");
out.push_str(&name_text(property));
out.push_str("<~");
if let Some(h) = hint {
out.push_str(&name_text(h));
}
return out + &step_suffix(s);
}
}
out.push_str(&matcher(&s.matcher));
out + &step_suffix(s)
}
fn reach_mark(r: &Reach) -> &'static str {
match r {
Reach::All => "",
Reach::Proximal => "?",
Reach::Distal => "!",
}
}
fn step_suffix(s: &Step) -> String {
let mut out = String::new();
if !s.traits.is_empty() {
let many = s.traits.len() > 1;
let clauses: Vec<String> = s
.traits
.iter()
.map(|t| {
let body = t.alts.join(" || ");
if many && t.alts.len() > 1 {
format!("({body})")
} else {
body
}
})
.collect();
out.push('<');
out.push_str(&clauses.join(" && "));
out.push('>');
}
for p in &s.predicates {
out.push_str(&predicate(p));
}
if s.leaf {
out.push('$');
}
out
}
fn matcher(m: &Matcher) -> String {
match m {
Matcher::Name(n) => name_text(n),
Matcher::Glob(g) => g.glob().glob().to_string(),
Matcher::Regex(r) => format!("~({})", r.as_str()),
Matcher::Any => "*".to_string(),
Matcher::Dot => ".".to_string(),
}
}
fn name_text(n: &str) -> String {
let bare = !n.is_empty()
&& n.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '.' | '-' | '_' | '+'))
&& !n.starts_with('.');
if bare {
n.to_string()
} else {
format!("'{n}'")
}
}
fn proj_name(k: &str) -> String {
if matches!(k, "and" | "or" | "not") {
format!("'{k}'")
} else {
name_text(k)
}
}
fn projection(p: &Projection) -> String {
match p {
Projection::Property(None) => "::".to_string(),
Projection::Property(Some(k)) => format!("::{}", proj_name(k)),
Projection::CoreMeta(k) => format!(":::{}", proj_name(k)),
Projection::AdapterMeta(k) => format!("::;{}", proj_name(k)),
}
}
fn predicate(p: &Predicate) -> String {
match p {
Predicate::Index(n) => format!("[{n}]"),
Predicate::Range(from, to) => {
let f = from.map(|v| v.to_string()).unwrap_or_default();
let t = to.map(|v| v.to_string()).unwrap_or_default();
format!("[{f}..{t}]")
}
Predicate::Expr(e) => format!("[{}]", pred_expr(e)),
}
}
fn pred_expr(e: &PredExpr) -> String {
match e {
PredExpr::Or(a, b) => format!("{} || {}", pred_term(a), pred_term(b)),
PredExpr::And(a, b) => format!("{} && {}", pred_term(a), pred_term(b)),
PredExpr::Not(a) => format!("!{}", pred_term(a)),
PredExpr::Compare(l, op, r) => {
format!("{} {} {}", operand(l), cmp(*op), operand(r))
}
PredExpr::Truthy(o) => operand(o),
}
}
fn pred_term(e: &PredExpr) -> String {
match e {
PredExpr::Or(..) | PredExpr::And(..) => {
format!("({})", pred_expr(e))
}
_ => pred_expr(e),
}
}
fn cmp(op: CmpOp) -> &'static str {
match op {
CmpOp::Eq => "=",
CmpOp::Ne => "!=",
CmpOp::Lt => "<",
CmpOp::Le => "<=",
CmpOp::Gt => ">",
CmpOp::Ge => ">=",
CmpOp::Match => "=~",
CmpOp::NotMatch => "!~",
CmpOp::Contains => "*=",
}
}
pub(crate) fn operand_text(o: &Operand) -> String {
operand(o)
}
fn operand(o: &Operand) -> String {
match o {
Operand::Match {
scrutinee,
arms,
other,
} => {
let mut out = format!("({} ?=", operand(scrutinee));
for (test, regex, result) in arms {
if *regex {
let Operand::Lit(pat) = test else {
unreachable!("regex arms hold their pattern literal");
};
out.push_str(&format!(" ~({pat})"));
} else {
out.push_str(&format!(" {}", operand(test)));
}
out.push_str(&format!(" ? {} :", operand(result)));
}
out.push_str(&format!(" {})", operand(other)));
out
}
Operand::Rel {
steps,
projection: p,
anchored,
mark,
} => {
let mut out = String::new();
if *anchored {
out.push('^');
}
if let Some(m) = mark {
out.push_str(&format!("({m})"));
}
out.extend(steps.iter().map(elem));
if let Some(p) = p {
out.push_str(&projection(p));
}
out
}
Operand::Lit(v) => literal(v),
Operand::Arith { op, left, right } => {
format!("{} {} {}", arith_term(left), arith(*op), arith_term(right))
}
Operand::Neg(inner) => format!("- {}", arith_term(inner)),
Operand::Group(e) => format!("({})", pred_expr(e)),
Operand::Recall(r) => reg(r),
Operand::Topic => "$_".to_string(),
Operand::Now => "now()".to_string(),
Operand::Edge { projection: p } => match p {
Some(p) => format!("$-{}", projection(p)),
None => "$-".to_string(),
},
Operand::Edges { projection: p } => match p {
Some(p) => format!("@-{}", projection(p)),
None => "@-".to_string(),
},
Operand::Capsae { projection: p } => match p {
Some(p) => format!("@*{}", projection(p)),
None => "@*".to_string(),
},
Operand::Piped { expr, stages } => {
let tail: Vec<String> = stages.iter().map(stage).collect();
format!("({} {})", operand(expr), tail.join(" "))
}
Operand::Cond { cond, then, other } => format!(
"({} ? {} : {})",
pred_expr(cond),
operand(then),
operand(other)
),
Operand::Ordinal => "$ord".to_string(),
Operand::Param(name) => format!("${name}"),
Operand::Capture(n) => format!("${n}"),
Operand::Outer(inner) => format!("${}", operand(inner)),
Operand::Interp(segs) => {
let mut out = String::from("\"");
for seg in segs {
match seg {
InterpSeg::Text(t) => {
for c in t.chars() {
if matches!(c, '"' | '\\' | '$') {
out.push('\\');
}
out.push(c);
}
}
InterpSeg::Expr(e) => {
out.push_str("${");
out.push_str(&operand(e));
out.push('}');
}
}
}
out.push('"');
out
}
Operand::Ctx {
index,
steps,
projection: p,
} => {
let mut out = match index {
Some(k) => format!("$*{k}"),
None => "$*".to_string(),
};
for e in steps {
out.push_str(&elem(e));
}
if let Some(p) = p {
out.push_str(&projection(p));
}
out
}
}
}
fn arith_term(o: &Operand) -> String {
match o {
Operand::Arith { .. } => format!("({})", operand(o)),
_ => operand(o),
}
}
fn arith(op: ArithOp) -> &'static str {
match op {
ArithOp::Add => "+",
ArithOp::Sub => "-",
ArithOp::Mul => "*",
ArithOp::Div => "div",
ArithOp::IDiv => "idiv",
ArithOp::Mod => "mod",
}
}
fn literal(v: &Value) -> String {
match v {
Value::Str(s) => {
if s.contains('\'') {
let mut out = String::from("\"");
for c in s.chars() {
if matches!(c, '"' | '\\' | '$') {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
} else {
format!("'{s}'")
}
}
Value::Null => "null".to_string(),
other => other.to_string(),
}
}
fn stage(st: &Stage) -> String {
match st {
Stage::Func(call)
if call.name == "s"
&& call.args.len() == 3
&& call.args.iter().all(|a| matches!(a, Arg::Lit(_))) =>
{
let lit = |a: &Arg| match a {
Arg::Lit(v) => v.to_string(),
_ => unreachable!("matched literals"),
};
format!(
"| s/{}/{}/{}",
lit(&call.args[0]).replace('/', "\\/"),
lit(&call.args[1]).replace('/', "\\/"),
lit(&call.args[2])
)
}
Stage::Func(call)
if call.name == "sh"
&& call.args.len() == 1
&& matches!(
call.args[0],
Arg::Lit(Value::Str(_)) | Arg::Expr(Operand::Interp(_))
) =>
{
let mut out = String::from("| `");
let escape = |out: &mut String, t: &str| {
for c in t.chars() {
if matches!(c, '`' | '\\' | '$') {
out.push('\\');
}
out.push(c);
}
};
match &call.args[0] {
Arg::Lit(Value::Str(t)) => escape(&mut out, t),
Arg::Expr(Operand::Interp(segs)) => {
for seg in segs {
match seg {
InterpSeg::Text(t) => escape(&mut out, t),
InterpSeg::Expr(e) => {
out.push_str("${");
out.push_str(&operand(e));
out.push('}');
}
}
}
}
_ => unreachable!("guarded above"),
}
out.push('`');
out
}
Stage::Func(call) => format!("| {}", fn_call(call)),
Stage::Agg(call) => format!("@| {}", fn_call(call)),
Stage::Spread { outer: false } => "| ...".to_string(),
Stage::Spread { outer: true } => "| ...?".to_string(),
Stage::Map(inner) => {
let body = stage(inner);
let body = body
.strip_prefix("@| ")
.or_else(|| body.strip_prefix("| "))
.unwrap_or(&body)
.to_string();
format!("$| {body}")
}
Stage::Push(None) => "| .".to_string(),
Stage::Push(Some(n)) => format!("| .{n}"),
Stage::Subcontext { name, body } => match name {
Some(n) => format!("| .{n}({})", unparse(body)),
None => format!("| .({})", unparse(body)),
},
Stage::Expr(e) => format!("| {}", operand(e)),
Stage::ExprPush { name, expr } => match name {
Some(n) => format!("| .{n}({})", operand(expr)),
None => format!("| .({})", operand(expr)),
},
Stage::Select(p) => format!("@| {}", predicate(p)),
Stage::Filter(e) => format!("| [{}]", pred_expr(e)),
Stage::Recall(r) => format!("| {}", reg(r)),
}
}
fn fn_call(call: &crate::ast::FnCall) -> String {
if call.args.is_empty() {
return call.name.clone();
}
let args: Vec<String> = call
.args
.iter()
.map(|a| match a {
Arg::Lit(v) => literal(v),
Arg::Expr(e) => operand(e),
Arg::Range(a, b) => format!(
"{}..{}",
a.map(|n| n.to_string()).unwrap_or_default(),
b.map(|n| n.to_string()).unwrap_or_default()
),
})
.collect();
format!("{}({})", call.name, args.join(", "))
}
fn reg(r: &RegRef) -> String {
match r {
RegRef::Top => "$.".to_string(),
RegRef::Index(n) => format!("$.{n}"),
RegRef::Named(n) => format!("$.{n}"),
RegRef::Whole => "@.".to_string(),
RegRef::Record => "%.".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::unparse;
fn rt(q: &str) -> String {
let toks = crate::lexer::lex(q).expect("lex");
let ast = crate::parser::parse(&toks).expect("parse");
unparse(&ast)
}
fn assert_fixpoint(q: &str) -> String {
let once = rt(q);
assert_eq!(rt(&once), once, "unparse is not a fixpoint for {q:?}");
once
}
#[test]
fn projection_keyword_key_is_quoted() {
assert_eq!(assert_fixpoint("/x::'and'"), "/x::'and'");
assert_eq!(assert_fixpoint("/x::'or'"), "/x::'or'");
assert_eq!(assert_fixpoint("/x:::'not'"), "/x:::'not'");
assert_eq!(assert_fixpoint("/x::;'and'"), "/x::;'and'");
assert_eq!(assert_fixpoint("/x::android"), "/x::android");
}
#[test]
fn double_quote_fallback_escapes() {
assert_eq!(
assert_fixpoint("/x[::msg = \"it's \\\"fine\\\"\"]"),
"/x[::msg = \"it's \\\"fine\\\"\"]"
);
assert_eq!(
assert_fixpoint("/x[::msg = \"don't pay \\${fee}\"]"),
"/x[::msg = \"don't pay \\${fee}\"]"
);
assert_eq!(
assert_fixpoint("/x[::msg = \"it's a\\\\b\"]"),
"/x[::msg = \"it's a\\\\b\"]"
);
}
#[test]
fn substitution_reescapes_slash() {
assert_eq!(assert_fixpoint("/x | s/a\\/b/x/"), "/x | s/a\\/b/x/");
assert_eq!(assert_fixpoint("/x | s/a/c\\/d/"), "/x | s/a/c\\/d/");
assert_eq!(assert_fixpoint("/x | s/a/b/g"), "/x | s/a/b/g");
}
}