tytanic_filter/eval/
mod.rs

1//! Test set evaluation.
2
3use std::collections::BTreeMap;
4use std::fmt::Debug;
5use std::fmt::Display;
6
7use ecow::EcoVec;
8use thiserror::Error;
9use tytanic_utils::fmt::Separators;
10use tytanic_utils::fmt::Term;
11
12use super::ast::Id;
13
14mod func;
15mod set;
16mod value;
17
18pub use self::func::Func;
19pub use self::set::Set;
20pub use self::value::TryFromValue;
21pub use self::value::Type;
22pub use self::value::Value;
23
24/// A marker trait for tests, this is automatically implemented for all cloneable
25/// static types.
26pub trait Test: Clone + 'static {
27    /// The id of a test, this is used for matching on tests using test sets
28    /// created from pattern literals.
29    fn id(&self) -> &str;
30}
31
32/// A trait for expressions to be evaluated and matched.
33pub trait Eval<T: Test> {
34    /// Evaluates this expression to a value.
35    fn eval(&self, ctx: &Context<T>) -> Result<Value<T>, Error>;
36}
37
38/// An evaluation context used to retrieve bindings in test set expressions.
39#[derive(Debug, Clone)]
40pub struct Context<T> {
41    /// The bindings available for evaluation.
42    bindings: BTreeMap<Id, Value<T>>,
43}
44
45impl<T> Context<T> {
46    /// Create a new evaluation context with no bindings.
47    pub fn new() -> Self {
48        Self {
49            bindings: BTreeMap::new(),
50        }
51    }
52}
53
54impl<T> Context<T> {
55    /// Inserts a new binding, possibly overriding an old one, returns the old
56    /// binding if there was one.
57    pub fn bind<V: Into<Value<T>>>(&mut self, id: Id, value: V) -> Option<Value<T>> {
58        tracing::trace!(id = %id.as_str(), "binding value into eval context");
59        self.bindings.insert(id, value.into())
60    }
61
62    /// Resolves a binding with the given identifier.
63    pub fn resolve<I: AsRef<str>>(&self, id: I) -> Result<Value<T>, Error>
64    where
65        T: Clone,
66    {
67        tracing::trace!(id = %id.as_ref(), "resolving value from eval context");
68        let id = id.as_ref();
69        self.bindings
70            .get(id)
71            .cloned()
72            .ok_or_else(|| Error::UnknownBinding { id: id.into() })
73    }
74
75    /// Find similar bindings to the given identifier.
76    pub fn find_similar(&self, id: &str) -> Vec<Id> {
77        self.bindings
78            .keys()
79            .filter(|cand| strsim::jaro(id, cand.as_str()) > 0.7)
80            .cloned()
81            .collect()
82    }
83}
84
85impl<T> Default for Context<T> {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91/// An error that occurs when a test set expression is evaluated.
92#[derive(Debug, Error)]
93pub enum Error {
94    /// The requested binding could not be found.
95    UnknownBinding {
96        /// The given identifier.
97        id: String,
98    },
99
100    /// A function received an incorrect argument count.
101    InvalidArgumentCount {
102        /// The identifier of the function.
103        func: String,
104
105        /// The minimum or exact expected number of arguments, interpretation
106        /// depends on `is_min`.
107        expected: usize,
108
109        /// Whether the expected number is the minimum and allows more arguments.
110        is_min: bool,
111
112        /// The number of arguments passed.
113        found: usize,
114    },
115
116    /// An invalid type was used in an expression.
117    TypeMismatch {
118        /// The expected types.
119        expected: EcoVec<Type>,
120
121        /// The given type.
122        found: Type,
123    },
124
125    /// A custom error type.
126    Custom(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
127}
128
129impl Display for Error {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            Error::UnknownBinding { id } => write!(f, "unknown binding: {id}"),
133            Error::InvalidArgumentCount {
134                func,
135                expected,
136                is_min,
137                found,
138            } => {
139                let (found, ex) = (*found, *expected);
140
141                if ex == 0 {
142                    write!(
143                        f,
144                        "function {func} expects no {}, got {}",
145                        Term::simple("argument").with(ex),
146                        found,
147                    )?;
148                } else if *is_min {
149                    write!(
150                        f,
151                        "function {func} expects at least {ex} {}, got {}",
152                        Term::simple("argument").with(ex),
153                        found,
154                    )?;
155                } else {
156                    write!(
157                        f,
158                        "function {func} expects exactly {ex} {}, got {}",
159                        Term::simple("argument").with(ex),
160                        found,
161                    )?;
162                }
163
164                Ok(())
165            }
166            Error::TypeMismatch { expected, found } => write!(
167                f,
168                "expected {}, found <{}>",
169                Separators::comma_or().with(expected.iter().map(|t| format!("<{}>", t.name()))),
170                found.name(),
171            ),
172            Error::Custom(err) => write!(f, "{err}"),
173        }
174    }
175}
176
177/// Ensure Context<T> is thread safe if T is.
178#[allow(dead_code)]
179fn assert_traits() {
180    tytanic_utils::assert::send::<Context<()>>();
181    tytanic_utils::assert::sync::<Context<()>>();
182}