Skip to main content

molframe_query/api/
aliases.rs

1//! Named query definitions and their typed, closed resolution.
2//!
3//! A named query is a declarative definition — `pocket` means "residues within
4//! five ångströms of the heme" — not the atoms that definition matched on some
5//! earlier structure. Keeping the definition is what lets a later redefinition
6//! of `heme` flow into `pocket` without anyone re-running `pocket` by hand, and
7//! what lets one definition be evaluated against many structures.
8//!
9//! A query refers to a definition by name, written `$name` (or the longer
10//! `group name`). Resolution replaces each reference with the referenced
11//! definition's typed plan, recursively, producing a *closed* query that no
12//! longer mentions any name. It works on the typed plan directly: nothing is
13//! parsed again and no atom is touched, and the closed query's canonical text
14//! is printed from the substituted plan rather than spliced from strings.
15//!
16//! Resolution is deterministic (definitions live in an ordered map), rejects an
17//! unknown name, a cycle, or a chain of references deeper than
18//! [`QueryAliases::MAX_DEPTH`], and costs `O(E)` in the size of the resolved
19//! plan: each definition is resolved at most once per call and reused for
20//! every further reference to it.
21//!
22//! A runtime [`crate::Groups`] map is a different thing — atoms already
23//! selected by someone else, supplied at evaluation time. A query whose names
24//! are meant to be runtime groups is evaluated as it is, without resolution.
25
26use crate::Query;
27use crate::ast::{Expr, GeometricExpr};
28use molframe_core::diagnostic::{Code, Diagnostic};
29use std::collections::{BTreeMap, BTreeSet};
30
31/// An ordered set of named query definitions.
32///
33/// # Examples
34///
35/// ```
36/// use molframe_query::{Query, QueryAliases};
37///
38/// let compile = |source| match Query::compile(source) {
39///     Ok(query) => query,
40///     Err(findings) => panic!("{findings:?}"),
41/// };
42/// let mut aliases = QueryAliases::new();
43/// assert!(aliases.define("heme", compile("resname HEM")).is_ok());
44/// let pocket = compile("byres (within 5 of $heme) and protein");
45/// let Ok(closed) = aliases.resolve(&pocket) else {
46///     panic!("every name is defined");
47/// };
48/// assert!(closed.references().is_empty());
49/// assert_eq!(
50///     closed.fingerprint(),
51///     compile("byres (within 5 of (resname HEM)) and protein").fingerprint(),
52/// );
53/// ```
54#[derive(Clone, Debug, Default)]
55pub struct QueryAliases {
56    definitions: BTreeMap<Box<str>, Query>,
57}
58
59impl QueryAliases {
60    /// The deepest chain of references resolution follows before refusing.
61    ///
62    /// Interactive definitions nest a handful of levels deep; the bound exists
63    /// so that a pathological or adversarial chain cannot exhaust the stack.
64    pub const MAX_DEPTH: usize = 64;
65
66    /// An empty set of definitions.
67    #[must_use]
68    pub fn new() -> Self {
69        Self::default()
70    }
71
72    /// Whether `name` is spelled as a definable name.
73    ///
74    /// A name starts with an ASCII letter or underscore and continues with
75    /// ASCII letters, digits and underscores, which is exactly what may follow
76    /// `$` in a query.
77    #[must_use]
78    pub fn is_valid_name(name: &str) -> bool {
79        crate::parser::is_query_name(name)
80    }
81
82    /// Defines or redefines `name`, returning the definition it replaced.
83    ///
84    /// The definition is stored as written: its own references are resolved
85    /// when a query that uses it is resolved, so redefining a name it depends on
86    /// changes what it means.
87    ///
88    /// # Errors
89    ///
90    /// Returns a syntax diagnostic when `name` is not a valid name.
91    pub fn define(
92        &mut self,
93        name: impl Into<Box<str>>,
94        query: Query,
95    ) -> Result<Option<Query>, Diagnostic> {
96        let name = name.into();
97        if !Self::is_valid_name(&name) {
98            return Err(Diagnostic::new(Code::E4001)
99                .with_message("a query name is a letter or underscore followed by letters, digits or underscores")
100                .with_context("name", name));
101        }
102        Ok(self.definitions.insert(name, query))
103    }
104
105    /// Removes `name`, returning its definition.
106    pub fn remove(&mut self, name: &str) -> Option<Query> {
107        self.definitions.remove(name)
108    }
109
110    /// The definition of `name`, as written.
111    #[must_use]
112    pub fn get(&self, name: &str) -> Option<&Query> {
113        self.definitions.get(name)
114    }
115
116    /// Whether `name` is defined.
117    #[must_use]
118    pub fn contains(&self, name: &str) -> bool {
119        self.definitions.contains_key(name)
120    }
121
122    /// Defined names in ascending order.
123    pub fn names(&self) -> impl Iterator<Item = &str> {
124        self.definitions.keys().map(AsRef::as_ref)
125    }
126
127    /// Number of definitions.
128    #[must_use]
129    pub fn len(&self) -> usize {
130        self.definitions.len()
131    }
132
133    /// Whether nothing is defined.
134    #[must_use]
135    pub fn is_empty(&self) -> bool {
136        self.definitions.is_empty()
137    }
138
139    /// Replaces every named reference in `query` with its definition.
140    ///
141    /// The result mentions no name, so it can be evaluated, fingerprinted or
142    /// sent elsewhere without these definitions. A query with no references is
143    /// returned unchanged, source text included.
144    ///
145    /// # Errors
146    ///
147    /// Returns `E4005` for a name with no definition, `E4006` for definitions
148    /// that refer to each other in a cycle, and `E4007` when references nest
149    /// deeper than [`Self::MAX_DEPTH`].
150    pub fn resolve(&self, query: &Query) -> Result<Query, Vec<Diagnostic>> {
151        if !has_reference(&query.expr) {
152            return Ok(query.clone());
153        }
154        let mut resolver = Resolver {
155            aliases: self,
156            resolved: BTreeMap::new(),
157            stack: Vec::new(),
158            warnings: query.warnings.clone(),
159        };
160        let expr = resolver
161            .substitute(&query.expr)
162            .map_err(|finding| vec![finding])?;
163        let mut warnings = resolver.warnings;
164        warnings.sort_by_key(Diagnostic::code);
165        let source = crate::print::print(&expr).into_boxed_str();
166        Ok(Query {
167            expr,
168            warnings,
169            source,
170        })
171    }
172}
173
174impl Query {
175    /// Names this query refers to, in ascending order and without repeats.
176    ///
177    /// These are the definitions the query depends on directly; a definition's
178    /// own references are its own dependencies. Runtime is `O(E)`.
179    #[must_use]
180    pub fn references(&self) -> Vec<&str> {
181        let mut names = BTreeSet::new();
182        collect_references(&self.expr, &mut names);
183        names.into_iter().collect()
184    }
185}
186
187struct Resolver<'a> {
188    aliases: &'a QueryAliases,
189    /// Each definition's closed plan, resolved at most once per call.
190    resolved: BTreeMap<&'a str, Expr>,
191    /// The names being resolved, outermost first, for cycle reporting.
192    stack: Vec<&'a str>,
193    warnings: Vec<Diagnostic>,
194}
195
196impl Resolver<'_> {
197    fn substitute(&mut self, expr: &Expr) -> Result<Expr, Diagnostic> {
198        Ok(match expr {
199            Expr::Group(name) => self.definition(name)?,
200            Expr::And(left, right) => Expr::And(
201                Box::new(self.substitute(left)?),
202                Box::new(self.substitute(right)?),
203            ),
204            Expr::Or(left, right) => Expr::Or(
205                Box::new(self.substitute(left)?),
206                Box::new(self.substitute(right)?),
207            ),
208            Expr::Not(value) => Expr::Not(Box::new(self.substitute(value)?)),
209            Expr::Global(value) => Expr::Global(Box::new(self.substitute(value)?)),
210            Expr::ByResidue(value) => Expr::ByResidue(Box::new(self.substitute(value)?)),
211            Expr::Same { key, target } => Expr::Same {
212                key: key.clone(),
213                target: Box::new(self.substitute(target)?),
214            },
215            Expr::Bonded { depth, target } => Expr::Bonded {
216                depth: *depth,
217                target: Box::new(self.substitute(target)?),
218            },
219            Expr::Geometric(geometric) => Expr::Geometric(self.geometric(geometric)?),
220            leaf => leaf.clone(),
221        })
222    }
223
224    fn geometric(&mut self, expr: &GeometricExpr) -> Result<GeometricExpr, Diagnostic> {
225        let mut resolved = expr.clone();
226        if let Some(target) = geometric_target_mut(&mut resolved) {
227            **target = self.substitute(target)?;
228        }
229        Ok(resolved)
230    }
231
232    fn definition(&mut self, name: &str) -> Result<Expr, Diagnostic> {
233        let Some((key, query)) = self.aliases.definitions.get_key_value(name) else {
234            return Err(Diagnostic::new(Code::E4005).with_context("name", name));
235        };
236        if let Some(expr) = self.resolved.get(key.as_ref()) {
237            return Ok(expr.clone());
238        }
239        if let Some(start) = self.stack.iter().position(|entry| *entry == name) {
240            let mut cycle = self.stack[start..].join(" -> ");
241            cycle.push_str(" -> ");
242            cycle.push_str(name);
243            return Err(Diagnostic::new(Code::E4006).with_context("cycle", cycle));
244        }
245        if self.stack.len() >= QueryAliases::MAX_DEPTH {
246            return Err(Diagnostic::new(Code::E4007).with_context("name", name));
247        }
248        self.stack.push(key);
249        let expr = self.substitute(&query.expr);
250        let _ = self.stack.pop();
251        let expr = expr?;
252        self.warnings.extend(query.warnings.iter().cloned());
253        let _ = self.resolved.insert(key, expr.clone());
254        Ok(expr)
255    }
256}
257
258fn geometric_target_mut(expr: &mut GeometricExpr) -> Option<&mut Box<Expr>> {
259    match expr {
260        GeometricExpr::Within { target, .. }
261        | GeometricExpr::Beyond { target, .. }
262        | GeometricExpr::Around { target, .. }
263        | GeometricExpr::SphereZone { target, .. }
264        | GeometricExpr::SphereLayer { target, .. }
265        | GeometricExpr::IsoLayer { target, .. }
266        | GeometricExpr::CylinderZone { target, .. }
267        | GeometricExpr::CylinderLayer { target, .. } => Some(target),
268        GeometricExpr::Point { .. } => None,
269    }
270}
271
272fn geometric_target(expr: &GeometricExpr) -> Option<&Expr> {
273    match expr {
274        GeometricExpr::Within { target, .. }
275        | GeometricExpr::Beyond { target, .. }
276        | GeometricExpr::Around { target, .. }
277        | GeometricExpr::SphereZone { target, .. }
278        | GeometricExpr::SphereLayer { target, .. }
279        | GeometricExpr::IsoLayer { target, .. }
280        | GeometricExpr::CylinderZone { target, .. }
281        | GeometricExpr::CylinderLayer { target, .. } => Some(target),
282        GeometricExpr::Point { .. } => None,
283    }
284}
285
286/// Calls `visit` on each direct sub-expression of `expr`.
287fn children<'e>(expr: &'e Expr, visit: &mut impl FnMut(&'e Expr)) {
288    match expr {
289        Expr::And(left, right) | Expr::Or(left, right) => {
290            visit(left);
291            visit(right);
292        }
293        Expr::Not(value) | Expr::Global(value) | Expr::ByResidue(value) => visit(value),
294        Expr::Same { target, .. } | Expr::Bonded { target, .. } => visit(target),
295        Expr::Geometric(geometric) => {
296            if let Some(target) = geometric_target(geometric) {
297                visit(target);
298            }
299        }
300        Expr::All
301        | Expr::None
302        | Expr::Comparison { .. }
303        | Expr::Membership { .. }
304        | Expr::Group(_)
305        | Expr::Atom { .. }
306        | Expr::Macro(_)
307        | Expr::Chirality(_)
308        | Expr::Smarts { .. } => {}
309    }
310}
311
312fn has_reference(expr: &Expr) -> bool {
313    if matches!(expr, Expr::Group(_)) {
314        return true;
315    }
316    let mut found = false;
317    children(expr, &mut |child| found = found || has_reference(child));
318    found
319}
320
321fn collect_references<'e>(expr: &'e Expr, names: &mut BTreeSet<&'e str>) {
322    if let Expr::Group(name) = expr {
323        let _ = names.insert(name);
324        return;
325    }
326    children(expr, &mut |child| collect_references(child, names));
327}
328
329#[cfg(test)]
330#[path = "aliases_tests.rs"]
331mod tests;