Is it possible to give my own names to the variables generated when using case analysis or induction?
If you are using structured proofs (starting with the proof keyword), you can use the case keywoard to select the case you want to prove and give names to the variables created by case analysis / induction:
lemma "length (rev xs) = length xs"
proof (induct xs)
case Nil
then show ?case ...
next
case (Cons x xs)
then show ?case ...
qed
Here case (Cons x xs) tells Isabelle that you want to prove the case where a list consists of a start element and a remaining list (i.e., the induction step) and name the variables x and xs.
In the proof block, you can see the list of cases with the print_cases command.
If on the other hand you are using apply-style, there is no direct way to name these variables (also, in this case you are likely to need case_tac instead of cases, as you will have to deal with bound variables instead of free variables). There is the method rename_tac which can be used to rename the outermost meta-quantified variables.
For most projects (with the notable exception of program verification, like in the L4.verified project), the common proof style is to use mostly structured proofs. Unstructured proofs are used for exploration and become seldom so complex that it is necessary to rename your variables.
Related
I have the following proof state:
1. ⋀i is s stk stack.
(⋀stack.
length (exec is s stack) = n' ⟹
length stack = n ⟹ ok n is n') ⟹
length (exec (i # is) s stack) = n' ⟹
length stack = n ⟹ ok n (i # is) n'
How do I perform a case split on i? Where i is of type:
datatype instr = LOADI val | LOAD vname | ADD
I'm doing this for exc 4.7 of concrete semantics so this should be possible to do with tactics.
If anything you should use cases i rule: instr.cases, but that will not work here because i is not a fixed variable but a bound variable. Also, the rule: instr.cases is not really needed because Isabelle will use that rule by default anyway.
Doing a case distinction on a bound variable without fixing it first is kind of discouraged; that said, it can be done by doing apply (case_tac i) instead of apply (cases i). But as I said, this is not the nice way to do it.
A more proper way to do it is to explicitly fix i using e.g. the subgoal command:
subgoal for i is s stk stack
apply (cases i)
An even better way would probably be to use a structured Isar proof instead.
However, I don't think the subgoal command or Isar proofs are something that you know about at this stage of the Concrete Semantics book, so my guess would be that there is a nicer way to do the proof where you don't have to do any manual case splitting.
Most probably you are doing an induction on the list of instructions; it would probably be better to do an induction on the predicate ok instead. But then again: Where is that predicate ok? I don't see it in your assumptions. It's hard to say what's going on there without knowing how you defined ok and what lemma you are trying to prove exactly and what tactics you applied already.
Is there any way in Isabelle (2021) to refer to assumptions in the old apply style proofs?
In particular, I am interested in using the assumptions as facts in the OF operator so that I can do (hypothetically):
apply(rule R[OF assm1 assm4])
, where assm1 and assm4 should refer to the 1st and 4th assumptions in the current proof state.
Often times, I can arrange assumptions of the current sugboal so that R[OF assm1 assm4] is the same as the subgoal. But then, I can't finish the proof because I don't know how to refer to assm1 assm4 etc. It seems that only global theorem names are allowed with OF.
I even tried to use the subgoal_tac method on the assumptions, but it does not seem to have an option of giving names to the fact.
In the end, I have to use an automatic script such as simp, which is somewhat opaque for something so obvious. By the way, this is for learning purposes. I tried setting up simp_trace, but still couldn't replicate the effect without using simp.
Moreover,
If there is no way to refer to assumptions, is this a limitation of the tactics or a fundamental limitation of natural deduction? (i.e. Is the rewriting style of R[OF assm1 assm4] not compatible with natural deduction?)
The whole point is Isar is that you can name assumptions...
The first low-level solution is to use drule (or frule to keep the assumptions).
Here is an example:
lemma
assumes ‹⋀x y. P x ⟹ Q y ⟹ R z› ‹P x› ‹Q y›
shows ‹R z›
using assms(2-) apply -
apply (drule assms(1))
apply assumption
apply assumption
done
Look at Chapter 5 for details on the destruction/elimination/intro rules.
The second solution is subgoal:
lemma
assumes ‹⋀x y. P x ⟹ Q y ⟹ R z› ‹P x› ‹Q y›
shows ‹R z›
using assms(2-) apply -
subgoal premises p
by (rule assms(1)[OF p])
done
but this creates hard-to-read proofs if you have very deep nesting.
The third and best solution is to use Isar proofs…
Here is a version that completely avoids using names:
lemma
assumes ‹⋀x y. P x ⟹ Q y ⟹ R z› ‹P x› ‹Q y›
shows ‹R z›
using assms apply -
apply (elim meta_allE[of _ x])
apply (elim meta_allE[of _ y])
apply (drule cut_rl)
apply assumption
apply (drule cut_rl)
apply assumption
apply assumption
done
You can see how ugly this is and why you should avoid that.
I was playing around with an example from the Isabelle/HOL tutorial to get a better understanding on the correspondence between Isar and Tactics proofs.
This is a version which works:
lemma rtrancl_converseD: "(x,y) ∈ (r ^-1 )^* ⟹ (y,x) ∈ r^* "
proof (induct y rule: rtrancl_induct)
case base
then show ?case ..
next case (step y z)
then have "(z, y) ∈ r" using rtrancl_converseD by simp
with `(y,x)∈ r^*` show "(z,x) ∈ r^*" using [[unify_trace_failure]]
apply (subgoal_tac "1=(1::nat)")
apply (rule converse_rtrancl_into_rtrancl)
apply simp_all
done
qed
I want to instantiate converse_rtrancl_into_rtrancl which proofs (?a, ?b) ∈ ?r ⟹ (?b, ?c) ∈ ?r^* ⟹ (?a, ?c) ∈ ?r^* .
But without the seemingly nonsensical apply (subgoal_tac "1=(1::nat)") line this errors with
Clash: r =/= Transitive_Closure.rtrancl
Failed to apply proof method⌂:
using this:
(y, x) ∈ r^*
(z, y) ∈ r
goal (1 subgoal):
1. (z, x) ∈ r^*
If I fully instantiate the rule apply (rule converse_rtrancl_into_rtrancl[of z y r x]) this becomes Clash: z__ =/= ya__.
This leaves me with three questions: Why does this specific case break? How can I fix it? And how can I figure out what went wrong in these cases since I can't really understand what the unify_trace_failure message wants to tell me.
rule-tactics are usually sensitive to the order of premises. The order of premises in converse_rtrancl_into_rtrancl and in your proof state don't match. Switching the order of premises in the proof state using rotate_tac will make them match the rule, so that you can directly apply fact like this:
... show "(z,x) ∈ r^*"
apply (rotate_tac)
apply (fact converse_rtrancl_into_rtrancl)
done
Or, if you want to include some kind of rule tactic, this would look like this:
apply (rotate_tac)
apply (erule converse_rtrancl_into_rtrancl)
apply (assumption)
(I personally don't use apply scripts ever in my everyday work. So apply-style gurus might know more elegant ways of handling this kind of situation. ;) )
Regarding your 1=(1::nat) / simp_all fix:
The whole goal can directly be solved by simp_all. So, attempts with adding stuff like 1=1 probably did not really tell you a lot about how much the other methods contributed to solving the proof.
However, the additional assumption seems to actually help Isabelle match converse_rtrancl_into_rtrancl correctly. (Don't ask me why!) So, one could indeed circumvent the problem by adding this spurious assumption and then eliminating it with refl again like:
apply (subgoal_tac "1=(1::nat)")
apply (erule converse_rtrancl_into_rtrancl)
apply (assumption)
apply (rule refl)
This does not look particularly elegant, of course.
The [[unify_trace_failure]] probably only really helps if one is familiar with the internal workings of Nipkow's higher-order unification algorithm. (I'm not.) I think the hint for the future here would really be that one must look closely at the order of premises for some tactics (rather than at the unifier debug output).
I found an explanation in the Isar reference 6.4.3 .
The with b1..bn command is equivalent to from b1..bn and this, i.e. it enters the proof chaining mode which adds them as (structured) assumptions to proof methods.
Basic proof methods (such as rule) expect multiple facts to be given
in their proper order, corresponding to a prefix of the premises of
the rule involved. Note that positions may be easily skipped using
something like from _ and a and b, for example. This involves the
trivial rule PROP ψ =⇒ PROP ψ, which is bound in Isabelle/Pure as “_”
(underscore).
Automated methods (such as simp or auto) just insert any given facts
before their usual operation. Depending on the kind of procedure
involved, the order of facts is less significant here.
Given the information about the 'with' translation and that rule expects chained facts in order, we could try to flip the chained facts. And indeed this works:
from this and `(y,x)∈ r^*` show "(z,x) ∈ r^*"
by (rule converse_rtrancl_into_rtrancl)
I think "6.4.3 Fundamental methods and attributes" is also relevant because it describes how the basic methods interact with incoming facts. Notably, the '-' noop which is sometimes used when starting proofs turns forward chaining into assumptions on the goal.
with `(y,x)∈ r^*` show "(z,x) ∈ r^*"
apply -
apply (rule converse_rtrancl_into_rtrancl; assumption)
done
This works because the first apply consumes all chained facts so the second apply is pure backwards chaining. This is also why the subgoal_tac or rotate_tac worked, but only if they are in seperate apply commands.
Some induction rules have case names: the default one has case 0 and case (Suc n) for example. Given a rule, e.g. int_induct, how do I find out its case names (if, indeed, it has these) without looking in the theory containing this lemma?
I'm not aware of any high-level way to do this. The case names are stored in the tags of a theorem that can be obtained with ML:
ML‹Thm.get_tags #{thm nat.induct}›
This prints:
val it = [("name", "Nat.nat.induct"), ("kind", "theorem"), ("case_names", "zero;Suc")]: Properties.T
As you can see, the case names are available under the key case_names.
How can I state a lemma in Isabelle/HOL that does not obey sort constraints?
To explain why that makes sense, consider the following example theory:
theory Test
imports Main
begin
class embeddable =
fixes embedding::"'a ⇒ nat"
assumes "inj embedding"
lemma OFCLASS_I:
assumes "inj (embedding::'a⇒_)"
shows "OFCLASS('a::type,embeddable_class)"
apply intro_classes by (fact assms)
instantiation nat :: embeddable begin
definition "embedding = id"
instance
apply (rule OFCLASS_I) unfolding embedding_nat_def by simp
end
end
This theory defines a type class "embeddable" with one type parameters "embedding". (Basically, the class embeddable characterizes countable numbers with an explicit enumeration, but I chose this simply to have a very simple example.)
To simplify instance-proofs for the type class, the theory states an auxiliary lemma OFCLASS_I that shows goals of the form OFCLASS(...,embeddable). This lemma can then be used to solve the proof obligations produced by instance.
First, why would I even want that? (In the present theory, apply (rule OFCLASS_I) does the same as the builtin apply intro_classes and hence is useless.) In more complex cases, there are two reasons for such a lemma:
The lemma can give alternative criteria for instantiation of a type class, making subsequent proofs simpler.
The lemma could be used in automated (ML-level) instantiations. Here using intro_classes is problematic, because the number of subgoals of intro_classes can vary depending on what instance proofs have been performed earlier. (A lemma such as OFCLASS_I has a stable, well-controlled subgoals.)
However, the above theory does not work in Isabelle/HOL (neither 2015 or 2016-RC1) because the lemma does not type-check. The sort constraint "embedding::(_::embeddable => _)" is not satisfied. However, this type contraint is not a logic necessity (it is not enforced by the logical kernel but instead of a higher level).
So, is there a clean way of stating the theory above? (I give a somewhat ugly solution in my answer, but I am looking for something cleaner.)
The checking of the sort constraints can be disabled temporarily on the ML-level, and reactivated afterwards. (See the complete example below.) But that solution looks very much like a hack. We have to change the sort constraints in the context, and remember to restore them afterwards.
theory Test
imports Main
begin
class embeddable =
fixes embedding::"'a ⇒ nat"
assumes "inj embedding"
ML {*
val consts_to_unconstrain = [#{const_name embedding}]
val consts_orig_constraints = map (Sign.the_const_constraint
#{theory}) consts_to_unconstrain
*}
setup {*
fold (fn c => fn thy => Sign.add_const_constraint (c,NONE) thy) consts_to_unconstrain
*}
lemma OFCLASS_I:
assumes "inj (embedding::'a⇒_)"
shows "OFCLASS('a::type,embeddable_class)"
apply intro_classes by (fact assms)
(* Recover stored type constraints *)
setup {*
fold2 (fn c => fn T => fn thy => Sign.add_const_constraint
(c,SOME (Logic.unvarifyT_global T)) thy)
consts_to_unconstrain consts_orig_constraints
*}
instantiation nat :: embeddable begin
definition "embedding = id"
instance
apply (rule OFCLASS_I) unfolding embedding_nat_def by simp
end
end
This theory is accepted by Isabelle/HOL. The approach works in more complex settings (I have used it several times), but I would prefer a more elegant one.
The following is a different solution that does not require to "clean up" after proving the lemma (this answer requires to "repair" the sort constraints afterwards).
The idea is to define a new constant (embedding_UNCONSTRAINED in my example) that is a copy of the original sort-constrained constant (embedding), except without sort constraints (using the local_setup command below). Then the lemma is stated using embedding_UNCONSTRAINED instead of embedding. But by adding the attribute [unfolded embedding_UNCONSTRAINED_def] the lemma that is actually stored uses the constant embedding without sort constraint.
The drawback of this approach is that during the proof of the lemma, we can never explicitly write any terms containing embedding (since that will add the unwanted sort constraints). But if we need to state a subgoal containing embedding, we can always state it using embedding_UNCONSTRAINED and then use, e.g., unfolding embedding_UNCONSTRAINED to transform it into embedding.
theory Test
imports Main
begin
class embeddable =
fixes embedding::"'a ⇒ nat"
assumes "inj embedding"
(* This does roughly:
definition "embedding_UNCONSTRAINED == (embedding::('a::type)=>nat)" *)
local_setup {*
Local_Theory.define ((#{binding embedding_UNCONSTRAINED},NoSyn),((#{binding embedding_UNCONSTRAINED_def},[]),
Const(#{const_name embedding},#{typ "'a ⇒ nat"}))) #> snd
*}
lemma OFCLASS_I [unfolded embedding_UNCONSTRAINED_def]:
assumes "inj (embedding_UNCONSTRAINED::'a⇒_)"
shows "OFCLASS('a::type,embeddable_class)"
apply intro_classes by (fact assms[unfolded embedding_UNCONSTRAINED_def])
thm OFCLASS_I (* The theorem now uses "embedding", but without sort "embeddable" *)
instantiation nat :: embeddable begin
definition "embedding = id"
instance
apply (rule OFCLASS_I) unfolding embedding_nat_def by simp
end
end