tytanic_filter/eval/
mod.rs1use 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
20pub trait Test: Clone + 'static {
23 fn id(&self) -> &str;
26}
27
28pub trait Eval<T: Test> {
30 fn eval(&self, ctx: &Context<T>) -> Result<Value<T>, Error>;
32}
33
34#[derive(Debug, Clone)]
36pub struct Context<T> {
37 bindings: BTreeMap<Id, Value<T>>,
39}
40
41impl<T> Context<T> {
42 pub fn new() -> Self {
44 Self {
45 bindings: BTreeMap::new(),
46 }
47 }
48}
49
50impl<T> Context<T> {
51 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 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 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#[derive(Debug, Error)]
87pub enum Error {
88 UnknownBinding {
90 id: String,
92 },
93
94 InvalidArgumentCount {
96 func: String,
98
99 expected: usize,
102
103 is_min: bool,
105
106 found: usize,
108 },
109
110 TypeMismatch {
112 expected: EcoVec<Type>,
114
115 found: Type,
117 },
118
119 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#[allow(dead_code)]
173fn assert_traits() {
174 tytanic_utils::assert::send::<Context<()>>();
175 tytanic_utils::assert::sync::<Context<()>>();
176}