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.invariant(),
148    {
149        proof {
150            lemma_pow2_positive(max_probes);
151            assert(hi as int - lo as int <= domain_size as int);
152        }
153        Bisection {
154            domain_size,
155            budget: crate::primitives::budget::Budget::new(max_probes),
156            lo,
157            hi,
158            threshold,
159        }
160    }
161
162    /// Whether the interval has been narrowed to a point (TLA+ Converged guard).
163    pub fn converged(&self) -> (c: bool)
164        requires
165            self.invariant(),
166        ensures
167            c == (self.hi - self.lo < 2),
168    {
169        self.hi - self.lo < 2
170    }
171
172    /// Probe the midpoint and narrow the interval -- one atomic step (TLA+
173    /// ProbeLeft / ProbeRight). Maintains MonotonicityPreservation, at least
174    /// halves the interval (the Halving property), and strictly decreases its
175    /// width (the loop-termination measure).
176    pub fn probe(&mut self)
177        requires
178            old(self).hi - old(self).lo >= 2,
179            old(self).invariant(),
180        ensures
181            final(self).invariant(),
182            // Halving: the probe at least halves the interval.
183            final(self).hi - final(self).lo <= (old(self).hi - old(self).lo) / 2,
184            // Loop-termination measure: the width strictly decreases.
185            final(self).hi - final(self).lo < old(self).hi - old(self).lo,
186            final(self).domain_size == old(self).domain_size,
187            final(self).budget.capacity == old(self).budget.capacity,
188            final(self).threshold == old(self).threshold,
189            crate::connectives::cursor::cursor_admitted(
190                old(self).lo as nat,
191                final(self).lo as nat,
192            ),
193            final(self).budget.allocated == old(self).budget.allocated + 1,
194            // The probe lands on the midpoint and frames the endpoint
195            // that does not move. The five clauses above are properties of the
196            // post-state interval's width and position -- including all three
197            // that Bisection.cfg checks -- and they do NOT pin the interval: the
198            // spec's ProbeLeft/ProbeRight are deterministic, and without this
199            // clause the contract admits post-states that are not steps of the
200            // spec. It does not state which side is taken because monotonicity
201            // already decides that branch.
202            (final(self).hi as int == old(self).mid() && final(self).lo == old(self).lo)
203                || (final(self).lo as int == old(self).mid() + 1
204                        && final(self).hi == old(self).hi),
205    {
206        let old_lo = self.lo;
207        let old_hi = self.hi;
208        let old_width = self.hi - self.lo;
209        let remaining = self.budget.capacity - self.budget.allocated;
210        let _ = (old_lo, old_hi, old_width, remaining);
211        proof {
212            if self.budget.allocated == self.budget.capacity {
213                assert(remaining == 0);
214                assert(pow2(remaining) == 1);
215                assert(self.hi as int - self.lo as int <= 1);
216                assert(false);
217            }
218            assert(self.budget.allocated < self.budget.capacity);
219            assert(remaining > 0);
220            lemma_pow2_step(remaining);
221            lemma_pow2_positive((remaining - 1) as u64);
222        }
223        let mid = self.lo + (self.hi - self.lo) / 2;
224        if mid >= self.threshold {
225            // P(mid) = FALSE: threshold in [lo, mid] -> hi' = mid
226            self.hi = mid;
227        } else {
228            // P(mid) = TRUE: threshold in [mid+1, hi] -> lo' = mid + 1
229            self.lo = mid + 1;
230        }
231        let _accepted = self.budget.try_allocate(1);
232        assert(_accepted);
233        proof {
234            assert(self.hi - self.lo <= old_width / 2);
235            assert(old_width as int <= pow2(remaining));
236            assert(old_width as int / 2 <= pow2((remaining - 1) as u64));
237            assert(self.hi as int - self.lo as int <= old_width as int / 2);
238            assert(self.budget.capacity - self.budget.allocated == remaining - 1);
239            assert(self.hi as int - self.lo as int
240                <= pow2((self.budget.capacity - self.budget.allocated) as u64));
241            assert(old_lo <= old_hi);
242        }
243    }
244
245    /// Drive probes to convergence (the TLA+ EventualConvergence under fairness).
246    /// The `decreases hi - lo` is the loop-termination witness: the loop
247    /// halts, and on exit the interval is a point (hi - lo < 2) that still
248    /// straddles the threshold.
249    pub fn bisect(&mut self)
250        requires
251            old(self).invariant(),
252        ensures
253            final(self).invariant(),
254            final(self).hi - final(self).lo < 2,
255            final(self).domain_size == old(self).domain_size,
256            final(self).budget.capacity == old(self).budget.capacity,
257            final(self).threshold == old(self).threshold,
258    {
259        while self.hi - self.lo >= 2
260            invariant
261                self.invariant(),
262                self.domain_size == old(self).domain_size,
263                self.budget.capacity == old(self).budget.capacity,
264                self.threshold == old(self).threshold,
265            decreases self.hi - self.lo,
266        {
267            self.probe();
268        }
269    }
270}
271
272// ── Applied realization: binary search (a Bisection instance) ─────────────
273
274/// A slice is sorted (non-strictly ascending) over its index range.
275pub open spec fn is_sorted(s: Seq<u64>) -> bool {
276    forall|i: int, j: int| 0 <= i <= j < s.len() ==> s[i] <= s[j]
277}
278
279/// Binary search: the search-level view of Bisection -- halve [lo, hi) until the
280/// target index is found, or return `sorted.len()` if absent. The interval
281/// invariants are carried by the `Bisection` machine above. `decreases hi - lo`
282/// states that each probe strictly shrinks the interval.
283pub fn bisection_find(sorted: &[u64], target: u64) -> (idx: usize)
284    requires
285        is_sorted(sorted@),
286    ensures
287        idx as int <= sorted@.len() as int,
288        idx < sorted@.len() ==> sorted@[idx as int] == target,
289{
290    let n: usize = sorted.len();
291    let mut lo: usize = 0;
292    let mut hi: usize = n;
293    while lo < hi
294        invariant
295            lo <= hi,
296            hi <= n,
297            n == sorted@.len(),
298            is_sorted(sorted@),
299        decreases hi - lo,
300    {
301        let mid = lo + (hi - lo) / 2;
302        let mid_val = sorted[mid];
303        if mid_val == target {
304            return mid;
305        } else if mid_val < target {
306            lo = mid + 1;
307        } else {
308            hi = mid;
309        }
310    }
311    n  // not present
312}
313
314}