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//!
9//! ```
10//! use adam_rs::{Filter, Sheet};
11//! use std::any::TypeId;
12//! let mut sheet = Sheet::new();
13//! let bound = sheet.add_source(10_i32);
14//! let low = sheet.add_source(0_i32);
15//! let raw = sheet.add_cell(5_i32);
16//! let zero = sheet.add_cell(-5_i32);
17//! let one = sheet.add_cell(20_i32);
18//! let two = sheet.add_cell(30_i32);
19//! sheet.add_filter(raw, Filter::new(TypeId::of::<i32>(), vec![], vec![],
20//!     |v, _| Ok(Box::new(*v.downcast_ref::<i32>().unwrap()))))?;
21//! sheet.add_filter(zero, Filter::from_fn_0(|v: &i32| Ok((*v).max(0))))?;
22//! sheet.add_filter(one, Filter::from_fn_1(bound, |v: &i32, hi: &i32| Ok((*v).min(*hi))))?;
23//! sheet.add_filter(two, Filter::from_fn_2([low, bound],
24//!     |v: &i32, lo: &i32, hi: &i32| Ok((*v).clamp(*lo, *hi))))?;
25//! sheet.propagate()?;
26//! assert_eq!(*sheet.read::<i32>(raw)?, 5);
27//! assert_eq!(*sheet.read::<i32>(zero)?, 0);
28//! assert_eq!(*sheet.read::<i32>(one)?, 10);
29//! assert_eq!(*sheet.read::<i32>(two)?, 10);
30//! assert_eq!(*sheet.source::<i32>(two)?, 30);
31//! # Ok::<(), adam_rs::Error>(())
32//! ```
33
34use std::any::{Any, TypeId};
35
36use crate::cell::CellId;
37
38/// Type-erased function stored inside a [`FilterData`].
39///
40/// Takes the candidate value and a slice of the filter's argument cells' current
41/// effective values, and returns the conformed value or an error.
42type FilterFn = Box<dyn Fn(&dyn Any, &[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error>>;
43
44/// What shape of validation/derivation a [`Filter`] performs, beyond its opaque function — set by
45/// `adam-lang`'s compile phase when a filter's expression matches a recognized structural form.
46/// `Opaque` carries no extra information; consumers that don't care about structure treat every
47/// kind identically at write/propagate time — `FilterKind` is purely informational, queried by
48/// consumers like `begin`'s UI that want to render a specialized editor without inspecting the
49/// filter's function.
50///
51/// # Examples
52/// See the live range filter in the [graph example](crate::sheet).
53pub enum FilterKind {
54    /// The filter's expression wasn't a recognized structural form.
55    Opaque,
56    /// Compiled from a `RangeInclusive<T>`-typed expression (`lo..=hi`). `bounds` re-evaluates
57    /// that expression against the filter's current argument values, returning the resulting
58    /// `(lo, hi)` as type-erased values of the filtered cell's own type `T`, or `None` if the
59    /// underlying expression fails to evaluate (e.g. a fallible arithmetic op in a range
60    /// endpoint, such as a `checked_*` operation overflowing).
61    /// The bound evaluator satisfies [`Filter`]'s purity and invocation contract.
62    Range {
63        /// Re-evaluates the range expression, returning `(lo, hi)` as type-erased values, or
64        /// `None` if evaluation fails.
65        ///
66        /// Adam-compiled evaluators also return `None` for missing or extra arguments,
67        /// incorrect scalar identities, or mismatched compound carriers or shapes.
68        /// They validate every argument before binding raw addresses or evaluating,
69        /// including on calls following a successful evaluation or rejection.
70        #[allow(clippy::type_complexity)]
71        bounds: Box<dyn Fn(&[&dyn Any]) -> Option<(Box<dyn Any>, Box<dyn Any>)>>,
72    },
73}
74
75/// An idempotent, per-cell domain constraint with optional dynamic arguments.
76///
77/// Constructed via [`Filter::from_fn_0`]/[`Filter::from_fn_1`]/[`Filter::from_fn_2`] for
78/// the common typed cases, or [`Filter::new`] for the fully type-erased form. Attached
79/// to a cell with [`crate::sheet::Sheet::add_filter`].
80///
81/// Its callbacks must be pure, deterministic functions of their input values and
82/// immutable captured constants, with no externally observable side effects.
83/// Equal inputs produce equivalent outputs or errors. There is no guarantee that
84/// a callback executes, or when or how often it executes. This includes range-bound
85/// evaluators; the conforming callback must also be idempotent.
86/// Holding dynamic arguments fixed, conforming the returned value again produces
87/// an equivalent value.
88///
89/// # Examples
90/// See the [filter example](crate::filter).
91pub struct Filter(pub(crate) FilterData);
92
93/// Internal storage for a single filter.
94///
95/// Argument identities and declared types have equal lengths. `value_type` declares
96/// the expected callback result type. Attachment validates argument identities and
97/// types; propagation validates successful results and records a soft failure for
98/// a result with a different runtime type.
99pub(crate) struct FilterData {
100    /// The `TypeId` of the value this filter operates on, validated against its cell's
101    /// registered type by `add_filter`.
102    pub(crate) value_type: TypeId,
103    /// Dynamic argument cells, resolved via `effective()` wherever the filter runs.
104    pub(crate) args: Vec<CellId>,
105    pub(crate) arg_types: Vec<TypeId>,
106    pub(crate) function: FilterFn,
107    /// What shape of validation/derivation this filter performs, beyond `function` — see
108    /// [`FilterKind`]. Purely informational; never consulted by `write`/`propagate`/`add_filter`.
109    #[allow(dead_code)]
110    pub(crate) kind: FilterKind,
111}
112
113impl Filter {
114    /// Creates a filter from an explicit value `TypeId`, argument `TypeId`s, and a
115    /// type-erased function.
116    ///
117    /// - Precondition: `args.len() == arg_types.len()`.
118    /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
119    ///
120    /// Propagation records a soft [`FilterViolation::Failed`] when a successful
121    /// callback result has a different runtime type from `value_type`.
122    ///
123    /// # Examples
124    /// See the [filter example](crate::filter).
125    #[must_use]
126    pub fn new<F>(value_type: TypeId, args: Vec<CellId>, arg_types: Vec<TypeId>, f: F) -> Self
127    where
128        F: Fn(&dyn Any, &[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error> + 'static,
129    {
130        debug_assert_eq!(args.len(), arg_types.len());
131        Filter(FilterData {
132            value_type,
133            args,
134            arg_types,
135            function: Box::new(f),
136            kind: FilterKind::Opaque,
137        })
138    }
139
140    /// Creates a filter with no dynamic arguments from a typed closure.
141    ///
142    /// The `TypeId` for `T` is captured automatically. The filter is validated against
143    /// its cell registration when passed to [`crate::sheet::Sheet::add_filter`].
144    ///
145    /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
146    ///
147    /// # Examples
148    /// See the [filter example](crate::filter).
149    #[must_use]
150    pub fn from_fn_0<T, F>(f: F) -> Self
151    where
152        T: Any + 'static,
153        F: Fn(&T) -> Result<T, anyhow::Error> + 'static,
154    {
155        Filter::new(TypeId::of::<T>(), vec![], vec![], move |value, _args| {
156            let value = value
157                .downcast_ref::<T>()
158                .expect("type checked at add_filter");
159            Ok(Box::new(f(value)?) as Box<dyn Any>)
160        })
161    }
162
163    /// Creates a filter with one dynamic argument cell from a typed closure.
164    ///
165    /// `TypeId`s for `A` and `T` are captured automatically. The filter is validated
166    /// against its cell registration when passed to [`crate::sheet::Sheet::add_filter`].
167    ///
168    /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
169    ///
170    /// # Examples
171    /// See the [filter example](crate::filter).
172    #[must_use]
173    pub fn from_fn_1<A, T, F>(arg: CellId, f: F) -> Self
174    where
175        A: Any + 'static,
176        T: Any + 'static,
177        F: Fn(&T, &A) -> Result<T, anyhow::Error> + 'static,
178    {
179        Filter::new(
180            TypeId::of::<T>(),
181            vec![arg],
182            vec![TypeId::of::<A>()],
183            move |value, args| {
184                let value = value
185                    .downcast_ref::<T>()
186                    .expect("type checked at add_filter");
187                let a = args[0]
188                    .downcast_ref::<A>()
189                    .expect("type checked at add_filter");
190                Ok(Box::new(f(value, a)?) as Box<dyn Any>)
191            },
192        )
193    }
194
195    /// Creates a filter with two dynamic argument cells from a typed closure.
196    ///
197    /// `args[0]` maps to `A` and `args[1]` maps to `B`. `TypeId`s for `A`, `B`, and `T`
198    /// are captured automatically. The filter is validated when passed to
199    /// [`crate::sheet::Sheet::add_filter`].
200    ///
201    /// - Precondition: `f` satisfies [`Filter`]'s purity and idempotence contract.
202    ///
203    /// # Examples
204    /// See the [filter example](crate::filter).
205    #[must_use]
206    pub fn from_fn_2<A, B, T, F>(args: [CellId; 2], f: F) -> Self
207    where
208        A: Any + 'static,
209        B: Any + 'static,
210        T: Any + 'static,
211        F: Fn(&T, &A, &B) -> Result<T, anyhow::Error> + 'static,
212    {
213        Filter::new(
214            TypeId::of::<T>(),
215            args.to_vec(),
216            vec![TypeId::of::<A>(), TypeId::of::<B>()],
217            move |value, cell_args| {
218                let value = value
219                    .downcast_ref::<T>()
220                    .expect("type checked at add_filter");
221                let a = cell_args[0]
222                    .downcast_ref::<A>()
223                    .expect("type checked at add_filter");
224                let b = cell_args[1]
225                    .downcast_ref::<B>()
226                    .expect("type checked at add_filter");
227                Ok(Box::new(f(value, a, b)?) as Box<dyn Any>)
228            },
229        )
230    }
231
232    /// Creates a range-clamp filter from an explicit value `TypeId`, argument `TypeId`s, a clamp
233    /// function, and a `bounds` re-evaluator — the tagged counterpart of what [`Filter::new`]
234    /// builds for [`FilterKind::Opaque`]. `clamp` is `Filter`'s actual per-write/per-propagate
235    /// function (called exactly like an opaque filter's); `bounds` is called independently, with
236    /// no candidate value, by [`crate::sheet::Sheet::filter_range`].
237    ///
238    /// - Precondition: `args.len() == arg_types.len()`.
239    /// - Precondition: `clamp` returns a value whose runtime type matches `value_type`.
240    /// - Precondition: when `bounds` returns `Some`, the pair's values have a runtime type
241    ///   matching `value_type`.
242    /// - Precondition: `clamp` and `bounds` satisfy [`Filter`]'s purity contract;
243    ///   `clamp` is also idempotent.
244    ///
245    /// # Examples
246    /// See the live range filter in the [graph example](crate::sheet).
247    #[must_use]
248    pub fn range<F, B>(
249        value_type: TypeId,
250        args: Vec<CellId>,
251        arg_types: Vec<TypeId>,
252        clamp: F,
253        bounds: B,
254    ) -> Self
255    where
256        F: Fn(&dyn Any, &[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error> + 'static,
257        B: Fn(&[&dyn Any]) -> Option<(Box<dyn Any>, Box<dyn Any>)> + 'static,
258    {
259        debug_assert_eq!(args.len(), arg_types.len());
260        Filter(FilterData {
261            value_type,
262            args,
263            arg_types,
264            function: Box::new(clamp),
265            kind: FilterKind::Range {
266                bounds: Box::new(bounds),
267            },
268        })
269    }
270}
271
272/// The outcome of re-checking a filter against a value a relationship's method
273/// derived, rather than a value written externally.
274///
275/// See [`crate::sheet::Sheet::filter_violation`].
276///
277/// # Examples
278/// ```
279/// use adam_rs::{Filter, FilterViolation, Method, Sheet};
280/// let mut sheet = Sheet::new();
281/// let input = sheet.add_source(20_i32);
282/// let output = sheet.add_cell(0_i32);
283/// sheet.add_filter(output, Filter::from_fn_0(|v: &i32| Ok((*v).min(10))))?;
284/// sheet.add_relationship(vec![Method::from_fn_1_1(input, output, |v: &i32| Ok(*v))])?;
285/// sheet.propagate()?;
286/// assert!(matches!(sheet.filter_violation(output), Some(FilterViolation::NotConformed)));
287/// assert_eq!(*sheet.read::<i32>(output)?, 20);
288/// # Ok::<(), adam_rs::Error>(())
289/// ```
290#[derive(Debug)]
291pub enum FilterViolation {
292    /// The filter succeeded but its output differs from the cell's current value. Only
293    /// ever recorded for a *derived* cell (`Sheet::propagate`'s post-execution
294    /// diagnostic phase) — a filtered source cell's filter output is unconditionally
295    /// authoritative once computed (see `PlanStep::FilterReclamp`), so this variant
296    /// never applies there.
297    NotConformed,
298    /// The filter's function itself returned an error, or returned a value of a
299    /// different type than the cell — both treated as an equally soft diagnostic (see
300    /// the design spec §4 for why a filter's own `Err` is not a propagation-aborting
301    /// failure the way a `Requirement`'s is). Can occur on either side: a derived
302    /// cell's read-only diagnostic check, or a source cell's live reclamp, in which
303    /// case the cell's stored value is left unchanged.
304    Failed(anyhow::Error),
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use slotmap::SlotMap;
311    use std::any::TypeId;
312
313    use crate::cell::CellId;
314
315    #[test]
316    /// Checks a static conformer's declared type, result and idempotence.
317    fn from_fn_0_stores_correct_value_type_and_computes_value() {
318        let filter = Filter::from_fn_0(|x: &i32| Ok((*x).max(10)));
319        assert_eq!(filter.0.value_type, TypeId::of::<i32>());
320        assert!(filter.0.args.is_empty());
321        let x: i32 = 5;
322        let result = (filter.0.function)(&x, &[]).unwrap();
323        assert_eq!(*result.downcast_ref::<i32>().unwrap(), 10);
324        let repeated = (filter.0.function)(result.as_ref(), &[]).unwrap();
325        assert_eq!(repeated.downcast_ref::<i32>(), result.downcast_ref::<i32>());
326    }
327
328    #[test]
329    /// Checks unary dynamic-filter types and its conformed value.
330    fn from_fn_1_stores_correct_type_ids_and_computes_value() {
331        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
332        let arg = map.insert(());
333
334        let filter = Filter::from_fn_1(arg, |x: &i32, bound: &i32| Ok((*x).min(*bound)));
335        assert_eq!(filter.0.value_type, TypeId::of::<i32>());
336        assert_eq!(filter.0.args, vec![arg]);
337        assert_eq!(filter.0.arg_types, vec![TypeId::of::<i32>()]);
338
339        let x: i32 = 50;
340        let bound: i32 = 10;
341        let result = (filter.0.function)(&x, &[&bound]).unwrap();
342        assert_eq!(*result.downcast_ref::<i32>().unwrap(), 10);
343    }
344
345    #[test]
346    /// Checks binary dynamic-filter types and its conformed value.
347    fn from_fn_2_stores_correct_type_ids_and_computes_value() {
348        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
349        let lo = map.insert(());
350        let hi = map.insert(());
351
352        let filter = Filter::from_fn_2([lo, hi], |x: &i32, lo: &i32, hi: &i32| {
353            Ok((*x).clamp(*lo, *hi))
354        });
355        assert_eq!(filter.0.value_type, TypeId::of::<i32>());
356        assert_eq!(filter.0.args, vec![lo, hi]);
357        assert_eq!(
358            filter.0.arg_types,
359            vec![TypeId::of::<i32>(), TypeId::of::<i32>()]
360        );
361
362        let x: i32 = 500;
363        let lo_v: i32 = 0;
364        let hi_v: i32 = 100;
365        let result = (filter.0.function)(&x, &[&lo_v, &hi_v]).unwrap();
366        assert_eq!(*result.downcast_ref::<i32>().unwrap(), 100);
367    }
368
369    #[test]
370    /// Checks preservation of a static conformer's callback failure.
371    fn from_fn_0_reports_the_error_a_failing_function_returns() {
372        let filter =
373            Filter::from_fn_0(|_x: &i32| Err(std::io::Error::other("cannot conform").into()));
374        let x: i32 = 1;
375        let err = (filter.0.function)(&x, &[]).unwrap_err();
376        assert_eq!(err.to_string(), "cannot conform");
377        assert!(err.downcast_ref::<std::io::Error>().is_some());
378    }
379
380    #[test]
381    /// Checks preservation of an erased filter's declared value type.
382    fn new_stores_explicit_value_type_and_arg_types() {
383        let filter = Filter::new(TypeId::of::<i32>(), vec![], vec![], |value, _args| {
384            let v = value.downcast_ref::<i32>().unwrap();
385            Ok(Box::new(*v) as Box<dyn std::any::Any>)
386        });
387        assert_eq!(filter.0.value_type, TypeId::of::<i32>());
388    }
389
390    /// Preserves heterogeneous argument order and typed failures in dynamic adapters.
391    #[test]
392    fn typed_dynamic_adapters_preserve_order_and_errors() {
393        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
394        let a = map.insert(());
395        let b = map.insert(());
396        let filter = Filter::from_fn_2([a, b], |v: &String, prefix: &String, suffix: &char| {
397            if v.starts_with(prefix) && v.ends_with(*suffix) {
398                Ok(v.clone())
399            } else {
400                Ok(format!("{prefix}{v}{suffix}"))
401            }
402        });
403        let result =
404            (filter.0.function)(&"value".to_string(), &[&"pre:".to_string(), &'!']).unwrap();
405        assert_eq!(result.downcast_ref::<String>().unwrap(), "pre:value!");
406        let repeated = (filter.0.function)(result.as_ref(), &[&"pre:".to_string(), &'!']).unwrap();
407        assert_eq!(
408            repeated.downcast_ref::<String>(),
409            result.downcast_ref::<String>()
410        );
411        let one = Filter::from_fn_1(a, |_: &i32, _: &bool| {
412            Err(std::io::Error::other("one").into())
413        });
414        let two = Filter::from_fn_2([a, b], |_: &i32, _: &bool, _: &char| {
415            Err(std::io::Error::other("two").into())
416        });
417        assert!(
418            (one.0.function)(&1_i32, &[&true])
419                .unwrap_err()
420                .downcast_ref::<std::io::Error>()
421                .is_some()
422        );
423        assert!(
424            (two.0.function)(&1_i32, &[&true, &'!'])
425                .unwrap_err()
426                .downcast_ref::<std::io::Error>()
427                .is_some()
428        );
429    }
430
431    #[test]
432    /// Checks that an erased filter carries opaque presentation metadata.
433    fn new_defaults_to_opaque_kind() {
434        let filter = Filter::new(TypeId::of::<i32>(), vec![], vec![], |value, _args| {
435            Ok(Box::new(*value.downcast_ref::<i32>().unwrap()) as Box<dyn Any>)
436        });
437        assert!(matches!(filter.0.kind, FilterKind::Opaque));
438    }
439
440    #[test]
441    /// Checks range metadata and callback clamping.
442    fn range_stores_range_kind_and_clamps_via_function() {
443        let filter = Filter::range(
444            TypeId::of::<i32>(),
445            vec![],
446            vec![],
447            |value: &dyn Any, _args: &[&dyn Any]| {
448                let v = *value.downcast_ref::<i32>().unwrap();
449                Ok(Box::new(v.clamp(0, 100)) as Box<dyn Any>)
450            },
451            |_args: &[&dyn Any]| {
452                Some((
453                    Box::new(0i32) as Box<dyn Any>,
454                    Box::new(100i32) as Box<dyn Any>,
455                ))
456            },
457        );
458        assert_eq!(filter.0.value_type, TypeId::of::<i32>());
459        let x: i32 = 500;
460        let result = (filter.0.function)(&x, &[]).unwrap();
461        assert_eq!(*result.downcast_ref::<i32>().unwrap(), 100);
462        let FilterKind::Range { bounds } = &filter.0.kind else {
463            panic!("expected FilterKind::Range");
464        };
465        let (lo, hi) = bounds(&[]).unwrap();
466        assert_eq!(*lo.downcast_ref::<i32>().unwrap(), 0);
467        assert_eq!(*hi.downcast_ref::<i32>().unwrap(), 100);
468    }
469}