Tc1
This is an example of an ADT for doing proofs about directed trees using the transitive closure encoding. It isn't really complete, but gives some idea how we can use manual instantiations in cases where we have to go outside the deciable theory. In this case, we need some manual proof effort because the definition of transitive closure is not expressible in first-order logic. We also wind up with one funciton cycle to deal with. Remarkably, only two manual proofs are needed and they are ony one line each.
module tree_tc(t) = {
action add(x:t,y:t)
action succ(x:t,y:t) returns (r:bool)
action reaches(x:t,y:t) returns (r:bool)
specification {
tc,
which tracks the transitive closure of the successor
relation.
relation tc(X:t,Y:t)
after init {
tc(X,Y) := false
}
around add {
require x ~= y; # no trivial cycles added
require ~tc(y,x); # no non-trivial cycles added
require ~(tc(Z,x) & tc(Z,y)); # no reconvergence added
require ~(tc(x,Z) & tc(Z,y)); # no reconvergence added
...
tc(X,Y) := tc(X,Y) | (X = x | tc(X,x)) & (Y = y | tc(y,Y))
}
after succ {
ensure r -> (tc(x,y) & ~(tc(x,Z) & tc(Z,y)));
ensure (tc(x,y) & ~exists Z. tc(x,Z) & tc(Z,y)) -> r
proof {
assume pre_fp with X=x,Y=y
}
}
reaches relation is
trivial.
after reaches {
ensure r = tc(x,y)
}
invariant ~tc(X,X)
invariant tc(X,Y) & tc(Z,Y) -> (X = Z | tc(X,Z) | tc(Z,X))
}
implementation {
tc above is the transitive closure.
relation tree(X:t,Y:t)
lfp ttc(X,Y) = tree(X,Y) | exists Z. (ttc(X,Z) & ttc(Z,Y))
However, since Horn definitions aren't implemented yet, we'll just write here the axioms that the Horn definition would generate.
We declare a relation ttc.
relation ttc(X:t,Y:t)
axiom (tree(X,Y) | ttc(X,Z) & ttc(Z,Y)) -> ttc(X,Y)
ttc, this lets us split cases
on whether it is tree edge or a composite edge.
schema pre_fp = {
property ttc(X,Y) -> (tree(X,Y) | exists Z. (ttc(X,Z) & ttc(Z,Y)))
}
schema lep = {
relation r(X:t,Y:t)
property forall X,Y. ((tree(X,Y) | exists Z. (r(X,Z) & r(Z,Y))) -> r(X,Y))
property ttc(X,Y) -> r(X,Y)
}
ttc defined in terms of tree, we can now define our
interface implementations. Notice that reaches here uses
ttc, which is not computable. For a real implementation,
we would need to use, say, DFS.
after init {
tree(X,Y) := false;
}
implement add {
tree(x,y) := true;
}
implement succ {
r := tree(x,y);
}
implement reaches {
r := ttc(x,y);
}
}
tc is equal
to the true transitive closure of tree, and that tree is the
set of non-composite edges in tc.
private {
The tc -> ttc direction only needs the fact that tc is a post fixed point. Because this has no quantifier alternation, Z3 can get the proof without any help.
invariant [fwd] tc(X,Y) -> ttc(X,Y)
invariant [rev] ttc(X,Y) -> tc(X,Y)
proof {
apply lep; showgoals
}
rev is established initially and
is preserved by every exported action. We might want a
separate proof in every case, but here the same argument
works for all cases. Right now there is not a machanism to
provide a different proof in every case.
With the above invariant, Z3 can prove that 'tree' is the
set of non-composite edges of tc. It suffices to show that
no tree edge is a composite edge in tc. The other
direction is a consequence the fact that tc is a post
fixed point, so we need not prove it as an invariant.
invariant ~(tree(X,Y) & tc(X,Z) & tc(Z,Y))
}
isolate iso = this
}
type t
instance tr : tree_tc(t)
export tr.add
export tr.succ
export tr.reaches