feat(core): CellKey is Ord and DataStore can log its cell changes (improvise-5r8c)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
56b7770282
commit
5c7a12fd3d
@@ -7,7 +7,7 @@ use super::symbol::{Symbol, SymbolTable};
|
|||||||
|
|
||||||
/// A cell key: (category, item) pairs, always sorted by category name. The
|
/// A cell key: (category, item) pairs, always sorted by category name. The
|
||||||
/// constructors are the only way to build one — context/code/core.md §1.
|
/// constructors are the only way to build one — context/code/core.md §1.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct CellKey(pub Vec<(String, String)>);
|
pub struct CellKey(pub Vec<(String, String)>);
|
||||||
|
|
||||||
impl CellKey {
|
impl CellKey {
|
||||||
@@ -194,6 +194,15 @@ impl RecordEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One cell's move in the store: `old` is `None` for a new cell, `new` is
|
||||||
|
/// `None` for a removal. ADR 009 clause 3; `context/code/improvise-incremental.md`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct CellChange {
|
||||||
|
pub key: CellKey,
|
||||||
|
pub old: Option<CellValue>,
|
||||||
|
pub new: Option<CellValue>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
thread_local! {
|
thread_local! {
|
||||||
/// How many lookups fell through to set narrowing, for the exact-path test.
|
/// How many lookups fell through to set narrowing, for the exact-path test.
|
||||||
@@ -215,6 +224,9 @@ pub struct DataStore {
|
|||||||
/// How many stored cells have each number of coordinates; its largest key
|
/// How many stored cells have each number of coordinates; its largest key
|
||||||
/// says when a partial can match only one exact cell.
|
/// says when a partial can match only one exact cell.
|
||||||
arities: std::collections::BTreeMap<usize, usize>,
|
arities: std::collections::BTreeMap<usize, usize>,
|
||||||
|
/// The changes since `record_changes`, or `None` while nobody listens, so an
|
||||||
|
/// unwatched store keeps no log.
|
||||||
|
changes: Option<Vec<CellChange>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolved through the symbol table and sorted wherever the storage is a
|
/// Resolved through the symbol table and sorted wherever the storage is a
|
||||||
@@ -227,6 +239,7 @@ impl std::fmt::Debug for DataStore {
|
|||||||
index,
|
index,
|
||||||
records,
|
records,
|
||||||
arities: _,
|
arities: _,
|
||||||
|
changes: _,
|
||||||
} = self;
|
} = self;
|
||||||
let cells: Vec<_> = cells
|
let cells: Vec<_> = cells
|
||||||
.iter()
|
.iter()
|
||||||
@@ -302,7 +315,19 @@ impl DataStore {
|
|||||||
self.index.entry(*pair).or_default().insert(ikey.clone());
|
self.index.entry(*pair).or_default().insert(ikey.clone());
|
||||||
}
|
}
|
||||||
let width = ikey.0.len();
|
let width = ikey.0.len();
|
||||||
let is_new_cell = self.cells.insert(ikey, value).is_none();
|
let previous = match &mut self.changes {
|
||||||
|
Some(log) => {
|
||||||
|
let previous = self.cells.insert(ikey, value.clone());
|
||||||
|
log.push(CellChange {
|
||||||
|
key,
|
||||||
|
old: previous.clone(),
|
||||||
|
new: Some(value),
|
||||||
|
});
|
||||||
|
previous
|
||||||
|
}
|
||||||
|
None => self.cells.insert(ikey, value),
|
||||||
|
};
|
||||||
|
let is_new_cell = previous.is_none();
|
||||||
if is_new_cell {
|
if is_new_cell {
|
||||||
*self.arities.entry(width).or_default() += 1;
|
*self.arities.entry(width).or_default() += 1;
|
||||||
}
|
}
|
||||||
@@ -316,6 +341,24 @@ impl DataStore {
|
|||||||
entry.cells += usize::from(is_new_cell);
|
entry.cells += usize::from(is_new_cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start logging every `set` and `remove` as a [`CellChange`], from empty.
|
||||||
|
pub fn record_changes(&mut self) {
|
||||||
|
self.changes = Some(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop logging and drop what was logged.
|
||||||
|
pub fn stop_recording_changes(&mut self) {
|
||||||
|
self.changes = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The changes since the last drain, oldest first. Logging goes on.
|
||||||
|
pub fn drain_changes(&mut self) -> Vec<CellChange> {
|
||||||
|
self.changes
|
||||||
|
.as_mut()
|
||||||
|
.map(std::mem::take)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &CellKey) -> Option<&CellValue> {
|
pub fn get(&self, key: &CellKey) -> Option<&CellValue> {
|
||||||
let ikey = self.lookup_key(key)?;
|
let ikey = self.lookup_key(key)?;
|
||||||
self.cells.get(&ikey)
|
self.cells.get(&ikey)
|
||||||
@@ -340,7 +383,14 @@ impl DataStore {
|
|||||||
let Some(ikey) = self.lookup_key(key) else {
|
let Some(ikey) = self.lookup_key(key) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if self.cells.shift_remove(&ikey).is_some() {
|
if let Some(old) = self.cells.shift_remove(&ikey) {
|
||||||
|
if let Some(log) = &mut self.changes {
|
||||||
|
log.push(CellChange {
|
||||||
|
key: key.clone(),
|
||||||
|
old: Some(old),
|
||||||
|
new: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
if let Some(n) = self.arities.get_mut(&ikey.0.len()) {
|
if let Some(n) = self.arities.get_mut(&ikey.0.len()) {
|
||||||
*n -= 1;
|
*n -= 1;
|
||||||
if *n == 0 {
|
if *n == 0 {
|
||||||
@@ -714,6 +764,17 @@ mod cell_key {
|
|||||||
assert!(!k.matches_partial(&partial));
|
assert!(!k.matches_partial(&partial));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keys_order_by_their_sorted_coordinates() {
|
||||||
|
let a = key(&[("A", "z")]);
|
||||||
|
let b = key(&[("A", "z"), ("B", "a")]);
|
||||||
|
let c = key(&[("A", "z"), ("B", "b")]);
|
||||||
|
let d = key(&[("C", "a")]);
|
||||||
|
let mut keys = vec![d.clone(), c.clone(), a.clone(), b.clone()];
|
||||||
|
keys.sort();
|
||||||
|
assert_eq!(keys, vec![a, b, c, d]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn display_format() {
|
fn display_format() {
|
||||||
let k = key(&[("Region", "East")]);
|
let k = key(&[("Region", "East")]);
|
||||||
@@ -723,7 +784,7 @@ mod cell_key {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod data_store {
|
mod data_store {
|
||||||
use super::{CellKey, CellValue, DataStore};
|
use super::{CellChange, CellKey, CellValue, DataStore};
|
||||||
|
|
||||||
fn key(pairs: &[(&str, &str)]) -> CellKey {
|
fn key(pairs: &[(&str, &str)]) -> CellKey {
|
||||||
CellKey::new(
|
CellKey::new(
|
||||||
@@ -757,6 +818,48 @@ mod data_store {
|
|||||||
assert_eq!(store.get(&k), Some(&CellValue::Number(99.0)));
|
assert_eq!(store.get(&k), Some(&CellValue::Number(99.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_store_reports_no_changes_until_asked_to_record_them() {
|
||||||
|
let mut store = DataStore::new();
|
||||||
|
store.set(key(&[("Region", "East")]), CellValue::Number(1.0));
|
||||||
|
assert!(store.drain_changes().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recording_store_reports_a_new_cell_an_overwrite_and_a_removal() {
|
||||||
|
let mut store = DataStore::new();
|
||||||
|
let k = key(&[("Region", "East")]);
|
||||||
|
store.record_changes();
|
||||||
|
store.set(k.clone(), CellValue::Number(1.0));
|
||||||
|
store.set(k.clone(), CellValue::Number(2.0));
|
||||||
|
store.remove(&k);
|
||||||
|
store.remove(&k);
|
||||||
|
let change = |old: Option<f64>, new: Option<f64>| CellChange {
|
||||||
|
key: k.clone(),
|
||||||
|
old: old.map(CellValue::Number),
|
||||||
|
new: new.map(CellValue::Number),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
store.drain_changes(),
|
||||||
|
vec![
|
||||||
|
change(None, Some(1.0)),
|
||||||
|
change(Some(1.0), Some(2.0)),
|
||||||
|
change(Some(2.0), None)
|
||||||
|
],
|
||||||
|
"removing an absent cell reports nothing"
|
||||||
|
);
|
||||||
|
assert!(store.drain_changes().is_empty(), "a drain empties the log");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_store_that_stopped_recording_reports_nothing_more() {
|
||||||
|
let mut store = DataStore::new();
|
||||||
|
store.record_changes();
|
||||||
|
store.stop_recording_changes();
|
||||||
|
store.set(key(&[("Region", "East")]), CellValue::Number(1.0));
|
||||||
|
assert!(store.drain_changes().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remove_evicts_key() {
|
fn remove_evicts_key() {
|
||||||
let mut store = DataStore::new();
|
let mut store = DataStore::new();
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ license = "Apache-2.0"
|
|||||||
description = "The seam between improvise-core and the incremental circuit: the Derived trait, the Eager oracle and the differential harness (ADR 009 clause 2)."
|
description = "The seam between improvise-core and the incremental circuit: the Derived trait, the Eager oracle and the differential harness (ADR 009 clause 2)."
|
||||||
|
|
||||||
# ADR 009 clause 2: depends on improvise-core, never the other way round. The
|
# ADR 009 clause 2: depends on improvise-core, never the other way round. The
|
||||||
# `zset` edge arrives with the first backend that uses it (step 3), not before.
|
# `zset` edge is the cell delta of step 3 (improvise-5r8c).
|
||||||
[dependencies]
|
[dependencies]
|
||||||
improvise-core = { path = "../improvise-core" }
|
improvise-core = { path = "../improvise-core" }
|
||||||
|
zset = { path = "../zset" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
improvise-io = { path = "../improvise-io" }
|
improvise-io = { path = "../improvise-io" }
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
//! The cell delta: what a write changed, as a Z-set (ADR 009 clause 3).
|
||||||
|
//! Reasoning: `context/code/improvise-incremental.md`.
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use improvise_core::model::cell::{CellChange, CellKey, CellValue};
|
||||||
|
use improvise_core::model::record::RecordHash;
|
||||||
|
use zset::ZSet;
|
||||||
|
|
||||||
|
/// A cell value that compares by the float rules, with every NaN one value: `-0.0`
|
||||||
|
/// equals `+0.0`. Built only by [`DeltaValue::new`], which makes both true of the
|
||||||
|
/// inner value, so the derived-looking order below is a total order.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DeltaValue(CellValue);
|
||||||
|
|
||||||
|
impl DeltaValue {
|
||||||
|
pub fn new(value: &CellValue) -> Self {
|
||||||
|
DeltaValue(match value {
|
||||||
|
CellValue::Number(n) if n.is_nan() => CellValue::Number(f64::NAN),
|
||||||
|
CellValue::Number(n) if *n == 0.0 => CellValue::Number(0.0),
|
||||||
|
other => other.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &CellValue {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for DeltaValue {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match (&self.0, &other.0) {
|
||||||
|
(CellValue::Number(a), CellValue::Number(b)) => a.total_cmp(b),
|
||||||
|
(CellValue::Text(a), CellValue::Text(b)) => a.cmp(b),
|
||||||
|
(CellValue::Error(a), CellValue::Error(b)) => a.cmp(b),
|
||||||
|
(a, b) => rank(a).cmp(&rank(b)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rank(value: &CellValue) -> u8 {
|
||||||
|
match value {
|
||||||
|
CellValue::Number(_) => 0,
|
||||||
|
CellValue::Text(_) => 1,
|
||||||
|
CellValue::Error(_) => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for DeltaValue {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for DeltaValue {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cmp(other) == Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for DeltaValue {}
|
||||||
|
|
||||||
|
/// What the store's cells changed by: `+(k, v)` new, `-(k, v0) + (k, v1)` overwritten,
|
||||||
|
/// `-(k, v0)` removed, and nothing for an overwrite that changes no value.
|
||||||
|
pub type CellDelta = ZSet<(CellKey, DeltaValue)>;
|
||||||
|
|
||||||
|
/// The delta of `changes`, in order. Changes that cancel leave nothing.
|
||||||
|
pub fn cell_delta<'a>(changes: impl IntoIterator<Item = &'a CellChange>) -> CellDelta {
|
||||||
|
let mut delta = CellDelta::new();
|
||||||
|
for change in changes {
|
||||||
|
if let Some(old) = &change.old {
|
||||||
|
delta.insert((change.key.clone(), DeltaValue::new(old)), -1);
|
||||||
|
}
|
||||||
|
if let Some(new) = &change.new {
|
||||||
|
delta.insert((change.key.clone(), DeltaValue::new(new)), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The records `delta` touches, ascending: the record tier of ADR 001's basis.
|
||||||
|
pub fn moved_records(delta: &CellDelta) -> Vec<RecordHash> {
|
||||||
|
let mut records: Vec<RecordHash> = delta
|
||||||
|
.iter()
|
||||||
|
.map(|((key, _), _)| RecordHash::of(key))
|
||||||
|
.collect();
|
||||||
|
records.sort_unstable();
|
||||||
|
records.dedup();
|
||||||
|
records
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn key(region: &str, measure: &str) -> CellKey {
|
||||||
|
CellKey::new(vec![
|
||||||
|
("Region".into(), region.into()),
|
||||||
|
("_Measure".into(), measure.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change(k: &CellKey, old: Option<CellValue>, new: Option<CellValue>) -> CellChange {
|
||||||
|
CellChange {
|
||||||
|
key: k.clone(),
|
||||||
|
old,
|
||||||
|
new,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn num(n: f64) -> CellValue {
|
||||||
|
CellValue::Number(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delta_of(old: Option<CellValue>, new: Option<CellValue>) -> CellDelta {
|
||||||
|
cell_delta(&[change(&key("East", "Revenue"), old, new)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_new_cell_an_overwrite_and_a_removal_have_the_clause_3_shapes() {
|
||||||
|
let k = key("East", "Revenue");
|
||||||
|
let at = |v: f64| (k.clone(), DeltaValue::new(&num(v)));
|
||||||
|
assert_eq!(delta_of(None, Some(num(1.0))), ZSet::singleton(at(1.0), 1));
|
||||||
|
assert_eq!(
|
||||||
|
delta_of(Some(num(1.0)), Some(num(2.0))),
|
||||||
|
ZSet::from_iter([(at(1.0), -1), (at(2.0), 1)])
|
||||||
|
);
|
||||||
|
assert_eq!(delta_of(Some(num(1.0)), None), ZSet::singleton(at(1.0), -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overwriting_a_value_with_one_that_compares_equal_is_the_zero_delta() {
|
||||||
|
let nans = [
|
||||||
|
f64::NAN,
|
||||||
|
-f64::NAN,
|
||||||
|
f64::from_bits(0x7ff0_0000_0000_0001),
|
||||||
|
f64::from_bits(0xfff8_dead_beef_0001),
|
||||||
|
];
|
||||||
|
for a in nans {
|
||||||
|
for b in nans {
|
||||||
|
assert!(
|
||||||
|
delta_of(Some(num(a)), Some(num(b))).is_zero(),
|
||||||
|
"NaN {:#x} over NaN {:#x}",
|
||||||
|
a.to_bits(),
|
||||||
|
b.to_bits()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(delta_of(Some(num(0.0)), Some(num(-0.0))).is_zero());
|
||||||
|
assert!(delta_of(Some(num(-0.0)), Some(num(0.0))).is_zero());
|
||||||
|
let text = || Some(CellValue::Text("x".into()));
|
||||||
|
assert!(delta_of(text(), text()).is_zero());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn values_that_differ_by_the_float_rules_stay_different() {
|
||||||
|
let pairs = [
|
||||||
|
(num(1.0), num(1.0000000000000002)),
|
||||||
|
(num(f64::INFINITY), num(f64::NEG_INFINITY)),
|
||||||
|
(num(f64::NAN), num(0.0)),
|
||||||
|
(num(0.0), CellValue::Text("0".into())),
|
||||||
|
(CellValue::Text("a".into()), CellValue::Error("a".into())),
|
||||||
|
];
|
||||||
|
for (a, b) in pairs {
|
||||||
|
assert!(
|
||||||
|
!delta_of(Some(a.clone()), Some(b.clone())).is_zero(),
|
||||||
|
"{a:?} over {b:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_order_is_total_and_agrees_with_equality() {
|
||||||
|
let values = [
|
||||||
|
num(f64::NEG_INFINITY),
|
||||||
|
num(-1.0),
|
||||||
|
num(-0.0),
|
||||||
|
num(0.0),
|
||||||
|
num(1.0),
|
||||||
|
num(f64::INFINITY),
|
||||||
|
num(f64::NAN),
|
||||||
|
num(-f64::NAN),
|
||||||
|
CellValue::Text(String::new()),
|
||||||
|
CellValue::Text("a".into()),
|
||||||
|
CellValue::Error("a".into()),
|
||||||
|
];
|
||||||
|
let wrapped: Vec<DeltaValue> = values.iter().map(DeltaValue::new).collect();
|
||||||
|
for a in &wrapped {
|
||||||
|
for b in &wrapped {
|
||||||
|
assert_eq!(a.cmp(b), b.cmp(a).reverse());
|
||||||
|
assert_eq!(a == b, a.cmp(b) == Ordering::Equal);
|
||||||
|
for c in &wrapped {
|
||||||
|
if a <= b && b <= c {
|
||||||
|
assert!(a <= c, "{a:?} {b:?} {c:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(wrapped[2], wrapped[3], "-0.0 is +0.0");
|
||||||
|
assert_eq!(wrapped[6], wrapped[7], "every NaN is one value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers_order_by_total_cmp_with_the_canonical_nan_last() {
|
||||||
|
let ascending = [f64::NEG_INFINITY, -1.5, -0.0, 2.0, f64::INFINITY, -f64::NAN];
|
||||||
|
let mut shuffled: Vec<DeltaValue> =
|
||||||
|
[2.0, -f64::NAN, -1.5, f64::INFINITY, -0.0, f64::NEG_INFINITY]
|
||||||
|
.iter()
|
||||||
|
.map(|n| DeltaValue::new(&num(*n)))
|
||||||
|
.collect();
|
||||||
|
shuffled.sort();
|
||||||
|
let want: Vec<DeltaValue> = ascending
|
||||||
|
.iter()
|
||||||
|
.map(|n| DeltaValue::new(&num(*n)))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(shuffled, want);
|
||||||
|
assert_eq!(
|
||||||
|
shuffled[2].value().as_f64().map(f64::to_bits),
|
||||||
|
Some(0),
|
||||||
|
"and the zero among them reads as +0.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wrapped_value_reads_back_with_the_canonical_nan_and_zero() {
|
||||||
|
let nan = DeltaValue::new(&num(-f64::NAN));
|
||||||
|
assert_eq!(
|
||||||
|
nan.value().as_f64().map(f64::to_bits),
|
||||||
|
Some(f64::NAN.to_bits())
|
||||||
|
);
|
||||||
|
let zero = DeltaValue::new(&num(-0.0));
|
||||||
|
assert_eq!(zero.value().as_f64().map(f64::to_bits), Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_change_that_undoes_an_earlier_one_cancels_it() {
|
||||||
|
let k = key("East", "Revenue");
|
||||||
|
let delta = cell_delta(&[
|
||||||
|
change(&k, None, Some(num(1.0))),
|
||||||
|
change(&k, Some(num(1.0)), Some(num(2.0))),
|
||||||
|
change(&k, Some(num(2.0)), None),
|
||||||
|
]);
|
||||||
|
assert!(delta.is_zero());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moved_records_names_each_record_once_whatever_the_measure() {
|
||||||
|
let delta = cell_delta(&[
|
||||||
|
change(&key("East", "Revenue"), None, Some(num(1.0))),
|
||||||
|
change(&key("East", "Cost"), None, Some(num(2.0))),
|
||||||
|
change(&key("West", "Cost"), Some(num(3.0)), Some(num(4.0))),
|
||||||
|
change(&key("South", "Cost"), Some(num(5.0)), Some(num(5.0))),
|
||||||
|
]);
|
||||||
|
let mut want = vec![
|
||||||
|
RecordHash::of(&key("East", "Revenue")),
|
||||||
|
RecordHash::of(&key("West", "Revenue")),
|
||||||
|
];
|
||||||
|
want.sort_unstable();
|
||||||
|
assert_eq!(moved_records(&delta), want, "South wrote nothing new");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
//! The plug: what the evaluator reads, and the oracle that answers it today.
|
//! The plug: what the evaluator reads, and the oracle that answers it today.
|
||||||
//! Reasoning: `context/code/improvise-incremental.md`.
|
//! Reasoning: `context/code/improvise-incremental.md`.
|
||||||
|
|
||||||
|
mod delta;
|
||||||
mod eager;
|
mod eager;
|
||||||
mod edit;
|
mod edit;
|
||||||
mod harness;
|
mod harness;
|
||||||
|
|
||||||
|
pub use delta::{CellDelta, DeltaValue, cell_delta, moved_records};
|
||||||
pub use eager::Eager;
|
pub use eager::Eager;
|
||||||
pub use edit::Edit;
|
pub use edit::Edit;
|
||||||
pub use harness::{
|
pub use harness::{
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use improvise_core::model::cell::{CellKey, CellValue};
|
|||||||
use improvise_core::model::{Aggregation, Model};
|
use improvise_core::model::{Aggregation, Model};
|
||||||
use improvise_core::view::Axis;
|
use improvise_core::view::Axis;
|
||||||
use improvise_incremental::{
|
use improvise_incremental::{
|
||||||
Derived, Divergence, Eager, Edit, Maintained, Probe, differential, probes, same_value,
|
CellDelta, DeltaValue, Derived, Divergence, Eager, Edit, Maintained, Probe, cell_delta,
|
||||||
|
differential, probes, same_value,
|
||||||
};
|
};
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -594,6 +595,50 @@ fn any_value() -> impl Strategy<Value = CellValue> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The store as a Z-set, every cell at weight one.
|
||||||
|
fn snapshot(model: &Model) -> CellDelta {
|
||||||
|
model
|
||||||
|
.data
|
||||||
|
.iter_cells()
|
||||||
|
.map(|(k, v)| ((k, DeltaValue::new(v)), 1))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Values the delta must treat as the float rules do: both zeros, several NaNs.
|
||||||
|
fn delta_value() -> impl Strategy<Value = CellValue> {
|
||||||
|
prop_oneof![
|
||||||
|
(-2i32..3).prop_map(|n| CellValue::Number(f64::from(n))),
|
||||||
|
Just(CellValue::Number(-0.0)),
|
||||||
|
Just(CellValue::Number(f64::NAN)),
|
||||||
|
Just(CellValue::Number(-f64::NAN)),
|
||||||
|
Just(CellValue::Number(f64::from_bits(0x7ff0_0000_0000_0001))),
|
||||||
|
prop::sample::select(vec!["n", "x"]).prop_map(|t| CellValue::Text(t.into())),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_edit() -> impl Strategy<Value = Edit> {
|
||||||
|
prop_oneof![
|
||||||
|
(any_key(), delta_value()).prop_map(|(k, v)| Edit::SetCell(k, v)),
|
||||||
|
any_key().prop_map(Edit::ClearCell),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn the_cell_delta_of_an_edit_is_the_store_after_minus_the_store_before(
|
||||||
|
edits in prop::collection::vec(cell_edit(), 0..12)
|
||||||
|
) {
|
||||||
|
let mut model = hand_model();
|
||||||
|
model.data.record_changes();
|
||||||
|
for edit in &edits {
|
||||||
|
let before = snapshot(&model);
|
||||||
|
edit.apply(&mut model);
|
||||||
|
let delta = cell_delta(&model.data.drain_changes());
|
||||||
|
prop_assert_eq!(delta, &snapshot(&model) - &before, "after {:?}", edit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn any_edit() -> impl Strategy<Value = Edit> {
|
fn any_edit() -> impl Strategy<Value = Edit> {
|
||||||
prop_oneof![
|
prop_oneof![
|
||||||
(any_key(), any_value()).prop_map(|(k, v)| Edit::SetCell(k, v)),
|
(any_key(), any_value()).prop_map(|(k, v)| Edit::SetCell(k, v)),
|
||||||
|
|||||||
Reference in New Issue
Block a user