Skip to main content

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}