How does Dafny support induction if Z3 does not? - z3

Dafny has the option to set {:induction false}. Also, as far as I know, whenever we use assertions in Dafny, what happens below it that it constructs proof obligations and then calculates on them using Z3.
However, I read Z3 does not support induction: e.g., ... however, any non-trivial property will require a proof by induction. Z3 currently does not support proofs by induction... (cross product in z3)
So my question is: whenever we set {:induction true}, how is this induction performed in Dafny if not using Z3? Are there different underlying solvers? Are there any kind of heuristics? This last one is suggested here (Z3 model for correct Dafny method).
However, which type of heuristics are we talking about?

Maybe not as detailed as #alias' answer, but in short, Dafny's implicit {:induction true} attribute will add assumptions about sub-terms so that you don't need to write them yourself.
So if you write a recursive lemma to prove that every third Fibonacci number is even, without {:induction}, you have to invoke the lemmas recursively. Dafny will require also require recursive calls to terminate:
function fib(x: nat): nat {
if x == 0 then 0 else
if x == 1 then 1 else
fib(x-1) + fib(x-2)
}
lemma {:induction false} FibIsDivisibleBy2(a: nat)
ensures a % 3 == 0 <==> fib(a) % 2 == 0
{
if a <= 2 {
} else {
FibIsDivisibleBy2(a-1);
FibIsDivisibleBy2(a-2);
}
}
Under the hood, the formula sent to Z3 for verification does not contain induction, but it will unroll fib using a fuel to bound unrolling, and the resulting formula will look like this ultra simplified and Dafny-pretending-to-be-SMT-LIB syntax:
(declare-fun a Int)
(declare-fun fib (Int) Int)
(assert forall fib[fuel+1](a) == fib[fuel](a))
(assert fib[n+1](a) == (if a == 0 then 0 else if a == 1 then 1 else fib[n](a))
(assert (
|| (a <= 2 // First branch
&& !( a % 3 == 0
<==> fib[2](a) % 2 == 0) // Postcondition, negated.
|| (a > 2
&& // We add the assumptions provided by the recursive calls to lemma:
( && ((a-1) % 3 == 0 <==> fib[1](a-1) % 2 == 0)
&& ((a-2) % 3 == 0 <==> fib[1](a-2) % 2 == 0)
&&
!(a % 3 == 0 <==> fib[2](a) % 2 == 0) /// Postcondition, negated
))))
(check-sat)
which z3 will return to be "unsat" without requiring induction. You can prove this formula by hand yourself, above.
Note that I omit the mandatory check for the termination, which will look like 0 <= a - 1 < a && 0 <= a - 2 < a, and the subset type 0 <= a.
Now, if you add {:induction true}, what Dafny will do under the hood is to
Every time it sees another fib(X), it will instantiate the lemma automatically for you.
Unfold the definition of fib once and reapply 1) as needed.
That way, you don't need to write these recursive calls yourself and your lemma can prove with zero proof statements:
lemma {:induction true} FibIsDivisibleBy2(a: nat)
ensures a % 3 == 0 <==> fib(a) % 2 == 0
{
}

Dafny is first a programming language, with built-in support for verification machinery of the programs you write. This machinery includes support for induction. Note that this has nothing to do with z3: It's something Dafny itself supports. So, to answer your question simply: Because Dafny's support for induction is not implemented by z3: It's implemented by Dafny itself. Compare this to the goal of z3: A push-button tool that understands SMTLib, excellent for applying decision procedures to (typically) quantifier-free subsets of first-order logic, algebraic data-types, reals, linear-integer arithmetic etc. In particular, z3 is not itself a programming language, nor it’s intended to be used in that way. This makes it suitable as a "backend" tool for custom tools like Dafny that focusses on specific programming styles.
The difficulty with induction is coming up with the inductive invariant. A good way to "guess" the invariant is to follow the structure of your program, which has been exploited to a great extent in the ACL2 theorem prover: The recursive nature of the program suggests an induction schema. Of course, this isn't always enough. The inductive hypothesis can be really difficult to come up with, and typically requires user guidance. This is the style used in most theorem provers, like HOL, Isabelle, Coq, Lean, and many others.
Looking back at Dafny, the need for induction typically comes from the loops in Dafny programs. (See: https://dafny.org/dafny/DafnyRef/DafnyRef#20141-loop-invariants-sec-loop-invariants). So, Dafny can construct the "inductive" proof by using the typical Hoare logic loop principle. (https://en.wikipedia.org/wiki/Hoare_logic) So, what Dafny does is takes your annotated program (and this is the key, you are expected to annotate loops with their invariants), sets up the inductive proof (i.e., the base-case and the inductive-step), and formulates those in terms of an SMTLib query and passes it to z3. (This is a simplified account, but it is roughly the strategy followed in similar tools like Why3 etc as well.) So, the "inductive" proof is done by Dafny, which keeps track of the proof status, how the subgoals for induction relate to each other and the overall goal etc., and z3 is merely used for establishing each of these individual goals that require no induction for their proofs.
A good paper to read on how Dafny works is Rustan's paper: Automating Theorem proving using SMT. This is a very good paper in the sense that it outlines how to build a theorem prover using SMT: While induction will be the main workhorse, there're many moving parts in such a system and Rustan gives a very nice overview of the techniques and challenges involved.
So, to summarize, Dafny supports induction because Dafny's verification system implements support for inductive proofs. z3 is simply a backend tool Dafny uses to discharge non-inductive goals; everything else is kept track of by Dafny itself.
Final note: Recent versions of SMTLib does include support for recursive-definitions, whose proofs will require induction. So, it is possible that SMT solvers will gain induction capabilities, and CVC5 already supports induction to some extent. (http://lara.epfl.ch/~reynolds/VMCAI2015-ind/smt-ind-RK.pdf). So, I'd expect more inductive capabilities to come to SMT-solvers in the upcoming years. However, tools like Dafny, ACL2, etc. will play a crucial role since coming up with inductive invariants is still an art more than engineering for most software applications; so user-intervention is still the name of the game. But it's reasonable to expect the automated systems to get better over time, as amply demonstrated by the last 50 years of theorem proving literature.

Related

Complexity of finding a solution to SMT system with quantifier

I need to find a solution to a problem by generating by using z3py. The formulas are generated depending on input of the user. During the generation of the formulas temporary SMT variables are created that can take on only a limited amount of values, eg is its an integer only even values are allowed. For this case let the temporary variables be a and b and their relation with global variables x and y are defined by the predicate P(a,b,x,y).
An example generated using SMT-LIB like syntax:
(set-info :status unknown)
(declare-fun y () Int)
(declare-fun x () Int)
(assert
(forall (
(a Int) (b Int) (z Int) )
(let
(($x22 (exists ((z Int))(and (< x z) (> z y)))))
(=>
P(a, b, x, y)
$x22))))
(check-sat)
where
z is a variable of which all possible values must be considered
a and b represent variables who's allowed values are restricted by the predicate P
the variable 'x' and 'y' need to be computed for which the formula is satisfied.
Questions:
Does the predicate P reduce the time needed by z3 to find a solution?
Alternatively: viewing that z3 perform search over all possible values for z and a will the predicate P reduce the size of the search space?
Note: The question was updated after remarks from Levent Erkok.
The SMTLib example you gave (generated or hand-written) doesn't make much sense to me. You have universal quantification over x and z, and inside of that you existentially quantify z again, and the whole formula seems meaningless. But perhaps that's not your point and this is just a toy. So, I'll simply ignore that.
Typically, "redundant equations" (as you put it), should not impact performance. (By redundant, I assume you mean things that are derivable from other facts you presented?) Aside: a=z in your above formula is not redundant at all.
This should be true as long as you remain in the decidable subset of the logics; which typically means linear and quantifier-free.
The issue here is that you have quantifier and in particular you have nested quantifiers. SMT-solvers do not deal well with them. (Search stack-overflow for many questions regarding quantifiers and z3.) So, if you have performance issues, the best strategy is to see if you really need them. Just by looking at the example you posted, it is impossible to tell as it doesn't seem to be stating a legitimate fact. So, see if you can express your property without quantifiers.
If you have to have quantifiers, then you are at the mercy of the e-matcher and the heuristics, and all bets are off. I've seen wild performance characteristics in that case. And if reasoning with quantifiers is your goal, then I'd argue that SMT solvers are just not the right tool for you, and you should instead use theorem provers like HOL/Isabelle/Coq etc., that have built-in support for quantifiers and higher-order logic.
If you were to post an actual example of what you're trying to have z3 prove for you, we might be able to see if there's another way to formulate it that might make it easier for z3 to handle. Without a specific goal and an example, it's impossible to opine any further on performance.

Prove() method of Z3?

Z3 has a prove() method, that can prove the equivalence of two formulas.
However, I cannot find technical documentation of this prove() method. What is the definition of "equivalence" that prove() is using behind the scene ? Is that the "partial equivalence" (proposed in the "Regression Verification" paper), or something more powerful ?
A reminder, the "partial equivalence" guarantees that two formulas are equivalent if given the same input, they produce the same output.
In "Regression Verification", we are checking whether a newer version of a program produces the same output as the earlier one. That is, it is an approach for checking program equivalence.
In this approach, theorem provers (SMT solvers) such as Z3 are used. That being said, we should not confuse program equivalence with formula equivalence in first-order logic. Z3 processes first-order logic formulas. First-order logic has well defined semantics. A key concept is satisfiability. For example, the formula p or q is satisfiable, because we can make it true by assigning p or q to true. On the other hand, p and (not p) is unsatisfiable. We can find additional information in this section of the Z3 tutorial.
The Z3 API provides procedures for checking the satisfiability of first-order formulas. The Z3 Python interface has a prove procedure. It shows that a formula is valid by showing that its negation is unsatisfiable. This is a simple function built on top of the Z3 API. Here is a link to its documentation.The documentation was automatically generated from the PyDoc annotations in the code.
Note that, prove(F) is checking whether a formula F is valid or not. Thus, we can use prove(F == G) to try to prove that two first-order formulas F and G are equivalent. That is, we are essentially showing that F iff G is a valid formula.

Z3 patterns and injectivity

In the Z3 tutorial, section 13.2.3, there is a nice example on how to reduce the number of patterns that have to be instantiated when dealing with the axiomatisation of injectivity. In the example, the function f that has to be stated injective, takes an object of type A as input and return an object of type B. As far as I understand the sorts A and B are disjunct.
I have an SMT problem (FOL+EUF) on which Z3 seems not to terminate, and I am trying to isolate the cause. I have a function f:A->A that I assert being injective. Could the problem be that the domain and codomain of f coincide?
Thanks in advance for any suggestion.
Z3 does not terminate because it keeps trying to build an interpretation for the problem.
Satisfiable problems containing injectivity axiom are usually hard for Z3.
They usually fall in a class of problems that can't be decided by Z3
The Z3 guide describes most of the classes that can be decided by Z3.
Moreover, Z3 can produce models for infinite domains such as integers and reals. However, in most cases, the functions produced by Z3 have finite ranges. For example, the quantifier forall x, y: x <= y implies f(x) <= f(y) can be satisfied by assigning f to a function that has a finite range. More information can be found in this article. Unfortunately, injectivity usually requires a range that is as "big" as the domain. Moreover, it is very easy to write axioms that can only be satisfied by an infinite universe. For example, the formula
(assert
(forall ((d1 Value)(d2 Value)(d3 Value)(d4 Value))
(! (=>
(and (= (ENC d1 d2) (ENC d3 d4)))
(and (= d1 d3) (= d2 d4))
)
:pattern ((ENC d1 d2) (ENC d3 d4)))
)
)
can only be satisfied if the universe of Value has one element or is infinite.
Another problem is combining the injectivity axiom for a function f with axioms of the form forall x: f(x) != a. If f is a function from A to A, then the formula can only be satisfied if A has an infinite universe.
That being said, we can prevent the non-termination by reducing the amount of "resources" used by the Z3 model finder for quantified formulas. The options
(set-option :auto-config false)
(set-option :mbqi-max-iterations 10)
If we use these options, Z3 will terminate in your example, but will return unknown. It also returns a "candidate" model. It is not really a model since it does not satisfy all universal quantifiers in the problem. The option
(set-option :mbqi-trace true)
will instruct Z3 to display which quantifiers were not satisfied.
Regarding the example in section 13.2.3, the function may use the same input and return types. Using the trick described in this section will only help unsatisfiable instances. Z3 will also not terminate (for satisfiable formulas) if you re-encode the injectivity axioms using this trick.
Note that the tutorial you cited is very old, and contains outdated information.

Defining custom quantifiers

I'm trying to get Z3 to verify some formal proofs that uses an iterated maximum in the notation. For example, for f a function (↑i: 0 ≤ i < N: f(i)) designates the highest value of f when it is applied to a value between 0 and N. It can be nicely axiomatized with:
(↑i: p(i): f(i)) ≤ x <=> (∀i: p(i): f(i) ≤ x)
with p a predicate over the type of i. Is there a way to define such a quantifier in Z3?
It is quite convenient for formulating my proofs so I'd like to keep it as close to this definition as possible.
Thanks!
There is no direct way to define such binders in Z3. Z3 is based on classical simply sorted first-order logic where the only binders are universal and exitential quantification. In particular, Z3 does not let you write lambda expressions directly. One approach for proving theorems using Z3 that include nested binders is to apply lambda-lifting first and then attempt to prove the resulting first-order formulation.
In your example, you want to define a constant max_p_f.
With the following properties:
forall i: p(i) => max_p_f >= f(i)
(exists i: p(i) & max_p_f = f(i)) or (forall i . not p(i))
say (assuming the supremum is defined on the domain, etc.)
You would have to create constants for each p,f combination where you want to apply the max function.
Defining such functions is standard in proof assistants for higher-order logic.
The Isabelle theorem prover applies transformations similar to the above when mapping
proof obligations to first-order backends (E, Vampire, Z3, etc).

Quantifier Elimination - More questions

Many thanks Josh and Leonardo for answering the previous question.
I have few more questions.
<1> Consider another example.
(exists k) i * k > = 4 and k > 1.
This has a simple solution i > 0. (both for Int and Real case)
However, when I tried following,
(declare-const i Int)
(assert (exists ((k Int)) (and (>= (* i k) 4) (> k 1))))
(apply (using-params qe :qe-nonlinear true))
Z3 Could not eliminate quantifier here.
However, it could eliminate for a Real case. (when i and k are both reals)
Is Quantifier Elimination more difficult for integers?
<2> I am using Z3 C API in my system. I am adding some non-linear constraints on Integers with quantifiers in my system.
Z3 currently checks for satisfiability and gives me a correct model when the system is satisfiable.
I know that after quantifier elimination, these constraints get reduced to linear constraints.
I thought that z3 does quantifier elimination automatically before checking satisfiability. But since, it couldn't do that in case 1 above, I now think, that it usually finds a model without Quantifier Elimination. Am I correct?
Currently z3 can solve the constraints in my system. But it may fail on complex systems.
In such case, is it a good idea to do quantifier elimination by some other method without z3 and add constraints to z3 later?
<3> I can think of adding Real non-linear constraints instead of Integer non-linear constraints in my system. In that case, how can I enforce z3 to do Quantifier Elimination using C-API ?
<4> Finally, is this a good idea to enforce z3 to do Quantifier Elimination? Or it usually finds a model more intelligently without doing Quantifier Elimination?
Thanks.
<1> The theory of nonlinear integer arithmetic does not admit quantifier elimination (qe).
Moreover, the decision problem for nonlinear integer arithmetic is undecidable.
Recall that, Z3 has limited support for quantifier elimination of nonlinear real arithmetic formulas. The current procedure is based on virtual term substitution. Future versions, may have full support for nonlinear real arithmetic.
<2> Quantifier elimination is not enabled by default. The user must request it.
Z3 may find models for satisfiable formulas even when quantifier elimination is not enabled.
It uses a technique called model-based quantifier instantiation (MBQI). The Z3 online tutorial has several examples describing capabilities and limitations of this technique.
<3> You have to enable it when you create the Z3_context object.
Any option that is set in the command line, can be provided during Z3_context object creation. Here is an example, that enables model construction and quantifier elimination:
Z3_config cfg = Z3_mk_config();
Z3_context ctx;
Z3_set_param_value(cfg, "MODEL", "true");
Z3_set_param_value(cfg, "ELIM_QUANTIFIERS", "true");
Z3_set_param_value(cfg, "ELIM_NLARITH_QUANTIFIERS", "true");
ctx = mk_context_custom(cfg, throw_z3_error);
Z3_del_config(cfg);
After that, ctx is pointing to a Z3 context object that supports model construction and quantifier elimination.
<4> The MBQI module is not complete even for the linear arithmetic fragment.
The Z3 online tutorial describes the fragments it is complete. MBQI module is a good option for problems that contain uninterpreted functions. If your problems only use arithmetic, then quantifier elimination is usually better and more efficient. That being said, several problems can be quickly solved using MBQI.

Resources