From what you've written, I'm pretty sure you mean syntactic equivalence. Two formulas are semantically equivalent if they evaluate to the same result for all valuations of variables; two formulas are syntactically equivalent if you can rewrite one to another given a certain set of rewrite rules (or, more generally, prove their equivalence using a certain set of inference rules). Semantic equivalence looks only at the values of the expressions and not at their structure; Syntactic equivalence looks only at the structure of the expressions and not the values they produce.
Now, on to answer the question of how to do this in Isabelle.
Defining the relation
The standard way is to define a datatype for your formulas (I added some nicer infix syntax for it):
type_synonym vname = nat
datatype formula =
Atom vname
| FTrue
| Neg formula
| Conj formula formula (infixl "and" 60)
| Disj formula formula (infixl "or" 50)
definition "FFalse = Neg FTrue"
Then you can define the concept of ‘evaluating’ such a formula w.r.t. a given variable valuation:
primrec eval_formula :: "(vname ⇒ bool) ⇒ formula ⇒ bool" where
"eval_formula s (Atom x) ⟷ s x"
| "eval_formula _ FTrue ⟷ True"
| "eval_formula s (Neg a) ⟷ ¬eval_formula s a"
| "eval_formula s (a and b) ⟷ eval_formula s a ∧ eval_formula s b"
| "eval_formula s (a or b) ⟷ eval_formula s a ∨ eval_formula s b"
lemma eval_formula_False [simp]: "eval_formula s FFalse = False"
by (simp add: FFalse_def)
And, building on that, you can define the concept of semantic equivalence: two formulas are semantically equivalent if they evaluate to the same thing for all valuations:
definition formula_equiv_sem :: "formula ⇒ formula ⇒ bool" (infixl "≈" 40) where
"a ≈ b ⟷ (∀s. eval_formula s a = eval_formula s b)"
From your question, I gather that what you want to do is to define some kind of equivalence relation based on rewrite rules: two formulas are syntactically equivalent if you can transform one into another by applying some set of given rewrite rules.
That can be done e.g. with Isabelle's package for inductive predicates:
inductive formula_equiv :: "formula ⇒ formula ⇒ bool" (infixl "∼" 40) where
formula_refl [simp]: "a ∼ a"
| formula_sym: "a ∼ b ⟹ b ∼ a"
| formula_trans [trans]: "a ∼ b ⟹ b ∼ c ⟹ a ∼ c"
| neg_cong: "a ∼ b ⟹ Neg a ∼ Neg b"
| conj_cong: "a1 ∼ b1 ⟹ a2 ∼ b2 ⟹ a1 and a2 ∼ b1 and b2"
| disj_cong: "a1 ∼ b1 ⟹ a2 ∼ b2 ⟹ a1 or a2 ∼ b1 or b2"
| conj_commute: "a and b ∼ b and a"
| disj_commute: "a or b ∼ b or a"
| conj_assoc: "(a and b) and c ∼ a and (b and c)"
| disj_assoc: "(a or b) or c ∼ a or (b or c)"
| disj_conj: "a or (b and c) ∼ (a or b) and (a or c)"
| conj_disj: "a and (b or c) ∼ (a and b) or (a and c)"
| de_morgan1: "Neg (a and b) ∼ Neg a or Neg b"
| de_morgan2: "Neg (a or b) ∼ Neg a and Neg b"
| neg_neg: "Neg (Neg a) ∼ a"
| tnd: "a or Neg a ∼ FTrue"
| contr: "a and Neg a ∼ FFalse"
| disj_idem: "a or a ∼ a"
| conj_idem: "a and a ∼ a"
| conj_True: "a and FTrue ∼ a"
| disj_True: "a or FTrue ∼ FTrue"
The first six rules essentially set up rewriting (you can rewrite anything to itself, you can rewrite from left to right or from right to left, you can chain rewrite steps, if you can rewrite subterms, you can also rewrite the full term). The remaining rules are a few example rules that you may want to have in there (with no claim of completeness).
For more information on inductive predicates, refer to the manual of the predicate tool.
Proving stuff about it
So what can you do with this? Well, I expect that you will want to show that this is sound, i.e. that two formulas that are syntactically equivalent are also semantically equivalent:
lemma formula_equiv_syntactic_imp_semantic:
"a ∼ b ⟹ a ≈ b"
by (induction a b rule: formula_equiv.induct)
(auto simp: formula_equiv_sem_def)
You might also want to prove a few derived syntactical rules. For this, it is useful to have some convenient transitivity rules and setup the simplifier with the congruence rules:
lemmas formula_congs [simp] = neg_cong conj_cong disj_cong
lemma formula_trans_cong1 [trans]:
"a ∼ f b ⟹ b ∼ c ⟹ (⋀x y. x ∼ y ⟹ f x ∼ f y) ⟹ a ∼ f c"
by (rule formula_trans) simp_all
lemma formula_trans_cong2 [trans]:
"a ∼ b ⟹ f b ∼ f c ⟹ (⋀x y. x ∼ y ⟹ f x ∼ f y) ⟹ f a ∼ f c"
by (rule formula_trans) simp_all
Then we can do proofs like this:
lemma conj_False: "a and FFalse ∼ FFalse"
proof -
have "a and FFalse ∼ Neg (Neg (a and FFalse))"
by (rule formula_sym, rule neg_neg)
also have "Neg (a and FFalse) ∼ Neg a or Neg FFalse"
by (rule de_morgan1)
also have "Neg FFalse ∼ FTrue" unfolding FFalse_def by (rule neg_neg)
also have "Neg a or FTrue ∼ FTrue" by (rule disj_True)
also have "Neg FTrue = FFalse" unfolding FFalse_def ..
finally show ?thesis by - simp
qed
lemma disj_False: "a or FFalse ∼ a"
proof -
have "a or FFalse ∼ Neg (Neg (a or FFalse))" by (rule formula_sym, rule neg_neg)
also have "Neg (a or FFalse) ∼ Neg a and Neg FFalse" by (rule de_morgan2)
also have "Neg FFalse ∼ FTrue" unfolding FFalse_def by (rule neg_neg)
also have "Neg a and FTrue ∼ Neg a" by (rule conj_True)
also have "Neg (Neg a) ∼ a" by (rule neg_neg)
finally show ?thesis by - simp
qed
One would, of course, also like to prove completeness, i.e. that any two formulas that are semantically equivalent are also syntactically equivalent. For that, I think you will need a few more rules and then the proof is pretty complicated.
Why not axiomatization?
You mentioned axiomatization, and you might ask why you were advised not to use this for this purpose. Well, one reason is that axiomatization allows you to introduce inconsistencies into the system. You might ‘define’ two things to be equivalent and also define something else at another place that implies that they are not equivalent and then you have derive False and break everything. With the inductive predicate package, that cannot happen, because it proves automatically that your definitions are consistent. (by restricting them to be monotonic)
A more practical reason is that, as you can see above, you can do induction over an inductive predication, i.e. if you have two formulas that are synactically equivalent, you can do induction over the proof tree of their syntactic equivalence. In particular, you know that if two formulas are syntactically equivalent, that must be provable from the rules that you specified. If you just do axiomatization, you have no such guarantee – there may be many more syntactically equivalent formulas; with axiomatization, you could not even disprove something like Atom 0 ≈ Atom 1
, unless you axiomatize something like that as well, which will be very ugly and very prone to accidental inconsistencies.
It is very rare for an Isabelle user to use axiomatization. I have been working with and on Isabelle for years and I have never used axiomatization. It is a very low-level feature designed to set up the basic underlying logic and much work has been invested in high-level definitional tools like typedef
, datatype
, fun
, inductive
, codatatype
etc. to offer a definitional interface to the user that (hopefully) guarantee consistency to the user.
Appendix: Deciding semantic equivalence
If you're interested in using code generation to do interesting things on formulas: We can even decide semantic equivalence: we can simply test if the two expressions evaluate to the same results on the set of variables they containt. (syntactic equivalence is possible as well, but more difficult because you will have to get the inductive predicate package to compile usable code for it, and I did not manage to do that)
primrec vars :: "formula ⇒ vname list" where
"vars (Atom x) = [x]"
| "vars FTrue = []"
| "vars (Neg a) = vars a"
| "vars (Conj a b) = vars a @ vars b"
| "vars (Disj a b) = vars a @ vars b"
lemma eval_formula_cong:
"(⋀x. x ∈ set (vars a) ⟹ s x = s' x) ⟹ eval_formula s a = eval_formula s' a"
by (induction a) simp_all
primrec valuations :: "vname list ⇒ (vname ⇒ bool) list" where
"valuations [] = [λ_. False]"
| "valuations (x#xs) = [f' . f ← valuations xs, f' ← [f, fun_upd f x True]]"
lemma set_valuations: "set (valuations xs) = {f. ∀x. x∉set xs ⟶ f x = False}"
proof safe
case (goal2 f)
thus ?case
proof (induction xs arbitrary: f)
case (Cons x xs)
def f' ≡ "fun_upd f x False"
from Cons.prems have f': "f' ∈ set (valuations xs)"
by (intro Cons) (auto simp: f'_def)
show ?case
proof (cases "f x")
case False
hence "f' = f" by (intro ext) (simp add: f'_def)
with f' show ?thesis by auto
next
case True
hence "fun_upd f' x True = f" by (intro ext) (simp add: f'_def)
with f' show ?thesis by auto
qed
qed auto
qed (induction xs, auto)
lemma formula_equiv_sem_code [code]:
"a ≈ b ⟷ (∀s∈set (valuations (remdups (vars a @ vars b))).
eval_formula s a = eval_formula s b)"
unfolding formula_equiv_sem_def
proof (rule iffI; rule ballI allI)
case (goal2 s)
def s' ≡ "λx. if x ∈ set (vars a @ vars b) then s x else False"
have "s' ∈ set (valuations (remdups (vars a @ vars b)))"
by (subst set_valuations) (auto simp: s'_def)
with goal2 have "eval_formula s' a = eval_formula s' b" by blast
also have "eval_formula s' a = eval_formula s a"
by (intro eval_formula_cong) (auto simp: s'_def)
also have "eval_formula s' b = eval_formula s b"
by (intro eval_formula_cong) (auto simp: s'_def)
finally show ?case .
qed auto
We can now simply ask Isabelle to compute whether two formulas are semantically equivalent:
value "Atom 0 and Atom 1 ≈ Atom 1 and Atom 0" (* True *)
value "Atom 0 and Atom 1 ≈ Atom 1 or Atom 0" (* False *)
You could even go further and write an automated proof method that decides a ≈ b
for any formulas a
and b
by substituting all free variables with fresh atoms and then deciding the equivalence of those formulas (e.g. decide a and FFalse ≈ FFalse
by deciding the equivalent Atom 0 and FFalse ≈ FFalse
).