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}