Skip to main content

strop_core/
editmap.rs

1//! The anchor-mapping kernel, verified (0045): `map_position` carries the
2//! proof of its contract in-tree; `cargo verus verify` checks it, and the
3//! normal build compiles the `verus!` block as plain Rust (ghost code
4//! erases). This is the production function `editor/transact.rs` calls —
5//! not a copy.
6//!
7//! Verified contract (Verus 0.2026.09.06, rustc 1.98.0, z3 4.16.0):
8//! - anchors before the edit never move;
9//! - anchors at/past the old end shift by the delta (right affinity);
10//! - anchors inside the replaced range collapse to the edit start;
11//! - the mapping is monotone and stays inside the new buffer length;
12//! - batch composition (0057 VF02, the 0045 stretch obligation): folding
13//!   the per-edit mappings over a validated batch in its reverse
14//!   application order — exactly what `apply_prepared` publishes and
15//!   `transact.rs` consumes — equals the direct whole-batch map over the
16//!   original coordinates, and the composed mapping is monotone and in
17//!   bounds.
18
19use vstd::prelude::*;
20
21verus! {
22
23/// The mapping, mathematically: right-affinity anchor through one edit.
24pub open spec fn map_spec(position: int, start: int, old_end: int, new_end: int) -> int {
25    if position < start {
26        position
27    } else if position >= old_end {
28        new_end + (position - old_end)
29    } else {
30        start
31    }
32}
33
34/// One anchor through one edit. Callers guarantee no overflow: positions
35/// are byte offsets bounded by the buffer length, and `new_end` is the
36/// post-edit end of the replaced range (`start + replacement length`).
37pub fn map_position(position: usize, start: usize, old_end: usize, new_end: usize) -> (mapped: usize)
38    requires
39        start <= old_end,
40        position >= old_end ==> new_end + (position - old_end) <= usize::MAX,
41    ensures
42        mapped as int == map_spec(position as int, start as int, old_end as int, new_end as int),
43{
44    if position < start {
45        position
46    } else if position >= old_end {
47        new_end.saturating_add(position - old_end)
48    } else {
49        start
50    }
51}
52
53/// Monotonicity: anchors keep their relative order through one edit.
54/// `start <= new_end` holds by construction (the post-edit end of a
55/// replacement is its start plus the replacement length).
56proof fn map_spec_monotone(a: int, b: int, start: int, old_end: int, new_end: int)
57    requires
58        start <= old_end,
59        start <= new_end,
60        a <= b,
61    ensures
62        map_spec(a, start, old_end, new_end) <= map_spec(b, start, old_end, new_end),
63{
64}
65
66/// Bounds: an anchor inside the old buffer stays inside the new one.
67proof fn map_spec_in_bounds(
68    position: int,
69    start: int,
70    old_end: int,
71    new_end: int,
72    old_len: int,
73    new_len: int,
74)
75    requires
76        0 <= start <= old_end <= old_len,
77        0 <= start <= new_end <= new_len,
78        0 <= position <= old_len,
79        new_len == old_len - (old_end - start) + (new_end - start),
80    ensures
81        0 <= map_spec(position, start, old_end, new_end) <= new_len,
82{
83}
84
85}
86
87verus! {
88
89/// Seq of usize pairs as spec ints (Seq::map carries the indexing axiom).
90pub open spec fn to_ints(edits: Seq<(usize, usize)>) -> Seq<(int, int)> {
91    edits.map_values(|pair: (usize, usize)| (pair.0 as int, pair.1 as int))
92}
93
94/// The geometry contract a prepared batch meets (buffer/mutation.rs).
95pub open spec fn batch_ok(edits: Seq<(int, int)>, len: int) -> bool {
96    forall|i: int| #![trigger edits[i]] 0 <= i < edits.len() ==> {
97        let (s, e) = edits[i];
98        &&& 0 <= s <= e <= len
99        &&& i + 1 < edits.len() ==> {
100            let (s2, _) = edits[i + 1];
101            &&& s < s2
102            &&& e <= s2
103        }
104    }
105}
106
107/// Insert algebra for one insertion-sort step: inserting (i, pair) at the
108/// walked position j keeps the index bookkeeping and consecutive sortedness.
109proof fn insert_preserves(
110    edits: Seq<(usize, usize)>,
111    pre: Seq<(usize, (usize, usize))>,
112    j: usize,
113    i: usize,
114    pair: (usize, usize),
115)
116    requires
117        j as int <= pre.len(),
118        pre.len() == i as int,
119        (i as int) < edits.len(),
120        pair == edits[i as int],
121        forall|k: int| #![trigger pre[k]] 0 <= k < pre.len() ==> {
122            &&& (pre[k].0 as int) < i as int
123            &&& pre[k].1 == edits[pre[k].0 as int]
124        },
125        forall|a: int, b: int| #![trigger pre[a], pre[b]]
126            0 <= a < b < pre.len() ==> pre[a].0 != pre[b].0,
127        forall|k: int| #![trigger pre[k]] 0 <= k < pre.len() - 1 ==>
128            pre[k].1.0 <= pre[k + 1].1.0,
129        j > 0 ==> pre[j as int - 1].1.0 as int <= pair.0 as int,
130        forall|p: int| #![trigger pre[p]] j as int <= p < pre.len() ==>
131            pre[p].1.0 as int > pair.0 as int,
132    ensures
133        ({
134            let post = pre.insert(j as int, (i, pair));
135            &&& post.len() == i as int + 1
136            &&& forall|k: int| #![trigger post[k]] 0 <= k < post.len() ==> {
137                &&& (post[k].0 as int) < i as int + 1
138                &&& post[k].1 == edits[post[k].0 as int]
139            }
140            &&& forall|a: int, b: int| #![trigger post[a], post[b]]
141                0 <= a < b < post.len() ==> post[a].0 != post[b].0
142            &&& forall|k: int| #![trigger post[k]] 0 <= k < post.len() - 1 ==>
143                post[k].1.0 <= post[k + 1].1.0
144        }),
145{
146    let post = pre.insert(j as int, (i, pair));
147    assert forall|k: int| #![trigger post[k]] 0 <= k < post.len() implies {
148        &&& (post[k].0 as int) < i as int + 1
149        &&& post[k].1 == edits[post[k].0 as int]
150    } by {
151        if k == j as int {
152            assert(post[k] == (i, pair));
153        } else if k < j as int {
154            assert(post[k] == pre[k]);
155        } else {
156            assert(post[k] == pre[k - 1]);
157        }
158    }
159    assert forall|a: int, b: int| #![trigger post[a], post[b]]
160        0 <= a < b < post.len() implies post[a].0 != post[b].0 by {
161        if a == j as int {
162            assert(post[b] == pre[b - 1]);
163        } else if b == j as int {
164            assert(post[a] == pre[a]);
165        } else {
166            assert(post[a] == if a < j as int { pre[a] } else { pre[a - 1] });
167            assert(post[b] == if b < j as int { pre[b] } else { pre[b - 1] });
168        }
169    }
170    assert forall|k: int| #![trigger post[k]] 0 <= k < post.len() - 1 implies
171        post[k].1.0 <= post[k + 1].1.0 by {
172        if k + 1 < j as int {
173            assert(post[k] == pre[k]);
174            assert(post[k + 1] == pre[k + 1]);
175        } else if k + 1 == j as int {
176            assert(post[k] == pre[k]);
177            assert(post[k + 1] == (i, pair));
178            // walk exit: j > 0 here, so pre[j-1].1.0 <= pair.0
179        } else if k == j as int {
180            assert(post[k] == (i, pair));
181            assert(post[k + 1] == pre[k]);
182            // walk invariant: pre[j].1.0 > pair.0
183        } else {
184            assert(post[k] == pre[k - 1]);
185            assert(post[k + 1] == pre[k]);
186        }
187    }
188}
189
190/// Insertion sort by start, carrying each edit's original index so error
191/// witnesses can name two DISTINCT input edits. Batches are small.
192/// clippy::ptr_arg: Verus's Vec specs reason over Vec, not slices — the
193/// proof covers this exact shape.
194#[allow(clippy::ptr_arg, clippy::needless_range_loop)]
195fn sort_by_start(edits: &Vec<(usize, usize)>) -> (out: Vec<(usize, (usize, usize))>)
196    ensures
197        out@.len() == edits@.len(),
198        // every entry carries an original index and that index's value
199        forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() ==> {
200            &&& (out@[k].0 as int) < edits@.len()
201            &&& out@[k].1 == edits@[out@[k].0 as int]
202        },
203        // carried indices are pairwise distinct
204        forall|a: int, b: int| #![trigger out@[a], out@[b]]
205            0 <= a < b < out@.len() ==> out@[a].0 != out@[b].0,
206        // sorted ascending by start (consecutive form)
207        forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() - 1 ==>
208            out@[k].1.0 <= out@[k + 1].1.0,
209{
210    let mut out: Vec<(usize, (usize, usize))> = Vec::new();
211    for i in 0..edits.len()
212        invariant
213            i <= edits.len(),
214            out@.len() == i as int,
215            forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() ==> {
216                &&& (out@[k].0 as int) < i as int
217                &&& out@[k].1 == edits@[out@[k].0 as int]
218            },
219            forall|a: int, b: int| #![trigger out@[a], out@[b]]
220                0 <= a < b < out@.len() ==> out@[a].0 != out@[b].0,
221            forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() - 1 ==>
222                out@[k].1.0 <= out@[k + 1].1.0,
223    {
224        let pair = edits[i];
225        let s = pair.0;
226        let ghost pre = out@;
227        // walk down past every entry whose start exceeds s
228        let mut j = out.len();
229        while j > 0 && out[j - 1].1.0 > s
230            invariant
231                j <= out.len(),
232                out@ == pre,
233                forall|p: int| #![trigger out@[p]] j <= p < out@.len() ==>
234                    out@[p].1.0 > s as int,
235            decreases j,
236        {
237            j -= 1;
238        }
239        out.insert(j, (i, pair));
240        proof {
241            insert_preserves(edits@, pre, j, i, pair);
242        }
243    }
244    out
245}
246
247/// Sort by start and validate: out is the batch production applies.
248/// (Sort, then sweep consecutive pairs, like prepare_replacements.)
249/// clippy::result_unit_err / needless_range_loop: the unit error is the
250/// verified contract (the ensures clauses carry the reason), and the
251/// indexed sweep is what the invariants prove over.
252#[allow(clippy::result_unit_err, clippy::needless_range_loop)]
253pub fn check_batch(len: usize, edits: Vec<(usize, usize)>) -> (out: Result<Vec<(usize, usize)>, ()>)
254    ensures
255        out.is_ok() ==> batch_ok(to_ints(out.unwrap()@), len as int),
256        out.is_err() ==> {
257            ||| exists|i: int| #![trigger edits@[i]] 0 <= i < edits@.len() && {
258                let (s, e) = edits@[i]; !(0 <= s <= e <= len as int)
259            }
260            ||| exists|i: int, j: int| #![trigger edits@[i], edits@[j]]
261                0 <= i < edits@.len() && 0 <= j < edits@.len() && i != j && {
262                let (si, ei) = edits@[i]; let (sj, _ej) = edits@[j];
263                &&& si == sj || (si <= sj && ei > sj)
264            }
265        },
266{
267    let sorted = sort_by_start(&edits);
268    let n = sorted.len();
269    let mut i = 0usize;
270    // sweep: bounds for each edit, conflict against the previous (sorted) edit
271    while i < n
272        invariant
273            0 <= i <= n,
274            n == sorted@.len(),
275            // sort_by_start postconditions, restated so the body can use them
276            forall|k: int| #![trigger sorted@[k]] 0 <= k < sorted@.len() ==> {
277                &&& (sorted@[k].0 as int) < edits@.len()
278                &&& sorted@[k].1 == edits@[sorted@[k].0 as int]
279            },
280            forall|a: int, b: int| #![trigger sorted@[a], sorted@[b]]
281                0 <= a < b < sorted@.len() ==> sorted@[a].0 != sorted@[b].0,
282            forall|k: int| #![trigger sorted@[k]] 0 <= k < sorted@.len() - 1 ==>
283                sorted@[k].1.0 <= sorted@[k + 1].1.0,
284            forall|k: int| #![trigger sorted@[k]] 0 <= k < i as int ==> {
285                let (sk, ek) = sorted@[k].1;
286                &&& 0 <= sk as int
287                &&& sk <= ek
288                &&& ek <= len
289            },
290            forall|k: int| #![trigger sorted@[k]] 0 <= k < i as int - 1 ==> {
291                let (sk, ek) = sorted@[k].1;
292                let (sk1, _ek1) = sorted@[k + 1].1;
293                &&& sk < sk1
294                &&& ek <= sk1
295            },
296        decreases n - i,
297    {
298        let (s, e) = sorted[i].1;
299        if !(s <= e && e <= len) {
300            proof {
301                let w = sorted@[i as int].0 as int;
302                assert(0 <= w < edits@.len());
303                assert(edits@[w] == (s, e));
304                assert(!(0 <= s as int && s <= e && e <= len));
305                assert(exists|i2: int| #![trigger edits@[i2]] 0 <= i2 < edits@.len() && {
306                    let (s2, e2) = edits@[i2]; !(0 <= s2 <= e2 <= len as int)
307                });
308            }
309            return Err(());
310        }
311        if i > 0 {
312            let (sp, ep) = sorted[i - 1].1;
313            if sp == s || ep > s {
314                proof {
315                    let a = sorted@[i as int - 1].0 as int;
316                    let b = sorted@[i as int].0 as int;
317                    assert(0 <= a < edits@.len());
318                    assert(0 <= b < edits@.len());
319                    assert(a != b);
320                    assert(edits@[a] == (sp, ep));
321                    assert(edits@[b] == (s, e));
322                    // sorted consecutive: sp <= s
323                    assert(sp as int <= s as int);
324                    assert(exists|i2: int, j2: int| #![trigger edits@[i2], edits@[j2]]
325                        0 <= i2 < edits@.len() && 0 <= j2 < edits@.len() && i2 != j2 && {
326                        let (si, ei) = edits@[i2]; let (sj, _ej) = edits@[j2];
327                        &&& si == sj || (si <= sj && ei > sj)
328                    });
329                }
330                return Err(());
331            }
332        }
333        i += 1;
334    }
335    // strip the carried indices
336    let mut result: Vec<(usize, usize)> = Vec::new();
337    let mut k = 0usize;
338    while k < n
339        invariant
340            0 <= k <= n,
341            n == sorted@.len(),
342            result@.len() == k as int,
343            forall|p: int| #![trigger result@[p]] 0 <= p < k as int ==>
344                result@[p] == sorted@[p].1,
345        decreases n - k,
346    {
347        result.push(sorted[k].1);
348        k += 1;
349    }
350    proof {
351        let ri = to_ints(result@);
352        assert forall|p: int| #![trigger ri[p]] 0 <= p < ri.len() implies {
353            let (s, e) = ri[p];
354            &&& 0 <= s <= e <= len as int
355            &&& p + 1 < ri.len() ==> {
356                let (s2, _) = ri[p + 1];
357                &&& s < s2 &&& e <= s2
358            }
359        } by {
360            assert(result@[p] == sorted@[p].1);
361            if p + 1 < ri.len() {
362                assert(result@[p + 1] == sorted@[p + 1].1);
363            }
364        }
365        assert(batch_ok(ri, len as int));
366    }
367    Ok(result)
368}
369
370}
371
372verus! {
373
374// ---- 0057 VF02: batch anchor-mapping composition (the 0045 stretch) -----
375//
376// Production applies a validated batch in REVERSE: `apply_prepared`
377// (buffer/mutation.rs, "reversed application preserves coordinates")
378// iterates `prepared.edits.into_iter().rev()`, publishing one `Change`
379// per edit, and `editor/transact.rs` folds `map_position` over the
380// published journal in order — each fold step using the edit's ORIGINAL
381// coordinates. The proofs below establish, for any batch meeting the
382// `check_batch` geometry contract plus the per-edit `start <= new_end`
383// construction (`new_end = start + replacement length`):
384//
385//   * coordinate preservation (`len_after_covers`): each edit's original
386//     range is still in bounds at the moment production applies it;
387//   * composition (`batch_map_composition`): folding the per-edit
388//     mappings in application order equals the direct whole-batch map —
389//     classify the anchor once against the original coordinates and
390//     accumulate the deltas of the edits fully before it;
391//   * the composed mapping is monotone (`batch_map_monotone`) and stays
392//     inside the final buffer length (`batch_map_in_bounds`).
393//
394// The four spec functions below are #[verifier::opaque] with explicit
395// reveal() at each use site: left transparent, their definitional axioms
396// inflated the (unrelated) insert_preserves query past the pinned
397// toolchain's default solver budget. Isolation keeps every proof at the
398// default rlimit — a proof needing more room is a red flag, not a
399// reason to raise the budget.
400
401/// A validated batch with replacement extents: (start, old_end, new_end)
402/// per edit — `check_batch`'s geometry plus `start <= new_end`, which
403/// holds by construction (the post-edit end is start + replacement len).
404#[verifier::opaque]
405pub open spec fn batch3_ok(edits: Seq<(int, int, int)>, len: int) -> bool {
406    forall|i: int| #![trigger edits[i]] 0 <= i < edits.len() ==> {
407        let (s, oe, ne) = edits[i];
408        &&& 0 <= s <= oe <= len
409        &&& s <= ne
410        &&& i + 1 < edits.len() ==> {
411            let (s2, _oe2, _ne2) = edits[i + 1];
412            &&& s < s2
413            &&& oe <= s2
414        }
415    }
416}
417
418/// Buffer length after the last `t` edits were applied (application runs
419/// in reverse: edit `edits.len() - t` is applied at step `t).
420#[verifier::opaque]
421pub open spec fn len_after(len0: int, edits: Seq<(int, int, int)>, t: int) -> int
422    decreases t,
423{
424    if t <= 0 || t > edits.len() {
425        len0
426    } else {
427        let (_s, oe, ne) = edits[edits.len() - t];
428        len_after(len0, edits, t - 1) + (ne - oe)
429    }
430}
431
432/// The composed mapping, exactly as production computes it: fold the
433/// per-edit mapping over the last `t` edits in application (reverse)
434/// order, each edit at its original coordinates.
435#[verifier::opaque]
436pub open spec fn batch_map_spec(pos: int, edits: Seq<(int, int, int)>, t: int) -> int
437    decreases t,
438{
439    if t <= 0 || t > edits.len() {
440        pos
441    } else {
442        let (s, oe, ne) = edits[edits.len() - t];
443        map_spec(batch_map_spec(pos, edits, t - 1), s, oe, ne)
444    }
445}
446
447/// The direct whole-batch map: one left-to-right pass from edit `i` over
448/// the ORIGINAL coordinates, with `delta` the accumulated length change
449/// of the edits fully before the anchor. This is the definition callers
450/// reason about; production computes the fold.
451#[verifier::opaque]
452pub open spec fn direct_map(pos: int, edits: Seq<(int, int, int)>, i: int, delta: int) -> int
453    decreases edits.len() - i,
454{
455    if i < 0 || i >= edits.len() {
456        pos + delta
457    } else {
458        let (s, oe, ne) = edits[i];
459        if pos < s {
460            pos + delta
461        } else if pos >= oe {
462            direct_map(pos, edits, i + 1, delta + (ne - oe))
463        } else {
464            // inside the replaced range: collapse to the edit's start,
465            // shifted by the deltas of the (disjoint, earlier) edits
466            s + delta
467        }
468    }
469}
470
471/// Sorted non-overlapping edits chain: an earlier edit's end never
472/// exceeds a later edit's start.
473proof fn lemma_batch3_chain(edits: Seq<(int, int, int)>, len: int, i: int, j: int)
474    requires
475        batch3_ok(edits, len),
476        0 <= i < j < edits.len(),
477    ensures
478        edits[i].1 <= edits[j].0,
479    decreases j - i,
480{
481    reveal(batch3_ok);
482    if j - i > 1 {
483        lemma_batch3_chain(edits, len, i + 1, j);
484        assert(edits[i].0 < edits[i + 1].0 && edits[i].1 <= edits[i + 1].0);
485        assert(edits[i + 1].0 <= edits[i + 1].1);
486    }
487}
488
489/// The accumulated delta distributes out of the direct map:
490/// classification is over the original coordinates, so every result
491/// shifts together.
492proof fn lemma_direct_map_delta(
493    pos: int,
494    edits: Seq<(int, int, int)>,
495    i: int,
496    delta: int,
497    extra: int,
498)
499    requires
500        0 <= i <= edits.len(),
501    ensures
502        direct_map(pos, edits, i, delta + extra) == direct_map(pos, edits, i, delta) + extra,
503    decreases edits.len() - i,
504{
505    reveal(direct_map);
506    if i < edits.len() {
507        let (_s, oe, ne) = edits[i];
508        if pos >= oe && pos >= _s {
509            lemma_direct_map_delta(pos, edits, i + 1, delta + (ne - oe), extra);
510            assert(direct_map(pos, edits, i, delta + extra)
511                == direct_map(pos, edits, i + 1, delta + (ne - oe) + extra));
512            assert(direct_map(pos, edits, i, delta)
513                == direct_map(pos, edits, i + 1, delta + (ne - oe)));
514        }
515    }
516}
517
518/// Lower bound: classifying an anchor at or past `bound` against edits
519/// that all start at or past `bound` never pulls it below `bound` —
520/// later deletions are disjoint and lie inside `[bound, pos]`.
521proof fn lemma_direct_lower(
522    pos: int,
523    edits: Seq<(int, int, int)>,
524    len: int,
525    i: int,
526    bound: int,
527)
528    requires
529        batch3_ok(edits, len),
530        0 <= i <= edits.len(),
531        pos >= bound,
532        forall|j: int| #![trigger edits[j]] i <= j < edits.len() ==> edits[j].0 >= bound,
533    ensures
534        direct_map(pos, edits, i, 0) >= bound,
535    decreases edits.len() - i,
536{
537    reveal(batch3_ok);
538    reveal(direct_map);
539    if i < edits.len() {
540        let (s, oe, ne) = edits[i];
541        if pos >= oe && pos >= s {
542            // every later edit starts at or past this edit's end
543            assert forall|j: int| #![trigger edits[j]]
544                i + 1 <= j < edits.len() implies edits[j].0 >= oe by {
545                lemma_batch3_chain(edits, len, i, j);
546            }
547            lemma_direct_lower(pos, edits, len, i + 1, oe);
548            lemma_direct_map_delta(pos, edits, i + 1, 0, ne - oe);
549            assert(direct_map(pos, edits, i, 0)
550                == direct_map(pos, edits, i + 1, ne - oe));
551            // >= oe + (ne - oe) == ne >= s >= bound
552        }
553    }
554}
555
556/// Coordinate preservation: at the moment production applies edit
557/// `edits.len() - t`, every not-yet-applied edit's original range is
558/// still in bounds — reverse application never invalidates pending
559/// coordinates.
560proof fn lemma_len_after_covers(len0: int, edits: Seq<(int, int, int)>, t: int)
561    requires
562        batch3_ok(edits, len0),
563        0 <= t <= edits.len(),
564    ensures
565        t > 0 ==> len_after(len0, edits, t) >= edits[edits.len() - t].2,
566        forall|i: int| #![trigger edits[i]]
567            0 <= i < edits.len() - t ==> edits[i].1 <= len_after(len0, edits, t),
568    decreases t,
569{
570    reveal(batch3_ok);
571    reveal(len_after);
572    if t > 0 {
573        lemma_len_after_covers(len0, edits, t - 1);
574        let j = edits.len() - t;
575        let (sj, oej, nej) = edits[j];
576        // IH at i == j: oej <= len_after(t - 1)
577        assert(len_after(len0, edits, t) == len_after(len0, edits, t - 1) + (nej - oej));
578        assert(len_after(len0, edits, t) >= nej && nej >= sj);
579        assert forall|i: int| #![trigger edits[i]]
580            0 <= i < edits.len() - t implies edits[i].1 <= len_after(len0, edits, t) by {
581            lemma_batch3_chain(edits, len0, i, j);
582            // edits[i].1 <= sj <= nej <= len_after(t)
583        }
584    }
585}
586
587/// Monotonicity of the composed mapping: anchors keep their relative
588/// order through the whole validated batch.
589pub proof fn batch_map_monotone(a: int, b: int, len0: int, edits: Seq<(int, int, int)>, t: int)
590    requires
591        batch3_ok(edits, len0),
592        0 <= t <= edits.len(),
593        a <= b,
594    ensures
595        batch_map_spec(a, edits, t) <= batch_map_spec(b, edits, t),
596    decreases t,
597{
598    reveal(batch3_ok);
599    reveal(batch_map_spec);
600    if t > 0 {
601        batch_map_monotone(a, b, len0, edits, t - 1);
602        let (s, oe, ne) = edits[edits.len() - t];
603        map_spec_monotone(batch_map_spec(a, edits, t - 1), batch_map_spec(b, edits, t - 1), s, oe, ne);
604    }
605}
606
607/// The composed mapping stays inside the final buffer length: an anchor
608/// inside the pre-batch buffer lands inside the post-batch buffer.
609pub proof fn batch_map_in_bounds(pos: int, len0: int, edits: Seq<(int, int, int)>, t: int)
610    requires
611        batch3_ok(edits, len0),
612        0 <= t <= edits.len(),
613        0 <= pos <= len0,
614    ensures
615        0 <= batch_map_spec(pos, edits, t) <= len_after(len0, edits, t),
616    decreases t,
617{
618    reveal(batch3_ok);
619    reveal(batch_map_spec);
620    reveal(len_after);
621    if t > 0 {
622        batch_map_in_bounds(pos, len0, edits, t - 1);
623        lemma_len_after_covers(len0, edits, t - 1);
624        let j = edits.len() - t;
625        let (s, oe, ne) = edits[j];
626        assert(len_after(len0, edits, t)
627            == len_after(len0, edits, t - 1) - (oe - s) + (ne - s));
628        map_spec_in_bounds(
629            batch_map_spec(pos, edits, t - 1),
630            s,
631            oe,
632            ne,
633            len_after(len0, edits, t - 1),
634            len_after(len0, edits, t),
635        );
636    }
637}
638
639/// One composition step: the direct map through edit `i` and the rest
640/// equals mapping the direct map of the rest through edit `i`. This is
641/// the heart of the theorem — a later fold step with the edit's
642/// ORIGINAL coordinates agrees with the single-pass direct map.
643proof fn lemma_direct_step(pos: int, edits: Seq<(int, int, int)>, len0: int, i: int)
644    requires
645        batch3_ok(edits, len0),
646        0 <= i < edits.len(),
647        0 <= pos <= len0,
648    ensures
649        direct_map(pos, edits, i, 0) == map_spec(
650            direct_map(pos, edits, i + 1, 0),
651            edits[i].0,
652            edits[i].1,
653            edits[i].2,
654        ),
655{
656    let (s, oe, ne) = edits[i];
657    let rest = direct_map(pos, edits, i + 1, 0);
658    reveal(batch3_ok);
659    reveal(direct_map);
660    if pos < s {
661        // the anchor precedes this edit and every later one (strict
662        // sort): both sides leave it alone
663        if i + 1 < edits.len() {
664            assert(pos < edits[i + 1].0);
665        }
666        assert(rest == pos);
667    } else if pos >= oe {
668        // the rest map never drops below this edit's end, so map_spec
669        // shifts it by exactly this edit's delta
670        assert forall|j: int| #![trigger edits[j]]
671            i + 1 <= j < edits.len() implies edits[j].0 >= oe by {
672            lemma_batch3_chain(edits, len0, i, j);
673        }
674        lemma_direct_lower(pos, edits, len0, i + 1, oe);
675        lemma_direct_map_delta(pos, edits, i + 1, 0, ne - oe);
676        assert(direct_map(pos, edits, i, 0) == direct_map(pos, edits, i + 1, ne - oe));
677        assert(rest >= oe);
678    } else {
679        // inside the replaced range: every later edit starts at or past
680        // oe, so the rest pass leaves the anchor alone and both sides
681        // collapse to the edit's start
682        if i + 1 < edits.len() {
683            assert(pos < edits[i + 1].0);
684        }
685        assert(rest == pos);
686    }
687}
688
689/// Fold/direct equality at every prefix of the application sequence.
690proof fn lemma_batch_compose_step(pos: int, len0: int, edits: Seq<(int, int, int)>, t: int)
691    requires
692        batch3_ok(edits, len0),
693        0 <= t <= edits.len(),
694        0 <= pos <= len0,
695    ensures
696        batch_map_spec(pos, edits, t) == direct_map(pos, edits, edits.len() - t, 0),
697    decreases t,
698{
699    reveal(batch_map_spec);
700    reveal(direct_map);
701    if t > 0 {
702        lemma_batch_compose_step(pos, len0, edits, t - 1);
703        lemma_direct_step(pos, edits, len0, edits.len() - t);
704        // batch_map_spec(pos, edits, t)
705        //   == map_spec(batch_map_spec(pos, edits, t-1), edits[len-t])   (def)
706        //   == map_spec(direct_map(pos, edits, len-t+1, 0), edits[len-t]) (IH)
707        //   == direct_map(pos, edits, len-t, 0)                           (step)
708        assert(batch_map_spec(pos, edits, t)
709            == map_spec(direct_map(pos, edits, edits.len() - t + 1, 0),
710                edits[edits.len() - t].0, edits[edits.len() - t].1, edits[edits.len() - t].2));
711    }
712}
713
714/// The composition theorem (0045 stretch obligation, closed for VF02):
715/// mapping an anchor through a validated batch — folding the per-edit
716/// mappings in the reverse application order production publishes —
717/// equals the direct whole-batch map over the original coordinates.
718/// Multicursor and collection anchors rely on exactly this equality.
719pub proof fn batch_map_composition(pos: int, len0: int, edits: Seq<(int, int, int)>)
720    requires
721        batch3_ok(edits, len0),
722        0 <= pos <= len0,
723    ensures
724        batch_map_spec(pos, edits, edits.len() as int) == direct_map(pos, edits, 0, 0),
725{
726    lemma_batch_compose_step(pos, len0, edits, edits.len() as int);
727}
728
729}