Skip to main content

automation_structures/primitives/
competitive_selection.rs

1// CompetitiveSelection executable correspondence boundary.
2//
3// CompetitiveSelection allocates scored candidates in three registered modes:
4// hard, soft, and ranked. This module exposes two hard carriers for different
5// consuming boundaries, plus one carrier for each other mode:
6//
7//   HardExclusive (CompetitiveSelectionHardExclusive) — multi-seat allocation
8//     with no candidate held twice; each seat receives the lowest-position
9//     argmax from its currently available pool. A score update invalidates the
10//     whole coupled assignment.
11//   Hard (CompetitiveSelectionHard) — the single-seat hard transition embedded
12//     in SelectThenActuate. Allocation typing, winner optimality, and the
13//     deterministic tie rule are stated separately.
14//
15//   Soft (CompetitiveSelectionSoft) — reserved-floor sequential Webster weights.
16//     BoundedTotal, terminal Normalization, UniversalContribution,
17//     ScoreOrderPreservation, and TieBoundedness are maintained explicitly.
18//
19//   Ranked (CompetitiveSelectionRanked) — top-K selection.
20//     BoundedMultiplicity : |selected| <= K
21//     ThresholdOptimality : every selected scores >= every non-selected
22//
23// Each mode discharges its checked invariants under its TLA+ actions.
24
25use vstd::prelude::*;
26
27verus! {
28
29// ── shared recursive sum (for Soft's Normalization) ─────────────────────
30
31/// Sum of `s[0..n]`, lifted to int (the SumWeights replacement).
32pub open spec fn sum_to(s: Seq<u64>, n: int) -> int
33    decreases n,
34{
35    if n <= 0 { 0 } else if n > s.len() as int { 0 } else { s[n - 1] as int + sum_to(s, n - 1) }
36}
37
38/// Updating index k shifts the sum by (nv - s[k]).
39pub proof fn lemma_sum_update(s: Seq<u64>, k: int, nv: u64, n: int)
40    requires 0 <= k < n <= s.len(),
41    ensures sum_to(s.update(k, nv), n) == sum_to(s, n) - s[k] as int + nv as int,
42    decreases n,
43{
44    if n == k + 1 {
45        lemma_sum_unaffected(s, k, nv, k);
46    } else {
47        lemma_sum_update(s, k, nv, n - 1);
48    }
49}
50
51/// Updating index k does not change the sum of a prefix that stops at or before k.
52pub proof fn lemma_sum_unaffected(s: Seq<u64>, k: int, nv: u64, m: int)
53    requires 0 <= m <= k < s.len(),
54    ensures sum_to(s.update(k, nv), m) == sum_to(s, m),
55    decreases m,
56{
57    if m > 0 {
58        lemma_sum_unaffected(s, k, nv, m - 1);
59    }
60}
61
62/// The prefix sum is non-negative.
63pub proof fn lemma_sum_nonneg(s: Seq<u64>, n: int)
64    requires 0 <= n <= s.len(),
65    ensures sum_to(s, n) >= 0,
66    decreases n,
67{
68    if n > 0 {
69        lemma_sum_nonneg(s, n - 1);
70    }
71}
72
73/// The whole sum is at least the sum of any two distinct elements.
74pub proof fn lemma_sum_ge_two(s: Seq<u64>, i: int, j: int, n: int)
75    requires 0 <= i < n <= s.len(), 0 <= j < n, i != j,
76    ensures sum_to(s, n) >= s[i] as int + s[j] as int,
77    decreases n,
78{
79    if n - 1 == i {
80        lemma_sum_ge_one(s, j, n - 1);
81        lemma_sum_nonneg(s, n - 1);
82    } else if n - 1 == j {
83        lemma_sum_ge_one(s, i, n - 1);
84        lemma_sum_nonneg(s, n - 1);
85    } else {
86        lemma_sum_ge_two(s, i, j, n - 1);
87    }
88}
89
90/// The whole sum is at least any single element.
91pub proof fn lemma_sum_ge_one(s: Seq<u64>, i: int, n: int)
92    requires 0 <= i < n <= s.len(),
93    ensures sum_to(s, n) >= s[i] as int,
94    decreases n,
95{
96    if n - 1 == i {
97        lemma_sum_nonneg(s, n - 1);
98    } else {
99        lemma_sum_ge_one(s, i, n - 1);
100    }
101}
102
103/// A prefix of a non-negative sequence cannot exceed a longer prefix.
104pub proof fn lemma_sum_prefix_le(s: Seq<u64>, prefix: int, end: int)
105    requires 0 <= prefix <= end <= s.len(),
106    ensures sum_to(s, prefix) <= sum_to(s, end),
107    decreases end - prefix,
108{
109    if prefix < end {
110        lemma_sum_prefix_le(s, prefix, end - 1);
111        assert(sum_to(s, end) == s[end - 1] as int + sum_to(s, end - 1));
112    }
113}
114
115/// Pushing onto a sequence does not change the sum of any existing prefix.
116pub proof fn lemma_sum_push_prefix(s: Seq<u64>, v: u64, m: int)
117    requires 0 <= m <= s.len(),
118    ensures sum_to(s.push(v), m) == sum_to(s, m),
119    decreases m,
120{
121    if m > 0 {
122        lemma_sum_push_prefix(s, v, m - 1);
123    }
124}
125
126/// An all-zero prefix has sum zero.
127pub proof fn lemma_sum_zero(s: Seq<u64>, n: int)
128    requires 0 <= n <= s.len(), forall|k: int| 0 <= k < n ==> s[k] == 0,
129    ensures sum_to(s, n) == 0,
130    decreases n,
131{
132    if n > 0 {
133        lemma_sum_zero(s, n - 1);
134    }
135}
136
137// ── Hard mode: one winner per seat (argmax) ─────────────────────────────
138
139/// Hard competitive selection over one seat's candidate scores.
140pub struct CompetitiveSelectionHard {
141    /// Candidate scores by candidate index.
142    pub scores: Vec<u64>,
143    /// The winner's index, or None for NULL (no allocation).
144    pub allocation: Option<usize>,
145}
146
147impl CompetitiveSelectionHard {
148    /// TLA+ `AllocationTyping`: a seat's allocation is a candidate or
149    /// NULL, captured by Option<usize> with an in-range winner. `winner_optimality`
150    /// entails it (that clause also requires `w < self.scores.len()`), so stating it
151    /// separately leaves the strength of `inv()` unchanged.
152    pub open spec fn allocation_typing(&self) -> bool {
153        self.allocation matches Option::Some(w) ==> w < self.scores.len()
154    }
155
156    /// TLA+ `WinnerOptimality`: a non-NULL winner scores at least as high as
157    /// every candidate.
158    pub open spec fn winner_optimality(&self) -> bool {
159        self.allocation matches Option::Some(w) ==>
160            (w < self.scores.len()
161                && forall|c: int| 0 <= c < self.scores.len() ==> #[trigger] self.scores@[c] <= self.scores@[w as int])
162    }
163
164    /// TLA+ `WinnerTieBreak`: among candidates tied with the winner on score,
165    /// the winner has the lowest index.
166    ///
167    /// `Evaluate` chooses the lowest-index argmax. Winner
168    /// optimality alone permits any argmax, so this separate contract carries
169    /// deterministic tie correspondence for all inputs.
170    pub open spec fn winner_tie_break(&self) -> bool {
171        self.allocation matches Option::Some(w) ==>
172            (w < self.scores.len()
173                && forall|c: int| 0 <= c < self.scores.len() ==>
174                        (#[trigger] self.scores@[c] == self.scores@[w as int] ==> w <= c))
175    }
176
177    /// Whether allocation typing, optimality, and deterministic tie-breaking hold.
178    pub open spec fn inv(&self) -> bool {
179        self.allocation_typing() && self.winner_optimality() && self.winner_tie_break()
180    }
181
182    /// Empty allocation, all scores 0 (TLA+ Init).
183    pub fn new(num_candidates: usize) -> (h: CompetitiveSelectionHard)
184        ensures
185            h.scores@.len() == num_candidates,
186            forall|c: int| 0 <= c < num_candidates ==> h.scores@[c] == 0,
187            h.allocation is None,
188            h.inv(),
189    {
190        let mut scores: Vec<u64> = Vec::new();
191        let mut i: usize = 0;
192        while i < num_candidates
193            invariant
194                i <= num_candidates,
195                scores.len() == i,
196                forall|k: int| 0 <= k < i ==> scores@[k] == 0,
197            decreases num_candidates - i,
198        {
199            scores.push(0);
200            i = i + 1;
201        }
202        CompetitiveSelectionHard { scores, allocation: None }
203    }
204
205    /// Evaluate: allocate the seat to the highest-scoring candidate (argmax).
206    /// Realises the TLA+ Evaluate action; re-establishes WinnerOptimality.
207    pub fn evaluate(&mut self)
208        requires old(self).scores.len() >= 1,
209        ensures
210            final(self).scores@ == old(self).scores@,
211            final(self).allocation is Some,
212            final(self).inv(),
213    {
214        let n = self.scores.len();
215        let mut best: usize = 0;
216        let mut i: usize = 1;
217        while i < n
218            invariant
219                1 <= i <= n,
220                best < i,
221                n == self.scores.len(),
222                forall|c: int| 0 <= c < i ==> #[trigger] self.scores@[c] <= self.scores@[best as int],
223                // Tie-break preservation: among the candidates seen so far that
224                // tie with `best`, `best` has the lowest index. Maintained by the
225                // strict `>` in the body: a `>=` would take the later index.
226                forall|c: int| 0 <= c < i ==>
227                    (#[trigger] self.scores@[c] == self.scores@[best as int] ==> best <= c),
228            decreases n - i,
229        {
230            if self.scores[i] > self.scores[best] {
231                best = i;
232            }
233            i = i + 1;
234        }
235        self.allocation = Some(best);
236    }
237
238    /// Update one candidate's score; invalidate the allocation (TLA+ UpdateScore).
239    pub fn update_score(&mut self, c: usize, v: u64)
240        requires c < old(self).scores.len(),
241        ensures
242            final(self).scores@ == old(self).scores@.update(c as int, v),
243            final(self).allocation is None,
244            final(self).inv(),
245    {
246        self.scores.set(c, v);
247        self.allocation = None;
248    }
249}
250
251// ── Hard-exclusive mode: multi-seat available-pool argmax ──────────────
252
253/// CompetitiveSelectionHardExclusive carrier. Candidate indices
254/// are the executable WEnum order, so numeric order is exactly Pos order for
255/// deterministic ties. The carrier applies the cross-seat availability filter
256/// during argmax selection and globally invalidates the coupled assignment on
257/// a score update.
258pub struct CompetitiveSelectionHardExclusive {
259    /// Number of independently allocated seats.
260    pub num_seats: usize,
261    /// Number of candidates shared by every seat.
262    pub num_candidates: usize,
263    /// Inclusive score ceiling.
264    pub max_score: u64,
265    /// Selected candidate by seat, encoded as `u64`.
266    pub allocation: Vec<Option<u64>>,
267    /// Candidate scores indexed by seat and candidate.
268    pub scores: Vec<Vec<u64>>,
269}
270
271impl CompetitiveSelectionHardExclusive {
272    /// Whether seat allocations and score rows have valid shape and bounds.
273    pub open spec fn type_invariant(&self) -> bool {
274        &&& self.num_candidates >= 1
275        &&& self.allocation.len() == self.num_seats
276        &&& self.scores.len() == self.num_seats
277        &&& (forall|s: int|
278            0 <= s < self.num_seats ==> (#[trigger] self.scores@[s]).len() == self.num_candidates)
279        &&& (forall|s: int|
280            #![trigger self.allocation@[s]]
281            0 <= s < self.num_seats ==> (self.allocation@[s] matches Some(w) ==>
282                (w as int) < self.num_candidates as int))
283        &&& (forall|s: int, c: int|
284            0 <= s < self.num_seats && 0 <= c < self.num_candidates as int ==>
285                #[trigger] self.scores@[s]@[c] <= self.max_score)
286    }
287
288    /// TLA+ Available(s), with Candidates represented by index order.
289    pub open spec fn available(&self, s: int, c: int) -> bool {
290        &&& 0 <= s < self.num_seats
291        &&& 0 <= c < self.num_candidates as int
292        &&& forall|t: int|
293            0 <= t < self.num_seats && t != s ==>
294                #[trigger] self.allocation@[t] != Some(c as u64)
295    }
296
297    /// Whether two seats never hold the same candidate.
298    pub open spec fn mutual_exclusion(&self) -> bool {
299        forall|s: int, t: int|
300            #![trigger self.allocation@[s], self.allocation@[t]]
301            0 <= s < self.num_seats && 0 <= t < self.num_seats && s != t
302                && self.allocation@[s] is Some ==>
303                    self.allocation@[s] != self.allocation@[t]
304    }
305
306    /// Whether every retained winner has maximal available score for its seat.
307    pub open spec fn winner_optimality(&self) -> bool {
308        forall|s: int|
309            #![trigger self.allocation@[s]]
310            0 <= s < self.num_seats ==> (self.allocation@[s] matches Some(w) ==>
311                forall|c: int| self.available(s, c) ==>
312                    #[trigger] self.scores@[s]@[c] <= self.scores@[s]@[w as int])
313    }
314
315    /// Whether equal-score winners use the lowest available candidate index.
316    pub open spec fn winner_tie_break(&self) -> bool {
317        forall|s: int|
318            #![trigger self.allocation@[s]]
319            0 <= s < self.num_seats ==> (self.allocation@[s] matches Some(w) ==>
320                forall|c: int| self.available(s, c) &&
321                    #[trigger] self.scores@[s]@[c] == self.scores@[s]@[w as int]
322                        ==> (w as int) <= c)
323    }
324
325    /// Whether all hard-exclusive selection contract clauses hold.
326    pub open spec fn inv(&self) -> bool {
327        self.type_invariant() && self.mutual_exclusion()
328            && self.winner_optimality() && self.winner_tie_break()
329    }
330
331    /// Construct empty allocations and zero scores for every seat.
332    pub fn new(
333        num_seats: usize,
334        num_candidates: usize,
335        max_score: u64,
336    ) -> (r: CompetitiveSelectionHardExclusive)
337        requires num_candidates >= 1,
338        ensures
339            r.num_seats == num_seats,
340            r.num_candidates == num_candidates,
341            r.max_score == max_score,
342            r.allocation.len() == num_seats,
343            r.scores.len() == num_seats,
344            forall|s: int| 0 <= s < num_seats ==> r.allocation@[s] is None,
345            forall|s: int| 0 <= s < num_seats ==>
346                (#[trigger] r.scores@[s]).len() == num_candidates,
347            forall|s: int, c: int|
348                0 <= s < num_seats && 0 <= c < num_candidates ==>
349                    #[trigger] r.scores@[s]@[c] == 0,
350            r.inv(),
351    {
352        let mut allocation: Vec<Option<u64>> = Vec::new();
353        let mut scores: Vec<Vec<u64>> = Vec::new();
354        let mut s: usize = 0;
355        while s < num_seats
356            invariant
357                s <= num_seats,
358                allocation.len() == s,
359                scores.len() == s,
360                forall|i: int| 0 <= i < s ==> allocation@[i] is None,
361                forall|i: int| 0 <= i < s ==>
362                    (#[trigger] scores@[i]).len() == num_candidates,
363                forall|i: int, j: int|
364                    0 <= i < s && 0 <= j < num_candidates ==>
365                        #[trigger] scores@[i]@[j] == 0,
366            decreases num_seats - s,
367        {
368            let mut row: Vec<u64> = Vec::new();
369            let mut c: usize = 0;
370            while c < num_candidates
371                invariant
372                    c <= num_candidates,
373                    row.len() == c,
374                    forall|j: int| 0 <= j < c ==> #[trigger] row@[j] == 0,
375                decreases num_candidates - c,
376            {
377                row.push(0);
378                c = c + 1;
379            }
380            allocation.push(None);
381            scores.push(row);
382            s = s + 1;
383        }
384        CompetitiveSelectionHardExclusive {
385            num_seats,
386            num_candidates,
387            max_score,
388            allocation,
389            scores,
390        }
391    }
392
393    /// Read one in-range seat-candidate score.
394    pub fn score_at(&self, s: usize, c: usize) -> (v: u64)
395        requires
396            s < self.scores.len(),
397            c < self.scores@[s as int].len(),
398        ensures v == self.scores@[s as int]@[c as int],
399    {
400        self.scores[s][c]
401    }
402
403    /// Executable Available(s) membership check.
404    pub fn candidate_available(&self, s: usize, c: usize) -> (available: bool)
405        requires
406            self.type_invariant(),
407            s < self.num_seats,
408            c < self.num_candidates,
409        ensures available == self.available(s as int, c as int),
410    {
411        let mut t: usize = 0;
412        while t < self.num_seats
413            invariant
414                t <= self.num_seats,
415                s < self.num_seats,
416                c < self.num_candidates,
417                self.type_invariant(),
418                forall|j: int| 0 <= j < t && j != s as int ==>
419                    #[trigger] self.allocation@[j] != Some(c as u64),
420            decreases self.num_seats - t,
421        {
422            if t != s && self.allocation[t] == Some(c as u64) {
423                return false;
424            }
425            t = t + 1;
426        }
427        true
428    }
429
430    /// Executable guard for `Available(s) /= {}`.
431    pub fn has_available(&self, s: usize) -> (available: bool)
432        requires
433            self.type_invariant(),
434            s < self.num_seats,
435        ensures available == (exists|c: int| self.available(s as int, c)),
436    {
437        let mut c: usize = 0;
438        while c < self.num_candidates
439            invariant
440                c <= self.num_candidates,
441                s < self.num_seats,
442                self.type_invariant(),
443                forall|j: int| 0 <= j < c ==>
444                    !self.available(s as int, j),
445            decreases self.num_candidates - c,
446        {
447            if self.candidate_available(s, c) {
448                return true;
449            }
450            c = c + 1;
451        }
452        false
453    }
454
455    /// TLA+ Evaluate(s): atomically select the lowest-index argmax from the
456    /// candidates not held by another seat.
457    pub fn evaluate(&mut self, s: usize)
458        requires
459            old(self).inv(),
460            s < old(self).num_seats,
461            old(self).allocation@[s as int] is None,
462            exists|c: int| old(self).available(s as int, c),
463        ensures
464            final(self).num_seats == old(self).num_seats,
465            final(self).num_candidates == old(self).num_candidates,
466            final(self).max_score == old(self).max_score,
467            final(self).scores@ == old(self).scores@,
468            final(self).allocation.len() == old(self).allocation.len(),
469            forall|t: int| 0 <= t < final(self).num_seats && t != s as int ==>
470                final(self).allocation@[t] == old(self).allocation@[t],
471            final(self).allocation@[s as int] is Some,
472            final(self).inv(),
473    {
474        let n = self.num_candidates;
475        let mut best: usize = 0;
476        let mut found: bool = false;
477        let mut i: usize = 0;
478        while i < n
479            invariant
480                i <= n,
481                n == self.num_candidates,
482                s < self.num_seats,
483                self.inv(),
484                self.scores@ == old(self).scores@,
485                self.allocation@ == old(self).allocation@,
486                self.num_seats == old(self).num_seats,
487                self.num_candidates == old(self).num_candidates,
488                self.max_score == old(self).max_score,
489                found == (exists|c: int| 0 <= c < i && self.available(s as int, c)),
490                found ==> best < i,
491                found ==> self.available(s as int, best as int),
492                found ==> forall|c: int| 0 <= c < i && self.available(s as int, c) ==>
493                    #[trigger] self.scores@[s as int]@[c]
494                        <= self.scores@[s as int]@[best as int],
495                found ==> forall|c: int| 0 <= c < i && self.available(s as int, c)
496                    && #[trigger] self.scores@[s as int]@[c]
497                        == self.scores@[s as int]@[best as int] ==> best as int <= c,
498            decreases n - i,
499        {
500            if self.candidate_available(s, i) {
501                if !found {
502                    best = i;
503                    found = true;
504                } else {
505                    let vi = self.score_at(s, i);
506                    let vb = self.score_at(s, best);
507                    if vi > vb {
508                        best = i;
509                    }
510                }
511            }
512            i = i + 1;
513        }
514        assert(found);
515        assert(self.available(s as int, best as int));
516        assert(forall|c: int| self.available(s as int, c) ==>
517            #[trigger] self.scores@[s as int]@[c]
518                <= self.scores@[s as int]@[best as int]);
519        assert(forall|c: int| self.available(s as int, c)
520            && #[trigger] self.scores@[s as int]@[c]
521                == self.scores@[s as int]@[best as int] ==> best as int <= c);
522        assert((best as u64) as int == best as int);
523        self.allocation.set(s, Some(best as u64));
524
525        assert(self.type_invariant()) by {
526            assert forall|t: int|
527                #![trigger self.allocation@[t]]
528                0 <= t < self.num_seats implies (self.allocation@[t] matches Some(w) ==>
529                    (w as int) < self.num_candidates as int) by {
530                if t != s as int {
531                    assert(self.allocation@[t] == old(self).allocation@[t]);
532                }
533            }
534        }
535        assert(self.mutual_exclusion()) by {
536            assert forall|a: int, b: int|
537                #![trigger self.allocation@[a], self.allocation@[b]]
538                0 <= a < self.num_seats && 0 <= b < self.num_seats && a != b
539                    && self.allocation@[a] is Some implies
540                        self.allocation@[a] != self.allocation@[b] by {
541                if a == s as int {
542                    assert(self.allocation@[a] == Some(best as u64));
543                    assert(old(self).available(s as int, best as int));
544                    assert(self.allocation@[b] == old(self).allocation@[b]);
545                } else if b == s as int {
546                    assert(self.allocation@[b] == Some(best as u64));
547                    assert(old(self).available(s as int, best as int));
548                    assert(self.allocation@[a] == old(self).allocation@[a]);
549                } else {
550                    assert(self.allocation@[a] == old(self).allocation@[a]);
551                    assert(self.allocation@[b] == old(self).allocation@[b]);
552                }
553            }
554        }
555        assert(self.winner_optimality()) by {
556            assert forall|t: int|
557                #![trigger self.allocation@[t]]
558                0 <= t < self.num_seats implies (self.allocation@[t] matches Some(w) ==>
559                    forall|c: int| self.available(t, c) ==>
560                        #[trigger] self.scores@[t]@[c] <= self.scores@[t]@[w as int]) by {
561                if t == s as int {
562                    assert(self.allocation@[t] == Some(best as u64));
563                    assert forall|c: int| self.available(t, c) implies
564                        old(self).available(t, c) by {
565                        assert forall|r: int|
566                            0 <= r < self.num_seats && r != t implies
567                                old(self).allocation@[r] != Some(c as u64) by {
568                            assert(r != s as int);
569                            assert(self.allocation@[r] == old(self).allocation@[r]);
570                        }
571                    }
572                } else {
573                    assert(self.allocation@[t] == old(self).allocation@[t]);
574                    assert forall|c: int| self.available(t, c) implies
575                        old(self).available(t, c) by {
576                        assert forall|r: int|
577                            0 <= r < self.num_seats && r != t implies
578                                old(self).allocation@[r] != Some(c as u64) by {
579                            if r == s as int {
580                                assert(old(self).allocation@[r] is None);
581                            } else {
582                                assert(self.allocation@[r] == old(self).allocation@[r]);
583                            }
584                        }
585                    }
586                }
587            }
588        }
589        assert(self.winner_tie_break()) by {
590            assert forall|t: int|
591                #![trigger self.allocation@[t]]
592                0 <= t < self.num_seats implies (self.allocation@[t] matches Some(w) ==>
593                    forall|c: int| self.available(t, c)
594                        && #[trigger] self.scores@[t]@[c] == self.scores@[t]@[w as int]
595                            ==> (w as int) <= c) by {
596                if t == s as int {
597                    assert(self.allocation@[t] == Some(best as u64));
598                    assert forall|c: int| self.available(t, c) implies
599                        old(self).available(t, c) by {
600                        assert forall|r: int|
601                            0 <= r < self.num_seats && r != t implies
602                                old(self).allocation@[r] != Some(c as u64) by {
603                            assert(r != s as int);
604                            assert(self.allocation@[r] == old(self).allocation@[r]);
605                        }
606                    }
607                } else {
608                    assert(self.allocation@[t] == old(self).allocation@[t]);
609                    assert forall|c: int| self.available(t, c) implies
610                        old(self).available(t, c) by {
611                        assert forall|r: int|
612                            0 <= r < self.num_seats && r != t implies
613                                old(self).allocation@[r] != Some(c as u64) by {
614                            if r == s as int {
615                                assert(old(self).allocation@[r] is None);
616                            } else {
617                                assert(self.allocation@[r] == old(self).allocation@[r]);
618                            }
619                        }
620                    }
621                }
622            }
623        }
624    }
625
626    /// TLA+ UpdateScore(s,c,v): update one score and invalidate every seat in
627    /// the same commit because availability couples their optimality.
628    pub fn update_score(&mut self, s: usize, c: usize, v: u64)
629        requires
630            old(self).inv(),
631            s < old(self).num_seats,
632            c < old(self).num_candidates,
633            v <= old(self).max_score,
634        ensures
635            final(self).num_seats == old(self).num_seats,
636            final(self).num_candidates == old(self).num_candidates,
637            final(self).max_score == old(self).max_score,
638            final(self).scores.len() == old(self).scores.len(),
639            forall|t: int| 0 <= t < final(self).num_seats && t != s as int ==>
640                final(self).scores@[t]@ == old(self).scores@[t]@,
641            final(self).scores@[s as int]@
642                == old(self).scores@[s as int]@.update(c as int, v),
643            forall|t: int| 0 <= t < final(self).num_seats ==>
644                final(self).allocation@[t] is None,
645            final(self).inv(),
646    {
647        let ghost old_row = self.scores@[s as int]@;
648        let row_len = self.scores[s].len();
649        let mut new_row: Vec<u64> = Vec::new();
650        let mut k: usize = 0;
651        while k < row_len
652            invariant
653                k <= row_len,
654                row_len == self.scores@[s as int].len(),
655                row_len == self.num_candidates,
656                c < row_len,
657                s < self.num_seats,
658                self.scores.len() == self.num_seats,
659                new_row.len() == k,
660                old_row == old(self).scores@[s as int]@,
661                old_row.len() == row_len,
662                forall|j: int| 0 <= j < k ==>
663                    new_row@[j] == (if j == c as int { v } else { old_row[j] }),
664                self.scores@ == old(self).scores@,
665                self.allocation@ == old(self).allocation@,
666                self.num_seats == old(self).num_seats,
667                self.num_candidates == old(self).num_candidates,
668                self.max_score == old(self).max_score,
669            decreases row_len - k,
670        {
671            if k == c {
672                new_row.push(v);
673            } else {
674                let x = self.score_at(s, k);
675                new_row.push(x);
676            }
677            k = k + 1;
678        }
679        assert(new_row@ == old_row.update(c as int, v)) by {
680            assert(new_row.len() == old_row.len());
681            assert forall|j: int| 0 <= j < new_row.len() implies
682                new_row@[j] == old_row.update(c as int, v)[j] by {
683                if j == c as int {
684                } else {
685                }
686            }
687        }
688        self.scores.set(s, new_row);
689        let ghost updated_scores = self.scores@;
690        assert(updated_scores.len() == old(self).scores@.len());
691        assert(updated_scores[s as int]@ == old(self).scores@[s as int]@.update(c as int, v));
692        assert forall|j: int| 0 <= j < self.num_seats && j != s as int implies
693            updated_scores[j]@ == old(self).scores@[j]@ by {
694        }
695
696        let mut t: usize = 0;
697        while t < self.num_seats
698            invariant
699                t <= self.num_seats,
700                self.num_seats == old(self).num_seats,
701                self.num_candidates == old(self).num_candidates,
702                self.max_score == old(self).max_score,
703                self.allocation.len() == self.num_seats,
704                forall|j: int| 0 <= j < t ==> self.allocation@[j] is None,
705                forall|j: int| t <= j < self.num_seats ==>
706                    self.allocation@[j] == old(self).allocation@[j],
707                self.scores@ == updated_scores,
708                updated_scores.len() == old(self).scores@.len(),
709                updated_scores[s as int]@
710                    == old(self).scores@[s as int]@.update(c as int, v),
711                forall|j: int| 0 <= j < self.num_seats && j != s as int ==>
712                    updated_scores[j]@ == old(self).scores@[j]@,
713            decreases self.num_seats - t,
714        {
715            self.allocation.set(t, None);
716            t = t + 1;
717        }
718
719        assert(self.type_invariant()) by {
720            assert forall|i: int| 0 <= i < self.num_seats implies
721                (#[trigger] self.scores@[i]).len() == self.num_candidates by {
722                if i != s as int {
723                    assert(self.scores@[i]@ == old(self).scores@[i]@);
724                }
725            }
726            assert forall|i: int, j: int|
727                0 <= i < self.num_seats && 0 <= j < self.num_candidates as int implies
728                    #[trigger] self.scores@[i]@[j] <= self.max_score by {
729                if i == s as int && j == c as int {
730                } else if i == s as int {
731                } else {
732                    assert(self.scores@[i]@ == old(self).scores@[i]@);
733                }
734            }
735        }
736    }
737}
738
739// ── Soft mode: reserved-floor sequential Sainte-Lague (Webster) ─────────
740//
741// Weights are derived from evolving `extra` state: one reserved unit per
742// candidate, followed by Pool awards to the current highest-priority candidate.
743// Priority is `scores[c] / (2*extra[c]+1)` and is compared by cross-
744// multiplication to avoid division and reals.
745
746/// Cross-multiplied priority comparison: priority(a) >= priority(b), i.e.
747/// scores[a]/(2*extra[a]+1) >= scores[b]/(2*extra[b]+1) without division.
748pub open spec fn priority_ge(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int) -> bool {
749    scores[a] as int * (2 * extra[b] as int + 1) >= scores[b] as int * (2 * extra[a] as int + 1)
750}
751
752/// Strict and equal forms of the same priority comparison. Equality is the
753/// seam at which the WEnum/Pos rule chooses the lowest index.
754pub open spec fn priority_gt(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int) -> bool {
755    scores[a] as int * (2 * extra[b] as int + 1) > scores[b] as int * (2 * extra[a] as int + 1)
756}
757
758/// Whether two candidates have equal cross-multiplied priority.
759pub open spec fn priority_equal(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int) -> bool {
760    scores[a] as int * (2 * extra[b] as int + 1) == scores[b] as int * (2 * extra[a] as int + 1)
761}
762
763/// A strict priority step followed by a non-strict one remains strict. This
764/// is the discriminator needed to preserve the lowest-index tie fact when a
765/// later scan element displaces the running winner.
766pub proof fn lemma_priority_gt_ge_not_equal(
767    scores: Seq<u64>, extra: Seq<u64>, a: int, b: int, c: int,
768)
769    requires
770        priority_gt(scores, extra, a, b),
771        priority_ge(scores, extra, b, c),
772    ensures
773        !priority_equal(scores, extra, a, c),
774{
775    let sa = scores[a] as int;
776    let sb = scores[b] as int;
777    let sc = scores[c] as int;
778    let a_den = 2 * extra[a] as int + 1;
779    let b_den = 2 * extra[b] as int + 1;
780    let c_den = 2 * extra[c] as int + 1;
781    assert(sa * b_den * c_den > sb * a_den * c_den) by (nonlinear_arith)
782        requires sa * b_den > sb * a_den, c_den > 0;
783    assert(sb * c_den * a_den >= sc * b_den * a_den) by (nonlinear_arith)
784        requires sb * c_den >= sc * b_den, a_den > 0;
785    assert(sa * b_den * c_den > sc * b_den * a_den) by (nonlinear_arith)
786        requires
787            sa * b_den * c_den > sb * a_den * c_den,
788            sb * c_den * a_den >= sc * b_den * a_den;
789    assert(sa * c_den > sc * a_den) by (nonlinear_arith)
790        requires sa * b_den * c_den > sc * b_den * a_den, b_den > 0;
791}
792
793/// Winner predicate for one `AssignNext` action, including the lowest-index tie.
794pub open spec fn priority_winner(scores: Seq<u64>, extra: Seq<u64>, w: int) -> bool {
795    0 <= w < scores.len()
796        && scores.len() == extra.len()
797        && (forall|c: int| 0 <= c < scores.len() ==> priority_ge(scores, extra, w, c))
798        && (forall|c: int| 0 <= c < scores.len() && priority_equal(scores, extra, w, c)
799            ==> w <= c)
800}
801
802/// Exact state reached by `steps` lowest-index priority awards from the reserved floor.
803pub open spec fn award_trace(scores: Seq<u64>, extra: Seq<u64>, steps: nat) -> bool
804    decreases steps,
805{
806    if steps == 0 {
807        scores.len() == extra.len()
808            && forall|index: int| 0 <= index < extra.len() ==>
809                #[trigger] extra[index] == 0
810    } else {
811        exists|before: Seq<u64>, winner: int| {
812            &&& award_trace(scores, before, (steps - 1) as nat)
813            &&& priority_winner(scores, before, winner)
814            &&& extra == before.update(winner, (before[winner] + 1) as u64)
815        }
816    }
817}
818
819/// Sainte-Lague's catch-up property: if a's priority dominates b's and a's
820/// score is strictly lower, a's extra must already be strictly lower too.
821/// This is what keeps a strictly-lower-scored candidate from ever catching
822/// up to or passing a strictly-higher-scored one (ScoreOrderPreservation).
823pub proof fn lemma_priority_strict(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int)
824    requires
825        priority_ge(scores, extra, a, b),
826        scores[a] < scores[b],
827    ensures
828        extra[a] < extra[b],
829{
830    if extra[a] >= extra[b] {
831        assert(scores[b] as int * (2 * extra[a] as int + 1)
832            > scores[a] as int * (2 * extra[b] as int + 1)) by (nonlinear_arith)
833            requires scores[a] < scores[b], extra[a] >= extra[b], scores[a] >= 0, scores[b] >= 0,
834                extra[a] >= 0, extra[b] >= 0;
835        assert(false);
836    }
837}
838
839/// Sainte-Lague's tie-bound property: if a's priority dominates b's and the
840/// two scores are equal, a's extra is at most b's (TieBoundedness).
841pub proof fn lemma_priority_tie(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int)
842    requires
843        priority_ge(scores, extra, a, b),
844        scores[a] == scores[b],
845        scores[a] > 0,
846    ensures
847        extra[a] <= extra[b],
848{
849    if extra[a] > extra[b] {
850        assert(scores[b] as int * (2 * extra[a] as int + 1)
851            > scores[a] as int * (2 * extra[b] as int + 1)) by (nonlinear_arith)
852            requires scores[a] == scores[b], scores[a] > 0, extra[a] > extra[b],
853                extra[a] >= 0, extra[b] >= 0;
854        assert(false);
855    }
856}
857
858/// Transitivity of the cross-multiplied priority order: needed because the
859/// linear scan for the current highest-priority candidate only ever compares
860/// the running best against one new candidate at a time, so replacing the
861/// running best requires knowing it still dominates everyone scanned so far,
862/// not just the candidate that just displaced it.  The three-term product
863/// argument: multiply each hypothesis by the missing denominator, chain the
864/// two resulting inequalities through their shared middle term, then cancel
865/// the common positive factor (2*extra[b]+1).
866pub proof fn lemma_priority_ge_trans(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int, c: int)
867    requires
868        priority_ge(scores, extra, a, b),
869        priority_ge(scores, extra, b, c),
870    ensures
871        priority_ge(scores, extra, a, c),
872{
873    let sa = scores[a] as int;
874    let sb = scores[b] as int;
875    let sc = scores[c] as int;
876    let a_den = 2 * extra[a] as int + 1;
877    let b_den = 2 * extra[b] as int + 1;
878    let c_den = 2 * extra[c] as int + 1;
879    assert(sa * b_den * c_den >= sb * a_den * c_den) by (nonlinear_arith)
880        requires sa * b_den >= sb * a_den, c_den >= 0;
881    assert(sb * c_den * a_den >= sc * b_den * a_den) by (nonlinear_arith)
882        requires sb * c_den >= sc * b_den, a_den >= 0;
883    assert(sa * b_den * c_den >= sc * b_den * a_den) by (nonlinear_arith)
884        requires
885            sa * b_den * c_den >= sb * a_den * c_den,
886            sb * c_den * a_den >= sc * b_den * a_den;
887    assert(sa * c_den >= sc * a_den) by (nonlinear_arith)
888        requires
889            sa * b_den * c_den >= sc * b_den * a_den,
890            b_den > 0;
891}
892
893/// Monotonicity of the priority product in `extra`: needed by the Webster
894/// no-transfer invariant's preservation step, where an award to `best`
895/// raises the odd multiplier on the left side of every retained inequality.
896pub proof fn lemma_priority_lhs_monotone(s: int, e: int, f: int)
897    requires s >= 0, e <= f,
898    ensures s * (2 * e + 1) <= s * (2 * f + 1),
899{
900    assert(s * (2 * e + 1) <= s * (2 * f + 1)) by (nonlinear_arith)
901        requires s >= 0, e <= f;
902}
903
904/// Soft competitive selection: weights derived from an evolving `extra` via
905/// the reserved-floor sequential Webster award process, not chosen freely.
906pub struct CompetitiveSelectionSoft {
907    /// Extra Webster awards by candidate index.
908    pub extra: Vec<u64>,
909    /// Candidate scores by candidate index.
910    pub scores: Vec<u64>,
911    /// Total weight available for assignment.
912    pub weight_total: u64,
913    /// MaxScore for this refinement profile.
914    pub max_score: u64,
915}
916
917impl CompetitiveSelectionSoft {
918    /// extra and scores share the Candidates domain, at least one candidate.
919    pub open spec fn well_formed(&self) -> bool {
920        self.extra.len() == self.scores.len() && self.extra.len() > 0
921    }
922
923    /// Executable instance of TLA+ `scores \in [Candidates -> 1..MaxScore]`.
924    pub open spec fn score_bounds(&self) -> bool {
925        forall|i: int| 0 <= i < self.scores.len() ==>
926            #[trigger] self.scores@[i] >= 1 && self.scores@[i] <= self.max_score
927    }
928
929    /// The reserved unit plus whatever extra units this candidate has been
930    /// awarded so far -- derived, not a state variable in its own right.
931    pub open spec fn weight(&self, i: int) -> int {
932        1 + self.extra@[i] as int
933    }
934
935    /// TLA+ `UniversalContribution`: holds unconditionally, weight >= 1.
936    pub open spec fn universal_contribution(&self) -> bool {
937        forall|i: int| 0 <= i < self.extra.len() ==> #[trigger] self.weight(i) > 0
938    }
939
940    /// TLA+ `ScoreOrderPreservation`: a strictly higher score never receives a
941    /// lower weight.
942    pub open spec fn score_order_preservation(&self) -> bool {
943        forall|i: int, j: int|
944            0 <= i < self.scores.len() && 0 <= j < self.scores.len()
945                && self.scores@[i] < self.scores@[j]
946                ==> #[trigger] self.weight(i) <= #[trigger] self.weight(j)
947    }
948
949    /// TLA+ `TieBoundedness`: tied candidates' weights differ by at most one.
950    pub open spec fn tie_boundedness(&self) -> bool {
951        forall|i: int, j: int|
952            0 <= i < self.scores.len() && 0 <= j < self.scores.len()
953                && self.scores@[i] == self.scores@[j]
954                ==> #[trigger] self.weight(i) <= #[trigger] self.weight(j) + 1
955    }
956
957    /// TLA+ `Normalization`: weights sum to the fixed total, Reserved + Pool.
958    pub open spec fn normalization(&self) -> bool {
959        sum_to(self.extra@, self.extra@.len() as int) + self.extra@.len() as int
960            == self.weight_total as int
961    }
962
963    /// TLA+ `BoundedTotal`: partial award states never exceed WeightTotal.
964    pub open spec fn bounded_total(&self) -> bool {
965        sum_to(self.extra@, self.extra@.len() as int) + self.extra@.len() as int
966            <= self.weight_total as int
967    }
968
969    /// TLA+ `Terminal`, stated independently in terms of awarded extra units.
970    pub open spec fn terminal(&self) -> bool {
971        sum_to(self.extra@, self.extra@.len() as int)
972            == self.weight_total as int - self.extra@.len() as int
973    }
974
975    /// TLA+ `Normalization`: exact total is required only at Terminal.
976    pub open spec fn normalization_when_terminal(&self) -> bool {
977        self.terminal() ==> self.normalization()
978    }
979
980    /// Proof-only overflow bound: every candidate's extra is at most
981    /// weight_total, so the cross-multiplied priority comparison and the
982    /// derived weight never overflow u64.
983    pub open spec fn bounded(&self) -> bool {
984        self.weight_total <= 1_000_000_000
985            && self.max_score <= 1_000_000_000
986            && forall|k: int| 0 <= k < self.extra.len() ==> #[trigger] self.extra@[k] <= self.weight_total
987    }
988
989    /// The invariant of every reachable CompetitiveSelectionSoftMutableScores state,
990    /// including the reserved-floor state after a mutable score update.
991    pub open spec fn mutable_score_inv(&self) -> bool {
992        self.well_formed()
993            && self.weight_total as int >= self.extra.len() as int
994            && self.score_bounds()
995            && self.bounded()
996            && self.bounded_total()
997            && self.normalization_when_terminal()
998            && self.universal_contribution()
999            && self.score_order_preservation()
1000            && self.tie_boundedness()
1001    }
1002
1003    /// The finished apportionment is a Webster (Sainte-Lague) one: no candidate
1004    /// holding an awarded unit could have been passed over for it -- the
1005    /// no-transfer characterization. With `normalization()` fixing the total it
1006    /// characterizes the returned Webster allocation as a set. It does not by
1007    /// itself select the unique tie-broken sequence produced by `Init` and
1008    /// `AssignNext` because this clause contains no tie-order premise.
1009    pub open spec fn webster_allocation(&self) -> bool {
1010        forall|i: int, j: int| #![trigger self.extra@[i], self.extra@[j]]
1011            0 <= i < self.extra.len() && 0 <= j < self.extra.len()
1012                && self.extra@[i] >= 1
1013                ==> self.scores@[i] as int * (2 * self.extra@[j] as int + 1)
1014                        >= self.scores@[j] as int * (2 * self.extra@[i] as int - 1)
1015    }
1016
1017    /// Whether mutable-score safety and terminal normalization hold.
1018    pub open spec fn inv(&self) -> bool {
1019        self.mutable_score_inv() && self.normalization()
1020    }
1021
1022    /// Executable accessor for the derived weight (`1 + extra[i]`).
1023    pub fn weight_at(&self, i: usize) -> (w: u64)
1024        requires i < self.extra.len(), self.bounded(),
1025        ensures w as int == self.weight(i as int),
1026    {
1027        self.extra[i] + 1
1028    }
1029
1030    /// Number of units currently assigned, including one reserved unit per candidate.
1031    pub fn assigned_weight(&self) -> (total: u64)
1032        requires self.mutable_score_inv(),
1033        ensures
1034            total as int
1035                == sum_to(self.extra@, self.extra@.len() as int) + self.extra@.len() as int,
1036            total <= self.weight_total,
1037    {
1038        let len = self.extra.len();
1039        let mut total = len as u64;
1040        let mut index: usize = 0;
1041        proof {
1042            assert(self.bounded_total());
1043        }
1044        while index < len
1045            invariant
1046                index <= len,
1047                len == self.extra.len(),
1048                total as int == sum_to(self.extra@, index as int) + len as int,
1049                total <= self.weight_total,
1050                sum_to(self.extra@, len as int) + len as int <= self.weight_total as int,
1051            decreases len - index,
1052        {
1053            proof {
1054                assert(sum_to(self.extra@, index as int + 1)
1055                    == self.extra@[index as int] as int
1056                        + sum_to(self.extra@, index as int));
1057                lemma_sum_prefix_le(self.extra@, index as int + 1, len as int);
1058                assert(sum_to(self.extra@, index as int + 1) + len as int
1059                    <= self.weight_total as int);
1060                assert(total as int + self.extra@[index as int] as int
1061                    <= self.weight_total as int);
1062            }
1063            total = total + self.extra[index];
1064            index += 1;
1065        }
1066        total
1067    }
1068
1069    /// TLA+ `Init`: scores are mutable state and every candidate begins with only
1070    /// its reserved unit. Unlike `new`, this does not run the award process to
1071    /// Terminal; it exposes the action-level carrier state.
1072    pub fn init(scores: Vec<u64>, weight_total: u64, max_score: u64) -> (s: CompetitiveSelectionSoft)
1073        requires
1074            scores.len() >= 1,
1075            weight_total >= scores.len() as u64,
1076            weight_total <= 1_000_000_000,
1077            max_score <= 1_000_000_000,
1078            forall|i: int| 0 <= i < scores.len() ==>
1079                #[trigger] scores@[i] >= 1 && scores@[i] <= max_score,
1080        ensures
1081            s.scores@ == scores@,
1082            s.weight_total == weight_total,
1083            s.max_score == max_score,
1084            s.extra@.len() == scores@.len(),
1085            forall|i: int| 0 <= i < s.extra.len() ==> s.extra@[i] == 0,
1086            award_trace(scores@, s.extra@, 0),
1087            s.mutable_score_inv(),
1088    {
1089        let n = scores.len();
1090        let mut extra: Vec<u64> = Vec::new();
1091        let mut i: usize = 0;
1092        while i < n
1093            invariant
1094                i <= n,
1095                extra.len() == i,
1096                forall|k: int| 0 <= k < i ==> extra@[k] == 0,
1097                sum_to(extra@, i as int) == 0,
1098            decreases n - i,
1099        {
1100            let ghost eold = extra@;
1101            extra.push(0);
1102            proof {
1103                lemma_sum_push_prefix(eold, 0, i as int);
1104                assert(sum_to(extra@, i as int + 1)
1105                    == extra@[i as int] as int + sum_to(extra@, i as int));
1106            }
1107            i = i + 1;
1108        }
1109        proof {
1110            assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1111                && scores@[a] < scores@[b]
1112                implies 1 + extra@[a] as int <= 1 + extra@[b] as int by {}
1113            assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1114                && scores@[a] == scores@[b]
1115                implies 1 + extra@[a] as int <= 1 + extra@[b] as int + 1 by {}
1116        }
1117        CompetitiveSelectionSoft { extra, scores, weight_total, max_score }
1118    }
1119
1120    /// Construct the reserved-floor sequential Webster allocation over
1121    /// `scores`: one reserved unit per candidate, then Pool further units
1122    /// awarded one at a time to the current highest-priority candidate.
1123    /// Mirrors CompetitiveSelectionSoft.tla's Init + AssignNext exactly.
1124    pub fn new(scores: Vec<u64>, weight_total: u64, max_score: u64) -> (s: CompetitiveSelectionSoft)
1125        requires
1126            scores.len() >= 1,
1127            weight_total >= scores.len() as u64,
1128            weight_total <= 1_000_000_000,
1129            max_score <= 1_000_000_000,
1130            forall|i: int| 0 <= i < scores.len() ==> #[trigger] scores@[i] >= 1 && scores@[i] <= max_score,
1131        ensures
1132            s.scores@ == scores@,
1133            s.weight_total == weight_total,
1134            s.max_score == max_score,
1135            s.mutable_score_inv(),
1136            s.inv(),
1137            s.webster_allocation(),
1138            award_trace(
1139                scores@,
1140                s.extra@,
1141                (weight_total - scores@.len() as u64) as nat,
1142            ),
1143    {
1144        let n = scores.len();
1145        let pool: u64 = weight_total - (n as u64);
1146        let mut extra: Vec<u64> = Vec::new();
1147        let mut i: usize = 0;
1148        while i < n
1149            invariant
1150                i <= n,
1151                extra.len() == i,
1152                forall|k: int| 0 <= k < i ==> extra@[k] == 0,
1153                sum_to(extra@, i as int) == 0,
1154            decreases n - i,
1155        {
1156            let ghost eold = extra@;
1157            extra.push(0);
1158            proof {
1159                lemma_sum_push_prefix(eold, 0, i as int);
1160                assert(sum_to(extra@, i as int + 1) == extra@[i as int] as int + sum_to(extra@, i as int));
1161            }
1162            i = i + 1;
1163        }
1164
1165        assert(award_trace(scores@, extra@, 0));
1166        let mut awarded: u64 = 0;
1167        while awarded < pool
1168            invariant
1169                extra.len() == n,
1170                n == scores.len(),
1171                n >= 1,
1172                weight_total >= n as u64,
1173                weight_total <= 1_000_000_000,
1174                pool == weight_total - (n as u64),
1175                awarded <= pool,
1176                forall|k: int| 0 <= k < n ==> #[trigger] scores@[k] >= 1 && scores@[k] <= 1_000_000_000,
1177                sum_to(extra@, n as int) == awarded as int,
1178                forall|k: int| 0 <= k < n ==> #[trigger] extra@[k] <= awarded,
1179                forall|c: int, d: int|
1180                    0 <= c < n && 0 <= d < n && scores@[c] < scores@[d]
1181                        ==> 1 + extra@[c] as int <= 1 + extra@[d] as int,
1182                forall|c: int, d: int|
1183                    0 <= c < n && 0 <= d < n && scores@[c] == scores@[d]
1184                        ==> 1 + extra@[c] as int <= 1 + extra@[d] as int + 1,
1185                // The Webster no-transfer invariant: every candidate
1186                // holding an awarded unit had priority at least everyone's at
1187                // the moment of its last award. Vacuous at the all-zero entry
1188                // state; preserved because each award goes to the scan's
1189                // priority-maximal index.
1190                forall|c: int, d: int|
1191                    0 <= c < n && 0 <= d < n && extra@[c] >= 1
1192                        ==> scores@[c] as int * (2 * extra@[d] as int + 1)
1193                                >= scores@[d] as int * (2 * extra@[c] as int - 1),
1194                award_trace(scores@, extra@, awarded as nat),
1195            decreases pool - awarded,
1196        {
1197            let mut best: usize = 0;
1198            let mut j: usize = 1;
1199            while j < n
1200                invariant
1201                    1 <= j <= n,
1202                    best < j,
1203                    extra.len() == n,
1204                    n == scores.len(),
1205                    weight_total <= 1_000_000_000,
1206                    pool == weight_total - (n as u64),
1207                    awarded <= pool,
1208                    forall|k: int| 0 <= k < n ==> #[trigger] scores@[k] <= 1_000_000_000,
1209                    forall|k: int| 0 <= k < n ==> #[trigger] extra@[k] <= awarded,
1210                    forall|c: int| 0 <= c < j ==> priority_ge(scores@, extra@, best as int, c),
1211                    forall|c: int| 0 <= c < j
1212                        && priority_equal(scores@, extra@, best as int, c)
1213                        ==> best as int <= c,
1214                decreases n - j,
1215            {
1216                proof {
1217                    assert((scores@[j as int] as int) * (2 * (extra@[best as int] as int) + 1)
1218                        <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1219                        requires
1220                            scores@[j as int] as int <= 1_000_000_000,
1221                            extra@[best as int] as int <= 1_000_000_000;
1222                    assert((scores@[best as int] as int) * (2 * (extra@[j as int] as int) + 1)
1223                        <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1224                        requires
1225                            scores@[best as int] as int <= 1_000_000_000,
1226                            extra@[j as int] as int <= 1_000_000_000;
1227                }
1228                let lhs: u128 = (scores[j] as u128) * (2 * (extra[best] as u128) + 1);
1229                let rhs: u128 = (scores[best] as u128) * (2 * (extra[j] as u128) + 1);
1230                proof {
1231                    assert(lhs as int == scores@[j as int] as int * (2 * extra@[best as int] as int + 1));
1232                    assert(rhs as int == scores@[best as int] as int * (2 * extra@[j as int] as int + 1));
1233                }
1234                let old_best: usize = best;
1235                let old_j: usize = j;
1236                let _ = (old_best, old_j);
1237                if lhs > rhs {
1238                    best = j;
1239                    proof {
1240                        assert(priority_ge(scores@, extra@, old_j as int, old_best as int));
1241                        assert(priority_gt(scores@, extra@, old_j as int, old_best as int));
1242                        assert forall|c: int| 0 <= c < old_j as int + 1
1243                            implies priority_ge(scores@, extra@, best as int, c)
1244                        by {
1245                            if c == old_best as int {
1246                                assert(priority_ge(scores@, extra@, old_j as int, old_best as int));
1247                            } else if c < old_j as int {
1248                                lemma_priority_ge_trans(scores@, extra@, old_j as int, old_best as int, c);
1249                            } else {
1250                                // c == old_j as int == best as int, reflexive
1251                            }
1252                        }
1253                        assert forall|c: int| 0 <= c < old_j as int + 1
1254                            && priority_equal(scores@, extra@, best as int, c)
1255                            implies best as int <= c
1256                        by {
1257                            if c < old_j as int {
1258                                lemma_priority_gt_ge_not_equal(
1259                                    scores@,
1260                                    extra@,
1261                                    old_j as int,
1262                                    old_best as int,
1263                                    c,
1264                                );
1265                                assert(false);
1266                            }
1267                        }
1268                    }
1269                } else {
1270                    proof {
1271                        assert(priority_ge(scores@, extra@, old_best as int, old_j as int));
1272                        assert forall|c: int| 0 <= c < old_j as int + 1
1273                            implies priority_ge(scores@, extra@, best as int, c)
1274                        by {
1275                            if c == old_j as int {
1276                                assert(priority_ge(scores@, extra@, old_best as int, old_j as int));
1277                            } else {
1278                                // c < old_j: unchanged from the pre-iteration invariant
1279                            }
1280                        }
1281                        assert forall|c: int| 0 <= c < old_j as int + 1
1282                            && priority_equal(scores@, extra@, best as int, c)
1283                            implies best as int <= c
1284                        by {
1285                            if c == old_j as int {
1286                                assert(best < old_j);
1287                            }
1288                        }
1289                    }
1290                }
1291                j = j + 1;
1292            }
1293            let ghost extra_old: Seq<u64> = extra@;
1294            let ghost best_g: int = best as int;
1295            // Capture the pre-award invariant explicitly, in terms of extra_old:
1296            // once extra is mutated below, the loop invariant clauses (stated in
1297            // terms of the current extra@) stop referring to this snapshot, so
1298            // the facts needed for the case split have to be pinned down first.
1299            proof {
1300                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] < scores@[d]
1301                    implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int
1302                by {}
1303                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] == scores@[d]
1304                    implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1
1305                by {}
1306                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && extra_old[c] >= 1
1307                    implies scores@[c] as int * (2 * extra_old[d] as int + 1)
1308                        >= scores@[d] as int * (2 * extra_old[c] as int - 1)
1309                by {}
1310                assert forall|c: int| 0 <= c < n
1311                    implies priority_ge(scores@, extra_old, best_g, c)
1312                by {}
1313                assert(priority_winner(scores@, extra_old, best_g));
1314            }
1315            extra.set(best, extra[best] + 1);
1316            proof {
1317                assert(award_trace(scores@, extra@, awarded as nat + 1)) by {
1318                    assert(exists|before: Seq<u64>, winner: int| {
1319                        &&& award_trace(scores@, before, awarded as nat)
1320                        &&& priority_winner(scores@, before, winner)
1321                        &&& extra@ == before.update(
1322                            winner,
1323                            (before[winner] + 1) as u64,
1324                        )
1325                    }) by {
1326                        assert(award_trace(scores@, extra_old, awarded as nat));
1327                        assert(priority_winner(scores@, extra_old, best_g));
1328                        assert(extra@ == extra_old.update(
1329                            best_g,
1330                            (extra_old[best_g] + 1) as u64,
1331                        ));
1332                    }
1333                }
1334                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] < scores@[d]
1335                    implies 1 + extra@[c] as int <= 1 + extra@[d] as int
1336                by {
1337                    if c == best_g && d == best_g {
1338                        assert(false);
1339                    } else if c == best_g {
1340                        lemma_priority_strict(scores@, extra_old, best_g, d);
1341                    } else if d == best_g {
1342                        assert(1 + extra_old[c] as int <= 1 + extra_old[best_g] as int);
1343                    } else {
1344                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int);
1345                    }
1346                }
1347                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] == scores@[d]
1348                    implies 1 + extra@[c] as int <= 1 + extra@[d] as int + 1
1349                by {
1350                    if c == best_g && d == best_g {
1351                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1352                    } else if c == best_g {
1353                        if scores@[best_g] > 0 {
1354                            lemma_priority_tie(scores@, extra_old, best_g, d);
1355                        } else {
1356                            assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1357                        }
1358                    } else if d == best_g {
1359                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1360                    } else {
1361                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1362                    }
1363                }
1364                // Preservation of the Webster no-transfer invariant.
1365                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && extra@[c] >= 1
1366                    implies scores@[c] as int * (2 * extra@[d] as int + 1)
1367                        >= scores@[d] as int * (2 * extra@[c] as int - 1)
1368                by {
1369                    if c == best_g && d == best_g {
1370                        lemma_priority_lhs_monotone(scores@[best_g] as int,
1371                            extra@[best_g] as int - 1, extra@[best_g] as int);
1372                    } else if c == best_g {
1373                        // extra@[best] - 1 == extra_old[best]: the scan's exit
1374                        // fact at d is literally the needed inequality.
1375                        assert(priority_ge(scores@, extra_old, best_g, d));
1376                        assert(extra@[d] == extra_old[d]);
1377                        assert(extra@[best_g] as int == extra_old[best_g] as int + 1);
1378                    } else if d == best_g {
1379                        // The old invariant instance at (c, best) plus the left
1380                        // side growing with best's award.
1381                        assert(scores@[c] as int * (2 * extra_old[best_g] as int + 1)
1382                            >= scores@[best_g] as int * (2 * extra_old[c] as int - 1));
1383                        lemma_priority_lhs_monotone(scores@[c] as int,
1384                            extra_old[best_g] as int, extra@[best_g] as int);
1385                        assert(extra@[c] == extra_old[c]);
1386                    } else {
1387                        assert(scores@[c] as int * (2 * extra_old[d] as int + 1)
1388                            >= scores@[d] as int * (2 * extra_old[c] as int - 1));
1389                        assert(extra@[c] == extra_old[c] && extra@[d] == extra_old[d]);
1390                    }
1391                }
1392                let nv: u64 = (extra_old[best_g] + 1) as u64;
1393                lemma_sum_update(extra_old, best_g, nv, n as int);
1394            }
1395            awarded = awarded + 1;
1396        }
1397
1398        proof {
1399            assert(sum_to(extra@, n as int) + n as int == weight_total as int) by {
1400                assert(sum_to(extra@, n as int) == pool as int);
1401            }
1402        }
1403        CompetitiveSelectionSoft { extra, scores, weight_total, max_score }
1404    }
1405
1406    /// Award one further pool unit (TLA+ AssignNext, one step): re-establishes
1407    /// every invariant that holds at every reachable state (ScoreOrderPreservation,
1408    /// TieBoundedness, UniversalContribution); Normalization only holds once
1409    /// the pool is exhausted, matching the TLA+ construction exactly.
1410    pub fn assign_next(&mut self) -> (winner: usize)
1411        requires
1412            old(self).mutable_score_inv(),
1413            (sum_to(old(self).extra@, old(self).extra@.len() as int) + old(self).extra@.len() as int)
1414                < (old(self).weight_total as int),
1415        ensures
1416            winner < old(self).scores.len(),
1417            priority_winner(old(self).scores@, old(self).extra@, winner as int),
1418            final(self).scores@ == old(self).scores@,
1419            final(self).weight_total == old(self).weight_total,
1420            final(self).max_score == old(self).max_score,
1421            final(self).extra@
1422                == old(self).extra@.update(
1423                    winner as int,
1424                    (old(self).extra@[winner as int] + 1) as u64,
1425                ),
1426            old(self).webster_allocation() ==> final(self).webster_allocation(),
1427            final(self).mutable_score_inv(),
1428    {
1429        let n = self.scores.len();
1430        let wtot = self.weight_total;
1431        let _ = wtot;
1432        let mut best: usize = 0;
1433        let mut j: usize = 1;
1434        while j < n
1435            invariant
1436                1 <= j <= n,
1437                best < j,
1438                n == self.scores.len(),
1439                self.extra.len() == n,
1440                self.mutable_score_inv(),
1441                wtot <= 1_000_000_000,
1442                forall|k: int| 0 <= k < n ==> #[trigger] self.scores@[k] <= 1_000_000_000,
1443                forall|k: int| 0 <= k < n ==> #[trigger] self.extra@[k] <= wtot,
1444                forall|c: int| 0 <= c < j ==> priority_ge(self.scores@, self.extra@, best as int, c),
1445                forall|c: int| 0 <= c < j
1446                    && priority_equal(self.scores@, self.extra@, best as int, c)
1447                    ==> best as int <= c,
1448            decreases n - j,
1449        {
1450            proof {
1451                assert((self.scores@[j as int] as int) * (2 * (self.extra@[best as int] as int) + 1)
1452                    <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1453                    requires
1454                        self.scores@[j as int] as int <= 1_000_000_000,
1455                        self.extra@[best as int] as int <= 1_000_000_000;
1456                assert((self.scores@[best as int] as int) * (2 * (self.extra@[j as int] as int) + 1)
1457                    <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1458                    requires
1459                        self.scores@[best as int] as int <= 1_000_000_000,
1460                        self.extra@[j as int] as int <= 1_000_000_000;
1461            }
1462            let lhs: u128 = (self.scores[j] as u128) * (2 * (self.extra[best] as u128) + 1);
1463            let rhs: u128 = (self.scores[best] as u128) * (2 * (self.extra[j] as u128) + 1);
1464            proof {
1465                assert(lhs as int == self.scores@[j as int] as int * (2 * self.extra@[best as int] as int + 1));
1466                assert(rhs as int == self.scores@[best as int] as int * (2 * self.extra@[j as int] as int + 1));
1467            }
1468            let old_best: usize = best;
1469            let old_j: usize = j;
1470            let _ = (old_best, old_j);
1471            if lhs > rhs {
1472                best = j;
1473                proof {
1474                    assert(priority_ge(self.scores@, self.extra@, old_j as int, old_best as int));
1475                    assert(priority_gt(self.scores@, self.extra@, old_j as int, old_best as int));
1476                    assert forall|c: int| 0 <= c < old_j as int + 1
1477                        implies priority_ge(self.scores@, self.extra@, best as int, c)
1478                    by {
1479                        if c == old_best as int {
1480                            assert(priority_ge(self.scores@, self.extra@, old_j as int, old_best as int));
1481                        } else if c < old_j as int {
1482                            lemma_priority_ge_trans(self.scores@, self.extra@, old_j as int, old_best as int, c);
1483                        } else {
1484                            // c == old_j as int == best as int, reflexive
1485                        }
1486                    }
1487                    assert forall|c: int| 0 <= c < old_j as int + 1
1488                        && priority_equal(self.scores@, self.extra@, best as int, c)
1489                        implies best as int <= c
1490                    by {
1491                        if c < old_j as int {
1492                            lemma_priority_gt_ge_not_equal(
1493                                self.scores@, self.extra@,
1494                                old_j as int, old_best as int, c,
1495                            );
1496                            assert(false);
1497                        }
1498                    }
1499                }
1500            } else {
1501                proof {
1502                    assert(priority_ge(self.scores@, self.extra@, old_best as int, old_j as int));
1503                    assert forall|c: int| 0 <= c < old_j as int + 1
1504                        implies priority_ge(self.scores@, self.extra@, best as int, c)
1505                    by {
1506                        if c == old_j as int {
1507                            assert(priority_ge(self.scores@, self.extra@, old_best as int, old_j as int));
1508                        } else {
1509                            // c < old_j: unchanged from the pre-iteration invariant
1510                        }
1511                    }
1512                    assert forall|c: int| 0 <= c < old_j as int + 1
1513                        && priority_equal(self.scores@, self.extra@, best as int, c)
1514                        implies best as int <= c
1515                    by {
1516                        if c == old_j as int {
1517                            assert(best < old_j);
1518                        }
1519                    }
1520                }
1521            }
1522            j = j + 1;
1523        }
1524        let ghost extra_old: Seq<u64> = self.extra@;
1525        let ghost best_g: int = best as int;
1526        proof {
1527            assert(self.scores@ == old(self).scores@);
1528            assert(extra_old == old(self).extra@);
1529            assert(priority_winner(self.scores@, extra_old, best_g));
1530            assert(old(self).score_order_preservation());
1531            assert(old(self).tie_boundedness());
1532            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] < self.scores@[d]
1533                implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int
1534            by {
1535                assert(old(self).weight(c) <= old(self).weight(d));
1536            }
1537            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] == self.scores@[d]
1538                implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1
1539            by {
1540                assert(old(self).weight(c) <= old(self).weight(d) + 1);
1541            }
1542        }
1543        self.extra.set(best, self.extra[best] + 1);
1544        proof {
1545            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] < self.scores@[d]
1546                implies 1 + self.extra@[c] as int <= 1 + self.extra@[d] as int
1547            by {
1548                if c == best_g && d == best_g {
1549                    assert(false);
1550                } else if c == best_g {
1551                    lemma_priority_strict(self.scores@, extra_old, best_g, d);
1552                } else if d == best_g {
1553                    assert(1 + extra_old[c] as int <= 1 + extra_old[best_g] as int);
1554                } else {
1555                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int);
1556                }
1557            }
1558            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] == self.scores@[d]
1559                implies 1 + self.extra@[c] as int <= 1 + self.extra@[d] as int + 1
1560            by {
1561                if c == best_g && d == best_g {
1562                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1563                } else if c == best_g {
1564                    if self.scores@[best_g] > 0 {
1565                        lemma_priority_tie(self.scores@, extra_old, best_g, d);
1566                    } else {
1567                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1568                    }
1569                } else if d == best_g {
1570                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1571                } else {
1572                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1573                }
1574            }
1575            if old(self).webster_allocation() {
1576                assert(self.webster_allocation()) by {
1577                    assert forall|c: int, d: int|
1578                        0 <= c < n && 0 <= d < n && self.extra@[c] >= 1
1579                        implies self.scores@[c] as int * (2 * self.extra@[d] as int + 1)
1580                            >= self.scores@[d] as int * (2 * self.extra@[c] as int - 1) by {
1581                        if c == best_g && d == best_g {
1582                            lemma_priority_lhs_monotone(
1583                                self.scores@[best_g] as int,
1584                                self.extra@[best_g] as int - 1,
1585                                self.extra@[best_g] as int,
1586                            );
1587                        } else if c == best_g {
1588                            assert(priority_ge(self.scores@, extra_old, best_g, d));
1589                            assert(self.extra@[d] == extra_old[d]);
1590                            assert(self.extra@[best_g] as int
1591                                == extra_old[best_g] as int + 1);
1592                        } else if d == best_g {
1593                            assert(self.scores@[c] as int
1594                                    * (2 * extra_old[best_g] as int + 1)
1595                                >= self.scores@[best_g] as int
1596                                    * (2 * extra_old[c] as int - 1));
1597                            lemma_priority_lhs_monotone(
1598                                self.scores@[c] as int,
1599                                extra_old[best_g] as int,
1600                                self.extra@[best_g] as int,
1601                            );
1602                            assert(self.extra@[c] == extra_old[c]);
1603                        } else {
1604                            assert(self.scores@[c] as int * (2 * extra_old[d] as int + 1)
1605                                >= self.scores@[d] as int * (2 * extra_old[c] as int - 1));
1606                            assert(self.extra@[c] == extra_old[c]
1607                                && self.extra@[d] == extra_old[d]);
1608                        }
1609                    }
1610                }
1611            }
1612            let old_sum = sum_to(extra_old, n as int);
1613            lemma_sum_ge_one(extra_old, best_g, n as int);
1614            lemma_sum_update(extra_old, best_g, (extra_old[best_g] + 1) as u64, n as int);
1615            assert(sum_to(self.extra@, n as int) == old_sum + 1);
1616            assert(sum_to(self.extra@, n as int) + n as int <= self.weight_total as int);
1617            assert forall|k: int| 0 <= k < n
1618                implies self.extra@[k] <= self.weight_total by {
1619                if k == best_g {
1620                    assert(extra_old[k] <= old_sum);
1621                }
1622            }
1623            assert(self.normalization_when_terminal()) by {
1624                if self.terminal() {
1625                    assert(self.normalization());
1626                }
1627            }
1628            assert(self.mutable_score_inv());
1629        }
1630        best
1631    }
1632
1633    /// CompetitiveSelectionSoftMutableScores `UpdateScore(c,v)`: update one mutable score
1634    /// and invalidate the partial apportionment in the same commit by resetting
1635    /// every extra award to the reserved floor.
1636    pub fn update_score(&mut self, c: usize, v: u64)
1637        requires
1638            old(self).mutable_score_inv(),
1639            c < old(self).scores.len(),
1640            1 <= v <= old(self).max_score,
1641        ensures
1642            final(self).weight_total == old(self).weight_total,
1643            final(self).max_score == old(self).max_score,
1644            final(self).scores@
1645                == old(self).scores@.update(c as int, v),
1646            final(self).extra@.len() == old(self).extra@.len(),
1647            forall|i: int| 0 <= i < final(self).extra.len() ==> final(self).extra@[i] == 0,
1648            final(self).mutable_score_inv(),
1649    {
1650        let ghost old_scores = self.scores@;
1651        self.scores.set(c, v);
1652        let ghost updated_scores = self.scores@;
1653        let n = self.extra.len();
1654        let mut i: usize = 0;
1655        while i < n
1656            invariant
1657                i <= n,
1658                n == self.extra.len(),
1659                n == self.scores.len(),
1660                self.scores@ == updated_scores,
1661                updated_scores == old_scores.update(c as int, v),
1662                self.weight_total == old(self).weight_total,
1663                self.max_score == old(self).max_score,
1664                forall|k: int| 0 <= k < i ==> self.extra@[k] == 0,
1665                forall|k: int| i <= k < n ==> self.extra@[k] == old(self).extra@[k],
1666            decreases n - i,
1667        {
1668            self.extra.set(i, 0);
1669            i = i + 1;
1670        }
1671        proof {
1672            lemma_sum_zero(self.extra@, n as int);
1673            assert(self.score_bounds()) by {
1674                assert forall|k: int| 0 <= k < n implies
1675                    self.scores@[k] >= 1 && self.scores@[k] <= 1_000_000_000 by {
1676                    if k != c as int {
1677                        assert(self.scores@[k] == old(self).scores@[k]);
1678                    }
1679                }
1680            }
1681            assert(self.score_order_preservation()) by {
1682                assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1683                    && self.scores@[a] < self.scores@[b]
1684                    implies self.weight(a) <= self.weight(b) by {}
1685            }
1686            assert(self.tie_boundedness()) by {
1687                assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1688                    && self.scores@[a] == self.scores@[b]
1689                    implies self.weight(a) <= self.weight(b) + 1 by {}
1690            }
1691            assert(self.normalization_when_terminal()) by {
1692                if self.terminal() {
1693                    assert(self.normalization());
1694                }
1695            }
1696            assert(self.mutable_score_inv());
1697        }
1698    }
1699}
1700
1701// ── Ranked mode: top-K selection ────────────────────────────────────────
1702
1703/// Count of `true` entries among `s[0..n]`.
1704pub open spec fn count_true(s: Seq<bool>, n: int) -> int
1705    decreases n,
1706{
1707    if n <= 0 { 0 } else if n > s.len() as int { 0 }
1708    else { (if s[n - 1] { 1int } else { 0int }) + count_true(s, n - 1) }
1709}
1710
1711/// Setting a `false` entry to `true` raises the count by one.
1712pub proof fn lemma_count_set(s: Seq<bool>, m: int, n: int)
1713    requires 0 <= m < n <= s.len(), !s[m],
1714    ensures count_true(s.update(m, true), n) == count_true(s, n) + 1,
1715    decreases n,
1716{
1717    if n == m + 1 {
1718        lemma_count_unaffected(s, m, n - 1);
1719    } else {
1720        lemma_count_set(s, m, n - 1);
1721    }
1722}
1723
1724/// Updating index m does not change the count of a prefix stopping at or before m.
1725pub proof fn lemma_count_unaffected(s: Seq<bool>, m: int, p: int)
1726    requires 0 <= p <= m < s.len(),
1727    ensures count_true(s.update(m, true), p) == count_true(s, p),
1728    decreases p,
1729{
1730    if p > 0 {
1731        lemma_count_unaffected(s, m, p - 1);
1732    }
1733}
1734
1735/// An all-false prefix has count zero.
1736pub proof fn lemma_count_zero(s: Seq<bool>, n: int)
1737    requires 0 <= n <= s.len(), forall|k: int| 0 <= k < n ==> !s[k],
1738    ensures count_true(s, n) == 0,
1739    decreases n,
1740{
1741    if n > 0 {
1742        lemma_count_zero(s, n - 1);
1743    }
1744}
1745
1746/// An all-true prefix has count equal to its length.
1747pub proof fn lemma_count_all_true(s: Seq<bool>, n: int)
1748    requires 0 <= n <= s.len(), forall|k: int| 0 <= k < n ==> s[k],
1749    ensures count_true(s, n) == n,
1750    decreases n,
1751{
1752    if n > 0 {
1753        lemma_count_all_true(s, n - 1);
1754    }
1755}
1756
1757/// A boolean prefix contains at most one true entry per position.
1758pub proof fn lemma_count_upper(s: Seq<bool>, n: int)
1759    requires 0 <= n <= s.len(),
1760    ensures count_true(s, n) <= n,
1761    decreases n,
1762{
1763    if n > 0 {
1764        lemma_count_upper(s, n - 1);
1765    }
1766}
1767
1768/// Ranked competitive selection: pick the top-K candidates by score.
1769pub struct CompetitiveSelectionRanked {
1770    /// Candidate scores by candidate index.
1771    pub scores: Vec<u64>,
1772    /// Current selected membership by candidate index.
1773    pub selected: Vec<bool>,
1774    /// Maximum selected cardinality.
1775    pub k: usize,
1776    /// MaxScore for this refinement profile.
1777    pub max_score: u64,
1778}
1779
1780impl CompetitiveSelectionRanked {
1781    /// selected and scores share the Candidates domain.
1782    pub open spec fn type_invariant(&self) -> bool {
1783        self.selected.len() == self.scores.len()
1784            && forall|i: int| 0 <= i < self.scores.len() ==>
1785                #[trigger] self.scores@[i] <= self.max_score
1786    }
1787
1788    /// TLA+ `BoundedMultiplicity`: at most K winners.
1789    pub open spec fn bounded_multiplicity(&self) -> bool {
1790        count_true(self.selected@, self.selected@.len() as int) <= self.k
1791    }
1792
1793    /// TLA+ `ThresholdOptimality`: every selected scores >= every non-selected.
1794    pub open spec fn threshold_optimality(&self) -> bool {
1795        forall|s: int, c: int|
1796            #![trigger self.selected@[s], self.selected@[c]]
1797            0 <= s < self.selected.len() && 0 <= c < self.selected.len()
1798                && self.selected@[s] && !self.selected@[c]
1799                ==> self.scores@[s] >= self.scores@[c]
1800    }
1801
1802    /// TLA+ `RankedTieBreak`: every selected candidate is strictly better
1803    /// than every unselected candidate under score-then-lowest-index order.
1804    /// The vector index is the executable realization of the fixed WEnum/Pos
1805    /// order used by the formal model.
1806    pub open spec fn ranked_tie_break(&self) -> bool {
1807        forall|s: int, c: int|
1808            #![trigger self.selected@[s], self.selected@[c]]
1809            0 <= s < self.selected.len() && 0 <= c < self.selected.len()
1810                && self.selected@[s] && !self.selected@[c]
1811                ==> (self.scores@[s] > self.scores@[c]
1812                    || (self.scores@[s] == self.scores@[c] && s < c))
1813    }
1814
1815    /// Whether all ranked-selection contract clauses hold.
1816    pub open spec fn inv(&self) -> bool {
1817        self.type_invariant() && self.bounded_multiplicity()
1818            && self.threshold_optimality() && self.ranked_tie_break()
1819    }
1820
1821    /// Construct from scores; nothing selected yet (TLA+ Init).
1822    pub fn new(scores: Vec<u64>, k: usize, max_score: u64) -> (r: CompetitiveSelectionRanked)
1823        requires
1824            forall|i: int| 0 <= i < scores.len() ==> #[trigger] scores@[i] <= max_score,
1825        ensures
1826            r.scores@ == scores@,
1827            r.k == k,
1828            r.max_score == max_score,
1829            r.selected@.len() == scores@.len(),
1830            forall|j: int| 0 <= j < r.selected@.len() ==> !r.selected@[j],
1831            r.inv(),
1832    {
1833        let n = scores.len();
1834        let mut selected: Vec<bool> = Vec::new();
1835        let mut i: usize = 0;
1836        while i < n
1837            invariant
1838                i <= n,
1839                selected.len() == i,
1840                forall|j: int| 0 <= j < i ==> !selected@[j],
1841            decreases n - i,
1842        {
1843            selected.push(false);
1844            i = i + 1;
1845        }
1846        proof { lemma_count_zero(selected@, selected@.len() as int); }
1847        CompetitiveSelectionRanked { scores, selected, k, max_score }
1848    }
1849
1850    /// The unselected candidate with the highest score, or None if all selected.
1851    fn find_max_unselected(&self) -> (r: Option<usize>)
1852        requires self.type_invariant(),
1853        ensures
1854            r is None ==> (forall|c: int| 0 <= c < self.selected.len() ==> self.selected@[c]),
1855            r matches Option::Some(m) ==> (m < self.scores.len() && !self.selected@[m as int]
1856                && (forall|c: int| 0 <= c < self.scores.len() && !self.selected@[c]
1857                    ==> #[trigger] self.scores@[c] <= self.scores@[m as int])
1858                && (forall|c: int| 0 <= c < self.scores.len() && !self.selected@[c]
1859                    && #[trigger] self.scores@[c] == self.scores@[m as int]
1860                    ==> m as int <= c)),
1861    {
1862        let n = self.scores.len();
1863        let mut best: Option<usize> = None;
1864        let mut i: usize = 0;
1865        while i < n
1866            invariant
1867                i <= n,
1868                n == self.scores.len(),
1869                self.selected.len() == n,
1870                best is None ==> (forall|c: int| 0 <= c < i ==> self.selected@[c]),
1871                best matches Option::Some(m) ==> (m < i && !self.selected@[m as int]
1872                    && (forall|c: int| 0 <= c < i && !self.selected@[c]
1873                        ==> #[trigger] self.scores@[c] <= self.scores@[m as int])
1874                    && (forall|c: int| 0 <= c < i && !self.selected@[c]
1875                        && #[trigger] self.scores@[c] == self.scores@[m as int]
1876                        ==> m as int <= c)),
1877            decreases n - i,
1878        {
1879            if !self.selected[i] {
1880                match best {
1881                    Option::Some(m) => {
1882                        if self.scores[i] > self.scores[m] {
1883                            best = Some(i);
1884                        }
1885                    }
1886                    Option::None => {
1887                        best = Some(i);
1888                    }
1889                }
1890            }
1891            i = i + 1;
1892        }
1893        best
1894    }
1895
1896    /// Select the top-K (TLA+ Select): mark up to K highest-scoring candidates,
1897    /// re-establishing BoundedMultiplicity and ThresholdOptimality.
1898    pub fn select(&mut self)
1899        requires old(self).type_invariant(),
1900        ensures
1901            final(self).scores@ == old(self).scores@,
1902            final(self).k == old(self).k,
1903            final(self).max_score == old(self).max_score,
1904            count_true(final(self).selected@, final(self).selected@.len() as int)
1905                == if final(self).k < final(self).selected.len() {
1906                    final(self).k as int
1907                } else {
1908                    final(self).selected.len() as int
1909                },
1910            final(self).inv(),
1911    {
1912        let n = self.scores.len();
1913        let original_len = self.selected.len();
1914        let original_k = self.k;
1915        let _ = (original_len, original_k);
1916        assert(original_len == n);
1917        // Reset selection to empty.
1918        let mut i: usize = 0;
1919        while i < n
1920            invariant
1921                i <= n,
1922                n == self.scores.len(),
1923                self.selected.len() == n,
1924                self.type_invariant(),
1925                self.scores@ == old(self).scores@,
1926                self.k == old(self).k,
1927                self.max_score == old(self).max_score,
1928                original_len == n,
1929                original_k == self.k,
1930                forall|j: int| 0 <= j < i ==> !self.selected@[j],
1931            decreases n - i,
1932        {
1933            self.selected.set(i, false);
1934            i = i + 1;
1935        }
1936        proof { lemma_count_zero(self.selected@, n as int); }
1937        // Greedily mark the highest unselected, up to K rounds.
1938        let mut round: usize = 0;
1939        while round < self.k
1940            invariant
1941                n == self.scores.len(),
1942                self.selected.len() == n,
1943                self.type_invariant(),
1944                self.scores@ == old(self).scores@,
1945                self.k == old(self).k,
1946                self.max_score == old(self).max_score,
1947                original_len == n,
1948                original_k == self.k,
1949                round <= self.k,
1950                count_true(self.selected@, n as int) == round,
1951                self.threshold_optimality(),
1952                self.ranked_tie_break(),
1953            decreases self.k - round,
1954        {
1955            let m_opt = self.find_max_unselected();
1956            match m_opt {
1957                Option::Some(m) => {
1958                    let ghost s0 = self.selected@;
1959                    assert forall|c: int| 0 <= c < n && !s0[c]
1960                        implies self.scores@[c] <= self.scores@[m as int] by {}
1961                    assert forall|c: int| 0 <= c < n && !s0[c]
1962                        && self.scores@[c] == self.scores@[m as int]
1963                        implies m as int <= c by {}
1964                    proof { lemma_count_set(s0, m as int, n as int); }
1965                    self.selected.set(m, true);
1966                    proof {
1967                        assert(self.threshold_optimality()) by {
1968                            assert forall|s: int, c: int|
1969                                #![trigger self.selected@[s], self.selected@[c]]
1970                                0 <= s < n && 0 <= c < n && self.selected@[s] && !self.selected@[c]
1971                                implies self.scores@[s] >= self.scores@[c] by {
1972                                if s != m as int {
1973                                    assert(s0[s]);
1974                                }
1975                            }
1976                        }
1977                        assert(self.ranked_tie_break()) by {
1978                            assert forall|s: int, c: int|
1979                                #![trigger self.selected@[s], self.selected@[c]]
1980                                0 <= s < n && 0 <= c < n
1981                                    && self.selected@[s] && !self.selected@[c]
1982                                implies self.scores@[s] > self.scores@[c]
1983                                    || (self.scores@[s] == self.scores@[c] && s < c) by {
1984                                if s != m as int {
1985                                    assert(s0[s]);
1986                                    assert(!s0[c]);
1987                                } else {
1988                                    assert(!s0[c]);
1989                                    assert(c != m as int);
1990                                    if self.scores@[m as int] == self.scores@[c] {
1991                                        assert(m as int <= c);
1992                                        assert((m as int) < c);
1993                                    }
1994                                }
1995                            }
1996                        }
1997                    }
1998                }
1999                Option::None => {
2000                    proof {
2001                        lemma_count_all_true(self.selected@, n as int);
2002                        assert(count_true(self.selected@, n as int) == n as int);
2003                        assert(round as int == n as int);
2004                        assert(round < self.k);
2005                        assert(n < self.k);
2006                        assert(original_len == n);
2007                        assert(original_k == self.k);
2008                        assert(!(original_k < original_len));
2009                    }
2010                    return;
2011                }
2012            }
2013            round = round + 1;
2014        }
2015        proof {
2016            lemma_count_upper(self.selected@, n as int);
2017            assert(round == self.k);
2018            assert(count_true(self.selected@, n as int) == self.k as int);
2019            assert(self.k <= n);
2020        }
2021    }
2022
2023    /// Replace the scores and clear the selection (TLA+ UpdateScores).
2024    pub fn update_scores(&mut self, new_scores: Vec<u64>)
2025        requires
2026            old(self).type_invariant(),
2027            new_scores.len() == old(self).scores.len(),
2028            forall|i: int| 0 <= i < new_scores.len() ==>
2029                #[trigger] new_scores@[i] <= old(self).max_score,
2030        ensures
2031            final(self).scores@ == new_scores@,
2032            final(self).k == old(self).k,
2033            final(self).max_score == old(self).max_score,
2034            forall|i: int| 0 <= i < final(self).selected.len() ==>
2035                !final(self).selected@[i],
2036            final(self).inv(),
2037    {
2038        let ghost ns = new_scores@;
2039        let n = new_scores.len();
2040        self.scores = new_scores;
2041        let mut i: usize = 0;
2042        while i < n
2043            invariant
2044                i <= n,
2045                n == self.scores.len(),
2046                self.selected.len() == n,
2047                self.scores@ == ns,
2048                self.k == old(self).k,
2049                self.max_score == old(self).max_score,
2050                forall|j: int| 0 <= j < i ==> !self.selected@[j],
2051            decreases n - i,
2052        {
2053            self.selected.set(i, false);
2054            i = i + 1;
2055        }
2056        proof { lemma_count_zero(self.selected@, n as int); }
2057    }
2058}
2059
2060}