Skip to main content

adam_rs/
relationship.rs

1//! Relationships and methods in the property model bipartite graph.
2//!
3//! Each relationship holds a list of [`Method`]s; the planner selects one
4//! method per relationship at propagation time based on cell strength.
5//!
6//! ```
7//! use adam_rs::{Method, Sheet};
8//! use std::any::TypeId;
9//! let mut sheet = Sheet::new();
10//! let a = sheet.add_source("prefix".to_string());
11//! let b = sheet.add_source('!');
12//! let raw = sheet.add_cell(String::new());
13//! let one = sheet.add_cell(String::new());
14//! let two = sheet.add_cell(String::new());
15//! sheet.add_relationship(vec![Method::new(vec![a], vec![raw],
16//!     vec![TypeId::of::<String>()], vec![TypeId::of::<String>()],
17//!     |v| Ok(vec![Box::new(v[0].downcast_ref::<String>().unwrap().clone())]))])?;
18//! sheet.add_relationship(vec![Method::from_fn_1_1(a, one, |v: &String| Ok(v.clone()))])?;
19//! sheet.add_relationship(vec![Method::from_fn_2_1([a, b], two,
20//!     |v: &String, suffix: &char| Ok(format!("{v}{suffix}")))])?;
21//! sheet.propagate()?;
22//! assert_eq!(sheet.read::<String>(raw)?, "prefix");
23//! assert_eq!(sheet.read::<String>(one)?, "prefix");
24//! assert_eq!(sheet.read::<String>(two)?, "prefix!");
25//! # Ok::<(), adam_rs::Error>(())
26//! ```
27
28use std::any::{Any, TypeId};
29
30use slotmap::new_key_type;
31
32use crate::cell::CellId;
33
34new_key_type! {
35    /// A stable handle to a relationship in a [`crate::sheet::Sheet`].
36    ///
37    /// # Examples
38    /// See the [relationship example](crate::relationship).
39    pub struct RelationshipId;
40}
41
42/// Type-erased function stored inside a [`Method`].
43///
44/// Takes a slice of type-erased input references and returns a `Vec` of
45/// type-erased boxed outputs, or an error.
46type MethodFn = Box<dyn Fn(&[&dyn Any]) -> Result<Vec<Box<dyn Any>>, anyhow::Error>>;
47
48/// A single method within a relationship.
49///
50/// Its callback must be a pure, deterministic function of its input values and
51/// immutable captured constants, with no externally observable side effects.
52/// Equal inputs produce equivalent outputs or errors. There is no guarantee that
53/// the callback executes, or when or how often it executes.
54/// A self-referencing method must also be idempotent.
55/// Holding its other inputs fixed, substituting its outputs for overlapping inputs
56/// and applying it again produces equivalent outputs.
57///
58/// Every method in a relationship must conform the same constraint and preserve
59/// an assignment that already satisfies it. The runtime checks shared cell sets,
60/// not this semantic consistency.
61///
62/// # Examples
63/// See the [relationship example](crate::relationship).
64pub struct Method {
65    pub(crate) inputs: Vec<CellId>,
66    pub(crate) outputs: Vec<CellId>,
67    pub(crate) input_types: Vec<TypeId>,
68    pub(crate) output_types: Vec<TypeId>,
69    pub(crate) function: MethodFn,
70}
71
72impl Method {
73    /// Creates a method from explicit TypeIds and a type-erased function.
74    ///
75    /// - Precondition: `inputs.len() == input_types.len()` and `outputs.len() == output_types.len()`.
76    /// - Precondition: `f` satisfies [`Method`]'s purity and idempotence contract.
77    ///
78    /// Propagation validates the callback's output count and concrete types before
79    /// publishing staged values. Declared output order determines positional binding.
80    /// - Complexity: O(1) storage work, excluding ownership transfer of declared edges.
81    ///
82    /// # Examples
83    /// See the [relationship example](crate::relationship).
84    pub fn new<F>(
85        inputs: Vec<CellId>,
86        outputs: Vec<CellId>,
87        input_types: Vec<TypeId>,
88        output_types: Vec<TypeId>,
89        f: F,
90    ) -> Self
91    where
92        F: Fn(&[&dyn Any]) -> Result<Vec<Box<dyn Any>>, anyhow::Error> + 'static,
93    {
94        debug_assert_eq!(inputs.len(), input_types.len());
95        debug_assert_eq!(outputs.len(), output_types.len());
96        Method {
97            inputs,
98            outputs,
99            input_types,
100            output_types,
101            function: Box::new(f),
102        }
103    }
104
105    /// Creates a 1-input, 1-output method from a typed closure.
106    ///
107    /// TypeIds for `A` and `B` are captured automatically. The method is validated
108    /// against its cell registrations when passed to [`crate::sheet::Sheet::add_relationship`].
109    ///
110    /// - Precondition: `f` satisfies [`Method`]'s purity and idempotence contract.
111    ///
112    /// # Examples
113    /// See the [relationship example](crate::relationship).
114    pub fn from_fn_1_1<A, B, F>(input: CellId, output: CellId, f: F) -> Self
115    where
116        A: Any + 'static,
117        B: Any + 'static,
118        F: Fn(&A) -> Result<B, anyhow::Error> + 'static,
119    {
120        Method {
121            inputs: vec![input],
122            outputs: vec![output],
123            input_types: vec![TypeId::of::<A>()],
124            output_types: vec![TypeId::of::<B>()],
125            function: Box::new(move |args| {
126                let a = args[0]
127                    .downcast_ref::<A>()
128                    .expect("type checked at add_relationship");
129                Ok(vec![Box::new(f(a)?)])
130            }),
131        }
132    }
133
134    /// Creates a 2-input, 1-output method from a typed closure.
135    ///
136    /// `inputs[0]` maps to `A` and `inputs[1]` maps to `B`. TypeIds are captured
137    /// automatically. The method is validated when passed to
138    /// [`crate::sheet::Sheet::add_relationship`].
139    ///
140    /// - Precondition: `f` satisfies [`Method`]'s purity and idempotence contract.
141    ///
142    /// # Examples
143    /// See the [relationship example](crate::relationship).
144    pub fn from_fn_2_1<A, B, C, F>(inputs: [CellId; 2], output: CellId, f: F) -> Self
145    where
146        A: Any + 'static,
147        B: Any + 'static,
148        C: Any + 'static,
149        F: Fn(&A, &B) -> Result<C, anyhow::Error> + 'static,
150    {
151        Method {
152            inputs: inputs.to_vec(),
153            outputs: vec![output],
154            input_types: vec![TypeId::of::<A>(), TypeId::of::<B>()],
155            output_types: vec![TypeId::of::<C>()],
156            function: Box::new(move |args| {
157                let a = args[0]
158                    .downcast_ref::<A>()
159                    .expect("type checked at add_relationship");
160                let b = args[1]
161                    .downcast_ref::<B>()
162                    .expect("type checked at add_relationship");
163                Ok(vec![Box::new(f(a, b)?)])
164            }),
165        }
166    }
167}
168
169/// Owns validated methods and their shared, duplicate-free adjacent cell set.
170pub(crate) struct RelationshipData {
171    pub(crate) methods: Vec<Method>,
172    /// The set of cells referenced by every method in this relationship — every method
173    /// references the same set (enforced by `Sheet::add_relationship`).
174    pub(crate) adj: Vec<CellId>,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    /// Preserves heterogeneous input order and typed errors through each typed adapter.
182    #[test]
183    fn typed_adapters_preserve_order_and_errors() {
184        let mut sheet = crate::Sheet::new();
185        let a = sheet.add_cell(String::new());
186        let b = sheet.add_cell('!');
187        let c = sheet.add_cell(String::new());
188        let method = Method::from_fn_2_1([a, b], c, |a: &String, b: &char| Ok(format!("{a}{b}")));
189        let result = (method.function)(&[&"prefix".to_string(), &'!']).unwrap();
190        assert_eq!(result[0].downcast_ref::<String>().unwrap(), "prefix!");
191        let one = Method::from_fn_1_1(a, c, |_: &String| -> anyhow::Result<String> {
192            Err(std::io::Error::other("one").into())
193        });
194        let two = Method::from_fn_2_1(
195            [a, b],
196            c,
197            |_: &String, _: &char| -> anyhow::Result<String> {
198                Err(std::io::Error::other("two").into())
199            },
200        );
201        assert!(
202            (one.function)(&[&String::new()])
203                .unwrap_err()
204                .downcast_ref::<std::io::Error>()
205                .is_some()
206        );
207        assert!(
208            (two.function)(&[&String::new(), &'!'])
209                .unwrap_err()
210                .downcast_ref::<std::io::Error>()
211                .is_some()
212        );
213    }
214
215    #[test]
216    /// Checks that relationship identities satisfy the Copy bound.
217    fn relationship_id_is_copy() {
218        /// Accepts only carriers that implement Copy.
219        fn takes_copy<T: Copy>(_: T) {}
220        // RelationshipId must be Copy so it can be stored in adjacency Vecs cheaply.
221        // This test fails to compile if RelationshipId is not Copy.
222        takes_copy(RelationshipId::default());
223    }
224
225    #[test]
226    /// Checks preservation of an erased method's positional edges and types.
227    fn method_new_stores_types_and_cell_ids() {
228        use slotmap::SlotMap;
229
230        // Create real CellIds from a SlotMap to avoid using the default (null) key.
231        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
232        let a = map.insert(());
233        let b = map.insert(());
234        let c = map.insert(());
235
236        let method = Method::new(
237            vec![a, b],
238            vec![c],
239            vec![TypeId::of::<i32>(), TypeId::of::<i32>()],
240            vec![TypeId::of::<i32>()],
241            |args| {
242                let x = args[0].downcast_ref::<i32>().unwrap();
243                let y = args[1].downcast_ref::<i32>().unwrap();
244                Ok(vec![Box::new(x + y)])
245            },
246        );
247
248        assert_eq!(method.inputs, vec![a, b]);
249        assert_eq!(method.outputs, vec![c]);
250        assert_eq!(
251            method.input_types,
252            vec![TypeId::of::<i32>(), TypeId::of::<i32>()]
253        );
254        assert_eq!(method.output_types, vec![TypeId::of::<i32>()]);
255
256        // Verify the stored function works.
257        let x: i32 = 3;
258        let y: i32 = 4;
259        let result = (method.function)(&[&x, &y]).unwrap();
260        assert_eq!(*result[0].downcast_ref::<i32>().unwrap(), 7);
261    }
262
263    #[test]
264    /// Checks unary method signature types and its result.
265    fn from_fn_1_1_stores_correct_type_ids() {
266        use slotmap::SlotMap;
267
268        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
269        let a = map.insert(());
270        let b = map.insert(());
271
272        let method = Method::from_fn_1_1(a, b, |x: &i32| Ok(*x * 2));
273
274        assert_eq!(method.inputs, vec![a]);
275        assert_eq!(method.outputs, vec![b]);
276        assert_eq!(method.input_types, vec![TypeId::of::<i32>()]);
277        assert_eq!(method.output_types, vec![TypeId::of::<i32>()]);
278
279        let x: i32 = 5;
280        let result = (method.function)(&[&x]).unwrap();
281        assert_eq!(*result[0].downcast_ref::<i32>().unwrap(), 10);
282    }
283
284    #[test]
285    /// Checks binary method signature types and its result.
286    fn from_fn_2_1_stores_correct_type_ids() {
287        use slotmap::SlotMap;
288
289        let mut map: SlotMap<CellId, ()> = SlotMap::with_key();
290        let a = map.insert(());
291        let b = map.insert(());
292        let c = map.insert(());
293
294        let method = Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| Ok((*x) * (*y)));
295
296        assert_eq!(method.inputs, vec![a, b]);
297        assert_eq!(method.outputs, vec![c]);
298        assert_eq!(
299            method.input_types,
300            vec![TypeId::of::<f64>(), TypeId::of::<f64>()]
301        );
302        assert_eq!(method.output_types, vec![TypeId::of::<f64>()]);
303
304        let x: f64 = 2.0;
305        let y: f64 = 3.0;
306        let result = (method.function)(&[&x, &y]).unwrap();
307        assert_eq!(*result[0].downcast_ref::<f64>().unwrap(), 6.0);
308    }
309}