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
8use std::any::{Any, TypeId};
9
10use slotmap::new_key_type;
11
12use crate::{cell::CellId, relationship::RelationshipId};
13
14new_key_type! {
15    /// A stable handle to a conditional in a [`crate::sheet::Sheet`].
16    pub struct ConditionalId;
17}
18
19/// Type-erased function stored inside a [`MatchExprData`].
20///
21/// Takes a slice of type-erased input references and returns a type-erased boxed
22/// result value, or an error.
23type MatchExprFn = Box<dyn Fn(&[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error>>;
24
25/// A conditional's match subject: an existing cell, or a method-like expression computed
26/// from a set of input cells.
27///
28/// Constructed via [`MatchExpr::cell`] for the common single-cell case, or
29/// [`MatchExpr::new`]/[`MatchExpr::from_fn_1`]/[`MatchExpr::from_fn_2`] to compute the
30/// match value from multiple cells (analogous to [`crate::relationship::Method`]).
31///
32/// Its expression and equality callbacks must be pure, deterministic functions of
33/// their input values and immutable captured constants, with no externally observable
34/// side effects. Equal inputs produce equivalent outputs or errors. There is no
35/// guarantee that a callback executes, or when or how often it executes.
36pub struct MatchExpr(pub(crate) MatchSource);
37
38pub(crate) enum MatchSource {
39    /// The match value is `cell`'s current effective value, read directly with no
40    /// allocation and no extra trait bounds.
41    Cell(CellId),
42    /// The match value is computed from `MatchExprData`.
43    Expr(MatchExprData),
44}
45
46pub(crate) struct MatchExprData {
47    pub(crate) inputs: Vec<CellId>,
48    pub(crate) input_types: Vec<TypeId>,
49    pub(crate) output_type: TypeId,
50    pub(crate) eq_fn: fn(&dyn Any, &dyn Any) -> bool,
51    pub(crate) function: MatchExprFn,
52}
53
54impl MatchExpr {
55    /// Wraps a single existing cell as the match subject.
56    ///
57    /// - Postcondition: behaves exactly as a plain-cell conditional does today — the match
58    ///   value is `cell`'s current effective value, with no extra allocation or trait
59    ///   bounds beyond what [`crate::sheet::Sheet::add_conditional`] itself requires.
60    #[must_use]
61    pub fn cell(cell: CellId) -> Self {
62        MatchExpr(MatchSource::Cell(cell))
63    }
64
65    /// Creates a match expression from explicit `TypeId`s and a type-erased function.
66    ///
67    /// - Precondition: `inputs.len() == input_types.len()`.
68    /// - Precondition: `f` returns a value whose runtime type matches `output_type`.
69    /// - Precondition: `eq_fn` correctly compares two values of the type identified by
70    ///   `output_type`.
71    /// - Precondition: `f` and `eq_fn` satisfy [`MatchExpr`]'s purity contract.
72    #[must_use]
73    pub fn new<F>(
74        inputs: Vec<CellId>,
75        input_types: Vec<TypeId>,
76        output_type: TypeId,
77        eq_fn: fn(&dyn Any, &dyn Any) -> bool,
78        f: F,
79    ) -> Self
80    where
81        F: Fn(&[&dyn Any]) -> Result<Box<dyn Any>, anyhow::Error> + 'static,
82    {
83        debug_assert_eq!(inputs.len(), input_types.len());
84        MatchExpr(MatchSource::Expr(MatchExprData {
85            inputs,
86            input_types,
87            output_type,
88            eq_fn,
89            function: Box::new(f),
90        }))
91    }
92
93    /// Creates a 1-input match expression from a typed closure.
94    ///
95    /// `TypeId`s for `A` and `T` are captured automatically, along with `T`'s equality
96    /// function. The expression is validated against its cell registration when passed to
97    /// [`crate::sheet::Sheet::add_conditional`].
98    ///
99    /// - Precondition: `f` satisfies [`MatchExpr`]'s purity contract.
100    #[must_use]
101    pub fn from_fn_1<A, T, F>(input: CellId, f: F) -> Self
102    where
103        A: Any + 'static,
104        T: Any + PartialEq + 'static,
105        F: Fn(&A) -> Result<T, anyhow::Error> + 'static,
106    {
107        MatchExpr::new(
108            vec![input],
109            vec![TypeId::of::<A>()],
110            TypeId::of::<T>(),
111            |a, b| a.downcast_ref::<T>() == b.downcast_ref::<T>(),
112            move |args| {
113                let a = args[0]
114                    .downcast_ref::<A>()
115                    .expect("type checked at add_conditional");
116                Ok(Box::new(f(a)?) as Box<dyn Any>)
117            },
118        )
119    }
120
121    /// Creates a 2-input match expression from a typed closure.
122    ///
123    /// `inputs[0]` maps to `A` and `inputs[1]` maps to `B`. `TypeId`s for `A`, `B`, and `T`
124    /// are captured automatically, along with `T`'s equality function. The expression is
125    /// validated when passed to [`crate::sheet::Sheet::add_conditional`].
126    ///
127    /// - Precondition: `f` satisfies [`MatchExpr`]'s purity contract.
128    #[must_use]
129    pub fn from_fn_2<A, B, T, F>(inputs: [CellId; 2], f: F) -> Self
130    where
131        A: Any + 'static,
132        B: Any + 'static,
133        T: Any + PartialEq + 'static,
134        F: Fn(&A, &B) -> Result<T, anyhow::Error> + 'static,
135    {
136        MatchExpr::new(
137            inputs.to_vec(),
138            vec![TypeId::of::<A>(), TypeId::of::<B>()],
139            TypeId::of::<T>(),
140            |a, b| a.downcast_ref::<T>() == b.downcast_ref::<T>(),
141            move |args| {
142                let a = args[0]
143                    .downcast_ref::<A>()
144                    .expect("type checked at add_conditional");
145                let b = args[1]
146                    .downcast_ref::<B>()
147                    .expect("type checked at add_conditional");
148                Ok(Box::new(f(a, b)?) as Box<dyn Any>)
149            },
150        )
151    }
152}
153
154/// One arm of a [`ConditionalData`]: a set of key values and the relationships
155/// to activate when the match value equals any key.
156pub(crate) struct Branch {
157    /// Type-erased key values; each `TypeId` matches the match subject's output type.
158    pub(crate) keys: Vec<Box<dyn Any>>,
159    /// Relationships activated when any key matches.
160    pub(crate) relationships: Vec<RelationshipId>,
161}
162
163/// Internal storage for a conditional.
164pub(crate) struct ConditionalData {
165    /// The match subject whose value is tested.
166    pub(crate) source: MatchSource,
167    /// Branches evaluated in definition order; first match wins.
168    pub(crate) branches: Vec<Branch>,
169    /// Relationships activated when no branch matches. Empty means no default.
170    pub(crate) default: Vec<RelationshipId>,
171}
172
173impl ConditionalData {
174    /// Returns the cells that determine this conditional's match value: a single cell for
175    /// [`MatchSource::Cell`], or every input of the expression for [`MatchSource::Expr`].
176    pub(crate) fn match_cells(&self) -> &[CellId] {
177        match &self.source {
178            MatchSource::Cell(id) => std::slice::from_ref(id),
179            MatchSource::Expr(expr) => &expr.inputs,
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn conditional_id_is_copy() {
190        fn takes_copy<T: Copy>(_: T) {}
191        takes_copy(ConditionalId::default());
192    }
193
194    #[test]
195    fn match_expr_cell_wraps_a_single_cell() {
196        use slotmap::SlotMap;
197        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
198        let cell = map.insert(());
199        let expr = MatchExpr::cell(cell);
200        match expr.0 {
201            MatchSource::Cell(id) => assert_eq!(id, cell),
202            MatchSource::Expr(_) => panic!("expected Cell variant"),
203        }
204    }
205
206    #[test]
207    fn match_expr_from_fn_1_stores_correct_type_ids_and_computes_value() {
208        use slotmap::SlotMap;
209        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
210        let a = map.insert(());
211
212        let expr = MatchExpr::from_fn_1(a, |x: &i32| Ok(*x * 2));
213        match expr.0 {
214            MatchSource::Expr(data) => {
215                assert_eq!(data.inputs, vec![a]);
216                assert_eq!(data.input_types, vec![TypeId::of::<i32>()]);
217                assert_eq!(data.output_type, TypeId::of::<i32>());
218                let x: i32 = 5;
219                let result = (data.function)(&[&x]).unwrap();
220                assert_eq!(*result.downcast_ref::<i32>().unwrap(), 10);
221                let y: i32 = 10;
222                assert!((data.eq_fn)(&y, &10_i32));
223                assert!(!(data.eq_fn)(&y, &11_i32));
224            }
225            MatchSource::Cell(_) => panic!("expected Expr variant"),
226        }
227    }
228
229    #[test]
230    fn match_expr_from_fn_2_stores_correct_type_ids_and_computes_value() {
231        use slotmap::SlotMap;
232        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
233        let a = map.insert(());
234        let b = map.insert(());
235
236        let expr = MatchExpr::from_fn_2([a, b], |x: &bool, y: &bool| Ok(*x && *y));
237        match expr.0 {
238            MatchSource::Expr(data) => {
239                assert_eq!(data.inputs, vec![a, b]);
240                assert_eq!(
241                    data.input_types,
242                    vec![TypeId::of::<bool>(), TypeId::of::<bool>()]
243                );
244                assert_eq!(data.output_type, TypeId::of::<bool>());
245                let x = true;
246                let y = false;
247                let result = (data.function)(&[&x, &y]).unwrap();
248                assert!(!*result.downcast_ref::<bool>().unwrap());
249            }
250            MatchSource::Cell(_) => panic!("expected Expr variant"),
251        }
252    }
253
254    #[test]
255    fn match_cells_returns_single_cell_for_cell_variant() {
256        use slotmap::SlotMap;
257        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
258        let cell = map.insert(());
259        let data = ConditionalData {
260            source: MatchSource::Cell(cell),
261            branches: Vec::new(),
262            default: Vec::new(),
263        };
264        assert_eq!(data.match_cells(), &[cell]);
265    }
266
267    #[test]
268    fn match_cells_returns_all_inputs_for_expr_variant() {
269        use slotmap::SlotMap;
270        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
271        let a = map.insert(());
272        let b = map.insert(());
273        let expr = MatchExpr::from_fn_2([a, b], |x: &i32, y: &i32| Ok(x + y));
274        let MatchSource::Expr(data) = expr.0 else {
275            panic!("expected Expr variant")
276        };
277        let cond = ConditionalData {
278            source: MatchSource::Expr(data),
279            branches: Vec::new(),
280            default: Vec::new(),
281        };
282        assert_eq!(cond.match_cells(), &[a, b]);
283    }
284
285    #[test]
286    fn match_expr_new_reports_the_error_a_failing_function_returns() {
287        use slotmap::SlotMap;
288        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
289        let a = map.insert(());
290
291        let expr = MatchExpr::new(
292            vec![a],
293            vec![TypeId::of::<i32>()],
294            TypeId::of::<i32>(),
295            |x, y| x.downcast_ref::<i32>() == y.downcast_ref::<i32>(),
296            |_args| Err(anyhow::anyhow!("boom")),
297        );
298        let MatchSource::Expr(data) = expr.0 else {
299            panic!("expected Expr variant")
300        };
301        let x: i32 = 1;
302        let err = (data.function)(&[&x]).unwrap_err();
303        assert_eq!(err.to_string(), "boom");
304    }
305}