Skip to main content

improv_verified/
graph.rs

1//! The dependency build order: a depth-first topological sort that rejects
2//! cycles, so building (and evaluating) the derived measures terminates.
3//!
4//! Nodes are indices `0..g.len()`; `g[u]` lists `u`'s dependencies, in the
5//! order they are visited. The result is the DFS post-order from `roots`.
6
7#[allow(unused_imports)]
8use vstd::prelude::*;
9
10verus! {
11
12broadcast use vstd::seq_lib::lemma_seq_contains_after_push;
13
14/// `w` is one of `u`'s dependencies.
15pub open spec fn edge(g: Seq<Vec<usize>>, u: usize, w: usize) -> bool {
16    u < g.len() && g[u as int]@.contains(w)
17}
18
19/// Every dependency names a node.
20pub open spec fn wf(g: Seq<Vec<usize>>) -> bool {
21    forall|u: int, k: int| 0 <= u < g.len() && 0 <= k < g[u]@.len() ==> (#[trigger] g[u]@[k]) < g.len()
22}
23
24/// Each node of `p` depends on the one after it.
25pub open spec fn is_path(g: Seq<Vec<usize>>, p: Seq<usize>) -> bool {
26    &&& p.len() >= 1
27    &&& forall|i: int| 0 <= i < p.len() - 1 ==> #[trigger] edge(g, p[i], p[i + 1])
28}
29
30/// `v` is reachable from `roots` along dependencies.
31pub open spec fn reachable(g: Seq<Vec<usize>>, roots: Seq<usize>, v: usize) -> bool {
32    exists|p: Seq<usize>| #[trigger] is_path(g, p) && roots.contains(p[0]) && p.last() == v
33}
34
35/// A cycle: a path whose last node depends on its first.
36pub open spec fn is_cycle(g: Seq<Vec<usize>>, c: Seq<usize>) -> bool {
37    is_path(g, c) && edge(g, c.last(), c[0])
38}
39
40/// Some cycle is reachable from `roots`.
41pub open spec fn has_reachable_cycle(g: Seq<Vec<usize>>, roots: Seq<usize>) -> bool {
42    exists|c: Seq<usize>| #[trigger] is_cycle(g, c) && reachable(g, roots, c[0])
43}
44
45/// No node twice, and every node after all of its dependencies.
46pub open spec fn topological(g: Seq<Vec<usize>>, order: Seq<usize>) -> bool {
47    &&& order.no_duplicates()
48    &&& forall|i: int, w: usize|
49        0 <= i < order.len() && #[trigger] edge(g, order[i], w) ==> order.subrange(0, i).contains(w)
50}
51
52/// How many nodes are unvisited (state 0): the DFS's termination measure.
53pub open spec fn unseen(st: Seq<u8>) -> nat
54    decreases st.len(),
55{
56    if st.len() == 0 {
57        0
58    } else {
59        unseen(st.drop_last()) + if st.last() == 0 {
60            1nat
61        } else {
62            0nat
63        }
64    }
65}
66
67/// Visiting only ever makes unvisited nodes visited: `unseen` cannot grow,
68/// and shrinks when node `k` is newly visited.
69proof fn lemma_unseen(a: Seq<u8>, b: Seq<u8>, k: int)
70    requires
71        a.len() == b.len(),
72        forall|u: int| 0 <= u < a.len() && b[u] == 0 ==> a[u] == 0,
73    ensures
74        unseen(b) <= unseen(a),
75        0 <= k < a.len() && a[k] == 0 && b[k] != 0 ==> unseen(b) < unseen(a),
76    decreases a.len(),
77{
78    if a.len() > 0 {
79        let (a0, b0) = (a.drop_last(), b.drop_last());
80        assert forall|u: int| 0 <= u < a0.len() && b0[u] == 0 implies a0[u] == 0 by {
81            assert(a0[u] == a[u] && b0[u] == b[u]);
82        }
83        lemma_unseen(a0, b0, k);
84    }
85}
86
87/// The DFS state: `st[u]` is 0 unvisited, 1 on the stack, 2 done. Done nodes
88/// are exactly `order`, which is topological and reachable; the stack nodes
89/// are exactly `stack`, a path from a root.
90pub open spec fn inv(
91    g: Seq<Vec<usize>>,
92    roots: Seq<usize>,
93    st: Seq<u8>,
94    order: Seq<usize>,
95    stack: Seq<usize>,
96) -> bool {
97    &&& st.len() == g.len()
98    &&& forall|u: int| 0 <= u < st.len() ==> #[trigger] st[u] <= 2
99    &&& forall|u: usize| u < st.len() ==> (st[u as int] == 2 <==> #[trigger] order.contains(u))
100    &&& forall|u: usize| u < st.len() ==> (st[u as int] == 1 <==> #[trigger] stack.contains(u))
101    &&& forall|k: int| 0 <= k < order.len() ==> #[trigger] order[k] < g.len()
102    &&& forall|k: int| 0 <= k < stack.len() ==> #[trigger] stack[k] < g.len()
103    &&& topological(g, order)
104    &&& forall|k: int| 0 <= k < order.len() ==> reachable(g, roots, #[trigger] order[k])
105    &&& stack.len() > 0 ==> is_path(g, stack) && roots.contains(stack[0])
106}
107
108/// A stack node that a stack node depends on closes a reachable cycle.
109proof fn lemma_back_edge(g: Seq<Vec<usize>>, roots: Seq<usize>, stk: Seq<usize>, w: usize)
110    requires
111        is_path(g, stk),
112        roots.contains(stk[0]),
113        stk.contains(w),
114        edge(g, stk.last(), w),
115    ensures
116        has_reachable_cycle(g, roots),
117{
118    let k = choose|k: int| 0 <= k < stk.len() && stk[k] == w;
119    let c = stk.subrange(k, stk.len() as int);
120    assert forall|i: int| 0 <= i < c.len() - 1 implies #[trigger] edge(g, c[i], c[i + 1]) by {
121        assert(edge(g, stk[k + i], stk[k + i + 1]));
122    }
123    assert(c.last() == stk.last());
124    assert(is_cycle(g, c));
125    let p = stk.subrange(0, k + 1);
126    assert forall|i: int| 0 <= i < p.len() - 1 implies #[trigger] edge(g, p[i], p[i + 1]) by {
127        assert(edge(g, stk[i], stk[i + 1]));
128    }
129    assert(is_path(g, p) && roots.contains(p[0]) && p.last() == c[0]);
130}
131
132/// The node at the end of a stack path is reachable.
133proof fn lemma_stack_reachable(g: Seq<Vec<usize>>, roots: Seq<usize>, stk: Seq<usize>)
134    requires
135        is_path(g, stk),
136        roots.contains(stk[0]),
137    ensures
138        reachable(g, roots, stk.last()),
139{
140}
141
142fn visit(
143    g: &Vec<Vec<usize>>,
144    Ghost(roots): Ghost<Seq<usize>>,
145    v: usize,
146    st: &mut Vec<u8>,
147    order: &mut Vec<usize>,
148    Ghost(stack): Ghost<Seq<usize>>,
149    Ghost(fuel): Ghost<nat>,
150) -> (r: Result<(), usize>)
151    requires
152        wf(g@),
153        v < g.len(),
154        inv(g@, roots, old(st)@, old(order)@, stack),
155        old(st)@[v as int] == 0,
156        stack.len() == 0 ==> roots.contains(v),
157        stack.len() > 0 ==> edge(g@, stack.last(), v),
158        fuel == unseen(old(st)@),
159    ensures
160        r is Ok ==> {
161            &&& inv(g@, roots, final(st)@, final(order)@, stack)
162            &&& final(st)@[v as int] == 2
163            &&& forall|u: int| 0 <= u < final(st)@.len() && final(st)@[u] == 0 ==> old(st)@[u] == 0
164            &&& forall|u: int| 0 <= u < final(st)@.len() && old(st)@[u] == 2 ==> final(st)@[u] == 2
165        },
166        r is Err ==> has_reachable_cycle(g@, roots),
167    decreases fuel,
168{
169    let ghost st0 = st@;
170    let ghost stk = stack.push(v);
171    st.set(v, 1);
172    proof {
173        lemma_unseen(st0, st@, v as int);
174        assert(!stack.contains(v));
175        assert forall|i: int| 0 <= i < stk.len() - 1 implies #[trigger] edge(g@, stk[i], stk[i + 1]) by {
176            if i < stack.len() - 1 {
177                assert(edge(g@, stack[i], stack[i + 1]));
178            }
179        }
180        assert(stk[0] == if stack.len() == 0 { v } else { stack[0] });
181        assert forall|u: usize| u < st.len() implies (st@[u as int] == 2 <==> #[trigger] order@.contains(u)) by {
182            if u == v {
183                assert(!order@.contains(v));
184            }
185        }
186    }
187    let n = g[v].len();
188    let mut i = 0;
189    while i < n
190        invariant
191            wf(g@),
192            v < g.len(),
193            n == g@[v as int]@.len(),
194            0 <= i <= n,
195            stk == stack.push(v),
196            !stack.contains(v),
197            is_path(g@, stk),
198            roots.contains(stk[0]),
199            inv(g@, roots, st@, order@, stk),
200            st@[v as int] == 1,
201            unseen(st@) < fuel,
202            forall|k: int| 0 <= k < i ==> st@[#[trigger] g@[v as int]@[k] as int] == 2,
203            forall|u: int| 0 <= u < st.len() && #[trigger] st@[u] == 0 ==> st0[u] == 0,
204            forall|u: int| 0 <= u < st.len() && #[trigger] st0[u] == 2 ==> st@[u] == 2,
205        decreases n - i,
206    {
207        let w = g[v][i];
208        assert(edge(g@, v, w)) by {
209            assert(g@[v as int]@[i as int] == w);
210        }
211        if st[w] == 1 {
212            proof {
213                assert(stk.contains(w));
214                lemma_back_edge(g@, roots, stk, w);
215            }
216            return Err(w);
217        }
218        if st[w] == 0 {
219            let ghost st1 = st@;
220            let ghost fuel1 = unseen(st@);
221            let r = visit(g, Ghost(roots), w, st, order, Ghost(stk), Ghost(fuel1));
222            r?;
223            proof {
224                lemma_unseen(st1, st@, 0);
225                assert(stk.contains(v));
226                assert forall|k: int| 0 <= k < i implies st@[#[trigger] g@[v as int]@[k] as int] == 2 by {
227                    assert(st1[g@[v as int]@[k] as int] == 2);
228                }
229                assert forall|u: int| 0 <= u < st.len() && #[trigger] st@[u] == 0 implies st0[u] == 0 by {
230                    assert(st1[u] == 0);
231                }
232                assert forall|u: int| 0 <= u < st.len() && #[trigger] st0[u] == 2 implies st@[u] == 2 by {
233                    assert(st1[u] == 2);
234                }
235            }
236        }
237        assert(st@[g@[v as int]@[i as int] as int] == 2);
238        i += 1;
239    }
240    let ghost order0 = order@;
241    let ghost st2 = st@;
242    st.set(v, 2);
243    order.push(v);
244    proof {
245        lemma_stack_reachable(g@, roots, stk);
246        assert(!order0.contains(v));
247        assert forall|k: int| 0 <= k < order@.len() implies reachable(g@, roots, #[trigger] order@[k]) by {
248            if k < order0.len() {
249                assert(order@[k] == order0[k]);
250            }
251        }
252        assert forall|k: int| 0 <= k < order@.len() implies #[trigger] order@[k] < g.len() by {
253            if k < order0.len() {
254                assert(order@[k] == order0[k]);
255            }
256        }
257        assert forall|u: usize| u < st.len() implies (st@[u as int] == 2 <==> #[trigger] order@.contains(u)) by {
258            if u != v {
259                assert(st@[u as int] == st2[u as int]);
260            }
261        }
262        assert forall|u: usize| u < st.len() implies (st@[u as int] == 1 <==> #[trigger] stack.contains(u)) by {
263            if u != v {
264                assert(st@[u as int] == st2[u as int]);
265                assert(stk.contains(u) <==> stack.contains(u));
266            }
267        }
268        assert(order@.no_duplicates()) by {
269            assert forall|a: int, b: int| 0 <= a < b < order@.len() implies order@[a] != order@[b] by {
270                if b == order0.len() {
271                    assert(order@[a] == order0[a]);
272                    assert(order0.contains(order0[a]));
273                }
274            }
275        }
276        assert forall|j: int, x: usize|
277            0 <= j < order@.len() && #[trigger] edge(g@, order@[j], x) implies order@.subrange(0, j).contains(x) by {
278            if j < order0.len() {
279                assert(order@[j] == order0[j]);
280                assert(order@.subrange(0, j) =~= order0.subrange(0, j));
281            } else {
282                let k = choose|k: int| 0 <= k < g@[v as int]@.len() && g@[v as int]@[k] == x;
283                assert(st2[x as int] == 2);
284                assert(order@.subrange(0, j) =~= order0);
285            }
286        }
287        if stack.len() > 0 {
288            assert forall|i: int| 0 <= i < stack.len() - 1 implies #[trigger] edge(g@, stack[i], stack[i + 1]) by {
289                assert(edge(g@, stk[i], stk[i + 1]));
290            }
291            assert(stack[0] == stk[0]);
292        }
293        assert forall|k: int| 0 <= k < stack.len() implies #[trigger] stack[k] < g.len() by {
294            assert(stack[k] == stk[k]);
295        }
296    }
297    Ok(())
298}
299
300/// A path from a node of a topological order stays in the order.
301proof fn lemma_closed_path(g: Seq<Vec<usize>>, order: Seq<usize>, p: Seq<usize>, k: int)
302    requires
303        topological(g, order),
304        is_path(g, p),
305        order.contains(p[0]),
306        0 <= k < p.len(),
307    ensures
308        order.contains(p[k]),
309    decreases k,
310{
311    if k > 0 {
312        lemma_closed_path(g, order, p, k - 1);
313        let i = choose|i: int| 0 <= i < order.len() && order[i] == p[k - 1];
314        let h = k - 1;
315        assert(edge(g, p[h], p[h + 1]));
316        assert(edge(g, order[i], p[k]));
317        let j = choose|j: int| 0 <= j < i && order.subrange(0, i)[j] == p[k];
318        assert(order[j] == p[k]);
319    }
320}
321
322/// The position of `x` in `order`.
323spec fn pos(order: Seq<usize>, x: usize) -> int {
324    choose|i: int| 0 <= i < order.len() && order[i] == x
325}
326
327/// A dependency comes strictly earlier.
328proof fn lemma_edge_pos(g: Seq<Vec<usize>>, order: Seq<usize>, a: usize, b: usize)
329    requires
330        topological(g, order),
331        order.contains(a),
332        edge(g, a, b),
333    ensures
334        order.contains(b),
335        pos(order, b) < pos(order, a),
336{
337    let i = pos(order, a);
338    assert(edge(g, order[i], b));
339    let j = choose|j: int| 0 <= j < i && order.subrange(0, i)[j] == b;
340    assert(order[j] == b);
341    let pb = pos(order, b);
342    assert(order[pb] == b);
343    assert(pb == j);
344}
345
346/// Along a path in a topological order, positions strictly fall.
347proof fn lemma_path_pos(g: Seq<Vec<usize>>, order: Seq<usize>, c: Seq<usize>, k: int)
348    requires
349        topological(g, order),
350        is_path(g, c),
351        order.contains(c[0]),
352        0 < k < c.len(),
353    ensures
354        order.contains(c[k]),
355        pos(order, c[k]) < pos(order, c[0]),
356    decreases k,
357{
358    lemma_closed_path(g, order, c, k - 1);
359    let h = k - 1;
360    assert(edge(g, c[h], c[h + 1]));
361    lemma_edge_pos(g, order, c[k - 1], c[k]);
362    if k > 1 {
363        lemma_path_pos(g, order, c, k - 1);
364    }
365}
366
367/// A topological order holding every reachable node rules out a reachable
368/// cycle.
369proof fn lemma_no_cycle(g: Seq<Vec<usize>>, roots: Seq<usize>, order: Seq<usize>)
370    requires
371        topological(g, order),
372        forall|v: usize| #[trigger] reachable(g, roots, v) ==> order.contains(v),
373    ensures
374        !has_reachable_cycle(g, roots),
375{
376    if has_reachable_cycle(g, roots) {
377        let c = choose|c: Seq<usize>| #[trigger] is_cycle(g, c) && reachable(g, roots, c[0]);
378        assert(order.contains(c[0]));
379        if c.len() > 1 {
380            lemma_path_pos(g, order, c, c.len() - 1);
381        }
382        lemma_edge_pos(g, order, c.last(), c[0]);
383    }
384}
385
386/// The dependency build order of `roots` in graph `g`: `Ok(order)`, the DFS
387/// post-order, holds exactly the nodes reachable from `roots`, each once and
388/// after all of its dependencies; `Err(w)`, at a node `w` on a cycle,
389/// exactly when a cycle is reachable from `roots`.
390pub fn build_order(g: &Vec<Vec<usize>>, roots: &[usize]) -> (r: Result<Vec<usize>, usize>)
391    requires
392        wf(g@),
393        forall|k: int| 0 <= k < roots.len() ==> #[trigger] roots[k] < g.len(),
394    ensures
395        r is Err <==> has_reachable_cycle(g@, roots@),
396        r matches Ok(order) ==> {
397            &&& topological(g@, order@)
398            &&& forall|v: usize| v < g.len() ==> (#[trigger] order@.contains(v) <==> reachable(g@, roots@, v))
399            &&& forall|k: int| 0 <= k < order.len() ==> #[trigger] order@[k] < g.len()
400        },
401{
402    let n = g.len();
403    let mut st: Vec<u8> = Vec::with_capacity(n);
404    while st.len() < n
405        invariant
406            st.len() <= n,
407            forall|u: int| 0 <= u < st.len() ==> st@[u] == 0,
408        decreases n - st.len(),
409    {
410        st.push(0);
411    }
412    let mut order: Vec<usize> = Vec::new();
413    let ghost empty = Seq::<usize>::empty();
414    let mut t = 0;
415    while t < roots.len()
416        invariant
417            wf(g@),
418            n == g.len(),
419            forall|k: int| 0 <= k < roots.len() ==> #[trigger] roots[k] < g.len(),
420            0 <= t <= roots.len(),
421            empty == Seq::<usize>::empty(),
422            inv(g@, roots@, st@, order@, empty),
423            forall|k: int| 0 <= k < t ==> st@[#[trigger] roots@[k] as int] == 2,
424        decreases roots.len() - t,
425    {
426        let v = roots[t];
427        if st[v] == 0 {
428            assert(roots@.contains(v)) by {
429                assert(roots@[t as int] == v);
430            }
431            let ghost fuel = unseen(st@);
432            let r = visit(g, Ghost(roots@), v, &mut st, &mut order, Ghost(empty), Ghost(fuel));
433            r?;
434        } else {
435            assert(!empty.contains(v));
436        }
437        assert(st@[roots@[t as int] as int] == 2);
438        t += 1;
439    }
440    proof {
441        assert forall|v: usize| #[trigger] reachable(g@, roots@, v) implies order@.contains(v) by {
442            let p = choose|p: Seq<usize>| #[trigger] is_path(g@, p) && roots@.contains(p[0]) && p.last() == v;
443            let k = choose|k: int| 0 <= k < roots.len() && roots@[k] == p[0];
444            assert(st@[roots@[k] as int] == 2);
445            lemma_closed_path(g@, order@, p, p.len() - 1);
446        }
447        lemma_no_cycle(g@, roots@, order@);
448        assert forall|v: usize| v < g.len() implies (#[trigger] order@.contains(v) <==> reachable(g@, roots@, v)) by {
449            if order@.contains(v) {
450                let k = choose|k: int| 0 <= k < order.len() && order@[k] == v;
451                assert(reachable(g@, roots@, order@[k]));
452            }
453        }
454    }
455    Ok(order)
456}
457
458} // verus!
459
460#[cfg(test)]
461#[cfg_attr(coverage_nightly, coverage(off))]
462mod tests {
463    use super::*;
464
465    /// The engine's former `derived_build_order`, over indices: recursive DFS,
466    /// error at the first dependency found on the stack.
467    fn reference(g: &[Vec<usize>], roots: &[usize]) -> Result<Vec<usize>, usize> {
468        fn go(
469            g: &[Vec<usize>],
470            v: usize,
471            order: &mut Vec<usize>,
472            done: &mut [bool],
473            on: &mut [bool],
474        ) -> Result<(), usize> {
475            if done[v] {
476                return Ok(());
477            }
478            if on[v] {
479                return Err(v);
480            }
481            on[v] = true;
482            for &w in &g[v] {
483                go(g, w, order, done, on)?;
484            }
485            on[v] = false;
486            done[v] = true;
487            order.push(v);
488            Ok(())
489        }
490        let (mut order, mut done, mut on) = (vec![], vec![false; g.len()], vec![false; g.len()]);
491        for &r in roots {
492            go(g, r, &mut order, &mut done, &mut on)?;
493        }
494        Ok(order)
495    }
496
497    #[test]
498    fn orders_and_rejects_cycles() {
499        let g = vec![vec![1, 2], vec![2], vec![], vec![0]];
500        assert_eq!(build_order(&g, &[0]), Ok(vec![2, 1, 0]));
501        assert_eq!(build_order(&g, &[3, 0]), Ok(vec![2, 1, 0, 3]));
502        assert_eq!(build_order(&g, &[]), Ok(vec![]));
503        let cyc = vec![vec![1], vec![0], vec![]];
504        assert_eq!(build_order(&cyc, &[2]), Ok(vec![2]));
505        assert_eq!(build_order(&cyc, &[0]), Err(0));
506    }
507
508    /// Random small graphs (with duplicate edges and cycles): the same order,
509    /// and the same error node, as the reference.
510    #[test]
511    fn matches_reference_dfs() {
512        let mut seed = 0x2545_f491_4f6c_dd1du64;
513        let mut next = |m: usize| {
514            seed ^= seed << 13;
515            seed ^= seed >> 7;
516            seed ^= seed << 17;
517            (seed % m as u64) as usize
518        };
519        for _ in 0..20_000 {
520            let n = 1 + next(8);
521            let mut g: Vec<Vec<usize>> = vec![Vec::new(); n];
522            for (v, deps) in g.iter_mut().enumerate() {
523                for _ in 0..next(4) {
524                    let w = next(n);
525                    if w != v || next(4) == 0 {
526                        deps.push(w);
527                    }
528                }
529            }
530            let roots: Vec<usize> = (0..next(4)).map(|_| next(n)).collect();
531            assert_eq!(
532                build_order(&g, &roots),
533                reference(&g, &roots),
534                "{g:?} {roots:?}"
535            );
536        }
537    }
538}