verus-lang / verus

Verified Rust for low-level systems code

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`assert forall by` doesn't work for math multi-trigger

jaylorch opened this issue · comments

The second proof below, which should be trivial, doesn't verify:

pub proof fn lemma_mod_equivalence(x: int, y: int, m: int)
    requires
        0 < m,
    ensures
        x % m == y % m <==> (x - y) % m == 0,
{
    assume(false);
}

pub proof fn lemma_mod_equivalence_auto()
    ensures forall |x: int, y: int, m: int| #![trigger (x % m), (y % m)] 0 < m ==> (x % m == y % m <==> (x - y) % m == 0),
{
    assert forall |x: int, y: int, m: int| #![trigger (x % m), (y % m)]
        0 < m implies
        x % m == y % m <==> (x - y) % m == 0 by
    {
        lemma_mod_equivalence(x, y, m);
    }
}

A couple of observations. First, this behavior is weirdly brittle. It persists if you replace your version of lemma_mod_equivalence with:

pub proof fn lemma_mod_equivalence(x: int, y: int, m: int)
    requires
        true,
    ensures
        false,
{
    assume(false);
}

but if you remove the requires true above, then everything verifies nicely.

Similarly, if you replace the call lemma_mod_equivalence(x, y, m); inside lemma_mod_equivalence_auto with assume(false), everything verifies successfully.

Z3 also quickly reaches unsat if you edit the output smt2 file to remove the definition of EucMod; i.e., delete:

(assert
 (forall ((x Int) (y Int)) (!
   (= (EucMod x y) (mod x y))
   :pattern ((EucMod x y))
   :qid prelude_eucmod
   :skolemid skolem_prelude_eucmod
)))

If you run Verus on the version with lemma_mod_equivalence under the profiler, the quantifier on the assert forall... is instantiated 22M times, with no other user-level quantifier instantiations reported. If we dig a big deeper, here are the internal quantifier instantiations:

prelude_eucmod created 2681 instantiations and cost 2681
prelude_mod_unsigned_in_bounds created 2681 instantiations and cost 2681
prelude_sub created 82 instantiations and cost 82
internal_ens__assert_forall_lorch!lemma_mod_equivalence._definition created 1 instantiations and cost 1
internal_req__assert_forall_lorch!lemma_mod_equivalence._definition created 1 instantiations and cost 1

They're all very modest compared to the 22M for the user-level version.

My rough conclusion is that this isn't a Verus issue. Instead, under certain circumstances known only to itself, Z3 gets caught in a trigger loop. In particular, given two terms x % m and y % m, the assert's quantifier introduces a
term (x - y) % m. Then we have two more possible pairs of terms (x % m with (x - y) % m, and y % m with (x - y) % m), which each introduce a new term, and so on.

If instead you write the lemma as:

pub proof fn lemma_mod_equivalence_auto()
    ensures forall |x: int, y: int, m: int| #![trigger (x % m), (y % m)] 0 < m ==> (x % m == y % m <==> (x - y) % m == 0),
{
    assert forall |x: int, y: int, m: int| #![trigger ((x - y) % m)]
        0 < m implies
        x % m == y % m <==> (x - y) % m == 0 by
    {
        lemma_mod_equivalence(x, y, m);
    }
}

then everything works nicely. This suggests considering a similar change to the quantifier in the ensures clause, so clients don't get hit with this behavior.

Thanks, Bryan!