use crate::Query;
use crate::ast::{Expr, GeometricExpr};
use molframe_core::diagnostic::{Code, Diagnostic};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, Default)]
pub struct QueryAliases {
definitions: BTreeMap<Box<str>, Query>,
}
impl QueryAliases {
pub const MAX_DEPTH: usize = 64;
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_valid_name(name: &str) -> bool {
crate::parser::is_query_name(name)
}
pub fn define(
&mut self,
name: impl Into<Box<str>>,
query: Query,
) -> Result<Option<Query>, Diagnostic> {
let name = name.into();
if !Self::is_valid_name(&name) {
return Err(Diagnostic::new(Code::E4001)
.with_message("a query name is a letter or underscore followed by letters, digits or underscores")
.with_context("name", name));
}
Ok(self.definitions.insert(name, query))
}
pub fn remove(&mut self, name: &str) -> Option<Query> {
self.definitions.remove(name)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Query> {
self.definitions.get(name)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.definitions.contains_key(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.definitions.keys().map(AsRef::as_ref)
}
#[must_use]
pub fn len(&self) -> usize {
self.definitions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.definitions.is_empty()
}
pub fn resolve(&self, query: &Query) -> Result<Query, Vec<Diagnostic>> {
if !has_reference(&query.expr) {
return Ok(query.clone());
}
let mut resolver = Resolver {
aliases: self,
resolved: BTreeMap::new(),
stack: Vec::new(),
warnings: query.warnings.clone(),
};
let expr = resolver
.substitute(&query.expr)
.map_err(|finding| vec![finding])?;
let mut warnings = resolver.warnings;
warnings.sort_by_key(Diagnostic::code);
let source = crate::print::print(&expr).into_boxed_str();
Ok(Query {
expr,
warnings,
source,
})
}
}
impl Query {
#[must_use]
pub fn references(&self) -> Vec<&str> {
let mut names = BTreeSet::new();
collect_references(&self.expr, &mut names);
names.into_iter().collect()
}
}
struct Resolver<'a> {
aliases: &'a QueryAliases,
resolved: BTreeMap<&'a str, Expr>,
stack: Vec<&'a str>,
warnings: Vec<Diagnostic>,
}
impl Resolver<'_> {
fn substitute(&mut self, expr: &Expr) -> Result<Expr, Diagnostic> {
Ok(match expr {
Expr::Group(name) => self.definition(name)?,
Expr::And(left, right) => Expr::And(
Box::new(self.substitute(left)?),
Box::new(self.substitute(right)?),
),
Expr::Or(left, right) => Expr::Or(
Box::new(self.substitute(left)?),
Box::new(self.substitute(right)?),
),
Expr::Not(value) => Expr::Not(Box::new(self.substitute(value)?)),
Expr::Global(value) => Expr::Global(Box::new(self.substitute(value)?)),
Expr::ByResidue(value) => Expr::ByResidue(Box::new(self.substitute(value)?)),
Expr::Same { key, target } => Expr::Same {
key: key.clone(),
target: Box::new(self.substitute(target)?),
},
Expr::Bonded { depth, target } => Expr::Bonded {
depth: *depth,
target: Box::new(self.substitute(target)?),
},
Expr::Geometric(geometric) => Expr::Geometric(self.geometric(geometric)?),
leaf => leaf.clone(),
})
}
fn geometric(&mut self, expr: &GeometricExpr) -> Result<GeometricExpr, Diagnostic> {
let mut resolved = expr.clone();
if let Some(target) = geometric_target_mut(&mut resolved) {
**target = self.substitute(target)?;
}
Ok(resolved)
}
fn definition(&mut self, name: &str) -> Result<Expr, Diagnostic> {
let Some((key, query)) = self.aliases.definitions.get_key_value(name) else {
return Err(Diagnostic::new(Code::E4005).with_context("name", name));
};
if let Some(expr) = self.resolved.get(key.as_ref()) {
return Ok(expr.clone());
}
if let Some(start) = self.stack.iter().position(|entry| *entry == name) {
let mut cycle = self.stack[start..].join(" -> ");
cycle.push_str(" -> ");
cycle.push_str(name);
return Err(Diagnostic::new(Code::E4006).with_context("cycle", cycle));
}
if self.stack.len() >= QueryAliases::MAX_DEPTH {
return Err(Diagnostic::new(Code::E4007).with_context("name", name));
}
self.stack.push(key);
let expr = self.substitute(&query.expr);
let _ = self.stack.pop();
let expr = expr?;
self.warnings.extend(query.warnings.iter().cloned());
let _ = self.resolved.insert(key, expr.clone());
Ok(expr)
}
}
fn geometric_target_mut(expr: &mut GeometricExpr) -> Option<&mut Box<Expr>> {
match expr {
GeometricExpr::Within { target, .. }
| GeometricExpr::Beyond { target, .. }
| GeometricExpr::Around { target, .. }
| GeometricExpr::SphereZone { target, .. }
| GeometricExpr::SphereLayer { target, .. }
| GeometricExpr::IsoLayer { target, .. }
| GeometricExpr::CylinderZone { target, .. }
| GeometricExpr::CylinderLayer { target, .. } => Some(target),
GeometricExpr::Point { .. } => None,
}
}
fn geometric_target(expr: &GeometricExpr) -> Option<&Expr> {
match expr {
GeometricExpr::Within { target, .. }
| GeometricExpr::Beyond { target, .. }
| GeometricExpr::Around { target, .. }
| GeometricExpr::SphereZone { target, .. }
| GeometricExpr::SphereLayer { target, .. }
| GeometricExpr::IsoLayer { target, .. }
| GeometricExpr::CylinderZone { target, .. }
| GeometricExpr::CylinderLayer { target, .. } => Some(target),
GeometricExpr::Point { .. } => None,
}
}
fn children<'e>(expr: &'e Expr, visit: &mut impl FnMut(&'e Expr)) {
match expr {
Expr::And(left, right) | Expr::Or(left, right) => {
visit(left);
visit(right);
}
Expr::Not(value) | Expr::Global(value) | Expr::ByResidue(value) => visit(value),
Expr::Same { target, .. } | Expr::Bonded { target, .. } => visit(target),
Expr::Geometric(geometric) => {
if let Some(target) = geometric_target(geometric) {
visit(target);
}
}
Expr::All
| Expr::None
| Expr::Comparison { .. }
| Expr::Membership { .. }
| Expr::Group(_)
| Expr::Atom { .. }
| Expr::Macro(_)
| Expr::Chirality(_)
| Expr::Smarts { .. } => {}
}
}
fn has_reference(expr: &Expr) -> bool {
if matches!(expr, Expr::Group(_)) {
return true;
}
let mut found = false;
children(expr, &mut |child| found = found || has_reference(child));
found
}
fn collect_references<'e>(expr: &'e Expr, names: &mut BTreeSet<&'e str>) {
if let Expr::Group(name) = expr {
let _ = names.insert(name);
return;
}
children(expr, &mut |child| collect_references(child, names));
}
#[cfg(test)]
#[path = "aliases_tests.rs"]
mod tests;