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}