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}