Skip to main content

formally_support/
scope.rs

1//
2// ::formally - the open-source formal methods toolchain
3//
4// Copyright (c) 2025 Nicola Gigante
5//
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to deal
8// in the Software without restriction, including without limitation the rights
9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10// copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12//
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15//
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22// SOFTWARE.
23//
24
25use crate::*;
26
27use perfect_derive::perfect_derive;
28
29use std::{hash::Hash, sync::Arc};
30
31use itertools::Itertools;
32
33/// Result of a lookup by name in a [Scope].
34///
35/// Elements represented by a [LookupSet] can be filtered and transformed using the
36/// [filter()](LookupSet::filter), [map()](LookupSet::map), [filter_map()](LookupSet::filter_map),
37/// and [filter_map_ok()](LookupSet::filter_map_ok) functions, which work similarly to
38/// their counterpart on iterators. `LookupSet<T, V>` represents elements coming from a `Scope<T>`
39/// and transformed (by [map()](LookupSet::map) or [filter_map()](LookupSet::filter_map)) into
40/// elements of type `V`.
41///
42/// The difference from a standard iterator is that [LookupSet] remembers enough information to
43/// allow the [one()](LookupSet::one) method to emit detailed error diagnostics. See
44/// [one()](LookupSet::one) for details.
45pub struct LookupSet<'s, 'i, T: Hash + Eq, V = &'s T> {
46    name: Identifier<'i>,
47    scope: &'s Scope<T>,
48    iterator: Box<dyn 's + Iterator<Item = &'s T>>,
49    filter_map: Box<dyn 's + Fn(&'s T) -> Result<Option<V>>>,
50    empty: bool,
51}
52
53impl<'s, 'i, T: 's + Hash + Eq, V: 's> LookupSet<'s, 'i, T, V> {
54    fn new(
55        name: Identifier<'i>,
56        scope: &'s Scope<T>,
57        entities: impl 's + IntoIterator<Item = &'s T>,
58        filter_map: impl 's + Fn(&'s T) -> Result<Option<V>>,
59    ) -> Self {
60        let mut iterator = entities.into_iter().peekable();
61        let empty = iterator.peek().is_none();
62        LookupSet {
63            name,
64            scope,
65            iterator: Box::new(iterator),
66            filter_map: Box::new(filter_map),
67            empty,
68        }
69    }
70
71    /// Get the [Identifier] the [LookupSet] was looked up for.
72    pub fn name(&self) -> &Identifier<'_> {
73        &self.name
74    }
75
76    /// Tell if the [LookupSet] is empty.
77    pub fn is_empty(&self) -> bool {
78        self.empty
79    }
80
81    /// Get a [LookupSet] of only the elements of `self` transformed by the function `f` for only
82    /// the elements `v` where `f(v)` returns `Ok(Some(_))`. If `f` returns `Err(_)`, the whole
83    /// [LookupSet] becomes erroneous and any call to `all()` or `one()` will return the same
84    /// `Err(_)`.
85    pub fn filter_map_ok<R: 's>(
86        self,
87        f: impl 's + Fn(V) -> Result<Option<R>>,
88    ) -> LookupSet<'s, 'i, T, R> {
89        let filter_map = self.filter_map;
90        LookupSet::new(
91            self.name,
92            self.scope,
93            self.iterator,
94            move |t| match filter_map(t) {
95                Ok(Some(v)) => f(v),
96                Ok(None) => Ok(None),
97                Err(err) => Err(err),
98            },
99        )
100    }
101
102    /// Get a [LookupSet] of the elements of `self` transformed by the function `f` for only
103    /// the elements `v` where `f(v)` returns `Some(_)`.
104    pub fn filter_map<R: 's>(self, f: impl 's + Fn(V) -> Option<R>) -> LookupSet<'s, 'i, T, R> {
105        self.filter_map_ok(move |v| Ok(f(v)))
106    }
107
108    /// Get a [LookupSet] of only the elements of `self` that satisfy the predicate `f`.
109    pub fn filter(self, f: impl 's + Fn(&V) -> bool) -> Self {
110        self.filter_map(move |v| if f(&v) { Some(v) } else { None })
111    }
112
113    /// Get a [LookupSet] of only the elements `v` of `self` such that `f(v)` returns `Ok(true)`.
114    /// If `Ok(false)` is returned, the element is discarded. If `Err(_)` is returned, the whole
115    /// [LookupSet] becomes erroneous and any call to `all()` or `one()` will return the same
116    /// `Err(_)`.
117    pub fn filter_ok(self, f: impl 's + Fn(&V) -> Result<bool>) -> Self {
118        self.filter_map_ok(move |v| if f(&v)? { Ok(Some(v)) } else { Ok(None) })
119    }
120
121    /// Get a [LookupSet] of the elements of `self` transformed by the function `f`
122    pub fn map<R: 's>(self, f: impl 's + Fn(V) -> R) -> LookupSet<'s, 'i, T, R> {
123        self.filter_map(move |v| Some(f(v)))
124    }
125
126    /// Get an iterator of all the elements in the [LookupSet].
127    pub fn all(self) -> Result<Vec<V>> {
128        self.iterator
129            .map(move |t| (self.filter_map)(t))
130            .process_results(|c| c.flatten().collect())
131    }
132
133    /// If the current [LookupSet] is not empty, return it as-is, otherwise call the `other` closure
134    /// and return its result.
135    pub fn or_else(self, other: impl 's + FnOnce() -> LookupSet<'s, 'i, T, V>) -> Self {
136        let mut this = self.iterator.peekable();
137        if this.peek().is_some() {
138            LookupSet::new(self.name, self.scope, this, self.filter_map)
139        } else {
140            other()
141        }
142    }
143}
144
145impl<'s, 'i, T: 's + Clone + Hash + Eq + Located, V: 's> LookupSet<'s, 'i, T, V> {
146    /// Return the single element in this [LookupSet] if only one exist, otherwise produce detailed
147    /// error diagnostics.
148    ///
149    /// The [one()](LookupSet::one) method assumes its usage is within name lookup for a typical
150    /// statically scoped language (this includes quantifiers or other similar constructs in most
151    /// logics). By calling this method one asserts that only a single result of the previous call
152    /// to [lookup()](Scope::lookup) is required. If this is the case, the single result is
153    /// returned. Otherwise, error diagnostics are produced.
154    ///
155    /// The diagnostics are produced after checking further conditions. If there were more than one
156    /// element the lookup is treated as ambiguous (as if you asked for the name of an overloaded
157    /// function in some C++-like language without specifying its argument types) and a list of
158    /// matching symbols is produced as attached notes. If the [LookupSet] is empty the lookup is
159    /// treated as a reference to an undefined symbol, but if it was
160    /// filtered (see [filter()](LookupSet::filter)), then a list of non-matching symbols is
161    /// produced as attached notes.
162    pub fn one(self) -> Result<V> {
163        let mut all = Vec::new();
164        let mut filtered = Vec::new();
165
166        for t in self.iterator {
167            if let Some(v) = (self.filter_map)(t)? {
168                filtered.push((t, v));
169            }
170            all.push(t);
171        }
172
173        let mut filtered = filtered.into_iter();
174        let all = all.into_iter();
175
176        match (filtered.next(), filtered.next()) {
177            (Some((_, v)), None) => Ok(v),
178            (Some((t1, _)), Some((t2, _))) => {
179                error!(
180                    self.name.span(),
181                    "ambiguous reference to symbol '{}'", self.name
182                );
183                note!(t1.span(), "matching symbol declared here");
184                note!(t2.span(), "matching symbol declared here");
185                for (t, _) in filtered {
186                    note!(t.span(), "matching symbol declared here");
187                }
188                Err(DiagnosticEmitted)
189            }
190            (None, _) => {
191                let mut all = all.peekable();
192                match all.peek() {
193                    Some(_) => {
194                        error!(self.name.span(), "no matching symbol '{}'", self.name);
195
196                        for element in all {
197                            if element.span().is_some() {
198                                note!(element.span(), "non-matching symbol declared here");
199                            }
200                        }
201                        Err(DiagnosticEmitted)
202                    }
203                    None => {
204                        error!(
205                            self.name.span(),
206                            "undefined reference to symbol '{}'", self.name
207                        );
208                        Err(DiagnosticEmitted)
209                    }
210                }
211            }
212        }
213    }
214}
215
216/// Utility to implement lookup scopes.
217///
218/// [Scope] implements the logic needed to support name lookup in a local scope, similar to what may
219/// many need in simple programming languages or in logical formalisms. It supports looking up by
220/// name elements of any given type and implements [Stack] to support incremental interfaces, e.g.,
221/// in SAT or SMT solvers.
222///
223/// A call to [lookup()](Scope::lookup) method produces an instance of [LookupSet] which can be used
224/// to further filter and navigate the objects found by name. [LookupSet] in turn is in charge of
225/// emitting detailed error diagnostics when a lookup fails, helping to standardize such diagnostics
226/// across all `formally` and its clients.
227///
228/// Example:
229/// ```rust
230/// # mod formally {
231/// #    pub extern crate formally_support as support;
232/// #    pub extern crate formally_smt as smt;
233/// # }
234/// use formally::{support::*, smt::*};
235///
236/// # fn main() -> Result<()> {
237/// let mut scope = Scope::new();
238/// scope.push();
239/// scope.add("x", Loc::new(42));
240///
241/// println!("x: {}", scope.lookup(Identifier::from("x")).one()?);
242///
243/// scope.pop();
244/// assert!(scope.lookup(Identifier::from("x")).all()?.is_empty());
245/// # Ok(())
246/// # }
247/// ```
248///
249/// Here the [LookupSet::one()] method requires a single result to exist and emits detailed
250/// diagnostics if this is not the case. See its documentation for details.
251///
252/// The element type needs to implement [Hash] and [Eq]. These are used to ensure that if the same
253/// element is added twice under the same name, it is treated as a single entry.
254///
255/// [Scope] objects support being *nested* under other [Scope] objects. Nesting simulates how
256/// one would implement a nested scope in a typical language, such as the scope under a function
257/// definition that sees its arguments locally but the elements declared outside globally. Elements
258/// defined in the local [Scope] shadow the ones defined in the parent, but if no element is defined
259/// at all with a given name, lookup is delegated to the parent.
260///
261/// Example:
262/// ```rust
263/// # mod formally {
264/// #    pub extern crate formally_support as support;
265/// #    pub extern crate formally_smt as smt;
266/// # }
267/// use formally::{support::*, smt::*};
268///
269/// # fn main() -> Result<()> {
270/// let mut parent = Scope::new();
271/// parent.add("x", term!(p));
272/// parent.add("x", term!(q));
273/// parent.add("y", term!(and p q));
274///
275/// let mut scope = Scope::new().with_parent(parent);
276///
277/// scope.add("x", term!(=> p q));
278///
279/// scope.lookup("x").one()?;
280/// scope.lookup("y").one()?;
281/// # Ok(())
282/// # }
283/// ```
284///
285/// Note that the first call to [one()](LookupSet::one) succeeds here because the only element
286/// declared in `scope` with name `"x"` shadows all the elements with the same name in `parent`. The
287/// second one succeeds as well because `"y"` is found in the parent.
288#[perfect_derive(Default, Clone)]
289pub struct Scope<T: Hash + Eq> {
290    elements: Stacked<rpds::HashTrieMapSync<String, rpds::HashTrieSetSync<T>>>,
291    parent: Option<Arc<Scope<T>>>,
292}
293
294impl<T: Hash + Eq> Scope<T> {
295    /// Construct an empty [Scope]
296    pub fn new() -> Self {
297        Scope::default()
298    }
299
300    /// Construct a new [Scope] similar to `self` but nested under a different parent.
301    pub fn with_parent(self, parent: Scope<T>) -> Scope<T> {
302        Scope {
303            parent: Some(Arc::new(parent)),
304            ..self
305        }
306    }
307
308    /// Set a new parent to the current [Scope].
309    pub fn set_parent(&mut self, parent: Scope<T>) {
310        self.parent = Some(Arc::new(parent));
311    }
312
313    /// Add an element to the [Scope] under the given name.
314    pub fn add(&mut self, name: &str, element: T) {
315        if !self.elements.contains_key(name) {
316            self.elements
317                .insert_mut(name.to_string(), rpds::HashTrieSetSync::new_sync());
318        }
319        self.elements.get_mut(name).unwrap().insert_mut(element);
320    }
321
322    pub fn lookup<'i>(&self, name: impl Into<Identifier<'i>>) -> LookupSet<'_, 'i, T, &'_ T> {
323        let name = name.into();
324        if let Some(elements) = self.elements.get(name.name()) {
325            LookupSet::new(name, self, elements.iter(), |t| Ok(Some(t)))
326        } else if let Some(parent) = &self.parent {
327            parent.lookup(name)
328        } else {
329            LookupSet::new(name, self, std::iter::empty(), |t| Ok(Some(t)))
330        }
331    }
332}
333
334impl<T: Clone + Hash + Eq> Scope<T> {
335    /// Clone all the elements of the given scope into `self`, registering them under the same
336    /// names.
337    pub fn merge(&mut self, other: &Scope<T>) {
338        for (name, vec) in &*other.elements {
339            for element in vec {
340                self.add(name.as_str(), element.clone());
341            }
342        }
343    }
344}
345
346impl<T: Hash + Eq> Stack for Scope<T> {
347    fn push(&mut self) -> Result<()> {
348        self.elements.push()
349    }
350
351    fn pop_n(&mut self, n: usize) -> Result<()> {
352        self.elements.pop_n(n)
353    }
354}