Skip to main content

sim_incremental_core/dataflow/
lattice.rs

1//! Checked semilattice state and transfer-policy admission.
2
3use crate::ValueFingerprint;
4use std::{error::Error, fmt};
5
6/// State that can report the memory it contributes to a dataflow solution.
7pub trait StateSize {
8    /// Returns the state payload's accounted size in bytes.
9    fn state_size(&self) -> usize;
10}
11
12/// A join semilattice with a least element.
13///
14/// Implementations are admitted only after [`LawSuite`] checks their observable
15/// laws over the representative states supplied by the consumer.
16pub trait JoinSemilattice: Clone + Eq + StateSize {
17    /// Returns the least element.
18    fn bottom(&self) -> Self;
19
20    /// Returns the least upper bound of `self` and `other`.
21    fn join(&self, other: &Self) -> Self;
22
23    /// Reports the semilattice partial order.
24    fn less_equal(&self, other: &Self) -> bool;
25}
26
27/// A deterministic, inflationary dataflow transfer with stable proof identity.
28///
29/// A policy is deliberately an object rather than a closure: its fingerprint
30/// participates in cache and proof identity.
31pub trait TransferPolicy<S> {
32    /// Stable identity of the policy's semantics and configuration.
33    fn fingerprint(&self) -> ValueFingerprint;
34
35    /// Accounts for policy configuration retained by an admitted analysis.
36    fn policy_size(&self) -> usize;
37
38    /// Computes the next state.
39    fn transfer(&self, state: &S) -> S;
40}
41
42/// A law whose failure makes a lattice or transfer policy inadmissible.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum DataflowLaw {
45    /// `a join a = a`.
46    JoinIdempotent,
47    /// `a join b = b join a`.
48    JoinCommutative,
49    /// `(a join b) join c = a join (b join c)`.
50    JoinAssociative,
51    /// Bottom is below every state and is the identity of join.
52    Bottom,
53    /// The declared order agrees with join and is reflexive and antisymmetric.
54    PartialOrderConsistent,
55    /// Repeated evaluation at one input produces the same result.
56    TransferDeterministic,
57    /// Transfer preserves ordering between comparable inputs.
58    TransferMonotone,
59    /// Transfer never retracts facts from its input.
60    TransferProgress,
61}
62
63/// A precise admission refusal.
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct LawViolation {
66    law: DataflowLaw,
67    witnesses: Box<[usize]>,
68}
69
70impl LawViolation {
71    fn new(law: DataflowLaw, witnesses: impl Into<Box<[usize]>>) -> Self {
72        Self {
73            law,
74            witnesses: witnesses.into(),
75        }
76    }
77
78    /// Returns the law that was broken.
79    #[must_use]
80    pub const fn law(&self) -> DataflowLaw {
81        self.law
82    }
83
84    /// Returns indices into the admission sample set that witness the failure.
85    #[must_use]
86    pub fn witnesses(&self) -> &[usize] {
87        &self.witnesses
88    }
89}
90
91impl fmt::Display for LawViolation {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(
94            formatter,
95            "dataflow admission failed {:?} at sample indices {:?}",
96            self.law, self.witnesses
97        )
98    }
99}
100
101impl Error for LawViolation {}
102
103/// Public reusable law suite for lattice and transfer admission.
104#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
105pub struct LawSuite;
106
107impl LawSuite {
108    /// Checks all semilattice laws over a non-empty representative state set.
109    pub fn check_lattice<S: JoinSemilattice>(samples: &[S]) -> Result<(), LawViolation> {
110        let Some(first) = samples.first() else {
111            return Err(LawViolation::new(DataflowLaw::Bottom, Vec::new()));
112        };
113        let bottom = first.bottom();
114        for (a_index, a) in samples.iter().enumerate() {
115            if a.join(a) != *a {
116                return Err(LawViolation::new(DataflowLaw::JoinIdempotent, [a_index]));
117            }
118            if !bottom.less_equal(a) || bottom.join(a) != *a || a.join(&bottom) != *a {
119                return Err(LawViolation::new(DataflowLaw::Bottom, [a_index]));
120            }
121            if !a.less_equal(a) {
122                return Err(LawViolation::new(
123                    DataflowLaw::PartialOrderConsistent,
124                    [a_index],
125                ));
126            }
127            for (b_index, b) in samples.iter().enumerate() {
128                if a.join(b) != b.join(a) {
129                    return Err(LawViolation::new(
130                        DataflowLaw::JoinCommutative,
131                        [a_index, b_index],
132                    ));
133                }
134                let join_order = a.join(b) == *b;
135                if a.less_equal(b) != join_order || (a.less_equal(b) && b.less_equal(a) && a != b) {
136                    return Err(LawViolation::new(
137                        DataflowLaw::PartialOrderConsistent,
138                        [a_index, b_index],
139                    ));
140                }
141                for (c_index, c) in samples.iter().enumerate() {
142                    if a.join(b).join(c) != a.join(&b.join(c)) {
143                        return Err(LawViolation::new(
144                            DataflowLaw::JoinAssociative,
145                            [a_index, b_index, c_index],
146                        ));
147                    }
148                }
149            }
150        }
151        Ok(())
152    }
153
154    /// Checks deterministic, monotone, inflationary transfer over the samples.
155    pub fn check_transfer<S, P>(policy: &P, samples: &[S]) -> Result<(), LawViolation>
156    where
157        S: JoinSemilattice,
158        P: TransferPolicy<S>,
159    {
160        for (a_index, a) in samples.iter().enumerate() {
161            let output = policy.transfer(a);
162            if output != policy.transfer(a) {
163                return Err(LawViolation::new(
164                    DataflowLaw::TransferDeterministic,
165                    [a_index],
166                ));
167            }
168            if !a.less_equal(&output) {
169                return Err(LawViolation::new(DataflowLaw::TransferProgress, [a_index]));
170            }
171            for (b_index, b) in samples.iter().enumerate() {
172                if a.less_equal(b) && !output.less_equal(&policy.transfer(b)) {
173                    return Err(LawViolation::new(
174                        DataflowLaw::TransferMonotone,
175                        [a_index, b_index],
176                    ));
177                }
178            }
179        }
180        Ok(())
181    }
182}
183
184/// A transfer policy that passed the public dataflow law suite.
185#[derive(Clone, Debug)]
186pub struct AdmittedTransfer<P> {
187    policy: P,
188    fingerprint: ValueFingerprint,
189    policy_size: usize,
190}
191
192impl<P> AdmittedTransfer<P> {
193    /// Admits a policy only when both lattice and transfer laws hold.
194    pub fn admit<S>(policy: P, samples: &[S]) -> Result<Self, LawViolation>
195    where
196        S: JoinSemilattice,
197        P: TransferPolicy<S>,
198    {
199        LawSuite::check_lattice(samples)?;
200        LawSuite::check_transfer(&policy, samples)?;
201        let fingerprint = policy.fingerprint();
202        let policy_size = policy.policy_size();
203        Ok(Self {
204            policy,
205            fingerprint,
206            policy_size,
207        })
208    }
209
210    /// Returns the stable identity captured at admission.
211    #[must_use]
212    pub const fn fingerprint(&self) -> ValueFingerprint {
213        self.fingerprint
214    }
215
216    /// Returns the retained policy size captured at admission.
217    #[must_use]
218    pub const fn policy_size(&self) -> usize {
219        self.policy_size
220    }
221
222    /// Applies the admitted policy.
223    pub fn transfer<S>(&self, state: &S) -> S
224    where
225        P: TransferPolicy<S>,
226    {
227        self.policy.transfer(state)
228    }
229
230    /// Returns the admitted policy object.
231    #[must_use]
232    pub const fn policy(&self) -> &P {
233        &self.policy
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    impl StateSize for u8 {
242        fn state_size(&self) -> usize {
243            size_of::<Self>()
244        }
245    }
246
247    impl JoinSemilattice for u8 {
248        fn bottom(&self) -> Self {
249            0
250        }
251
252        fn join(&self, other: &Self) -> Self {
253            *self | *other
254        }
255
256        fn less_equal(&self, other: &Self) -> bool {
257            self & other == *self
258        }
259    }
260
261    #[derive(Clone, Debug)]
262    struct AddFacts(u8);
263
264    impl TransferPolicy<u8> for AddFacts {
265        fn fingerprint(&self) -> ValueFingerprint {
266            ValueFingerprint::new(u64::from(self.0))
267        }
268
269        fn policy_size(&self) -> usize {
270            size_of::<Self>()
271        }
272
273        fn transfer(&self, state: &u8) -> u8 {
274            state | self.0
275        }
276    }
277
278    #[test]
279    fn public_law_suite_admits_sound_policy_and_accounts_identity() {
280        let samples = [0, 1, 2, 3];
281        LawSuite::check_lattice(&samples).unwrap();
282        LawSuite::check_transfer(&AddFacts(2), &samples).unwrap();
283
284        let admitted = AdmittedTransfer::admit(AddFacts(2), &samples).unwrap();
285        assert_eq!(admitted.fingerprint(), ValueFingerprint::new(2));
286        assert_eq!(admitted.policy_size(), 1);
287        assert_eq!(admitted.transfer(&1), 3);
288        assert_eq!(3_u8.state_size(), 1);
289    }
290
291    #[derive(Clone, Debug)]
292    struct NonMonotone;
293
294    impl TransferPolicy<u8> for NonMonotone {
295        fn fingerprint(&self) -> ValueFingerprint {
296            ValueFingerprint::new(99)
297        }
298
299        fn policy_size(&self) -> usize {
300            0
301        }
302
303        fn transfer(&self, state: &u8) -> u8 {
304            if *state == 0 { 3 } else { *state }
305        }
306    }
307
308    #[test]
309    fn non_monotone_transfer_is_refused_at_admission_with_named_law() {
310        let refusal = AdmittedTransfer::admit(NonMonotone, &[0, 1, 2, 3]).unwrap_err();
311        assert_eq!(refusal.law(), DataflowLaw::TransferMonotone);
312        assert_eq!(refusal.witnesses(), &[0, 1]);
313    }
314}