Skip to main content

automation_structures/compositions/
bisection.rs

1// Executable carrier for Bisection.tla.
2//
3// Bisection maintains a candidate interval [lo, hi] over an ordered domain with a
4// monotone boundary (the threshold) and halves the interval per probe. The TLA+
5// spec is the lo/hi/probes machine and checks:
6//
7//   MonotonicityPreservation (INVARIANT) -- lo <= threshold <= hi
8//   Halving (PROPERTY)        -- per-probe contraction: hi' - lo' <= (hi - lo) / 2
9//   ProbeBound (INVARIANT)    -- probes_taken <= MaxProbes, with
10//                                EventualConvergence (<>(hi - lo < 2)) the liveness companion.
11//
12//   - `Bisection { domain_size, max_probes, lo, hi, threshold, probes_taken }`
13//     mirrors the TLA+ constants and variables. The counter is executable so
14//     the budget boundary is observable rather than only a ghost assertion.
15//   - `monotonicity()` is the MonotonicityPreservation invariant.
16//   - `probe()` is the TLA+ ProbeLeft/ProbeRight action — one atomic step that
17//     maintains monotonicity and ensures hi'-lo' <= (hi-lo)/2.
18//   - `bisect()` drives probe() to convergence; `decreases hi - lo` is the
19//     loop-termination witness.
20//   - `bisection_find()` is a binary-search realization: the search-level view.
21//
22// Monotonicity, halving, and the termination measure do not
23// determine the post-state selected by ProbeLeft/ProbeRight. `probe` therefore
24// also ensures the specified midpoint endpoint update and frames the other
25// endpoint; this action correspondence is stronger than the checked invariants.
26
27use vstd::prelude::*;
28
29verus! {
30
31// Local power-of-two definition used by the probe-budget proof. It is local to
32// this module so the bound does not depend on another carrier's specification.
33/// Return two raised to the nonnegative exponent `k`.
34pub open spec fn pow2(k: u64) -> int
35    decreases k,
36{
37    if k == 0 { 1 } else { 2 * pow2((k - 1) as u64) }
38}
39
40/// Prove that every value returned by [`pow2`] is positive.
41pub proof fn lemma_pow2_positive(k: u64)
42    ensures pow2(k) >= 1,
43    decreases k,
44{
45    if k > 0 {
46        lemma_pow2_positive((k - 1) as u64);
47    }
48}
49
50/// Prove the successor recurrence for [`pow2`].
51pub proof fn lemma_pow2_step(k: u64)
52    requires k > 0,
53    ensures pow2(k) == 2 * pow2((k - 1) as u64),
54{
55}
56
57// ── The Bisection machine (TLA+ Bisection.tla: lo/hi/probes) ──────────────
58
59/// Bounded monotone-boundary bisection carrier.
60pub struct Bisection {
61    /// Size of the ordered domain.
62    pub domain_size: u64,
63    /// Budget that counts admitted probes.
64    pub budget: crate::primitives::budget::Budget,
65    /// Inclusive lower interval endpoint.
66    pub lo: u64,
67    /// Inclusive upper interval endpoint.
68    pub hi: u64,
69    /// Monotone boundary retained inside the interval.
70    pub threshold: u64,
71}
72
73impl Bisection {
74    /// TLA+ constant assumptions and TypeInvariant, including the finite
75    /// ordered-domain bounds represented by executable u64 values.
76    pub open spec fn type_invariant(&self) -> bool {
77        &&& self.domain_size >= 2
78        &&& 1 <= self.threshold < self.domain_size
79        &&& self.lo <= self.hi
80        &&& self.hi <= self.domain_size
81    }
82
83    /// TLA+ MonotonicityPreservation: the threshold always lies in [lo, hi].
84    pub open spec fn monotonicity(&self) -> bool {
85        self.lo <= self.threshold && self.threshold <= self.hi
86    }
87
88    /// TLA+ DomainFitsProbes constant assumption.
89    pub open spec fn domain_fits_probes(&self) -> bool {
90        self.domain_size as int <= pow2(self.budget.capacity)
91    }
92
93    /// TLA+ ProbeBound safety invariant.
94    pub open spec fn probe_bound(&self) -> bool {
95        &&& self.budget.safety_invariant()
96        &&& self.budget.reserved == 0
97        &&& self.budget.pending_eviction == 0
98    }
99
100    /// The TLAPS proof's exact inductive strengthening. ProbeBound alone is
101    /// not inductive at the budget edge; this relates the remaining width to
102    /// the remaining power-of-two probe capacity.
103    pub open spec fn width_exp_bound(&self) -> bool {
104        &&& self.probe_bound()
105        &&& self.hi as int - self.lo as int
106            <= pow2((self.budget.capacity - self.budget.allocated) as u64)
107    }
108
109    /// Whether the interval, threshold, cursor, and probe budget are mutually consistent.
110    pub open spec fn invariant(&self) -> bool {
111        &&& self.type_invariant()
112        &&& self.monotonicity()
113        &&& self.domain_fits_probes()
114        &&& self.width_exp_bound()
115    }
116
117    /// The probe point: the TLA+ `Mid == (lo + hi) \div 2`, written in the
118    /// overflow-avoiding form the executable uses. The two are equal for
119    /// lo <= hi; the form here exists only so the sum cannot overflow u64.
120    pub open spec fn mid(&self) -> int {
121        self.lo as int + (self.hi as int - self.lo as int) / 2
122    }
123
124    /// Init (TLA+ Init): a candidate interval straddling the threshold, no
125    /// probes taken yet.
126    pub fn new(
127        lo: u64,
128        hi: u64,
129        threshold: u64,
130        domain_size: u64,
131        max_probes: u64,
132    ) -> (b: Bisection)
133        requires
134            domain_size >= 2,
135            1 <= threshold < domain_size,
136            lo <= threshold,
137            threshold <= hi,
138            hi <= domain_size,
139            domain_size as int <= pow2(max_probes),
140        ensures
141            b.domain_size == domain_size,
142            b.budget.capacity == max_probes,
143            b.lo == lo,
144            b.hi == hi,
145            b.threshold == threshold,
146            b.budget.allocated == 0,
147            b.budget.reserved == 0,
148            b.budget.pending_eviction == 0,
149            b.invariant(),
150    {
151        proof {
152            lemma_pow2_positive(max_probes);
153            assert(hi as int - lo as int <= domain_size as int);
154        }
155        Bisection {
156            domain_size,
157            budget: crate::primitives::budget::Budget::new(max_probes),
158            lo,
159            hi,
160            threshold,
161        }
162    }
163
164    /// Whether the interval has been narrowed to a point (TLA+ Converged guard).
165    pub fn converged(&self) -> (c: bool)
166        requires
167            self.invariant(),
168        ensures
169            c == (self.hi - self.lo < 2),
170    {
171        self.hi - self.lo < 2
172    }
173
174    /// Probe the midpoint and narrow the interval -- one atomic step (TLA+
175    /// ProbeLeft / ProbeRight). Maintains MonotonicityPreservation, at least
176    /// halves the interval (the Halving property), and strictly decreases its
177    /// width (the loop-termination measure).
178    pub fn probe(&mut self)
179        requires
180            old(self).hi - old(self).lo >= 2,
181            old(self).invariant(),
182        ensures
183            final(self).invariant(),
184            // Halving: the probe at least halves the interval.
185            final(self).hi - final(self).lo <= (old(self).hi - old(self).lo) / 2,
186            // Loop-termination measure: the width strictly decreases.
187            final(self).hi - final(self).lo < old(self).hi - old(self).lo,
188            final(self).domain_size == old(self).domain_size,
189            final(self).budget.capacity == old(self).budget.capacity,
190            final(self).threshold == old(self).threshold,
191            crate::connectives::cursor::cursor_admitted(
192                old(self).lo as nat,
193                final(self).lo as nat,
194            ),
195            final(self).budget.allocated == old(self).budget.allocated + 1,
196            // The probe lands on the midpoint and frames the endpoint
197            // that does not move. The five clauses above are properties of the
198            // post-state interval's width and position -- including all three
199            // that Bisection.cfg checks -- and they do NOT pin the interval: the
200            // spec's ProbeLeft/ProbeRight are deterministic, and without this
201            // clause the contract admits post-states that are not steps of the
202            // spec. It does not state which side is taken because monotonicity
203            // already decides that branch.
204            (final(self).hi as int == old(self).mid() && final(self).lo == old(self).lo)
205                || (final(self).lo as int == old(self).mid() + 1
206                        && final(self).hi == old(self).hi),
207    {
208        let old_lo = self.lo;
209        let old_hi = self.hi;
210        let old_width = self.hi - self.lo;
211        let remaining = self.budget.capacity - self.budget.allocated;
212        let _ = (old_lo, old_hi, old_width, remaining);
213        proof {
214            if self.budget.allocated == self.budget.capacity {
215                assert(remaining == 0);
216                assert(pow2(remaining) == 1);
217                assert(self.hi as int - self.lo as int <= 1);
218                assert(false);
219            }
220            assert(self.budget.allocated < self.budget.capacity);
221            assert(remaining > 0);
222            lemma_pow2_step(remaining);
223            lemma_pow2_positive((remaining - 1) as u64);
224        }
225        let mid = self.lo + (self.hi - self.lo) / 2;
226        if mid >= self.threshold {
227            // P(mid) = FALSE: threshold in [lo, mid] -> hi' = mid
228            self.hi = mid;
229        } else {
230            // P(mid) = TRUE: threshold in [mid+1, hi] -> lo' = mid + 1
231            self.lo = mid + 1;
232        }
233        let _accepted = self.budget.try_allocate(1);
234        assert(_accepted);
235        proof {
236            assert(self.hi - self.lo <= old_width / 2);
237            assert(old_width as int <= pow2(remaining));
238            assert(old_width as int / 2 <= pow2((remaining - 1) as u64));
239            assert(self.hi as int - self.lo as int <= old_width as int / 2);
240            assert(self.budget.capacity - self.budget.allocated == remaining - 1);
241            assert(self.hi as int - self.lo as int
242                <= pow2((self.budget.capacity - self.budget.allocated) as u64));
243            assert(old_lo <= old_hi);
244        }
245    }
246
247    /// Drive probes to convergence by choosing every next probe inside this call.
248    /// The `decreases hi - lo` is the loop-termination witness: the loop
249    /// halts, and on exit the interval is a point (hi - lo < 2) that still
250    /// straddles the threshold. When probe selection is delegated to an external scheduler,
251    /// temporal convergence instead depends on the corresponding scheduling fairness rely.
252    pub fn bisect(&mut self)
253        requires
254            old(self).invariant(),
255        ensures
256            final(self).invariant(),
257            final(self).hi - final(self).lo < 2,
258            final(self).domain_size == old(self).domain_size,
259            final(self).budget.capacity == old(self).budget.capacity,
260            final(self).threshold == old(self).threshold,
261    {
262        while self.hi - self.lo >= 2
263            invariant
264                self.invariant(),
265                self.domain_size == old(self).domain_size,
266                self.budget.capacity == old(self).budget.capacity,
267                self.threshold == old(self).threshold,
268            decreases self.hi - self.lo,
269        {
270            self.probe();
271        }
272    }
273}
274
275// ── Applied realization: binary search (a Bisection instance) ─────────────
276
277/// A slice is sorted (non-strictly ascending) over its index range.
278pub open spec fn is_sorted(s: Seq<u64>) -> bool {
279    forall|i: int, j: int| 0 <= i <= j < s.len() ==> s[i] <= s[j]
280}
281
282/// Binary search: the search-level view of Bisection -- halve [lo, hi) until the
283/// target index is found, or return `sorted.len()` if absent. The interval
284/// invariants are carried by the `Bisection` machine above. `decreases hi - lo`
285/// states that each probe strictly shrinks the interval.
286pub fn bisection_find(sorted: &[u64], target: u64) -> (idx: usize)
287    requires
288        is_sorted(sorted@),
289    ensures
290        idx as int <= sorted@.len() as int,
291        idx < sorted@.len() ==> sorted@[idx as int] == target,
292        idx == sorted@.len() ==> forall|index: int|
293            0 <= index < sorted@.len() ==> #[trigger] sorted@[index] != target,
294{
295    let n: usize = sorted.len();
296    let mut lo: usize = 0;
297    let mut hi: usize = n;
298    while lo < hi
299        invariant
300            lo <= hi,
301            hi <= n,
302            n == sorted@.len(),
303            is_sorted(sorted@),
304            forall|index: int| 0 <= index < lo ==>
305                #[trigger] sorted@[index] < target,
306            forall|index: int| hi <= index < n ==>
307                #[trigger] sorted@[index] > target,
308        decreases hi - lo,
309    {
310        let mid = lo + (hi - lo) / 2;
311        let mid_val = sorted[mid];
312        if mid_val == target {
313            return mid;
314        } else if mid_val < target {
315            assert forall|index: int| lo <= index <= mid implies
316                #[trigger] sorted@[index] < target by {
317                assert(sorted@[index] <= sorted@[mid as int]);
318            }
319            lo = mid + 1;
320        } else {
321            assert forall|index: int| mid <= index < hi implies
322                #[trigger] sorted@[index] > target by {
323                assert(sorted@[mid as int] <= sorted@[index]);
324            }
325            hi = mid;
326        }
327    }
328    assert forall|index: int| 0 <= index < n implies
329        #[trigger] sorted@[index] != target by {
330        if index < lo {
331            assert(sorted@[index] < target);
332        } else {
333            assert(index >= hi);
334            assert(sorted@[index] > target);
335        }
336    }
337    n  // not present
338}
339
340}