adam_rs/filter.rs
1//! Input filters: idempotent, per-cell domain constraints.
2//!
3//! A [`Filter`] conforms or rejects a value written externally to its cell, applied
4//! live by [`crate::sheet::Sheet::propagate`] against a source cell's own current
5//! value (see [`crate::sheet::Sheet::add_filter`]), and is separately re-evaluated as
6//! a non-gating diagnostic against a value a relationship's method derives for that
7//! cell (also in [`crate::sheet::Sheet::propagate`]).
8
9use std::any::{Any, TypeId};
10
11use crate::cell::CellId;
12
13/// Type-erased function stored inside a [`FilterData`].
14///
15/// Takes the candidate value and a slice of the filter's argument cells' current
16/// effective values, and returns the conformed value or an error.
17type FilterFn = Box<dyn Fn(&dyn Any, &[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error>>;
18
19/// What shape of validation/derivation a [`Filter`] performs, beyond its opaque function — set by
20/// `adam-lang`'s compile phase when a filter's expression matches a recognized structural form.
21/// `Opaque` carries no extra information; consumers that don't care about structure treat every
22/// kind identically at write/propagate time — `FilterKind` is purely informational, queried by
23/// consumers like `begin`'s UI that want to render a specialized editor without inspecting the
24/// filter's function.
25pub enum FilterKind {
26 /// The filter's expression wasn't a recognized structural form.
27 Opaque,
28 /// Compiled from a `RangeInclusive<T>`-typed expression (`lo..=hi`). `bounds` re-evaluates
29 /// that expression against the filter's current argument values, returning the resulting
30 /// `(lo, hi)` as type-erased values of the filtered cell's own type `T`, or `None` if the
31 /// underlying expression fails to evaluate (e.g. a fallible arithmetic op in a range
32 /// endpoint, such as a `checked_*` operation overflowing).
33 /// The bound evaluator satisfies [`Filter`]'s purity and invocation contract.
34 Range {
35 /// Re-evaluates the range expression, returning `(lo, hi)` as type-erased values, or
36 /// `None` if evaluation fails.
37 ///
38 /// Adam-compiled evaluators also return `None` for missing or extra arguments,
39 /// incorrect scalar identities, or mismatched compound carriers or shapes.
40 /// They validate every argument before binding raw addresses or evaluating,
41 /// including on calls following a successful evaluation or rejection.
42 #[allow(clippy::type_complexity)]
43 bounds: Box<dyn Fn(&[&dyn Any]) -> Option<(Box<dyn Any>, Box<dyn Any>)>>,
44 },
45}
46
47/// An idempotent, per-cell domain constraint with optional dynamic arguments.
48///
49/// Constructed via [`Filter::from_fn_0`]/[`Filter::from_fn_1`]/[`Filter::from_fn_2`] for
50/// the common typed cases, or [`Filter::new`] for the fully type-erased form. Attached
51/// to a cell with [`crate::sheet::Sheet::add_filter`].
52///
53/// Its callbacks must be pure, deterministic functions of their input values and
54/// immutable captured constants, with no externally observable side effects.
55/// Equal inputs produce equivalent outputs or errors. There is no guarantee that
56/// a callback executes, or when or how often it executes. This includes range-bound
57/// evaluators; the conforming callback must also be idempotent.
58pub struct Filter(pub(crate) FilterData);
59
60/// Internal storage for a single filter.
61pub(crate) struct FilterData {
62 /// The `TypeId` of the value this filter operates on, validated against its cell's
63 /// registered type by `add_filter`.
64 pub(crate) value_type: TypeId,
65 /// Dynamic argument cells, resolved via `effective()` wherever the filter runs.
66 pub(crate) args: Vec<CellId>,
67 pub(crate) arg_types: Vec<TypeId>,
68 pub(crate) function: FilterFn,
69 /// What shape of validation/derivation this filter performs, beyond `function` — see
70 /// [`FilterKind`]. Purely informational; never consulted by `write`/`propagate`/`add_filter`.
71 #[allow(dead_code)]
72 pub(crate) kind: FilterKind,
73}
74
75impl Filter {
76 /// Creates a filter from an explicit value `TypeId`, argument `TypeId`s, and a
77 /// type-erased function.
78 ///
79 /// - Precondition: `args.len() == arg_types.len()`.
80 /// - Precondition: `f` returns a value whose runtime type matches `value_type`.
81 /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
82 #[must_use]
83 pub fn new<F>(value_type: TypeId, args: Vec<CellId>, arg_types: Vec<TypeId>, f: F) -> Self
84 where
85 F: Fn(&dyn Any, &[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error> + 'static,
86 {
87 debug_assert_eq!(args.len(), arg_types.len());
88 Filter(FilterData {
89 value_type,
90 args,
91 arg_types,
92 function: Box::new(f),
93 kind: FilterKind::Opaque,
94 })
95 }
96
97 /// Creates a filter with no dynamic arguments from a typed closure.
98 ///
99 /// The `TypeId` for `T` is captured automatically. The filter is validated against
100 /// its cell registration when passed to [`crate::sheet::Sheet::add_filter`].
101 ///
102 /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
103 #[must_use]
104 pub fn from_fn_0<T, F>(f: F) -> Self
105 where
106 T: Any + 'static,
107 F: Fn(&T) -> Result<T, anyhow::Error> + 'static,
108 {
109 Filter::new(TypeId::of::<T>(), vec![], vec![], move |value, _args| {
110 let value = value
111 .downcast_ref::<T>()
112 .expect("type checked at add_filter");
113 Ok(Box::new(f(value)?) as Box<dyn Any>)
114 })
115 }
116
117 /// Creates a filter with one dynamic argument cell from a typed closure.
118 ///
119 /// `TypeId`s for `A` and `T` are captured automatically. The filter is validated
120 /// against its cell registration when passed to [`crate::sheet::Sheet::add_filter`].
121 ///
122 /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
123 #[must_use]
124 pub fn from_fn_1<A, T, F>(arg: CellId, f: F) -> Self
125 where
126 A: Any + 'static,
127 T: Any + 'static,
128 F: Fn(&T, &A) -> Result<T, anyhow::Error> + 'static,
129 {
130 Filter::new(
131 TypeId::of::<T>(),
132 vec![arg],
133 vec![TypeId::of::<A>()],
134 move |value, args| {
135 let value = value
136 .downcast_ref::<T>()
137 .expect("type checked at add_filter");
138 let a = args[0]
139 .downcast_ref::<A>()
140 .expect("type checked at add_filter");
141 Ok(Box::new(f(value, a)?) as Box<dyn Any>)
142 },
143 )
144 }
145
146 /// Creates a filter with two dynamic argument cells from a typed closure.
147 ///
148 /// `args[0]` maps to `A` and `args[1]` maps to `B`. `TypeId`s for `A`, `B`, and `T`
149 /// are captured automatically. The filter is validated when passed to
150 /// [`crate::sheet::Sheet::add_filter`].
151 ///
152 /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
153 #[must_use]
154 pub fn from_fn_2<A, B, T, F>(args: [CellId; 2], f: F) -> Self
155 where
156 A: Any + 'static,
157 B: Any + 'static,
158 T: Any + 'static,
159 F: Fn(&T, &A, &B) -> Result<T, anyhow::Error> + 'static,
160 {
161 Filter::new(
162 TypeId::of::<T>(),
163 args.to_vec(),
164 vec![TypeId::of::<A>(), TypeId::of::<B>()],
165 move |value, cell_args| {
166 let value = value
167 .downcast_ref::<T>()
168 .expect("type checked at add_filter");
169 let a = cell_args[0]
170 .downcast_ref::<A>()
171 .expect("type checked at add_filter");
172 let b = cell_args[1]
173 .downcast_ref::<B>()
174 .expect("type checked at add_filter");
175 Ok(Box::new(f(value, a, b)?) as Box<dyn Any>)
176 },
177 )
178 }
179
180 /// Creates a range-clamp filter from an explicit value `TypeId`, argument `TypeId`s, a clamp
181 /// function, and a `bounds` re-evaluator — the tagged counterpart of what [`Filter::new`]
182 /// builds for [`FilterKind::Opaque`]. `clamp` is `Filter`'s actual per-write/per-propagate
183 /// function (called exactly like an opaque filter's); `bounds` is called independently, with
184 /// no candidate value, by [`crate::sheet::Sheet::filter_range`].
185 ///
186 /// - Precondition: `args.len() == arg_types.len()`.
187 /// - Precondition: `clamp` returns a value whose runtime type matches `value_type`.
188 /// - Precondition: when `bounds` returns `Some`, the pair's values have a runtime type
189 /// matching `value_type`.
190 /// - Precondition: `clamp` and `bounds` satisfy [`Filter`]'s purity contract;
191 /// `clamp` is also idempotent.
192 #[must_use]
193 pub fn range<F, B>(
194 value_type: TypeId,
195 args: Vec<CellId>,
196 arg_types: Vec<TypeId>,
197 clamp: F,
198 bounds: B,
199 ) -> Self
200 where
201 F: Fn(&dyn Any, &[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error> + 'static,
202 B: Fn(&[&dyn Any]) -> Option<(Box<dyn Any>, Box<dyn Any>)> + 'static,
203 {
204 debug_assert_eq!(args.len(), arg_types.len());
205 Filter(FilterData {
206 value_type,
207 args,
208 arg_types,
209 function: Box::new(clamp),
210 kind: FilterKind::Range {
211 bounds: Box::new(bounds),
212 },
213 })
214 }
215}
216
217/// The outcome of re-checking a filter against a value a relationship's method
218/// derived, rather than a value written externally.
219///
220/// See [`crate::sheet::Sheet::filter_violation`].
221#[derive(Debug)]
222pub enum FilterViolation {
223 /// The filter succeeded but its output differs from the cell's current value. Only
224 /// ever recorded for a *derived* cell (`Sheet::propagate`'s post-execution
225 /// diagnostic phase) — a filtered source cell's filter output is unconditionally
226 /// authoritative once computed (see `PlanStep::FilterReclamp`), so this variant
227 /// never applies there.
228 NotConformed,
229 /// The filter's function itself returned an error, or returned a value of a
230 /// different type than the cell — both treated as an equally soft diagnostic (see
231 /// the design spec §4 for why a filter's own `Err` is not a propagation-aborting
232 /// failure the way a `Requirement`'s is). Can occur on either side: a derived
233 /// cell's read-only diagnostic check, or a source cell's live reclamp, in which
234 /// case the cell's stored value is left unchanged.
235 Failed(anyhow::Error),
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use slotmap::SlotMap;
242 use std::any::TypeId;
243
244 use crate::cell::CellId;
245
246 #[test]
247 fn from_fn_0_stores_correct_value_type_and_computes_value() {
248 let filter = Filter::from_fn_0(|x: &i32| Ok(*x * 2));
249 assert_eq!(filter.0.value_type, TypeId::of::<i32>());
250 assert!(filter.0.args.is_empty());
251 let x: i32 = 5;
252 let result = (filter.0.function)(&x, &[]).unwrap();
253 assert_eq!(*result.downcast_ref::<i32>().unwrap(), 10);
254 }
255
256 #[test]
257 fn from_fn_1_stores_correct_type_ids_and_computes_value() {
258 let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
259 let arg = map.insert(());
260
261 let filter = Filter::from_fn_1(arg, |x: &i32, bound: &i32| Ok((*x).min(*bound)));
262 assert_eq!(filter.0.value_type, TypeId::of::<i32>());
263 assert_eq!(filter.0.args, vec![arg]);
264 assert_eq!(filter.0.arg_types, vec![TypeId::of::<i32>()]);
265
266 let x: i32 = 50;
267 let bound: i32 = 10;
268 let result = (filter.0.function)(&x, &[&bound]).unwrap();
269 assert_eq!(*result.downcast_ref::<i32>().unwrap(), 10);
270 }
271
272 #[test]
273 fn from_fn_2_stores_correct_type_ids_and_computes_value() {
274 let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
275 let lo = map.insert(());
276 let hi = map.insert(());
277
278 let filter = Filter::from_fn_2([lo, hi], |x: &i32, lo: &i32, hi: &i32| {
279 Ok((*x).clamp(*lo, *hi))
280 });
281 assert_eq!(filter.0.value_type, TypeId::of::<i32>());
282 assert_eq!(filter.0.args, vec![lo, hi]);
283 assert_eq!(
284 filter.0.arg_types,
285 vec![TypeId::of::<i32>(), TypeId::of::<i32>()]
286 );
287
288 let x: i32 = 500;
289 let lo_v: i32 = 0;
290 let hi_v: i32 = 100;
291 let result = (filter.0.function)(&x, &[&lo_v, &hi_v]).unwrap();
292 assert_eq!(*result.downcast_ref::<i32>().unwrap(), 100);
293 }
294
295 #[test]
296 fn from_fn_0_reports_the_error_a_failing_function_returns() {
297 let filter = Filter::from_fn_0(|_x: &i32| Err(anyhow::anyhow!("cannot conform")));
298 let x: i32 = 1;
299 let err = (filter.0.function)(&x, &[]).unwrap_err();
300 assert_eq!(err.to_string(), "cannot conform");
301 }
302
303 #[test]
304 fn new_stores_explicit_value_type_and_arg_types() {
305 let filter = Filter::new(TypeId::of::<i32>(), vec![], vec![], |value, _args| {
306 let v = value.downcast_ref::<i32>().unwrap();
307 Ok(Box::new(*v) as Box<dyn std::any::Any>)
308 });
309 assert_eq!(filter.0.value_type, TypeId::of::<i32>());
310 }
311
312 #[test]
313 fn new_defaults_to_opaque_kind() {
314 let filter = Filter::new(TypeId::of::<i32>(), vec![], vec![], |value, _args| {
315 Ok(Box::new(*value.downcast_ref::<i32>().unwrap()) as Box<dyn Any>)
316 });
317 assert!(matches!(filter.0.kind, FilterKind::Opaque));
318 }
319
320 #[test]
321 fn range_stores_range_kind_and_clamps_via_function() {
322 let filter = Filter::range(
323 TypeId::of::<i32>(),
324 vec![],
325 vec![],
326 |value: &dyn Any, _args: &[&dyn Any]| {
327 let v = *value.downcast_ref::<i32>().unwrap();
328 Ok(Box::new(v.clamp(0, 100)) as Box<dyn Any>)
329 },
330 |_args: &[&dyn Any]| {
331 Some((
332 Box::new(0i32) as Box<dyn Any>,
333 Box::new(100i32) as Box<dyn Any>,
334 ))
335 },
336 );
337 assert_eq!(filter.0.value_type, TypeId::of::<i32>());
338 let x: i32 = 500;
339 let result = (filter.0.function)(&x, &[]).unwrap();
340 assert_eq!(*result.downcast_ref::<i32>().unwrap(), 100);
341 let FilterKind::Range { bounds } = &filter.0.kind else {
342 panic!("expected FilterKind::Range");
343 };
344 let (lo, hi) = bounds(&[]).unwrap();
345 assert_eq!(*lo.downcast_ref::<i32>().unwrap(), 0);
346 assert_eq!(*hi.downcast_ref::<i32>().unwrap(), 100);
347 }
348}