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
13use vstd::prelude::*;
14
15verus! {
16
17/// The mapping, mathematically: right-affinity anchor through one edit.
18pub open spec fn map_spec(position: int, start: int, old_end: int, new_end: int) -> int {
19    if position < start {
20        position
21    } else if position >= old_end {
22        new_end + (position - old_end)
23    } else {
24        start
25    }
26}
27
28/// One anchor through one edit. Callers guarantee no overflow: positions
29/// are byte offsets bounded by the buffer length, and `new_end` is the
30/// post-edit end of the replaced range (`start + replacement length`).
31pub fn map_position(position: usize, start: usize, old_end: usize, new_end: usize) -> (mapped: usize)
32    requires
33        start <= old_end,
34        position >= old_end ==> new_end + (position - old_end) <= usize::MAX,
35    ensures
36        mapped as int == map_spec(position as int, start as int, old_end as int, new_end as int),
37{
38    if position < start {
39        position
40    } else if position >= old_end {
41        new_end.saturating_add(position - old_end)
42    } else {
43        start
44    }
45}
46
47/// Monotonicity: anchors keep their relative order through one edit.
48/// `start <= new_end` holds by construction (the post-edit end of a
49/// replacement is its start plus the replacement length).
50proof fn map_spec_monotone(a: int, b: int, start: int, old_end: int, new_end: int)
51    requires
52        start <= old_end,
53        start <= new_end,
54        a <= b,
55    ensures
56        map_spec(a, start, old_end, new_end) <= map_spec(b, start, old_end, new_end),
57{
58}
59
60/// Bounds: an anchor inside the old buffer stays inside the new one.
61proof fn map_spec_in_bounds(
62    position: int,
63    start: int,
64    old_end: int,
65    new_end: int,
66    old_len: int,
67    new_len: int,
68)
69    requires
70        0 <= start <= old_end <= old_len,
71        0 <= start <= new_end <= new_len,
72        0 <= position <= old_len,
73        new_len == old_len - (old_end - start) + (new_end - start),
74    ensures
75        0 <= map_spec(position, start, old_end, new_end) <= new_len,
76{
77}
78
79}
80
81verus! {
82
83/// Seq of usize pairs as spec ints (Seq::map carries the indexing axiom).
84pub open spec fn to_ints(edits: Seq<(usize, usize)>) -> Seq<(int, int)> {
85    edits.map_values(|pair: (usize, usize)| (pair.0 as int, pair.1 as int))
86}
87
88/// The geometry contract a prepared batch meets (buffer/mutation.rs).
89pub open spec fn batch_ok(edits: Seq<(int, int)>, len: int) -> bool {
90    forall|i: int| #![trigger edits[i]] 0 <= i < edits.len() ==> {
91        let (s, e) = edits[i];
92        &&& 0 <= s <= e <= len
93        &&& i + 1 < edits.len() ==> {
94            let (s2, _) = edits[i + 1];
95            &&& s < s2
96            &&& e <= s2
97        }
98    }
99}
100
101/// Insert algebra for one insertion-sort step: inserting (i, pair) at the
102/// walked position j keeps the index bookkeeping and consecutive sortedness.
103proof fn insert_preserves(
104    edits: Seq<(usize, usize)>,
105    pre: Seq<(usize, (usize, usize))>,
106    j: usize,
107    i: usize,
108    pair: (usize, usize),
109)
110    requires
111        j as int <= pre.len(),
112        pre.len() == i as int,
113        (i as int) < edits.len(),
114        pair == edits[i as int],
115        forall|k: int| #![trigger pre[k]] 0 <= k < pre.len() ==> {
116            &&& (pre[k].0 as int) < i as int
117            &&& pre[k].1 == edits[pre[k].0 as int]
118        },
119        forall|a: int, b: int| #![trigger pre[a], pre[b]]
120            0 <= a < b < pre.len() ==> pre[a].0 != pre[b].0,
121        forall|k: int| #![trigger pre[k]] 0 <= k < pre.len() - 1 ==>
122            pre[k].1.0 <= pre[k + 1].1.0,
123        j > 0 ==> pre[j as int - 1].1.0 as int <= pair.0 as int,
124        forall|p: int| #![trigger pre[p]] j as int <= p < pre.len() ==>
125            pre[p].1.0 as int > pair.0 as int,
126    ensures
127        ({
128            let post = pre.insert(j as int, (i, pair));
129            &&& post.len() == i as int + 1
130            &&& forall|k: int| #![trigger post[k]] 0 <= k < post.len() ==> {
131                &&& (post[k].0 as int) < i as int + 1
132                &&& post[k].1 == edits[post[k].0 as int]
133            }
134            &&& forall|a: int, b: int| #![trigger post[a], post[b]]
135                0 <= a < b < post.len() ==> post[a].0 != post[b].0
136            &&& forall|k: int| #![trigger post[k]] 0 <= k < post.len() - 1 ==>
137                post[k].1.0 <= post[k + 1].1.0
138        }),
139{
140    let post = pre.insert(j as int, (i, pair));
141    assert forall|k: int| #![trigger post[k]] 0 <= k < post.len() implies {
142        &&& (post[k].0 as int) < i as int + 1
143        &&& post[k].1 == edits[post[k].0 as int]
144    } by {
145        if k == j as int {
146            assert(post[k] == (i, pair));
147        } else if k < j as int {
148            assert(post[k] == pre[k]);
149        } else {
150            assert(post[k] == pre[k - 1]);
151        }
152    }
153    assert forall|a: int, b: int| #![trigger post[a], post[b]]
154        0 <= a < b < post.len() implies post[a].0 != post[b].0 by {
155        if a == j as int {
156            assert(post[b] == pre[b - 1]);
157        } else if b == j as int {
158            assert(post[a] == pre[a]);
159        } else {
160            assert(post[a] == if a < j as int { pre[a] } else { pre[a - 1] });
161            assert(post[b] == if b < j as int { pre[b] } else { pre[b - 1] });
162        }
163    }
164    assert forall|k: int| #![trigger post[k]] 0 <= k < post.len() - 1 implies
165        post[k].1.0 <= post[k + 1].1.0 by {
166        if k + 1 < j as int {
167            assert(post[k] == pre[k]);
168            assert(post[k + 1] == pre[k + 1]);
169        } else if k + 1 == j as int {
170            assert(post[k] == pre[k]);
171            assert(post[k + 1] == (i, pair));
172            // walk exit: j > 0 here, so pre[j-1].1.0 <= pair.0
173        } else if k == j as int {
174            assert(post[k] == (i, pair));
175            assert(post[k + 1] == pre[k]);
176            // walk invariant: pre[j].1.0 > pair.0
177        } else {
178            assert(post[k] == pre[k - 1]);
179            assert(post[k + 1] == pre[k]);
180        }
181    }
182}
183
184/// Insertion sort by start, carrying each edit's original index so error
185/// witnesses can name two DISTINCT input edits. Batches are small.
186/// clippy::ptr_arg: Verus's Vec specs reason over Vec, not slices — the
187/// proof covers this exact shape.
188#[allow(clippy::ptr_arg, clippy::needless_range_loop)]
189fn sort_by_start(edits: &Vec<(usize, usize)>) -> (out: Vec<(usize, (usize, usize))>)
190    ensures
191        out@.len() == edits@.len(),
192        // every entry carries an original index and that index's value
193        forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() ==> {
194            &&& (out@[k].0 as int) < edits@.len()
195            &&& out@[k].1 == edits@[out@[k].0 as int]
196        },
197        // carried indices are pairwise distinct
198        forall|a: int, b: int| #![trigger out@[a], out@[b]]
199            0 <= a < b < out@.len() ==> out@[a].0 != out@[b].0,
200        // sorted ascending by start (consecutive form)
201        forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() - 1 ==>
202            out@[k].1.0 <= out@[k + 1].1.0,
203{
204    let mut out: Vec<(usize, (usize, usize))> = Vec::new();
205    for i in 0..edits.len()
206        invariant
207            i <= edits.len(),
208            out@.len() == i as int,
209            forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() ==> {
210                &&& (out@[k].0 as int) < i as int
211                &&& out@[k].1 == edits@[out@[k].0 as int]
212            },
213            forall|a: int, b: int| #![trigger out@[a], out@[b]]
214                0 <= a < b < out@.len() ==> out@[a].0 != out@[b].0,
215            forall|k: int| #![trigger out@[k]] 0 <= k < out@.len() - 1 ==>
216                out@[k].1.0 <= out@[k + 1].1.0,
217    {
218        let pair = edits[i];
219        let s = pair.0;
220        let ghost pre = out@;
221        // walk down past every entry whose start exceeds s
222        let mut j = out.len();
223        while j > 0 && out[j - 1].1.0 > s
224            invariant
225                j <= out.len(),
226                out@ == pre,
227                forall|p: int| #![trigger out@[p]] j <= p < out@.len() ==>
228                    out@[p].1.0 > s as int,
229            decreases j,
230        {
231            j -= 1;
232        }
233        out.insert(j, (i, pair));
234        proof {
235            insert_preserves(edits@, pre, j, i, pair);
236        }
237    }
238    out
239}
240
241/// Sort by start and validate: out is the batch production applies.
242/// (Sort, then sweep consecutive pairs, like prepare_replacements.)
243/// clippy::result_unit_err / needless_range_loop: the unit error is the
244/// verified contract (the ensures clauses carry the reason), and the
245/// indexed sweep is what the invariants prove over.
246#[allow(clippy::result_unit_err, clippy::needless_range_loop)]
247pub fn check_batch(len: usize, edits: Vec<(usize, usize)>) -> (out: Result<Vec<(usize, usize)>, ()>)
248    ensures
249        out.is_ok() ==> batch_ok(to_ints(out.unwrap()@), len as int),
250        out.is_err() ==> {
251            ||| exists|i: int| #![trigger edits@[i]] 0 <= i < edits@.len() && {
252                let (s, e) = edits@[i]; !(0 <= s <= e <= len as int)
253            }
254            ||| exists|i: int, j: int| #![trigger edits@[i], edits@[j]]
255                0 <= i < edits@.len() && 0 <= j < edits@.len() && i != j && {
256                let (si, ei) = edits@[i]; let (sj, _ej) = edits@[j];
257                &&& si == sj || (si <= sj && ei > sj)
258            }
259        },
260{
261    let sorted = sort_by_start(&edits);
262    let n = sorted.len();
263    let mut i = 0usize;
264    // sweep: bounds for each edit, conflict against the previous (sorted) edit
265    while i < n
266        invariant
267            0 <= i <= n,
268            n == sorted@.len(),
269            // sort_by_start postconditions, restated so the body can use them
270            forall|k: int| #![trigger sorted@[k]] 0 <= k < sorted@.len() ==> {
271                &&& (sorted@[k].0 as int) < edits@.len()
272                &&& sorted@[k].1 == edits@[sorted@[k].0 as int]
273            },
274            forall|a: int, b: int| #![trigger sorted@[a], sorted@[b]]
275                0 <= a < b < sorted@.len() ==> sorted@[a].0 != sorted@[b].0,
276            forall|k: int| #![trigger sorted@[k]] 0 <= k < sorted@.len() - 1 ==>
277                sorted@[k].1.0 <= sorted@[k + 1].1.0,
278            forall|k: int| #![trigger sorted@[k]] 0 <= k < i as int ==> {
279                let (sk, ek) = sorted@[k].1;
280                &&& 0 <= sk as int
281                &&& sk <= ek
282                &&& ek <= len
283            },
284            forall|k: int| #![trigger sorted@[k]] 0 <= k < i as int - 1 ==> {
285                let (sk, ek) = sorted@[k].1;
286                let (sk1, _ek1) = sorted@[k + 1].1;
287                &&& sk < sk1
288                &&& ek <= sk1
289            },
290        decreases n - i,
291    {
292        let (s, e) = sorted[i].1;
293        if !(s <= e && e <= len) {
294            proof {
295                let w = sorted@[i as int].0 as int;
296                assert(0 <= w < edits@.len());
297                assert(edits@[w] == (s, e));
298                assert(!(0 <= s as int && s <= e && e <= len));
299                assert(exists|i2: int| #![trigger edits@[i2]] 0 <= i2 < edits@.len() && {
300                    let (s2, e2) = edits@[i2]; !(0 <= s2 <= e2 <= len as int)
301                });
302            }
303            return Err(());
304        }
305        if i > 0 {
306            let (sp, ep) = sorted[i - 1].1;
307            if sp == s || ep > s {
308                proof {
309                    let a = sorted@[i as int - 1].0 as int;
310                    let b = sorted@[i as int].0 as int;
311                    assert(0 <= a < edits@.len());
312                    assert(0 <= b < edits@.len());
313                    assert(a != b);
314                    assert(edits@[a] == (sp, ep));
315                    assert(edits@[b] == (s, e));
316                    // sorted consecutive: sp <= s
317                    assert(sp as int <= s as int);
318                    assert(exists|i2: int, j2: int| #![trigger edits@[i2], edits@[j2]]
319                        0 <= i2 < edits@.len() && 0 <= j2 < edits@.len() && i2 != j2 && {
320                        let (si, ei) = edits@[i2]; let (sj, _ej) = edits@[j2];
321                        &&& si == sj || (si <= sj && ei > sj)
322                    });
323                }
324                return Err(());
325            }
326        }
327        i += 1;
328    }
329    // strip the carried indices
330    let mut result: Vec<(usize, usize)> = Vec::new();
331    let mut k = 0usize;
332    while k < n
333        invariant
334            0 <= k <= n,
335            n == sorted@.len(),
336            result@.len() == k as int,
337            forall|p: int| #![trigger result@[p]] 0 <= p < k as int ==>
338                result@[p] == sorted@[p].1,
339        decreases n - k,
340    {
341        result.push(sorted[k].1);
342        k += 1;
343    }
344    proof {
345        let ri = to_ints(result@);
346        assert forall|p: int| #![trigger ri[p]] 0 <= p < ri.len() implies {
347            let (s, e) = ri[p];
348            &&& 0 <= s <= e <= len as int
349            &&& p + 1 < ri.len() ==> {
350                let (s2, _) = ri[p + 1];
351                &&& s < s2 &&& e <= s2
352            }
353        } by {
354            assert(result@[p] == sorted@[p].1);
355            if p + 1 < ri.len() {
356                assert(result@[p + 1] == sorted@[p + 1].1);
357            }
358        }
359        assert(batch_ok(ri, len as int));
360    }
361    Ok(result)
362}
363
364}