use std::fmt;
use std::sync::Arc;
use crate::ast::generated::visit::Visit;
use crate::ast::precedence::{Assoc, BindingPower};
use crate::ast::render::{
DynExt, Render, RenderConfig, RenderCtx, RenderExt as _, RenderMode, render_extension_infix,
render_extension_prefix,
};
use crate::ast::{Expr, SelectItem, SetExpr, Span, Spanned, Statement};
use crate::error::ParseResult;
use crate::parser::{Dialect, Parsed, Parser, parse_with};
use crate::tokenizer::{Operator, Token, TokenKind};
const MATCH_BP: BindingPower = BindingPower {
left: 64,
right: 65,
assoc: Assoc::Left,
};
const NEG_PREFIX_BP: u8 = 82;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct DynMatch {
left: Box<Expr<DynExt>>,
right: Box<Expr<DynExt>>,
span: Span,
}
impl Spanned for DynMatch {
fn span(&self) -> Span {
self.span
}
}
impl Render for DynMatch {
fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
render_extension_infix(ctx, f, MATCH_BP, (&self.left, &self.right), |f| {
f.write_str(" ^ ")
})
}
fn operand_binding_power(&self) -> Option<BindingPower> {
Some(MATCH_BP)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct DynNeg {
operand: Box<Expr<DynExt>>,
span: Span,
}
impl Spanned for DynNeg {
fn span(&self) -> Span {
self.span
}
}
impl Render for DynNeg {
fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
render_extension_prefix(ctx, f, NEG_PREFIX_BP, |f| f.write_str("~"), &self.operand)
}
fn operand_binding_power(&self) -> Option<BindingPower> {
Some(BindingPower {
left: NEG_PREFIX_BP,
right: NEG_PREFIX_BP,
assoc: Assoc::Right,
})
}
}
#[derive(Clone, Copy)]
struct DynDialect;
impl Dialect for DynDialect {
type Ext = DynExt;
fn features(&self) -> &crate::ast::dialect::FeatureSet {
&crate::ast::dialect::FeatureSet::ANSI
}
fn peek_infix_operator_hook<'a>(
parser: &mut Parser<'a, Self>,
) -> ParseResult<Option<BindingPower>> {
match parser.peek()? {
Some(token) if token.kind == TokenKind::Operator(Operator::Caret) => Ok(Some(MATCH_BP)),
_ => Ok(None),
}
}
fn build_infix_operator<'a>(
parser: &mut Parser<'a, Self>,
op: Token,
left: Expr<DynExt>,
right: Expr<DynExt>,
) -> ParseResult<Expr<DynExt>> {
debug_assert_eq!(op.kind, TokenKind::Operator(Operator::Caret));
let span = left.span().union(right.span());
let ext = DynExt::new(DynMatch {
left: Box::new(left),
right: Box::new(right),
span,
});
let meta = parser.make_meta(span);
Ok(Expr::Other { ext, meta })
}
fn peek_prefix_operator_hook<'a>(parser: &mut Parser<'a, Self>) -> ParseResult<Option<u8>> {
match parser.peek()? {
Some(token) if token.kind == TokenKind::Operator(Operator::Tilde) => {
Ok(Some(NEG_PREFIX_BP))
}
_ => Ok(None),
}
}
fn build_prefix_operator<'a>(
parser: &mut Parser<'a, Self>,
op: Token,
operand: Expr<DynExt>,
) -> ParseResult<Expr<DynExt>> {
debug_assert_eq!(op.kind, TokenKind::Operator(Operator::Tilde));
let span = op.span.union(operand.span());
let ext = DynExt::new(DynNeg {
operand: Box::new(operand),
span,
});
let meta = parser.make_meta(span);
Ok(Expr::Other { ext, meta })
}
}
fn projection_expr(parsed: &Parsed<Arc<str>, DynExt>) -> &Expr<DynExt> {
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let SelectItem::Expr { expr, .. } = &select.projection[0] else {
panic!("expected a bare projection expression");
};
expr
}
fn canonical(src: &str) -> String {
parse_with(src, crate::ParseConfig::new(DynDialect))
.expect("DynDialect parses the test expression")
.to_string()
}
#[test]
fn dynamic_dialect_parses_and_renders_with_no_parser_changes() {
assert_eq!(canonical("SELECT ~ a ^ b"), "SELECT ~a ^ b");
assert_eq!(canonical("SELECT a ^ b + c"), "SELECT a ^ b + c");
assert_eq!(canonical("SELECT (a + b) ^ c"), "SELECT (a + b) ^ c");
assert_eq!(canonical("SELECT a ^ (b ^ c)"), "SELECT a ^ (b ^ c)");
}
#[test]
fn dynamic_custom_operator_trees_round_trip() {
for src in [
"SELECT ~ a ^ b",
"SELECT a ^ b + c",
"SELECT a ^ (b ^ c)",
"SELECT (a + b) ^ c",
"SELECT ~ (a ^ b)",
] {
let once = canonical(src);
let twice = canonical(&once);
assert_eq!(once, twice, "canonical render of `{src}` is not a fixpoint");
}
}
#[test]
fn parenthesized_mode_wraps_erased_operators_like_built_ins() {
let parsed = parse_with("SELECT ~ a ^ b", crate::ParseConfig::new(DynDialect)).expect("parses");
let config = RenderConfig {
mode: RenderMode::Parenthesized,
..RenderConfig::default()
};
let ctx = RenderCtx::new(parsed.resolver(), parsed.source(), &config);
assert_eq!(
parsed.statements()[0].displayed(&ctx).to_string(),
"SELECT ((~a) ^ b)",
);
}
#[test]
fn visitor_downcasts_heterogeneous_erased_nodes() {
#[derive(Default)]
struct Count {
matches: usize,
negs: usize,
}
impl<'ast> Visit<'ast, DynExt> for Count {
fn visit_extension(&mut self, node: &'ast DynExt) {
if let Some(m) = node.downcast_ref::<DynMatch>() {
self.matches += 1;
self.visit_expr(&m.left);
self.visit_expr(&m.right);
} else if let Some(n) = node.downcast_ref::<DynNeg>() {
self.negs += 1;
self.visit_expr(&n.operand);
}
}
}
let parsed = parse_with("SELECT ~ a ^ b", crate::ParseConfig::new(DynDialect)).expect("parses");
let mut count = Count::default();
count.visit_expr(projection_expr(&parsed));
assert_eq!(count.matches, 1, "one infix `^` (DynMatch)");
assert_eq!(count.negs, 1, "one prefix `~` (DynNeg)");
}