Skip to main content

adam_rs/
conditional.rs

1//! Conditionals in the property model: match-subject branching.
2//!
3//! Each conditional evaluates a match subject — either a single existing cell, read
4//! directly, or a [`MatchExpr`] computed from multiple input cells — and holds a list of
5//! branches. During propagation the branch whose keys contain the current match value is
6//! activated; its relationships participate in the general planning pass.
7//!
8//! ```
9//! use adam_rs::{MatchExpr, Sheet};
10//! use std::any::TypeId;
11//! let mut sheet = Sheet::new();
12//! let flag = sheet.add_cell(true);
13//! let text = sheet.add_cell("yes!".to_string());
14//! let suffix = sheet.add_cell('!');
15//! let raw = MatchExpr::new(vec![flag], vec![TypeId::of::<bool>()], TypeId::of::<bool>(),
16//!     |a, b| a.downcast_ref::<bool>() == b.downcast_ref::<bool>(),
17//!     |v| Ok(Box::new(*v[0].downcast_ref::<bool>().unwrap())));
18//! for expression in [MatchExpr::cell(flag), raw,
19//!     MatchExpr::from_fn_1(text, |v: &String| Ok(!v.is_empty())),
20//!     MatchExpr::from_fn_2([text, suffix], |v: &String, c: &char| Ok(v.ends_with(*c)))] {
21//!     let id = sheet.add_conditional(expression, vec![(vec![true], vec![])], vec![])?;
22//!     assert_eq!(sheet.conditional_active_branch(id)?, Some(0));
23//! }
24//! # Ok::<(), adam_rs::Error>(())
25//! ```
26
27use std::any::{Any, TypeId};
28
29use slotmap::new_key_type;
30
31use crate::{cell::CellId, relationship::RelationshipId};
32
33new_key_type! {
34    /// A stable handle to a conditional in a [`crate::sheet::Sheet`].
35    ///
36    /// # Examples
37    /// See the [match-expression example](crate::conditional).
38    pub struct ConditionalId;
39}
40
41/// Type-erased function stored inside a [`MatchExprData`].
42///
43/// Takes a slice of type-erased input references and returns a type-erased boxed
44/// result value, or an error.
45type MatchExprFn = Box<dyn Fn(&[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error>>;
46
47/// A conditional's match subject: an existing cell, or a method-like expression computed
48/// from a set of input cells.
49///
50/// Constructed via [`MatchExpr::cell`] for the common single-cell case, or
51/// [`MatchExpr::new`]/[`MatchExpr::from_fn_1`]/[`MatchExpr::from_fn_2`] to compute the
52/// match value from multiple cells (analogous to [`crate::relationship::Method`]).
53///
54/// Its expression and equality callbacks must be pure, deterministic functions of
55/// their input values and immutable captured constants, with no externally observable
56/// side effects. Equal inputs produce equivalent outputs or errors. There is no
57/// guarantee that a callback executes, or when or how often it executes.
58///
59/// # Examples
60/// See the [match-expression example](crate::conditional).
61pub struct MatchExpr(pub(crate) MatchSource);
62
63/// Selects borrowed-cell evaluation or an owned expression result.
64pub(crate) enum MatchSource {
65    /// The match value is `cell`'s current effective value, read directly with no
66    /// allocation and no extra trait bounds.
67    Cell(CellId),
68    /// The match value is computed from `MatchExprData`.
69    Expr(MatchExprData),
70}
71
72/// Owns matching positional input/type signatures and an output-typed expression.
73///
74/// The equality function compares values of `output_type`; the callback returns
75/// that type on success. Sheet registration validates input cell identities.
76pub(crate) struct MatchExprData {
77    pub(crate) inputs: Vec<CellId>,
78    pub(crate) input_types: Vec<TypeId>,
79    pub(crate) output_type: TypeId,
80    pub(crate) eq_fn: fn(&dyn Any, &dyn Any) -> bool,
81    pub(crate) function: MatchExprFn,
82}
83
84impl MatchExpr {
85    /// Wraps a single existing cell as the match subject.
86    ///
87    /// - Postcondition: behaves exactly as a plain-cell conditional does today — the match
88    ///   value is `cell`'s current effective value, with no extra allocation or trait
89    ///   bounds beyond what [`crate::sheet::Sheet::add_conditional`] itself requires.
90    ///
91    /// # Examples
92    /// See the [match-expression example](crate::conditional).
93    #[must_use]
94    pub fn cell(cell: CellId) -> Self {
95        MatchExpr(MatchSource::Cell(cell))
96    }
97
98    /// Creates a match expression from explicit `TypeId`s and a type-erased function.
99    ///
100    /// - Precondition: `inputs.len() == input_types.len()`.
101    /// - Precondition: `f` returns a value whose runtime type matches `output_type`.
102    /// - Precondition: `eq_fn` correctly compares two values of the type identified by
103    ///   `output_type`.
104    /// - Precondition: `f` and `eq_fn` satisfy [`MatchExpr`]'s purity contract.
105    ///
106    /// # Examples
107    /// See the [match-expression example](crate::conditional).
108    #[must_use]
109    pub fn new<F>(
110        inputs: Vec<CellId>,
111        input_types: Vec<TypeId>,
112        output_type: TypeId,
113        eq_fn: fn(&dyn Any, &dyn Any) -> bool,
114        f: F,
115    ) -> Self
116    where
117        F: Fn(&[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error> + 'static,
118    {
119        debug_assert_eq!(inputs.len(), input_types.len());
120        MatchExpr(MatchSource::Expr(MatchExprData {
121            inputs,
122            input_types,
123            output_type,
124            eq_fn,
125            function: Box::new(f),
126        }))
127    }
128
129    /// Creates a 1-input match expression from a typed closure.
130    ///
131    /// `TypeId`s for `A` and `T` are captured automatically, along with `T`'s equality
132    /// function. The expression is validated against its cell registration when passed to
133    /// [`crate::sheet::Sheet::add_conditional`].
134    ///
135    /// - Precondition: `f` satisfies [`MatchExpr`]'s purity contract.
136    ///
137    /// # Examples
138    /// See the [match-expression example](crate::conditional).
139    #[must_use]
140    pub fn from_fn_1<A, T, F>(input: CellId, f: F) -> Self
141    where
142        A: Any + 'static,
143        T: Any + PartialEq + 'static,
144        F: Fn(&A) -> Result<T, anyhow::Error> + 'static,
145    {
146        MatchExpr::new(
147            vec![input],
148            vec![TypeId::of::<A>()],
149            TypeId::of::<T>(),
150            |a, b| a.downcast_ref::<T>() == b.downcast_ref::<T>(),
151            move |args| {
152                let a = args[0]
153                    .downcast_ref::<A>()
154                    .expect("type checked at add_conditional");
155                Ok(Box::new(f(a)?) as Box<dyn Any>)
156            },
157        )
158    }
159
160    /// Creates a 2-input match expression from a typed closure.
161    ///
162    /// `inputs[0]` maps to `A` and `inputs[1]` maps to `B`. `TypeId`s for `A`, `B`, and `T`
163    /// are captured automatically, along with `T`'s equality function. The expression is
164    /// validated when passed to [`crate::sheet::Sheet::add_conditional`].
165    ///
166    /// - Precondition: `f` satisfies [`MatchExpr`]'s purity contract.
167    ///
168    /// # Examples
169    /// See the [match-expression example](crate::conditional).
170    #[must_use]
171    pub fn from_fn_2<A, B, T, F>(inputs: [CellId; 2], f: F) -> Self
172    where
173        A: Any + 'static,
174        B: Any + 'static,
175        T: Any + PartialEq + 'static,
176        F: Fn(&A, &B) -> Result<T, anyhow::Error> + 'static,
177    {
178        MatchExpr::new(
179            inputs.to_vec(),
180            vec![TypeId::of::<A>(), TypeId::of::<B>()],
181            TypeId::of::<T>(),
182            |a, b| a.downcast_ref::<T>() == b.downcast_ref::<T>(),
183            move |args| {
184                let a = args[0]
185                    .downcast_ref::<A>()
186                    .expect("type checked at add_conditional");
187                let b = args[1]
188                    .downcast_ref::<B>()
189                    .expect("type checked at add_conditional");
190                Ok(Box::new(f(a, b)?) as Box<dyn Any>)
191            },
192        )
193    }
194}
195
196/// One arm of a [`ConditionalData`]: a set of key values and the relationships
197/// to activate when the match value equals any key.
198pub(crate) struct Branch {
199    /// Type-erased key values; each `TypeId` matches the match subject's output type.
200    pub(crate) keys: Vec<Box<dyn Any>>,
201    /// Relationships activated when any key matches.
202    pub(crate) relationships: Vec<RelationshipId>,
203}
204
205/// Internal storage for a conditional.
206pub(crate) struct ConditionalData {
207    /// The match subject whose value is tested.
208    pub(crate) source: MatchSource,
209    /// Branches evaluated in definition order; first match wins.
210    pub(crate) branches: Vec<Branch>,
211    /// Relationships activated when no branch matches. Empty means no default.
212    pub(crate) default: Vec<RelationshipId>,
213}
214
215impl ConditionalData {
216    /// Returns the cells that determine this conditional's match value: a single cell for
217    /// [`MatchSource::Cell`], or every input of the expression for [`MatchSource::Expr`].
218    pub(crate) fn match_cells(&self) -> &[CellId] {
219        match &self.source {
220            MatchSource::Cell(id) => std::slice::from_ref(id),
221            MatchSource::Expr(expr) => &expr.inputs,
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    /// Preserves heterogeneous match arguments and concrete failures in typed adapters.
231    #[test]
232    fn typed_adapters_preserve_order_and_errors() {
233        let mut sheet = crate::Sheet::new();
234        let a = sheet.add_cell(String::new());
235        let b = sheet.add_cell('!');
236        let MatchSource::Expr(expr) =
237            MatchExpr::from_fn_2([a, b], |a: &String, b: &char| Ok(format!("{a}{b}"))).0
238        else {
239            unreachable!()
240        };
241        assert_eq!(
242            (expr.function)(&[&"prefix".to_string(), &'!'])
243                .unwrap()
244                .downcast_ref::<String>()
245                .unwrap(),
246            "prefix!"
247        );
248        let MatchSource::Expr(one) =
249            MatchExpr::from_fn_1(a, |_: &String| -> anyhow::Result<String> {
250                Err(std::io::Error::other("one").into())
251            })
252            .0
253        else {
254            unreachable!()
255        };
256        let MatchSource::Expr(two) =
257            MatchExpr::from_fn_2([a, b], |_: &String, _: &char| -> anyhow::Result<String> {
258                Err(std::io::Error::other("two").into())
259            })
260            .0
261        else {
262            unreachable!()
263        };
264        assert!(
265            (one.function)(&[&String::new()])
266                .unwrap_err()
267                .downcast_ref::<std::io::Error>()
268                .is_some()
269        );
270        assert!(
271            (two.function)(&[&String::new(), &'!'])
272                .unwrap_err()
273                .downcast_ref::<std::io::Error>()
274                .is_some()
275        );
276    }
277
278    #[test]
279    /// Checks that conditional identities satisfy the Copy bound.
280    fn conditional_id_is_copy() {
281        /// Accepts only carriers that implement Copy.
282        fn takes_copy<T: Copy>(_: T) {}
283        takes_copy(ConditionalId::default());
284    }
285
286    #[test]
287    /// Checks the wrapped cell identity of a cell-backed match expression.
288    fn match_expr_cell_wraps_a_single_cell() {
289        use slotmap::SlotMap;
290        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
291        let cell = map.insert(());
292        let expr = MatchExpr::cell(cell);
293        match expr.0 {
294            MatchSource::Cell(id) => assert_eq!(id, cell),
295            MatchSource::Expr(_) => panic!("expected Cell variant"),
296        }
297    }
298
299    #[test]
300    /// Checks unary match-expression types and its computed value.
301    fn match_expr_from_fn_1_stores_correct_type_ids_and_computes_value() {
302        use slotmap::SlotMap;
303        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
304        let a = map.insert(());
305
306        let expr = MatchExpr::from_fn_1(a, |x: &i32| Ok(*x * 2));
307        match expr.0 {
308            MatchSource::Expr(data) => {
309                assert_eq!(data.inputs, vec![a]);
310                assert_eq!(data.input_types, vec![TypeId::of::<i32>()]);
311                assert_eq!(data.output_type, TypeId::of::<i32>());
312                let x: i32 = 5;
313                let result = (data.function)(&[&x]).unwrap();
314                assert_eq!(*result.downcast_ref::<i32>().unwrap(), 10);
315                let y: i32 = 10;
316                assert!((data.eq_fn)(&y, &10_i32));
317                assert!(!(data.eq_fn)(&y, &11_i32));
318            }
319            MatchSource::Cell(_) => panic!("expected Expr variant"),
320        }
321    }
322
323    #[test]
324    /// Checks binary match-expression types and its computed value.
325    fn match_expr_from_fn_2_stores_correct_type_ids_and_computes_value() {
326        use slotmap::SlotMap;
327        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
328        let a = map.insert(());
329        let b = map.insert(());
330
331        let expr = MatchExpr::from_fn_2([a, b], |x: &bool, y: &bool| Ok(*x && *y));
332        match expr.0 {
333            MatchSource::Expr(data) => {
334                assert_eq!(data.inputs, vec![a, b]);
335                assert_eq!(
336                    data.input_types,
337                    vec![TypeId::of::<bool>(), TypeId::of::<bool>()]
338                );
339                assert_eq!(data.output_type, TypeId::of::<bool>());
340                let x = true;
341                let y = false;
342                let result = (data.function)(&[&x, &y]).unwrap();
343                assert!(!*result.downcast_ref::<bool>().unwrap());
344            }
345            MatchSource::Cell(_) => panic!("expected Expr variant"),
346        }
347    }
348
349    #[test]
350    /// Checks that a cell-backed subject exposes its single input.
351    fn match_cells_returns_single_cell_for_cell_variant() {
352        use slotmap::SlotMap;
353        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
354        let cell = map.insert(());
355        let data = ConditionalData {
356            source: MatchSource::Cell(cell),
357            branches: Vec::new(),
358            default: Vec::new(),
359        };
360        assert_eq!(data.match_cells(), &[cell]);
361    }
362
363    #[test]
364    /// Checks that a computed subject exposes every declared input in order.
365    fn match_cells_returns_all_inputs_for_expr_variant() {
366        use slotmap::SlotMap;
367        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
368        let a = map.insert(());
369        let b = map.insert(());
370        let expr = MatchExpr::from_fn_2([a, b], |x: &i32, y: &i32| Ok(x + y));
371        let MatchSource::Expr(data) = expr.0 else {
372            panic!("expected Expr variant")
373        };
374        let cond = ConditionalData {
375            source: MatchSource::Expr(data),
376            branches: Vec::new(),
377            default: Vec::new(),
378        };
379        assert_eq!(cond.match_cells(), &[a, b]);
380    }
381
382    #[test]
383    /// Checks preservation of an erased match callback's failure.
384    fn match_expr_new_reports_the_error_a_failing_function_returns() {
385        use slotmap::SlotMap;
386        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
387        let a = map.insert(());
388
389        let expr = MatchExpr::new(
390            vec![a],
391            vec![TypeId::of::<i32>()],
392            TypeId::of::<i32>(),
393            |x, y| x.downcast_ref::<i32>() == y.downcast_ref::<i32>(),
394            |_args| Err(anyhow::anyhow!("boom")),
395        );
396        let MatchSource::Expr(data) = expr.0 else {
397            panic!("expected Expr variant")
398        };
399        let x: i32 = 1;
400        let err = (data.function)(&[&x]).unwrap_err();
401        assert_eq!(err.to_string(), "boom");
402    }
403}