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}