adam_rs/lib.rs
1//! # adam-rs
2//!
3//! A library for constructing and executing property model constraint graphs.
4//!
5//! A property model is a bipartite graph of **value cells** and **relationships**.
6//! Cells hold type-erased values. Relationships define multi-way constraints: each
7//! relationship supplies multiple methods, and at propagation time the planner
8//! selects one method per relationship based on cell write-recency (strength),
9//! then executes the selected methods in dependency order.
10//!
11//! # Callback purity
12//!
13//! Every [`Method`], [`Filter`], [`MatchExpr`], and [`Requirement`] callback must be
14//! purely functional: its result depends only on its explicit input values and
15//! immutable captured constants. Equal input values must produce equivalent output
16//! values or errors. This also applies to range-filter bound evaluators and
17//! conditional equality functions.
18//!
19//! Callbacks must not mutate inputs or captured state, perform I/O, or read mutable
20//! external state such as clocks, random generators, or independently changing shared
21//! data. Represent changing dependencies as input cells instead.
22//!
23//! There is no guarantee that a callback will execute, or when or how often it will
24//! execute. The runtime may omit evaluation, reuse a previously computed result, or
25//! reevaluate a callback while preserving the sheet's value and diagnostic semantics.
26//! Observe propagation through cell values and diagnostics, not callback side effects.
27//! Rust's `Fn` bound does not enforce purity; this is a caller obligation.
28//!
29//! # Example
30//!
31//! ```rust
32//! use adam_rs::{Sheet, Method};
33//!
34//! let mut sheet = Sheet::new();
35//! let a = sheet.add_cell(2.0_f64);
36//! let b = sheet.add_cell(3.0_f64);
37//! let c = sheet.add_cell(0.0_f64);
38//!
39//! // Three methods encoding a × b = c in each direction.
40//! let methods = vec![
41//! Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| Ok((*x) * (*y))),
42//! Method::from_fn_2_1([b, c], a, |x: &f64, y: &f64| Ok((*y) / (*x))),
43//! Method::from_fn_2_1([a, c], b, |x: &f64, y: &f64| Ok((*y) / (*x))),
44//! ];
45//! sheet.add_relationship(methods).unwrap();
46//!
47//! sheet.write(a, 2.0_f64).unwrap();
48//! sheet.write(b, 3.0_f64).unwrap();
49//! sheet.propagate().unwrap();
50//!
51//! assert_eq!(*sheet.read::<f64>(c).unwrap(), 6.0);
52//! ```
53//!
54//! # Out cells and requirements
55//!
56//! An out cell is always derived by exactly one fixed writer method, with optional
57//! requirements checked after every `propagate()`. Display labels belong to the caller,
58//! not the sheet. Unlike an ordinary derived cell, an
59//! out cell can never be `write()`-ed or claimed as another method's output — but it
60//! remains an ordinary, freely-referenceable cell everywhere else (as another
61//! relationship's input, a conditional's match subject, and so on).
62//!
63//! ```rust
64//! use adam_rs::{Requirement, Method, Sheet};
65//!
66//! let mut sheet = Sheet::new();
67//! let width = sheet.add_cell(0_i32);
68//! let height = sheet.add_cell(0_i32);
69//! let max_area = sheet.add_cell(100_i32);
70//! let area = sheet.add_cell(0_i32);
71//!
72//! let writer = Method::from_fn_2_1([width, height], area, |w: &i32, h: &i32| {
73//! w.checked_mul(*h).ok_or_else(|| anyhow::anyhow!("overflow"))
74//! });
75//! let area_cell = sheet
76//! .add_out(
77//! writer,
78//! vec![Requirement::from_fn_2([area, max_area], |a: &i32, max: &i32| Ok(a <= max))],
79//! )
80//! .unwrap();
81//!
82//! sheet.write(width, 20_i32).unwrap();
83//! sheet.write(height, 3_i32).unwrap();
84//! sheet.propagate().unwrap();
85//! assert!(sheet.cell_requirements_valid(area_cell));
86//!
87//! sheet.write(height, 30_i32).unwrap();
88//! sheet.propagate().unwrap();
89//! assert!(!sheet.cell_requirements_valid(area_cell));
90//! ```
91//!
92//! # Filters
93//!
94//! A filter conforms or rejects a value written externally to its cell — applied
95//! live by `propagate()` against the cell's own current value, not synchronously by
96//! `write()`. It's also re-checked, as a non-gating diagnostic only, against a value
97//! a relationship's method derives for that cell — a derived value is never
98//! corrected, only flagged.
99//!
100//! ```rust
101//! use adam_rs::{Filter, Method, Sheet};
102//!
103//! let mut sheet = Sheet::new();
104//! let a = sheet.add_cell(0_i32);
105//! let b = sheet.add_cell(0_i32);
106//! sheet
107//! .add_filter(a, Filter::from_fn_0(|x: &i32| Ok((*x).clamp(0, 100))))
108//! .unwrap();
109//! sheet
110//! .add_filter(b, Filter::from_fn_0(|x: &i32| Ok((*x).clamp(0, 100))))
111//! .unwrap();
112//! sheet
113//! .add_relationship(vec![Method::from_fn_1_1(a, b, |x: &i32| Ok(*x * 2))])
114//! .unwrap();
115//!
116//! // write() leaves the raw value in source; it is conformed by propagate()...
117//! sheet.write(a, 500_i32).unwrap();
118//! assert_eq!(*sheet.read::<i32>(a).unwrap(), 500);
119//!
120//! sheet.propagate().unwrap();
121//! assert_eq!(*sheet.read::<i32>(a).unwrap(), 100);
122//!
123//! // A derived value that would fail the same filter is only diagnosed, never
124//! // corrected: `b` doubles `a`'s now-conformed value, still exceeding the filter's range.
125//! assert_eq!(*sheet.read::<i32>(b).unwrap(), 200);
126//! assert!(sheet.filter_violated_cells().any(|id| id == b));
127//! ```
128//!
129//! # Invariants
130//!
131//! Enforced by code:
132//!
133//! - Every relationship's methods share the same `inputs ∪ outputs` cell set
134//! ([`Sheet::add_relationship`] validation; [`Error::MismatchedMethodCells`]).
135//! - No two relationships may claim the same cell as an output in one round —
136//! self-referencing outputs are claimed exactly like any other
137//! ([`Error::Conflict`] when infeasible).
138//! - The selected methods' induced dependency digraph is acyclic before execution
139//! ([`Error::Cycle`] when not).
140//! - A self-referencing input never reads a same-round derived value. Instead it reads a
141//! seed reconstructed from `source`: `planner::evaluate_seeds` folds every other
142//! relationship incident to the cell (excluding its own claimant) through that
143//! relationship's cell-producing method, giving the cell's aspiration before its
144//! claimant tightens it; a cell no other relationship contributes to just reads its
145//! own `source`. The seed is rebuilt from `source` every round, so it is never stale
146//! and never accumulates.
147//! - [`Sheet::write`]/[`Sheet::add_cell`] establish `source` aspirations. Ordinary
148//! unshadowed method outputs replace `source` at commit; self-referencing outputs
149//! and source filters preserve aspirations and publish derived overrides instead.
150//! - An `Out`-kind cell can never be [`Sheet::write`]-ed or claimed as another method's
151//! output ([`Error::InvalidCellKind`]).
152//!
153//! Enforced only by convention (caller contract, not checked by the runtime):
154//!
155//! - All method, filter, conditional-expression, and requirement callbacks satisfy
156//! the callback-purity contract above, with no invocation guarantee.
157//! - Every method of a relationship conforms the same constraint and preserves an
158//! assignment that already satisfies it. This semantic consistency is stronger
159//! than the runtime's check that methods reference the same cell set.
160//! - A self-referencing method must be idempotent: applying it twice to the same inputs
161//! must produce the same result as applying it once.
162//! - A filter must be idempotent and conform its cell's value using its argument
163//! cells' values.
164//! - Iteration order used to break ties among equal-strength cells is not stable API
165//! and must not be relied on by callers.
166//!
167//! # Capture and playback
168//!
169//! After successful propagation, `sheet.source_cells(&[])` identifies all unclaimed
170//! cells and selected self-referencing inputs. Pass a nonempty slice of result cells
171//! to [`Sheet::source_cells`] to restrict capture to their contributing snapshot
172//! sources. This follows selected producer paths, conforming filters, and relevant
173//! conditional selectors, not the alternative method directions considered by
174//! [`Sheet::contributing_cells`] for widget relevance.
175//!
176//! Capture effective [`Sheet::read`] values, not [`Sheet::source`] aspirations.
177//! Pure derived outputs are never recorded. Duplicate results and overlapping
178//! dependency paths do not duplicate sources.
179//!
180//! Replay writes all captured values, in any order, before calling
181//! [`Sheet::propagate`]. For a settled solution satisfying its relationships and
182//! filters, a compatible sheet with matching uncaptured inputs reproduces the same
183//! requested effective values, or all values for an empty result slice. Semantic
184//! consistency and idempotence make the recorded solution a fixed point even when
185//! the new write priorities select different methods. No strengths or write history
186//! need to be recorded.
187//!
188//! Clients choose their initial state after construction and own change comparison,
189//! relevance filtering, stable cell names or schema keys, and typed codecs. Do not
190//! persist live [`CellId`] values. Filtering out an input makes its replay value
191//! part of the context that must match; it is not reconstructed by capture.
192//!
193//! A value snapshot is not an editable-state checkpoint. It does not preserve
194//! original aspirations, priority history, diagnostics, or behavior under subsequent
195//! edits, including spring-back. Capture only after successful propagation and
196//! before further edits; this API neither consumes a journal nor changes the sheet.
197
198pub mod cell;
199pub mod conditional;
200pub mod error;
201pub mod filter;
202mod planner;
203pub mod relationship;
204pub mod requirement;
205pub mod sheet;
206pub mod template;
207
208pub use cell::{CellId, CellKind};
209pub use conditional::{ConditionalId, MatchExpr};
210pub use error::{Error, ErrorSite};
211pub use filter::{Filter, FilterKind, FilterViolation};
212pub use relationship::{Method, RelationshipId};
213pub use requirement::{Requirement, RequirementId};
214pub use sheet::Sheet;
215pub use template::{
216 CellSlot, ConstructionSite, Instance, InstanceOccurrence, MethodOrigin, MethodRecipe,
217 OccurrenceId, RelationshipSlot, SheetTemplate, TemplateBuilder, TemplateError, TemplateId,
218 TemplateInclusion,
219};