Skip to main content

automation_structures/compositions/
reduction.rs

1// AuditSink-backed ReductionStream named composition.
2//
3// ReductionStreamFromAuditSink.tla has no held glue state: the source is
4// immutable configuration, the AuditSink log is the consumed prefix,
5// `result` is `last_hash`, and `pos` is `Len(log)`. AdditiveChain instantiates
6// AuditSink's operation with the reduction operator.
7//
8// Overflow ceiling: values bounded to <= 1e9, inputs to <= 1e9 elements,
9// so the accumulator stays under 1e18 < u64::MAX.
10
11use vstd::prelude::*;
12
13#[allow(unused_imports)]
14use crate::primitives::audit_sink::{AdditiveChain, AuditSink, ChainOperation};
15
16verus! {
17
18/// Fold of `s[0..n]`.
19pub open spec fn sum_to(s: Seq<u64>, n: int) -> int
20    decreases n,
21{
22    if n <= 0 {
23        0
24    } else if n > s.len() as int {
25        0
26    } else {
27        s[n - 1] as int + sum_to(s, n - 1)
28    }
29}
30
31/// Fold of the entire sequence.
32pub open spec fn sum_spec(s: Seq<u64>) -> int {
33    sum_to(s, s.len() as int)
34}
35
36/// Partial fold bounded by (max element) * n.
37pub proof fn lemma_sum_to_bounded(s: Seq<u64>, n: int)
38    requires
39        forall|k: int| 0 <= k < s.len() ==> s[k] <= 1_000_000_000u64,
40        0 <= n <= s.len() as int,
41    ensures
42        sum_to(s, n) <= 1_000_000_000 * n,
43    decreases n,
44{
45    if n > 0 {
46        lemma_sum_to_bounded(s, n - 1);
47    }
48}
49
50/// sum_to(s.push(x), n) == sum_to(s, n) for 0 <= n <= |s|.
51proof fn lemma_sum_to_push_prefix(s: Seq<u64>, x: u64, n: int)
52    requires
53        0 <= n <= s.len() as int,
54    ensures
55        sum_to(s.push(x), n) == sum_to(s, n),
56    decreases n,
57{
58    if n > 0 {
59        lemma_sum_to_push_prefix(s, x, n - 1);
60        assert(s.push(x)[n - 1] == s[n - 1]);
61    }
62}
63
64/// sum_spec(s.push(x)) == sum_spec(s) + x.
65/// Re-association: the fold of a prefix plus one element equals the fold of the whole.
66pub proof fn lemma_sum_push(s: Seq<u64>, x: u64)
67    ensures
68        sum_spec(s.push(x)) == sum_spec(s) + x as int,
69{
70    let t = s.push(x);
71    let n = s.len() as int;
72    assert(t.len() == n + 1);
73    lemma_sum_to_push_prefix(s, x, n);
74    assert(t[n] == x);
75    assert(sum_to(t, n + 1) == t[n] as int + sum_to(t, n));
76}
77
78/// Additive ReductionStream assembled from the AuditSink owner.
79pub struct Reducer {
80    /// Immutable reduction source.
81    pub source: Vec<u64>,
82    /// Owner of the consumed prefix, position, and result.
83    pub audit: AuditSink<AdditiveChain>,
84}
85
86impl Reducer {
87    /// AuditSink's log is exactly the consumed source prefix.
88    pub open spec fn prefix_binding(&self) -> bool {
89        &&& self.audit.log.len() <= self.source.len()
90        &&& forall|index: int|
91            #![trigger self.audit.log@[index]]
92            0 <= index < self.audit.log.len() ==>
93                self.audit.log@[index].operation == self.source@[index]
94    }
95
96    /// AuditSink's carry is the fold of the consumed source prefix.
97    pub open spec fn aggregate(&self) -> bool {
98        self.audit.last_hash as int == sum_to(self.source@, self.audit.log.len() as int)
99    }
100
101    /// Overflow ceiling for the additive AuditSink operation.
102    pub open spec fn bounded(&self) -> bool {
103        &&& forall|k: int| 0 <= k < self.source@.len() ==> self.source@[k] <= 1_000_000_000u64
104        &&& self.source@.len() <= 1_000_000_000
105    }
106
107    /// Complete representation invariant of the named composition.
108    pub open spec fn inv(&self) -> bool {
109        &&& self.audit.inv()
110        &&& self.audit.max_log_len == self.source.len()
111        &&& self.prefix_binding()
112        &&& self.aggregate()
113        &&& self.bounded()
114    }
115
116    /// Empty AuditSink over the immutable source.
117    pub fn new(items: Vec<u64>) -> (r: Reducer)
118        requires
119            forall|k: int| 0 <= k < items@.len() ==> items@[k] <= 1_000_000_000u64,
120            items@.len() <= 1_000_000_000,
121        ensures
122            r.inv(),
123            r.source@ == items@,
124            r.audit.log@.len() == 0,
125            r.audit.last_hash == 0,
126    {
127        let audit = AuditSink::with_operator(items.len(), AdditiveChain);
128        let r = Reducer { source: items, audit };
129        proof {
130            assert(r.audit.log@.len() == 0);
131            assert(sum_to(r.source@, 0) == 0);
132        }
133        r
134    }
135
136    /// Number of source elements already consumed.
137    pub fn position(&self) -> (position: usize)
138        ensures position == self.audit.log@.len(),
139    {
140        self.audit.log.len()
141    }
142
143    /// Current additive result, projected from AuditSink's carry.
144    pub fn result(&self) -> (result: u64)
145        ensures result == self.audit.last_hash,
146    {
147        self.audit.last_hash
148    }
149
150    /// Number of source elements not yet consumed.
151    pub fn remaining_len(&self) -> (remaining: usize)
152        requires self.prefix_binding(),
153        ensures remaining == self.source@.len() - self.audit.log@.len(),
154    {
155        self.source.len() - self.audit.log.len()
156    }
157
158    /// Whether the complete source prefix has been reduced.
159    pub fn done(&self) -> (d: bool)
160        requires self.prefix_binding(),
161        ensures
162            d == (self.audit.log@.len() == self.source@.len()),
163    {
164        self.audit.log.len() == self.source.len()
165    }
166
167    /// Consume the next source value through AuditSink's `Record` action.
168    pub fn process(&mut self)
169        requires
170            old(self).inv(),
171            old(self).audit.log@.len() < old(self).source@.len(),
172        ensures
173            final(self).inv(),
174            final(self).source@ == old(self).source@,
175            final(self).audit.log@.len() == old(self).audit.log@.len() + 1,
176            final(self).audit.last_hash
177                == old(self).audit.last_hash
178                    + old(self).source@[old(self).audit.log@.len() as int],
179    {
180        let old_position = self.audit.log.len();
181        let x = self.source[old_position];
182        let ghost old_log = self.audit.log@;
183        let ghost source = self.source@;
184        proof {
185            lemma_sum_to_bounded(self.source@, old_position as int);
186            assert(self.audit.last_hash as int
187                == sum_to(self.source@, old_position as int));
188            assert(self.audit.last_hash as int <= 1_000_000_000 * old_position as int);
189            assert(x <= 1_000_000_000u64);
190            assert(old_position < 1_000_000_000usize);
191            assert(self.audit.last_hash as int + x as int <= 1_000_000_000_000_000_000int);
192            assert(1_000_000_000_000_000_000int < u64::MAX as int);
193            assert(self.audit.operator.enabled(self.audit.last_hash, x));
194        }
195        let accepted = self.audit.record(x);
196        assert(accepted);
197        let _ = accepted;
198
199        proof {
200            assert(self.audit.log@[old_position as int].operation == x);
201            assert(self.prefix_binding()) by {
202                assert forall|index: int|
203                    #![trigger self.audit.log@[index]]
204                    0 <= index < self.audit.log.len() implies
205                        self.audit.log@[index].operation == self.source@[index] by {
206                    if index < old_position {
207                        assert(self.audit.log@[index] == old_log[index]);
208                    } else {
209                        assert(index == old_position);
210                    }
211                }
212            }
213            assert(sum_to(source, old_position as int + 1)
214                == source[old_position as int] as int
215                    + sum_to(source, old_position as int));
216            assert(self.aggregate());
217        }
218    }
219}
220
221// ---------------------------------------------------------------------------
222// Operator-generic batch fold. `Reducer` remains the incremental sum state
223// machine above. `reduce_sum` and `reduce_max` instantiate the standalone fold
224// with distinct operators and identities. Genericity lives at the
225// spec level (`fold_to`, parametrized by a `spec_fn` operator -- Verus's
226// pure, total ghost-function type, callable directly in spec/proof context;
227// an ordinary generic `F: Fn(u64,u64)->u64` cannot be called from ghost code
228// in this Verus version). `reduce_sum`/`reduce_max` are two concrete exec
229// entry points, each proven against the SAME ordered-prefix spec instantiated
230// with its own operator. No reassociation theorem or arbitrary executable
231// operator is claimed.
232/// Fold the first `n` values of `s` from `identity` with `op`.
233pub open spec fn fold_to(s: Seq<u64>, n: int, identity: u64, op: spec_fn(u64, u64) -> u64) -> u64
234    decreases n,
235{
236    if n <= 0 {
237        identity
238    } else if n > s.len() as int {
239        identity
240    } else {
241        op(fold_to(s, n - 1, identity, op), s[n - 1])
242    }
243}
244
245/// Sum-specific boundedness for the generic fold's exec loop: mirrors
246/// lemma_sum_to_bounded above. Boundedness is operator-specific (sum needs a
247/// multiplicative bound; max needs only the input ceiling), so it sits outside
248/// the generic lemmas.
249proof fn lemma_fold_to_bounded_sum(s: Seq<u64>, n: int)
250    requires
251        forall|k: int| 0 <= k < s.len() ==> s[k] <= 1_000_000_000u64,
252        0 <= n <= s.len() as int,
253    ensures
254        fold_to(s, n, 0, |a: u64, b: u64| (a + b) as u64) <= 1_000_000_000 * n,
255    decreases n,
256{
257    if n > 0 {
258        lemma_fold_to_bounded_sum(s, n - 1);
259    }
260}
261
262/// Additive fold, stated against the generic fold_to spec. The value is
263/// identical to sum_spec; this entry point instantiates the generic spec rather
264/// than the sum-specific one above.
265pub fn reduce_sum(items: &[u64]) -> (result: u64)
266    requires
267        forall|k: int| 0 <= k < items@.len() ==> items@[k] <= 1_000_000_000u64,
268        items@.len() <= 1_000_000_000,
269    ensures
270        result as int == fold_to(items@, items@.len() as int, 0, |a: u64, b: u64| (a + b) as u64) as int,
271{
272    let n: usize = items.len();
273    let mut result: u64 = 0;
274    let mut i: usize = 0;
275    while i < n
276        invariant
277            i <= n,
278            n == items@.len(),
279            n <= 1_000_000_000,
280            forall|k: int| 0 <= k < items@.len() ==> items@[k] <= 1_000_000_000u64,
281            result as int == fold_to(items@, i as int, 0, |a: u64, b: u64| (a + b) as u64) as int,
282        decreases n - i,
283    {
284        proof {
285            lemma_fold_to_bounded_sum(items@, i as int);
286        }
287        result = result + items[i];
288        i = i + 1;
289    }
290    result
291}
292
293/// Max fold: a second, idempotent instance of the same ordered-prefix spec.
294pub fn reduce_max(items: &[u64]) -> (result: u64)
295    requires
296        forall|k: int| 0 <= k < items@.len() ==> items@[k] <= 1_000_000_000u64,
297        items@.len() <= 1_000_000_000,
298    ensures
299        result as int == fold_to(items@, items@.len() as int, 0, |a: u64, b: u64| if a > b { a } else { b }) as int,
300{
301    let n: usize = items.len();
302    let mut result: u64 = 0;
303    let mut i: usize = 0;
304    while i < n
305        invariant
306            i <= n,
307            n == items@.len(),
308            forall|k: int| 0 <= k < items@.len() ==> items@[k] <= 1_000_000_000u64,
309            result <= 1_000_000_000u64,
310            result as int == fold_to(items@, i as int, 0, |a: u64, b: u64| if a > b { a } else { b }) as int,
311        decreases n - i,
312    {
313        if items[i] > result {
314            result = items[i];
315        }
316        i = i + 1;
317    }
318    result
319}
320
321}