Expand description
§adam-rs
A library for constructing and executing property model constraint graphs.
A property model is a bipartite graph of value cells and relationships. Cells hold type-erased values. Relationships define multi-way constraints: each relationship supplies multiple methods, and at propagation time the planner selects one method per relationship based on cell write-recency (strength), then executes the selected methods in dependency order.
§Callback purity
Every Method, Filter, MatchExpr, and Requirement callback must be
purely functional: its result depends only on its explicit input values and
immutable captured constants. Equal input values must produce equivalent output
values or errors. This also applies to range-filter bound evaluators and
conditional equality functions.
Callbacks must not mutate inputs or captured state, perform I/O, or read mutable external state such as clocks, random generators, or independently changing shared data. Represent changing dependencies as input cells instead.
There is no guarantee that a callback will execute, or when or how often it will
execute. The runtime may omit evaluation, reuse a previously computed result, or
reevaluate a callback while preserving the sheet’s value and diagnostic semantics.
Observe propagation through cell values and diagnostics, not callback side effects.
Rust’s Fn bound does not enforce purity; this is a caller obligation.
§Example
use adam_rs::{Sheet, Method};
let mut sheet = Sheet::new();
let a = sheet.add_cell(2.0_f64);
let b = sheet.add_cell(3.0_f64);
let c = sheet.add_cell(0.0_f64);
// Three methods encoding a × b = c in each direction.
let methods = vec![
Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| Ok((*x) * (*y))),
Method::from_fn_2_1([b, c], a, |x: &f64, y: &f64| Ok((*y) / (*x))),
Method::from_fn_2_1([a, c], b, |x: &f64, y: &f64| Ok((*y) / (*x))),
];
sheet.add_relationship(methods).unwrap();
sheet.write(a, 2.0_f64).unwrap();
sheet.write(b, 3.0_f64).unwrap();
sheet.propagate().unwrap();
assert_eq!(*sheet.read::<f64>(c).unwrap(), 6.0);§Out cells and requirements
An out cell is always derived by exactly one fixed writer method, with optional
requirements checked after every propagate(). Display labels belong to the caller,
not the sheet. Unlike an ordinary derived cell, an
out cell can never be write()-ed or claimed as another method’s output — but it
remains an ordinary, freely-referenceable cell everywhere else (as another
relationship’s input, a conditional’s match subject, and so on).
use adam_rs::{Requirement, Method, Sheet};
let mut sheet = Sheet::new();
let width = sheet.add_cell(0_i32);
let height = sheet.add_cell(0_i32);
let max_area = sheet.add_cell(100_i32);
let area = sheet.add_cell(0_i32);
let writer = Method::from_fn_2_1([width, height], area, |w: &i32, h: &i32| {
w.checked_mul(*h).ok_or_else(|| anyhow::anyhow!("overflow"))
});
let area_cell = sheet
.add_out(
writer,
vec![Requirement::from_fn_2([area, max_area], |a: &i32, max: &i32| Ok(a <= max))],
)
.unwrap();
sheet.write(width, 20_i32).unwrap();
sheet.write(height, 3_i32).unwrap();
sheet.propagate().unwrap();
assert!(sheet.cell_requirements_valid(area_cell));
sheet.write(height, 30_i32).unwrap();
sheet.propagate().unwrap();
assert!(!sheet.cell_requirements_valid(area_cell));§Filters
A filter conforms or rejects a value written externally to its cell — applied
live by propagate() against the cell’s own current value, not synchronously by
write(). It’s also re-checked, as a non-gating diagnostic only, against a value
a relationship’s method derives for that cell — a derived value is never
corrected, only flagged.
use adam_rs::{Filter, Method, Sheet};
let mut sheet = Sheet::new();
let a = sheet.add_cell(0_i32);
let b = sheet.add_cell(0_i32);
sheet
.add_filter(a, Filter::from_fn_0(|x: &i32| Ok((*x).clamp(0, 100))))
.unwrap();
sheet
.add_filter(b, Filter::from_fn_0(|x: &i32| Ok((*x).clamp(0, 100))))
.unwrap();
sheet
.add_relationship(vec![Method::from_fn_1_1(a, b, |x: &i32| Ok(*x * 2))])
.unwrap();
// write() leaves the raw value in source; it is conformed by propagate()...
sheet.write(a, 500_i32).unwrap();
assert_eq!(*sheet.read::<i32>(a).unwrap(), 500);
sheet.propagate().unwrap();
assert_eq!(*sheet.read::<i32>(a).unwrap(), 100);
// A derived value that would fail the same filter is only diagnosed, never
// corrected: `b` doubles `a`'s now-conformed value, still exceeding the filter's range.
assert_eq!(*sheet.read::<i32>(b).unwrap(), 200);
assert!(sheet.filter_violated_cells().any(|id| id == b));§Invariants
Enforced by code:
- Every relationship’s methods share the same
inputs ∪ outputscell set (Sheet::add_relationshipvalidation;Error::MismatchedMethodCells). - No two relationships may claim the same cell as an output in one round —
self-referencing outputs are claimed exactly like any other
(
Error::Conflictwhen infeasible). - The selected methods’ induced dependency digraph is acyclic before execution
(
Error::Cyclewhen not). - A self-referencing input never reads a same-round derived value. Instead it reads a
seed reconstructed from
source:planner::evaluate_seedsfolds every other relationship incident to the cell (excluding its own claimant) through that relationship’s cell-producing method, giving the cell’s aspiration before its claimant tightens it; a cell no other relationship contributes to just reads its ownsource. The seed is rebuilt fromsourceevery round, so it is never stale and never accumulates. sourceis written only bySheet::write/Sheet::add_cell, never by method or filter execution.- An
Out-kind cell can never beSheet::write-ed or claimed as another method’s output (Error::InvalidCellKind).
Enforced only by convention (caller contract, not checked by the runtime):
- All method, filter, conditional-expression, and requirement callbacks satisfy the callback-purity contract above, with no invocation guarantee.
- A self-referencing method must be idempotent: applying it twice to the same inputs must produce the same result as applying it once.
- A filter must be idempotent and conform its cell’s value using its argument cells’ values.
- Iteration order used to break ties among equal-strength cells is not stable API and must not be relied on by callers.
The source-capture property:
Capturing every cell’s source value and reapplying the highest-strength sources
reconstructs the sheet’s current state, but not what a subsequent edit will do,
since no method-selection state is captured, only values. This holds even for a
self-referencing cell: planner::evaluate_seeds reconstructs its contribution to a
sibling relationship’s method purely from current source values (recursively, for
any other self-referencing cell that sibling method itself reads), so no state
beyond source needs to be captured to reproduce the derived state.
Re-exports§
pub use cell::CellId;pub use cell::CellKind;pub use conditional::ConditionalId;pub use conditional::MatchExpr;pub use error::Error;pub use error::ErrorSite;pub use filter::Filter;pub use filter::FilterKind;pub use filter::FilterViolation;pub use relationship::Method;pub use relationship::RelationshipId;pub use requirement::Requirement;pub use requirement::RequirementId;pub use sheet::Sheet;
Modules§
- cell
- Value cells in the property model bipartite graph.
- conditional
- Conditionals in the property model: match-subject branching.
- error
- The
Errortype returned by all fallible operations in this crate. - filter
- Input filters: idempotent, per-cell domain constraints.
- relationship
- Relationships and methods in the property model bipartite graph.
- requirement
- Boolean checks attached to a cell, identified by stable handles.
- sheet
- The
Sheetowns and manages a property model constraint graph.