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}