Whiley / WhileyTheoremProver

The Whiley Theorem Prover (WyTP) is an automatic and interactive theorem prover designed to discharge verification conditions generated by the Whiley Compiler. WyTP operates over a variant of first-order logic which includes integer arithmetic, arrays and quantification.
Apache License 2.0
8 stars 2 forks source link

Congruence Closure #68

Closed DavePearce closed 7 years ago

DavePearce commented 7 years ago

The concept of congruence is a pretty thorny issue. At the moment, there are numerous issues related to this (e.g. #67), and I really want to design a proper interface (e.g. #63).

Normal Forms (The Good)

Roughly speaking, the problem revolves around representation of formula. At the moment, I attempt to ensure that formula are always stored in normal form (and most of the issues arise when this is not the case). Consider this super-contrived example:

1. x == y
2. x < y

Here, apply congruence closure to (2) gives y < y and, hence, a contradiction. In summary, there are several reasons why we want to represent expressions in a normal form:

Normal Forms (The Bad)

On the negative side, there is the problem of "lost representation". Consider this example:

define contains(int[] items, int item) is:
    exists(int k).(items[k] == item)

assert:
   forall(int[] xs, int x):
       if:
          |xs| > 0
          !contains(xs,x)
       then:
          xs[0] > x      

In a proof-by-contradiction, we'll end up with the equivalence xs[0] == x. In order to prove the contradiction, we need to instantiate against xs[0]. But, if we had decided xs[0] was the representative of the equivalence class containing both xs[0] and x, then we can end up missing xs[0] and not triggering instantiation. This happens because automatically substitution moves terms to their normal form.

Other situations where normal form representation can cause problems:

The other difficult thing caused by normal form representation is that the printed proofs don't resemble their original form. The normal form tends to be a bit ugly in places as well (especially with have inequalities always represented by >= because you end with gross things like |A| >= (i+1) which is much better denoted as i < |A|.

Strong vs Weak Normal Forms

There are two basic approaches to using normal forms. Either maintain all formula in normal form, or convert expressions to normal form as necessary:

The latter approach may actually reduce the size of the overall proofs generated though. This is because they won't include steps for normalising formula, as currently occurs.

DavePearce commented 7 years ago

Congruence versus Normal Forms

An interesting question: is congruence closure a kind of normal form?

This is is interesting as it goes the question of whether we should be distinguishing congruence from normal forms. We can roughly distinguish them:

The key here is whether it's actually possible to order congruence versus normalisation. If not, then separating them makes sense and we just iterate them to a fixed point. A rough algorithm is:

  1. Traverse expression tree. For each TERM encountered:
  2. Simplify TERM
  3. Lookup TERM in congruence map
  4. Consider parent

For example, consider applying this to x+x > y in the context of a congruence x==y+1. First, we simplify x (which does nothing) and then substitute x for y+1, to give: (y+1) + (y+1) > y. At this point, we simplify the parent to give (2*y) + 2 > y and, finally, simplify the whole equation to give y+2 > 0.

In simplifying a parent, the question is: how far does does the recursion go? If NormalForm and Congruence are completely separate, then calling NormalForm.simplify() on a parent would naturally recurse all children first. Is this what we want?

DavePearce commented 7 years ago

Congruence / Simplification as Proof Rules?

The other option which I think can be pulled off is to actually implement congruence closure and simplification as rewrite rules. This could be done with special support from a Prover.