`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!