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 obligations 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/// Sainte-Lague's catch-up property: if a's priority dominates b's and a's
803/// score is strictly lower, a's extra must already be strictly lower too.
804/// This is what keeps a strictly-lower-scored candidate from ever catching
805/// up to or passing a strictly-higher-scored one (ScoreOrderPreservation).
806pub proof fn lemma_priority_strict(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int)
807    requires
808        priority_ge(scores, extra, a, b),
809        scores[a] < scores[b],
810    ensures
811        extra[a] < extra[b],
812{
813    if extra[a] >= extra[b] {
814        assert(scores[b] as int * (2 * extra[a] as int + 1)
815            > scores[a] as int * (2 * extra[b] as int + 1)) by (nonlinear_arith)
816            requires scores[a] < scores[b], extra[a] >= extra[b], scores[a] >= 0, scores[b] >= 0,
817                extra[a] >= 0, extra[b] >= 0;
818        assert(false);
819    }
820}
821
822/// Sainte-Lague's tie-bound property: if a's priority dominates b's and the
823/// two scores are equal, a's extra is at most b's (TieBoundedness).
824pub proof fn lemma_priority_tie(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int)
825    requires
826        priority_ge(scores, extra, a, b),
827        scores[a] == scores[b],
828        scores[a] > 0,
829    ensures
830        extra[a] <= extra[b],
831{
832    if extra[a] > extra[b] {
833        assert(scores[b] as int * (2 * extra[a] as int + 1)
834            > scores[a] as int * (2 * extra[b] as int + 1)) by (nonlinear_arith)
835            requires scores[a] == scores[b], scores[a] > 0, extra[a] > extra[b],
836                extra[a] >= 0, extra[b] >= 0;
837        assert(false);
838    }
839}
840
841/// Transitivity of the cross-multiplied priority order: needed because the
842/// linear scan for the current highest-priority candidate only ever compares
843/// the running best against one new candidate at a time, so replacing the
844/// running best requires knowing it still dominates everyone scanned so far,
845/// not just the candidate that just displaced it.  The three-term product
846/// argument: multiply each hypothesis by the missing denominator, chain the
847/// two resulting inequalities through their shared middle term, then cancel
848/// the common positive factor (2*extra[b]+1).
849pub proof fn lemma_priority_ge_trans(scores: Seq<u64>, extra: Seq<u64>, a: int, b: int, c: int)
850    requires
851        priority_ge(scores, extra, a, b),
852        priority_ge(scores, extra, b, c),
853    ensures
854        priority_ge(scores, extra, a, c),
855{
856    let sa = scores[a] as int;
857    let sb = scores[b] as int;
858    let sc = scores[c] as int;
859    let a_den = 2 * extra[a] as int + 1;
860    let b_den = 2 * extra[b] as int + 1;
861    let c_den = 2 * extra[c] as int + 1;
862    assert(sa * b_den * c_den >= sb * a_den * c_den) by (nonlinear_arith)
863        requires sa * b_den >= sb * a_den, c_den >= 0;
864    assert(sb * c_den * a_den >= sc * b_den * a_den) by (nonlinear_arith)
865        requires sb * c_den >= sc * b_den, a_den >= 0;
866    assert(sa * b_den * c_den >= sc * b_den * a_den) by (nonlinear_arith)
867        requires
868            sa * b_den * c_den >= sb * a_den * c_den,
869            sb * c_den * a_den >= sc * b_den * a_den;
870    assert(sa * c_den >= sc * a_den) by (nonlinear_arith)
871        requires
872            sa * b_den * c_den >= sc * b_den * a_den,
873            b_den > 0;
874}
875
876/// Monotonicity of the priority product in `extra`: needed by the Webster
877/// no-transfer invariant's preservation step, where an award to `best`
878/// raises the odd multiplier on the left side of every retained inequality.
879pub proof fn lemma_priority_lhs_monotone(s: int, e: int, f: int)
880    requires s >= 0, e <= f,
881    ensures s * (2 * e + 1) <= s * (2 * f + 1),
882{
883    assert(s * (2 * e + 1) <= s * (2 * f + 1)) by (nonlinear_arith)
884        requires s >= 0, e <= f;
885}
886
887/// Soft competitive selection: weights derived from an evolving `extra` via
888/// the reserved-floor sequential Webster award process, not chosen freely.
889pub struct CompetitiveSelectionSoft {
890    /// Extra Webster awards by candidate index.
891    pub extra: Vec<u64>,
892    /// Candidate scores by candidate index.
893    pub scores: Vec<u64>,
894    /// Total weight available for assignment.
895    pub weight_total: u64,
896    /// MaxScore for this refinement profile.
897    pub max_score: u64,
898}
899
900impl CompetitiveSelectionSoft {
901    /// extra and scores share the Candidates domain, at least one candidate.
902    pub open spec fn well_formed(&self) -> bool {
903        self.extra.len() == self.scores.len() && self.extra.len() > 0
904    }
905
906    /// Executable instance of TLA+ `scores \in [Candidates -> 1..MaxScore]`.
907    pub open spec fn score_bounds(&self) -> bool {
908        forall|i: int| 0 <= i < self.scores.len() ==>
909            #[trigger] self.scores@[i] >= 1 && self.scores@[i] <= self.max_score
910    }
911
912    /// The reserved unit plus whatever extra units this candidate has been
913    /// awarded so far -- derived, not a state variable in its own right.
914    pub open spec fn weight(&self, i: int) -> int {
915        1 + self.extra@[i] as int
916    }
917
918    /// TLA+ `UniversalContribution`: holds unconditionally, weight >= 1.
919    pub open spec fn universal_contribution(&self) -> bool {
920        forall|i: int| 0 <= i < self.extra.len() ==> #[trigger] self.weight(i) > 0
921    }
922
923    /// TLA+ `ScoreOrderPreservation`: a strictly higher score never receives a
924    /// lower weight.
925    pub open spec fn score_order_preservation(&self) -> bool {
926        forall|i: int, j: int|
927            0 <= i < self.scores.len() && 0 <= j < self.scores.len()
928                && self.scores@[i] < self.scores@[j]
929                ==> #[trigger] self.weight(i) <= #[trigger] self.weight(j)
930    }
931
932    /// TLA+ `TieBoundedness`: tied candidates' weights differ by at most one.
933    pub open spec fn tie_boundedness(&self) -> bool {
934        forall|i: int, j: int|
935            0 <= i < self.scores.len() && 0 <= j < self.scores.len()
936                && self.scores@[i] == self.scores@[j]
937                ==> #[trigger] self.weight(i) <= #[trigger] self.weight(j) + 1
938    }
939
940    /// TLA+ `Normalization`: weights sum to the fixed total, Reserved + Pool.
941    pub open spec fn normalization(&self) -> bool {
942        sum_to(self.extra@, self.extra@.len() as int) + self.extra@.len() as int
943            == self.weight_total as int
944    }
945
946    /// TLA+ `BoundedTotal`: partial award states never exceed WeightTotal.
947    pub open spec fn bounded_total(&self) -> bool {
948        sum_to(self.extra@, self.extra@.len() as int) + self.extra@.len() as int
949            <= self.weight_total as int
950    }
951
952    /// TLA+ `Terminal`, stated independently in terms of awarded extra units.
953    pub open spec fn terminal(&self) -> bool {
954        sum_to(self.extra@, self.extra@.len() as int)
955            == self.weight_total as int - self.extra@.len() as int
956    }
957
958    /// TLA+ `Normalization`: exact total is required only at Terminal.
959    pub open spec fn normalization_when_terminal(&self) -> bool {
960        self.terminal() ==> self.normalization()
961    }
962
963    /// Proof-only overflow bound: every candidate's extra is at most
964    /// weight_total, so the cross-multiplied priority comparison and the
965    /// derived weight never overflow u64.
966    pub open spec fn bounded(&self) -> bool {
967        self.weight_total <= 1_000_000_000
968            && self.max_score <= 1_000_000_000
969            && forall|k: int| 0 <= k < self.extra.len() ==> #[trigger] self.extra@[k] <= self.weight_total
970    }
971
972    /// The invariant of every reachable CompetitiveSelectionSoftMutableScores state,
973    /// including the reserved-floor state after a mutable score update.
974    pub open spec fn mutable_score_inv(&self) -> bool {
975        self.well_formed()
976            && self.weight_total as int >= self.extra.len() as int
977            && self.score_bounds()
978            && self.bounded()
979            && self.bounded_total()
980            && self.normalization_when_terminal()
981            && self.universal_contribution()
982            && self.score_order_preservation()
983            && self.tie_boundedness()
984    }
985
986    /// The finished apportionment is a Webster (Sainte-Lague) one: no candidate
987    /// holding an awarded unit could have been passed over for it -- the
988    /// no-transfer characterization. With `normalization()` fixing the total it
989    /// characterizes the returned Webster allocation as a set. It does not by
990    /// itself select the unique tie-broken sequence produced by `Init` and
991    /// `AssignNext` because this clause contains no tie-order premise.
992    pub open spec fn webster_allocation(&self) -> bool {
993        forall|i: int, j: int| #![trigger self.extra@[i], self.extra@[j]]
994            0 <= i < self.extra.len() && 0 <= j < self.extra.len()
995                && self.extra@[i] >= 1
996                ==> self.scores@[i] as int * (2 * self.extra@[j] as int + 1)
997                        >= self.scores@[j] as int * (2 * self.extra@[i] as int - 1)
998    }
999
1000    /// Whether mutable-score safety and terminal normalization hold.
1001    pub open spec fn inv(&self) -> bool {
1002        self.mutable_score_inv() && self.normalization()
1003    }
1004
1005    /// Executable accessor for the derived weight (`1 + extra[i]`).
1006    pub fn weight_at(&self, i: usize) -> (w: u64)
1007        requires i < self.extra.len(), self.bounded(),
1008        ensures w as int == self.weight(i as int),
1009    {
1010        self.extra[i] + 1
1011    }
1012
1013    /// Number of units currently assigned, including one reserved unit per candidate.
1014    pub fn assigned_weight(&self) -> (total: u64)
1015        requires self.mutable_score_inv(),
1016        ensures
1017            total as int
1018                == sum_to(self.extra@, self.extra@.len() as int) + self.extra@.len() as int,
1019            total <= self.weight_total,
1020    {
1021        let len = self.extra.len();
1022        let mut total = len as u64;
1023        let mut index: usize = 0;
1024        proof {
1025            assert(self.bounded_total());
1026        }
1027        while index < len
1028            invariant
1029                index <= len,
1030                len == self.extra.len(),
1031                total as int == sum_to(self.extra@, index as int) + len as int,
1032                total <= self.weight_total,
1033                sum_to(self.extra@, len as int) + len as int <= self.weight_total as int,
1034            decreases len - index,
1035        {
1036            proof {
1037                assert(sum_to(self.extra@, index as int + 1)
1038                    == self.extra@[index as int] as int
1039                        + sum_to(self.extra@, index as int));
1040                lemma_sum_prefix_le(self.extra@, index as int + 1, len as int);
1041                assert(sum_to(self.extra@, index as int + 1) + len as int
1042                    <= self.weight_total as int);
1043                assert(total as int + self.extra@[index as int] as int
1044                    <= self.weight_total as int);
1045            }
1046            total = total + self.extra[index];
1047            index += 1;
1048        }
1049        total
1050    }
1051
1052    /// TLA+ `Init`: scores are mutable state and every candidate begins with only
1053    /// its reserved unit. Unlike `new`, this does not run the award process to
1054    /// Terminal; it exposes the action-level carrier state.
1055    pub fn init(scores: Vec<u64>, weight_total: u64, max_score: u64) -> (s: CompetitiveSelectionSoft)
1056        requires
1057            scores.len() >= 1,
1058            weight_total >= scores.len() as u64,
1059            weight_total <= 1_000_000_000,
1060            max_score <= 1_000_000_000,
1061            forall|i: int| 0 <= i < scores.len() ==>
1062                #[trigger] scores@[i] >= 1 && scores@[i] <= max_score,
1063        ensures
1064            s.scores@ == scores@,
1065            s.weight_total == weight_total,
1066            s.max_score == max_score,
1067            s.extra@.len() == scores@.len(),
1068            forall|i: int| 0 <= i < s.extra.len() ==> s.extra@[i] == 0,
1069            s.mutable_score_inv(),
1070    {
1071        let n = scores.len();
1072        let mut extra: Vec<u64> = Vec::new();
1073        let mut i: usize = 0;
1074        while i < n
1075            invariant
1076                i <= n,
1077                extra.len() == i,
1078                forall|k: int| 0 <= k < i ==> extra@[k] == 0,
1079                sum_to(extra@, i as int) == 0,
1080            decreases n - i,
1081        {
1082            let ghost eold = extra@;
1083            extra.push(0);
1084            proof {
1085                lemma_sum_push_prefix(eold, 0, i as int);
1086                assert(sum_to(extra@, i as int + 1)
1087                    == extra@[i as int] as int + sum_to(extra@, i as int));
1088            }
1089            i = i + 1;
1090        }
1091        proof {
1092            assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1093                && scores@[a] < scores@[b]
1094                implies 1 + extra@[a] as int <= 1 + extra@[b] as int by {}
1095            assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1096                && scores@[a] == scores@[b]
1097                implies 1 + extra@[a] as int <= 1 + extra@[b] as int + 1 by {}
1098        }
1099        CompetitiveSelectionSoft { extra, scores, weight_total, max_score }
1100    }
1101
1102    /// Construct the reserved-floor sequential Webster allocation over
1103    /// `scores`: one guaranteed unit per candidate, then Pool further units
1104    /// awarded one at a time to the current highest-priority candidate.
1105    /// Mirrors CompetitiveSelectionSoft.tla's Init + AssignNext exactly.
1106    pub fn new(scores: Vec<u64>, weight_total: u64, max_score: u64) -> (s: CompetitiveSelectionSoft)
1107        requires
1108            scores.len() >= 1,
1109            weight_total >= scores.len() as u64,
1110            weight_total <= 1_000_000_000,
1111            max_score <= 1_000_000_000,
1112            forall|i: int| 0 <= i < scores.len() ==> #[trigger] scores@[i] >= 1 && scores@[i] <= max_score,
1113        ensures
1114            s.scores@ == scores@,
1115            s.weight_total == weight_total,
1116            s.max_score == max_score,
1117            s.mutable_score_inv(),
1118            s.inv(),
1119            s.webster_allocation(),
1120    {
1121        let n = scores.len();
1122        let pool: u64 = weight_total - (n as u64);
1123        let mut extra: Vec<u64> = Vec::new();
1124        let mut i: usize = 0;
1125        while i < n
1126            invariant
1127                i <= n,
1128                extra.len() == i,
1129                forall|k: int| 0 <= k < i ==> extra@[k] == 0,
1130                sum_to(extra@, i as int) == 0,
1131            decreases n - i,
1132        {
1133            let ghost eold = extra@;
1134            extra.push(0);
1135            proof {
1136                lemma_sum_push_prefix(eold, 0, i as int);
1137                assert(sum_to(extra@, i as int + 1) == extra@[i as int] as int + sum_to(extra@, i as int));
1138            }
1139            i = i + 1;
1140        }
1141
1142        let mut awarded: u64 = 0;
1143        while awarded < pool
1144            invariant
1145                extra.len() == n,
1146                n == scores.len(),
1147                n >= 1,
1148                weight_total >= n as u64,
1149                weight_total <= 1_000_000_000,
1150                pool == weight_total - (n as u64),
1151                awarded <= pool,
1152                forall|k: int| 0 <= k < n ==> #[trigger] scores@[k] >= 1 && scores@[k] <= 1_000_000_000,
1153                sum_to(extra@, n as int) == awarded as int,
1154                forall|k: int| 0 <= k < n ==> #[trigger] extra@[k] <= awarded,
1155                forall|c: int, d: int|
1156                    0 <= c < n && 0 <= d < n && scores@[c] < scores@[d]
1157                        ==> 1 + extra@[c] as int <= 1 + extra@[d] as int,
1158                forall|c: int, d: int|
1159                    0 <= c < n && 0 <= d < n && scores@[c] == scores@[d]
1160                        ==> 1 + extra@[c] as int <= 1 + extra@[d] as int + 1,
1161                // The Webster no-transfer invariant: every candidate
1162                // holding an awarded unit had priority at least everyone's at
1163                // the moment of its last award. Vacuous at the all-zero entry
1164                // state; preserved because each award goes to the scan's
1165                // priority-maximal index.
1166                forall|c: int, d: int|
1167                    0 <= c < n && 0 <= d < n && extra@[c] >= 1
1168                        ==> scores@[c] as int * (2 * extra@[d] as int + 1)
1169                                >= scores@[d] as int * (2 * extra@[c] as int - 1),
1170            decreases pool - awarded,
1171        {
1172            let mut best: usize = 0;
1173            let mut j: usize = 1;
1174            while j < n
1175                invariant
1176                    1 <= j <= n,
1177                    best < j,
1178                    extra.len() == n,
1179                    n == scores.len(),
1180                    weight_total <= 1_000_000_000,
1181                    pool == weight_total - (n as u64),
1182                    awarded <= pool,
1183                    forall|k: int| 0 <= k < n ==> #[trigger] scores@[k] <= 1_000_000_000,
1184                    forall|k: int| 0 <= k < n ==> #[trigger] extra@[k] <= awarded,
1185                    forall|c: int| 0 <= c < j ==> priority_ge(scores@, extra@, best as int, c),
1186                decreases n - j,
1187            {
1188                proof {
1189                    assert((scores@[j as int] as int) * (2 * (extra@[best as int] as int) + 1)
1190                        <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1191                        requires
1192                            scores@[j as int] as int <= 1_000_000_000,
1193                            extra@[best as int] as int <= 1_000_000_000;
1194                    assert((scores@[best as int] as int) * (2 * (extra@[j as int] as int) + 1)
1195                        <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1196                        requires
1197                            scores@[best as int] as int <= 1_000_000_000,
1198                            extra@[j as int] as int <= 1_000_000_000;
1199                }
1200                let lhs: u128 = (scores[j] as u128) * (2 * (extra[best] as u128) + 1);
1201                let rhs: u128 = (scores[best] as u128) * (2 * (extra[j] as u128) + 1);
1202                proof {
1203                    assert(lhs as int == scores@[j as int] as int * (2 * extra@[best as int] as int + 1));
1204                    assert(rhs as int == scores@[best as int] as int * (2 * extra@[j as int] as int + 1));
1205                }
1206                let old_best: usize = best;
1207                let old_j: usize = j;
1208                let _ = (old_best, old_j);
1209                if lhs > rhs {
1210                    best = j;
1211                    proof {
1212                        assert(priority_ge(scores@, extra@, old_j as int, old_best as int));
1213                        assert forall|c: int| 0 <= c < old_j as int + 1
1214                            implies priority_ge(scores@, extra@, best as int, c)
1215                        by {
1216                            if c == old_best as int {
1217                                assert(priority_ge(scores@, extra@, old_j as int, old_best as int));
1218                            } else if c < old_j as int {
1219                                lemma_priority_ge_trans(scores@, extra@, old_j as int, old_best as int, c);
1220                            } else {
1221                                // c == old_j as int == best as int, reflexive
1222                            }
1223                        }
1224                    }
1225                } else {
1226                    proof {
1227                        assert(priority_ge(scores@, extra@, old_best as int, old_j as int));
1228                        assert forall|c: int| 0 <= c < old_j as int + 1
1229                            implies priority_ge(scores@, extra@, best as int, c)
1230                        by {
1231                            if c == old_j as int {
1232                                assert(priority_ge(scores@, extra@, old_best as int, old_j as int));
1233                            } else {
1234                                // c < old_j: unchanged from the pre-iteration invariant
1235                            }
1236                        }
1237                    }
1238                }
1239                j = j + 1;
1240            }
1241            let ghost extra_old: Seq<u64> = extra@;
1242            let ghost best_g: int = best as int;
1243            // Capture the pre-award invariant explicitly, in terms of extra_old:
1244            // once extra is mutated below, the loop invariant clauses (stated in
1245            // terms of the current extra@) stop referring to this snapshot, so
1246            // the facts needed for the case split have to be pinned down first.
1247            proof {
1248                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] < scores@[d]
1249                    implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int
1250                by {}
1251                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] == scores@[d]
1252                    implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1
1253                by {}
1254                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && extra_old[c] >= 1
1255                    implies scores@[c] as int * (2 * extra_old[d] as int + 1)
1256                        >= scores@[d] as int * (2 * extra_old[c] as int - 1)
1257                by {}
1258                assert forall|c: int| 0 <= c < n
1259                    implies priority_ge(scores@, extra_old, best_g, c)
1260                by {}
1261            }
1262            extra.set(best, extra[best] + 1);
1263            proof {
1264                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] < scores@[d]
1265                    implies 1 + extra@[c] as int <= 1 + extra@[d] as int
1266                by {
1267                    if c == best_g && d == best_g {
1268                        assert(false);
1269                    } else if c == best_g {
1270                        lemma_priority_strict(scores@, extra_old, best_g, d);
1271                    } else if d == best_g {
1272                        assert(1 + extra_old[c] as int <= 1 + extra_old[best_g] as int);
1273                    } else {
1274                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int);
1275                    }
1276                }
1277                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && scores@[c] == scores@[d]
1278                    implies 1 + extra@[c] as int <= 1 + extra@[d] as int + 1
1279                by {
1280                    if c == best_g && d == best_g {
1281                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1282                    } else if c == best_g {
1283                        if scores@[best_g] > 0 {
1284                            lemma_priority_tie(scores@, extra_old, best_g, d);
1285                        } else {
1286                            assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1287                        }
1288                    } else if d == best_g {
1289                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1290                    } else {
1291                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1292                    }
1293                }
1294                // Preservation of the Webster no-transfer invariant.
1295                assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && extra@[c] >= 1
1296                    implies scores@[c] as int * (2 * extra@[d] as int + 1)
1297                        >= scores@[d] as int * (2 * extra@[c] as int - 1)
1298                by {
1299                    if c == best_g && d == best_g {
1300                        lemma_priority_lhs_monotone(scores@[best_g] as int,
1301                            extra@[best_g] as int - 1, extra@[best_g] as int);
1302                    } else if c == best_g {
1303                        // extra@[best] - 1 == extra_old[best]: the scan's exit
1304                        // fact at d is literally the needed inequality.
1305                        assert(priority_ge(scores@, extra_old, best_g, d));
1306                        assert(extra@[d] == extra_old[d]);
1307                        assert(extra@[best_g] as int == extra_old[best_g] as int + 1);
1308                    } else if d == best_g {
1309                        // The old invariant instance at (c, best) plus the left
1310                        // side growing with best's award.
1311                        assert(scores@[c] as int * (2 * extra_old[best_g] as int + 1)
1312                            >= scores@[best_g] as int * (2 * extra_old[c] as int - 1));
1313                        lemma_priority_lhs_monotone(scores@[c] as int,
1314                            extra_old[best_g] as int, extra@[best_g] as int);
1315                        assert(extra@[c] == extra_old[c]);
1316                    } else {
1317                        assert(scores@[c] as int * (2 * extra_old[d] as int + 1)
1318                            >= scores@[d] as int * (2 * extra_old[c] as int - 1));
1319                        assert(extra@[c] == extra_old[c] && extra@[d] == extra_old[d]);
1320                    }
1321                }
1322                let nv: u64 = (extra_old[best_g] + 1) as u64;
1323                lemma_sum_update(extra_old, best_g, nv, n as int);
1324            }
1325            awarded = awarded + 1;
1326        }
1327
1328        proof {
1329            assert(sum_to(extra@, n as int) + n as int == weight_total as int) by {
1330                assert(sum_to(extra@, n as int) == pool as int);
1331            }
1332        }
1333        CompetitiveSelectionSoft { extra, scores, weight_total, max_score }
1334    }
1335
1336    /// Award one further pool unit (TLA+ AssignNext, one step): re-establishes
1337    /// every invariant that holds at every reachable state (ScoreOrderPreservation,
1338    /// TieBoundedness, UniversalContribution); Normalization only holds once
1339    /// the pool is exhausted, matching the TLA+ construction exactly.
1340    pub fn assign_next(&mut self) -> (winner: usize)
1341        requires
1342            old(self).mutable_score_inv(),
1343            (sum_to(old(self).extra@, old(self).extra@.len() as int) + old(self).extra@.len() as int)
1344                < (old(self).weight_total as int),
1345        ensures
1346            winner < old(self).scores.len(),
1347            priority_winner(old(self).scores@, old(self).extra@, winner as int),
1348            final(self).scores@ == old(self).scores@,
1349            final(self).weight_total == old(self).weight_total,
1350            final(self).max_score == old(self).max_score,
1351            final(self).extra@
1352                == old(self).extra@.update(
1353                    winner as int,
1354                    (old(self).extra@[winner as int] + 1) as u64,
1355                ),
1356            final(self).mutable_score_inv(),
1357    {
1358        let n = self.scores.len();
1359        let wtot = self.weight_total;
1360        let _ = wtot;
1361        let mut best: usize = 0;
1362        let mut j: usize = 1;
1363        while j < n
1364            invariant
1365                1 <= j <= n,
1366                best < j,
1367                n == self.scores.len(),
1368                self.extra.len() == n,
1369                self.mutable_score_inv(),
1370                wtot <= 1_000_000_000,
1371                forall|k: int| 0 <= k < n ==> #[trigger] self.scores@[k] <= 1_000_000_000,
1372                forall|k: int| 0 <= k < n ==> #[trigger] self.extra@[k] <= wtot,
1373                forall|c: int| 0 <= c < j ==> priority_ge(self.scores@, self.extra@, best as int, c),
1374                forall|c: int| 0 <= c < j
1375                    && priority_equal(self.scores@, self.extra@, best as int, c)
1376                    ==> best as int <= c,
1377            decreases n - j,
1378        {
1379            proof {
1380                assert((self.scores@[j as int] as int) * (2 * (self.extra@[best as int] as int) + 1)
1381                    <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1382                    requires
1383                        self.scores@[j as int] as int <= 1_000_000_000,
1384                        self.extra@[best as int] as int <= 1_000_000_000;
1385                assert((self.scores@[best as int] as int) * (2 * (self.extra@[j as int] as int) + 1)
1386                    <= 1_000_000_000 * (2 * 1_000_000_000 + 1)) by (nonlinear_arith)
1387                    requires
1388                        self.scores@[best as int] as int <= 1_000_000_000,
1389                        self.extra@[j as int] as int <= 1_000_000_000;
1390            }
1391            let lhs: u128 = (self.scores[j] as u128) * (2 * (self.extra[best] as u128) + 1);
1392            let rhs: u128 = (self.scores[best] as u128) * (2 * (self.extra[j] as u128) + 1);
1393            proof {
1394                assert(lhs as int == self.scores@[j as int] as int * (2 * self.extra@[best as int] as int + 1));
1395                assert(rhs as int == self.scores@[best as int] as int * (2 * self.extra@[j as int] as int + 1));
1396            }
1397            let old_best: usize = best;
1398            let old_j: usize = j;
1399            let _ = (old_best, old_j);
1400            if lhs > rhs {
1401                best = j;
1402                proof {
1403                    assert(priority_ge(self.scores@, self.extra@, old_j as int, old_best as int));
1404                    assert(priority_gt(self.scores@, self.extra@, old_j as int, old_best as int));
1405                    assert forall|c: int| 0 <= c < old_j as int + 1
1406                        implies priority_ge(self.scores@, self.extra@, best as int, c)
1407                    by {
1408                        if c == old_best as int {
1409                            assert(priority_ge(self.scores@, self.extra@, old_j as int, old_best as int));
1410                        } else if c < old_j as int {
1411                            lemma_priority_ge_trans(self.scores@, self.extra@, old_j as int, old_best as int, c);
1412                        } else {
1413                            // c == old_j as int == best as int, reflexive
1414                        }
1415                    }
1416                    assert forall|c: int| 0 <= c < old_j as int + 1
1417                        && priority_equal(self.scores@, self.extra@, best as int, c)
1418                        implies best as int <= c
1419                    by {
1420                        if c < old_j as int {
1421                            lemma_priority_gt_ge_not_equal(
1422                                self.scores@, self.extra@,
1423                                old_j as int, old_best as int, c,
1424                            );
1425                            assert(false);
1426                        }
1427                    }
1428                }
1429            } else {
1430                proof {
1431                    assert(priority_ge(self.scores@, self.extra@, old_best as int, old_j as int));
1432                    assert forall|c: int| 0 <= c < old_j as int + 1
1433                        implies priority_ge(self.scores@, self.extra@, best as int, c)
1434                    by {
1435                        if c == old_j as int {
1436                            assert(priority_ge(self.scores@, self.extra@, old_best as int, old_j as int));
1437                        } else {
1438                            // c < old_j: unchanged from the pre-iteration invariant
1439                        }
1440                    }
1441                    assert forall|c: int| 0 <= c < old_j as int + 1
1442                        && priority_equal(self.scores@, self.extra@, best as int, c)
1443                        implies best as int <= c
1444                    by {
1445                        if c == old_j as int {
1446                            assert(best < old_j);
1447                        }
1448                    }
1449                }
1450            }
1451            j = j + 1;
1452        }
1453        let ghost extra_old: Seq<u64> = self.extra@;
1454        let ghost best_g: int = best as int;
1455        proof {
1456            assert(self.scores@ == old(self).scores@);
1457            assert(extra_old == old(self).extra@);
1458            assert(priority_winner(self.scores@, extra_old, best_g));
1459            assert(old(self).score_order_preservation());
1460            assert(old(self).tie_boundedness());
1461            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] < self.scores@[d]
1462                implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int
1463            by {
1464                assert(old(self).weight(c) <= old(self).weight(d));
1465            }
1466            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] == self.scores@[d]
1467                implies 1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1
1468            by {
1469                assert(old(self).weight(c) <= old(self).weight(d) + 1);
1470            }
1471        }
1472        self.extra.set(best, self.extra[best] + 1);
1473        proof {
1474            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] < self.scores@[d]
1475                implies 1 + self.extra@[c] as int <= 1 + self.extra@[d] as int
1476            by {
1477                if c == best_g && d == best_g {
1478                    assert(false);
1479                } else if c == best_g {
1480                    lemma_priority_strict(self.scores@, extra_old, best_g, d);
1481                } else if d == best_g {
1482                    assert(1 + extra_old[c] as int <= 1 + extra_old[best_g] as int);
1483                } else {
1484                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int);
1485                }
1486            }
1487            assert forall|c: int, d: int| 0 <= c < n && 0 <= d < n && self.scores@[c] == self.scores@[d]
1488                implies 1 + self.extra@[c] as int <= 1 + self.extra@[d] as int + 1
1489            by {
1490                if c == best_g && d == best_g {
1491                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1492                } else if c == best_g {
1493                    if self.scores@[best_g] > 0 {
1494                        lemma_priority_tie(self.scores@, extra_old, best_g, d);
1495                    } else {
1496                        assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1497                    }
1498                } else if d == best_g {
1499                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1500                } else {
1501                    assert(1 + extra_old[c] as int <= 1 + extra_old[d] as int + 1);
1502                }
1503            }
1504            let old_sum = sum_to(extra_old, n as int);
1505            lemma_sum_ge_one(extra_old, best_g, n as int);
1506            lemma_sum_update(extra_old, best_g, (extra_old[best_g] + 1) as u64, n as int);
1507            assert(sum_to(self.extra@, n as int) == old_sum + 1);
1508            assert(sum_to(self.extra@, n as int) + n as int <= self.weight_total as int);
1509            assert forall|k: int| 0 <= k < n
1510                implies self.extra@[k] <= self.weight_total by {
1511                if k == best_g {
1512                    assert(extra_old[k] <= old_sum);
1513                }
1514            }
1515            assert(self.normalization_when_terminal()) by {
1516                if self.terminal() {
1517                    assert(self.normalization());
1518                }
1519            }
1520            assert(self.mutable_score_inv());
1521        }
1522        best
1523    }
1524
1525    /// CompetitiveSelectionSoftMutableScores `UpdateScore(c,v)`: update one mutable score
1526    /// and invalidate the partial apportionment in the same commit by resetting
1527    /// every extra award to the reserved floor.
1528    pub fn update_score(&mut self, c: usize, v: u64)
1529        requires
1530            old(self).mutable_score_inv(),
1531            c < old(self).scores.len(),
1532            1 <= v <= old(self).max_score,
1533        ensures
1534            final(self).weight_total == old(self).weight_total,
1535            final(self).max_score == old(self).max_score,
1536            final(self).scores@
1537                == old(self).scores@.update(c as int, v),
1538            final(self).extra@.len() == old(self).extra@.len(),
1539            forall|i: int| 0 <= i < final(self).extra.len() ==> final(self).extra@[i] == 0,
1540            final(self).mutable_score_inv(),
1541    {
1542        let ghost old_scores = self.scores@;
1543        self.scores.set(c, v);
1544        let ghost updated_scores = self.scores@;
1545        let n = self.extra.len();
1546        let mut i: usize = 0;
1547        while i < n
1548            invariant
1549                i <= n,
1550                n == self.extra.len(),
1551                n == self.scores.len(),
1552                self.scores@ == updated_scores,
1553                updated_scores == old_scores.update(c as int, v),
1554                self.weight_total == old(self).weight_total,
1555                self.max_score == old(self).max_score,
1556                forall|k: int| 0 <= k < i ==> self.extra@[k] == 0,
1557                forall|k: int| i <= k < n ==> self.extra@[k] == old(self).extra@[k],
1558            decreases n - i,
1559        {
1560            self.extra.set(i, 0);
1561            i = i + 1;
1562        }
1563        proof {
1564            lemma_sum_zero(self.extra@, n as int);
1565            assert(self.score_bounds()) by {
1566                assert forall|k: int| 0 <= k < n implies
1567                    self.scores@[k] >= 1 && self.scores@[k] <= 1_000_000_000 by {
1568                    if k != c as int {
1569                        assert(self.scores@[k] == old(self).scores@[k]);
1570                    }
1571                }
1572            }
1573            assert(self.score_order_preservation()) by {
1574                assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1575                    && self.scores@[a] < self.scores@[b]
1576                    implies self.weight(a) <= self.weight(b) by {}
1577            }
1578            assert(self.tie_boundedness()) by {
1579                assert forall|a: int, b: int| 0 <= a < n && 0 <= b < n
1580                    && self.scores@[a] == self.scores@[b]
1581                    implies self.weight(a) <= self.weight(b) + 1 by {}
1582            }
1583            assert(self.normalization_when_terminal()) by {
1584                if self.terminal() {
1585                    assert(self.normalization());
1586                }
1587            }
1588            assert(self.mutable_score_inv());
1589        }
1590    }
1591}
1592
1593// ── Ranked mode: top-K selection ────────────────────────────────────────
1594
1595/// Count of `true` entries among `s[0..n]`.
1596pub open spec fn count_true(s: Seq<bool>, n: int) -> int
1597    decreases n,
1598{
1599    if n <= 0 { 0 } else if n > s.len() as int { 0 }
1600    else { (if s[n - 1] { 1int } else { 0int }) + count_true(s, n - 1) }
1601}
1602
1603/// Setting a `false` entry to `true` raises the count by one.
1604pub proof fn lemma_count_set(s: Seq<bool>, m: int, n: int)
1605    requires 0 <= m < n <= s.len(), !s[m],
1606    ensures count_true(s.update(m, true), n) == count_true(s, n) + 1,
1607    decreases n,
1608{
1609    if n == m + 1 {
1610        lemma_count_unaffected(s, m, n - 1);
1611    } else {
1612        lemma_count_set(s, m, n - 1);
1613    }
1614}
1615
1616/// Updating index m does not change the count of a prefix stopping at or before m.
1617pub proof fn lemma_count_unaffected(s: Seq<bool>, m: int, p: int)
1618    requires 0 <= p <= m < s.len(),
1619    ensures count_true(s.update(m, true), p) == count_true(s, p),
1620    decreases p,
1621{
1622    if p > 0 {
1623        lemma_count_unaffected(s, m, p - 1);
1624    }
1625}
1626
1627/// An all-false prefix has count zero.
1628pub proof fn lemma_count_zero(s: Seq<bool>, n: int)
1629    requires 0 <= n <= s.len(), forall|k: int| 0 <= k < n ==> !s[k],
1630    ensures count_true(s, n) == 0,
1631    decreases n,
1632{
1633    if n > 0 {
1634        lemma_count_zero(s, n - 1);
1635    }
1636}
1637
1638/// An all-true prefix has count equal to its length.
1639pub proof fn lemma_count_all_true(s: Seq<bool>, n: int)
1640    requires 0 <= n <= s.len(), forall|k: int| 0 <= k < n ==> s[k],
1641    ensures count_true(s, n) == n,
1642    decreases n,
1643{
1644    if n > 0 {
1645        lemma_count_all_true(s, n - 1);
1646    }
1647}
1648
1649/// A boolean prefix contains at most one true entry per position.
1650pub proof fn lemma_count_upper(s: Seq<bool>, n: int)
1651    requires 0 <= n <= s.len(),
1652    ensures count_true(s, n) <= n,
1653    decreases n,
1654{
1655    if n > 0 {
1656        lemma_count_upper(s, n - 1);
1657    }
1658}
1659
1660/// Ranked competitive selection: pick the top-K candidates by score.
1661pub struct CompetitiveSelectionRanked {
1662    /// Candidate scores by candidate index.
1663    pub scores: Vec<u64>,
1664    /// Current selected membership by candidate index.
1665    pub selected: Vec<bool>,
1666    /// Maximum selected cardinality.
1667    pub k: usize,
1668    /// MaxScore for this refinement profile.
1669    pub max_score: u64,
1670}
1671
1672impl CompetitiveSelectionRanked {
1673    /// selected and scores share the Candidates domain.
1674    pub open spec fn type_invariant(&self) -> bool {
1675        self.selected.len() == self.scores.len()
1676            && forall|i: int| 0 <= i < self.scores.len() ==>
1677                #[trigger] self.scores@[i] <= self.max_score
1678    }
1679
1680    /// TLA+ `BoundedMultiplicity`: at most K winners.
1681    pub open spec fn bounded_multiplicity(&self) -> bool {
1682        count_true(self.selected@, self.selected@.len() as int) <= self.k
1683    }
1684
1685    /// TLA+ `ThresholdOptimality`: every selected scores >= every non-selected.
1686    pub open spec fn threshold_optimality(&self) -> bool {
1687        forall|s: int, c: int|
1688            #![trigger self.selected@[s], self.selected@[c]]
1689            0 <= s < self.selected.len() && 0 <= c < self.selected.len()
1690                && self.selected@[s] && !self.selected@[c]
1691                ==> self.scores@[s] >= self.scores@[c]
1692    }
1693
1694    /// TLA+ `RankedTieBreak`: every selected candidate is strictly better
1695    /// than every unselected candidate under score-then-lowest-index order.
1696    /// The vector index is the executable realization of the fixed WEnum/Pos
1697    /// order used by the formal model.
1698    pub open spec fn ranked_tie_break(&self) -> bool {
1699        forall|s: int, c: int|
1700            #![trigger self.selected@[s], self.selected@[c]]
1701            0 <= s < self.selected.len() && 0 <= c < self.selected.len()
1702                && self.selected@[s] && !self.selected@[c]
1703                ==> (self.scores@[s] > self.scores@[c]
1704                    || (self.scores@[s] == self.scores@[c] && s < c))
1705    }
1706
1707    /// Whether all ranked-selection obligations hold.
1708    pub open spec fn inv(&self) -> bool {
1709        self.type_invariant() && self.bounded_multiplicity()
1710            && self.threshold_optimality() && self.ranked_tie_break()
1711    }
1712
1713    /// Construct from scores; nothing selected yet (TLA+ Init).
1714    pub fn new(scores: Vec<u64>, k: usize, max_score: u64) -> (r: CompetitiveSelectionRanked)
1715        requires
1716            forall|i: int| 0 <= i < scores.len() ==> #[trigger] scores@[i] <= max_score,
1717        ensures
1718            r.scores@ == scores@,
1719            r.k == k,
1720            r.max_score == max_score,
1721            r.selected@.len() == scores@.len(),
1722            forall|j: int| 0 <= j < r.selected@.len() ==> !r.selected@[j],
1723            r.inv(),
1724    {
1725        let n = scores.len();
1726        let mut selected: Vec<bool> = Vec::new();
1727        let mut i: usize = 0;
1728        while i < n
1729            invariant
1730                i <= n,
1731                selected.len() == i,
1732                forall|j: int| 0 <= j < i ==> !selected@[j],
1733            decreases n - i,
1734        {
1735            selected.push(false);
1736            i = i + 1;
1737        }
1738        proof { lemma_count_zero(selected@, selected@.len() as int); }
1739        CompetitiveSelectionRanked { scores, selected, k, max_score }
1740    }
1741
1742    /// The unselected candidate with the highest score, or None if all selected.
1743    fn find_max_unselected(&self) -> (r: Option<usize>)
1744        requires self.type_invariant(),
1745        ensures
1746            r is None ==> (forall|c: int| 0 <= c < self.selected.len() ==> self.selected@[c]),
1747            r matches Option::Some(m) ==> (m < self.scores.len() && !self.selected@[m as int]
1748                && (forall|c: int| 0 <= c < self.scores.len() && !self.selected@[c]
1749                    ==> #[trigger] self.scores@[c] <= self.scores@[m as int])
1750                && (forall|c: int| 0 <= c < self.scores.len() && !self.selected@[c]
1751                    && #[trigger] self.scores@[c] == self.scores@[m as int]
1752                    ==> m as int <= c)),
1753    {
1754        let n = self.scores.len();
1755        let mut best: Option<usize> = None;
1756        let mut i: usize = 0;
1757        while i < n
1758            invariant
1759                i <= n,
1760                n == self.scores.len(),
1761                self.selected.len() == n,
1762                best is None ==> (forall|c: int| 0 <= c < i ==> self.selected@[c]),
1763                best matches Option::Some(m) ==> (m < i && !self.selected@[m as int]
1764                    && (forall|c: int| 0 <= c < i && !self.selected@[c]
1765                        ==> #[trigger] self.scores@[c] <= self.scores@[m as int])
1766                    && (forall|c: int| 0 <= c < i && !self.selected@[c]
1767                        && #[trigger] self.scores@[c] == self.scores@[m as int]
1768                        ==> m as int <= c)),
1769            decreases n - i,
1770        {
1771            if !self.selected[i] {
1772                match best {
1773                    Option::Some(m) => {
1774                        if self.scores[i] > self.scores[m] {
1775                            best = Some(i);
1776                        }
1777                    }
1778                    Option::None => {
1779                        best = Some(i);
1780                    }
1781                }
1782            }
1783            i = i + 1;
1784        }
1785        best
1786    }
1787
1788    /// Select the top-K (TLA+ Select): mark up to K highest-scoring candidates,
1789    /// re-establishing BoundedMultiplicity and ThresholdOptimality.
1790    pub fn select(&mut self)
1791        requires old(self).type_invariant(),
1792        ensures
1793            final(self).scores@ == old(self).scores@,
1794            final(self).k == old(self).k,
1795            final(self).max_score == old(self).max_score,
1796            count_true(final(self).selected@, final(self).selected@.len() as int)
1797                == if final(self).k < final(self).selected.len() {
1798                    final(self).k as int
1799                } else {
1800                    final(self).selected.len() as int
1801                },
1802            final(self).inv(),
1803    {
1804        let n = self.scores.len();
1805        let original_len = self.selected.len();
1806        let original_k = self.k;
1807        let _ = (original_len, original_k);
1808        assert(original_len == n);
1809        // Reset selection to empty.
1810        let mut i: usize = 0;
1811        while i < n
1812            invariant
1813                i <= n,
1814                n == self.scores.len(),
1815                self.selected.len() == n,
1816                self.type_invariant(),
1817                self.scores@ == old(self).scores@,
1818                self.k == old(self).k,
1819                self.max_score == old(self).max_score,
1820                original_len == n,
1821                original_k == self.k,
1822                forall|j: int| 0 <= j < i ==> !self.selected@[j],
1823            decreases n - i,
1824        {
1825            self.selected.set(i, false);
1826            i = i + 1;
1827        }
1828        proof { lemma_count_zero(self.selected@, n as int); }
1829        // Greedily mark the highest unselected, up to K rounds.
1830        let mut round: usize = 0;
1831        while round < self.k
1832            invariant
1833                n == self.scores.len(),
1834                self.selected.len() == n,
1835                self.type_invariant(),
1836                self.scores@ == old(self).scores@,
1837                self.k == old(self).k,
1838                self.max_score == old(self).max_score,
1839                original_len == n,
1840                original_k == self.k,
1841                round <= self.k,
1842                count_true(self.selected@, n as int) == round,
1843                self.threshold_optimality(),
1844                self.ranked_tie_break(),
1845            decreases self.k - round,
1846        {
1847            let m_opt = self.find_max_unselected();
1848            match m_opt {
1849                Option::Some(m) => {
1850                    let ghost s0 = self.selected@;
1851                    assert forall|c: int| 0 <= c < n && !s0[c]
1852                        implies self.scores@[c] <= self.scores@[m as int] by {}
1853                    assert forall|c: int| 0 <= c < n && !s0[c]
1854                        && self.scores@[c] == self.scores@[m as int]
1855                        implies m as int <= c by {}
1856                    proof { lemma_count_set(s0, m as int, n as int); }
1857                    self.selected.set(m, true);
1858                    proof {
1859                        assert(self.threshold_optimality()) by {
1860                            assert forall|s: int, c: int|
1861                                #![trigger self.selected@[s], self.selected@[c]]
1862                                0 <= s < n && 0 <= c < n && self.selected@[s] && !self.selected@[c]
1863                                implies self.scores@[s] >= self.scores@[c] by {
1864                                if s != m as int {
1865                                    assert(s0[s]);
1866                                }
1867                            }
1868                        }
1869                        assert(self.ranked_tie_break()) by {
1870                            assert forall|s: int, c: int|
1871                                #![trigger self.selected@[s], self.selected@[c]]
1872                                0 <= s < n && 0 <= c < n
1873                                    && self.selected@[s] && !self.selected@[c]
1874                                implies self.scores@[s] > self.scores@[c]
1875                                    || (self.scores@[s] == self.scores@[c] && s < c) by {
1876                                if s != m as int {
1877                                    assert(s0[s]);
1878                                    assert(!s0[c]);
1879                                } else {
1880                                    assert(!s0[c]);
1881                                    assert(c != m as int);
1882                                    if self.scores@[m as int] == self.scores@[c] {
1883                                        assert(m as int <= c);
1884                                        assert((m as int) < c);
1885                                    }
1886                                }
1887                            }
1888                        }
1889                    }
1890                }
1891                Option::None => {
1892                    proof {
1893                        lemma_count_all_true(self.selected@, n as int);
1894                        assert(count_true(self.selected@, n as int) == n as int);
1895                        assert(round as int == n as int);
1896                        assert(round < self.k);
1897                        assert(n < self.k);
1898                        assert(original_len == n);
1899                        assert(original_k == self.k);
1900                        assert(!(original_k < original_len));
1901                    }
1902                    return;
1903                }
1904            }
1905            round = round + 1;
1906        }
1907        proof {
1908            lemma_count_upper(self.selected@, n as int);
1909            assert(round == self.k);
1910            assert(count_true(self.selected@, n as int) == self.k as int);
1911            assert(self.k <= n);
1912        }
1913    }
1914
1915    /// Replace the scores and clear the selection (TLA+ UpdateScores).
1916    pub fn update_scores(&mut self, new_scores: Vec<u64>)
1917        requires
1918            old(self).type_invariant(),
1919            new_scores.len() == old(self).scores.len(),
1920            forall|i: int| 0 <= i < new_scores.len() ==>
1921                #[trigger] new_scores@[i] <= old(self).max_score,
1922        ensures
1923            final(self).scores@ == new_scores@,
1924            final(self).k == old(self).k,
1925            final(self).max_score == old(self).max_score,
1926            forall|i: int| 0 <= i < final(self).selected.len() ==>
1927                !final(self).selected@[i],
1928            final(self).inv(),
1929    {
1930        let ghost ns = new_scores@;
1931        let n = new_scores.len();
1932        self.scores = new_scores;
1933        let mut i: usize = 0;
1934        while i < n
1935            invariant
1936                i <= n,
1937                n == self.scores.len(),
1938                self.selected.len() == n,
1939                self.scores@ == ns,
1940                self.k == old(self).k,
1941                self.max_score == old(self).max_score,
1942                forall|j: int| 0 <= j < i ==> !self.selected@[j],
1943            decreases n - i,
1944        {
1945            self.selected.set(i, false);
1946            i = i + 1;
1947        }
1948        proof { lemma_count_zero(self.selected@, n as int); }
1949    }
1950}
1951
1952}