tytanic_filter/eval/
mod.rs

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