1(*
2 * Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 *)
6
7theory TacticTutorial imports
8  Main
9begin
10
11\<comment> \<open>
12  This is a simple lemma with a boring proof. We're going to replicate it in
13  ML.
14\<close>
15lemma my_conj_commute: "(A \<and> B) = (B \<and> A)"
16  apply (rule iffI)
17   apply (rule conjI)
18    apply (erule conjunct2)
19   apply (erule conjunct1)
20  apply (erule conjE)
21  apply (rule conjI)
22   apply assumption
23  apply assumption
24  done
25
26\<comment> \<open>
27  The goal of this exercise is *not* for you to have a deep and comprehensive
28  understanding of every utility available to you that concerns ML tactic
29  internals. The goal is for you to know the basics needed to understand the
30  (somewhat sparse) documentation on tactics and how they work, and to not get
31  too confused when you see them in the wild.
32
33  However, if you *do* come up with an interesting example that demonstrates a
34  principle really well, or you discover a cool and useful trick, feel free to
35  add it here!
36\<close>
37
38section "Starting a proof"
39ML \<open>
40  \<comment> \<open>
41    To start off a proof for some statement, we use @{ML "Goal.init"}.
42  \<close>
43
44  val goal_cterm = @{cterm "Trueprop ((A \<and> B) = (B \<and> A))"};
45  val proof_state = Goal.init goal_cterm;
46
47  \<comment> \<open>
48    At this point, @{ML proof_state} looks like this:
49
50    Subgoal                 (Protected) goal
51    vvvvvvvvvvvvvvvvvvv     vvvvvvvvvvvvvvvvvvvvv
52    (A /\ B) = (B /\ A) ==> ((A /\ B) = (B /\ A))
53
54    This says "if you prove the subgoal(s), then you have proven the goal".
55    This looks very similar to how theorems are presented; in a theorem, the
56    subgoals would be called premises and the goal would be called the
57    conclusion.
58
59    In fact, in Isabelle a proof state *is* a special kind of theorem: we start
60    off with the uncontroversial claim that our goal implies itself, then
61    transform that theorem into one with *no* subgoals.
62
63    In this tutorial, the section "Proof States" outlines the difference
64    between a "normal" thm and a proof state, specifically what it means for
65    the goal to be "protected".
66
67    This topic is covered in the Isabelle implementation manual, section 4.1
68    ("Goals").
69  \<close>
70\<close>
71
72subsection "What's this Trueprop thing?"
73ML \<open>
74  \<comment> \<open>
75    Isabelle lemmas can only talk about results in @{type prop}, but @{term "x
76    = y"} is in @{type bool} (this distinction is what lets Isabelle handle
77    different logics generically, like HOL vs FOL vs ZF. This idea is explained
78    in more detail in the old Isabelle introduction, chapter 2 ("Formalizing
79    logical rules in Isabelle")).
80  
81    `Trueprop` is a wrapper that does the conversion from a HOL bool to an
82    Isabelle prop. However, other things are props by default, and don't need
83    the `Trueprop` wrapper.
84  \<close>
85
86  val prop_cterm = @{cterm "(A \<Longrightarrow> B) :: prop"};
87
88  val another_prop_cterm = @{cterm "(A \<equiv> B) :: prop"}
89\<close>
90
91section "Modifying proof state"
92ML \<open>
93  \<comment> \<open>
94    To recap, our @{ML proof_state} is currently this:
95  
96    Subgoal                   Goal
97    vvvvvvvvvvvvvvvvvvvvv     vvvvvvvvvvvvvvvvvvvvv
98    ((A /\ B) = (B /\ A)) ==> ((A /\ B) = (B /\ A))
99  
100    If you were writing an apply script, you'd use @{method rule} here. The
101    equivalent is @{ML resolve_tac}.
102  \<close>
103
104  val proof_state =
105      proof_state |> resolve_tac @{context} @{thms iffI} 1 |> Seq.hd;
106
107  \<comment> \<open>
108    Now our proof state looks like this:
109
110    Subgoal 1               Subgoal 2               Goal
111    vvvvvvvvvvvvvvvvvvv     vvvvvvvvvvvvvvvvvvv     vvvvvvvvvvvvvvvvvvvvv
112    (A /\ B ==> B /\ A) ==> (B /\ A ==> A /\ B) ==> ((A /\ B) = (B /\ A))
113
114    Our old subgoal is gone, and has been replaced by the subgoals introduced
115    by @{thm iffI}.
116  \<close>
117
118  \<comment> \<open>
119    Let's look at the signature of `resolve_tac`:
120
121    @{ML "resolve_tac: Proof.context -> thm list -> int -> tactic"}
122
123    - The proof context is the same mysterious global state that gets passed
124      around all over the place in most Isabelle/ML code.
125
126    - The thm list is the list of facts that `resolve_tac` will try and use to
127      change the subgoal (like @{method rule}).
128
129    - The int (`1` in our call) specifies which subgoal to work on. Notice that
130      this means we can't use it to modify the final goal.
131
132    The result of `resolve_tac` is a tactic, which is a type alias for
133    @{ML_type "thm -> thm Seq.seq"}. A tactic takes a thm (a proof state) and
134    produces a lazy list of new thms (new proof states). We get the first such
135    new proof state with @{ML "Seq.hd"}
136
137    A tactic can succeed or fail.
138    - If it failed, the resulting `seq` will be empty.
139    - If it succeeded, the `seq` will have one or more new proof states (for
140      example, if we passed more than one thm to `resolve_tac`, we'd get a
141      result for each successfully resolved thm).
142    For now, we're going to use tactics that will return either zero or one new
143    proof states.
144
145    The signature @{ML_type "int -> tactic"} *almost always* means "a tactic
146    that can be applied to a specific subgoal", but sometimes the int means
147    something else.
148
149    Now that we have two subgoals, we can see what happens if we use a rule on
150    something other than the first subgoal.
151  \<close>
152
153  val after_rule_on_2nd_subgoal =
154      proof_state |> resolve_tac @{context} @{thms conjI} 2 |> Seq.hd;
155
156  \<comment> \<open>
157    The proof state @{ML after_rule_on_2nd_subgoal} looks like this:
158
159    Subgoal 1 (old)         Subgoal 2 (new)    Subgoal 3 (new)    Goal
160    vvvvvvvvvvvvvvvvvvv     vvvvvvvvvvvvvv     vvvvvvvvvvvvvv     vvvvvvvvvvvvvvvvvvvvv
161    (A /\ B ==> B /\ A) ==> (B /\ A ==> A) ==> (B /\ A ==> B) ==> ((A /\ B) = (B /\ A))
162
163    The result isn't very surprising, if you're used to using `rule`.
164    `resolve_tac` matched the conclusion of @{thm conjI} against the conclusion
165    of the second subgoal of @{ML proof_state}, then asks us to prove the
166    premises of @{thm conjI} with the premises of the (original) second
167    subgoal.
168  \<close>
169\<close>
170
171subsection "Subgoals and apply scripts"
172\<comment> \<open>
173  If we look at the definition of @{method rule}, we see that it uses @{ML
174  "HEADGOAL"} and @{ML "Classical.rule_tac"}.
175  - `rule_tac` is like `resolve_tac` plus some extra features.
176  - `HEADGOAL` turns a tactic that can be applied to a subgoal
177    (@{ML_type "int -> tactic"}) into one that only applies to the first
178    subgoal.
179
180  In fact, most apply-script methods will only use tactics that modify the
181  first subgoal.  @{method tactic} lets us use an ML tactic in an apply script.
182  Let's use it to apply a tactic to the second subgoal.
183\<close>
184lemma "X \<or> Y \<Longrightarrow> A \<and> B"
185 apply (erule disjE)
186  apply (tactic \<open>resolve_tac @{context} @{thms conjI} 2\<close>)
187 oops
188
189subsection "Elimination and assumption"
190ML \<open>
191  \<comment> \<open>
192    The equivalent of @{method erule} is @{ML eresolve_tac}. Let's use it to
193    solve subgoal 1, continuing from where we left off with @{ML proof_state}.
194  \<close>
195  val after_conjI =
196      proof_state |> resolve_tac @{context} @{thms conjI} 1 |> Seq.hd;
197
198  val after_conjunct2 =
199      after_conjI |> eresolve_tac @{context} @{thms conjunct2} 1 |> Seq.hd;
200
201  \<comment> \<open>
202    Notice that, since `eresolve_tac` replaces the matched premise with any
203    additional premises of the matched rule, and since `conjunct2` doesn't have
204    any such premises, the relevant subgoal has just... disappeared!
205
206    Let's deal with the rest of the subgoals, following the original apply
207    script.
208  \<close>
209  val after_conjunct1 =
210      after_conjunct2 |> eresolve_tac @{context} @{thms conjunct1} 1 |> Seq.hd;
211
212  val after_conjE =
213      after_conjunct1 |> eresolve_tac @{context} @{thms conjE} 1 |> Seq.hd;
214
215  val after_conjI =
216      after_conjE |> resolve_tac @{context} @{thms conjI} 1 |> Seq.hd;
217
218  \<comment> \<open>
219    The equivalent of @{method assumption} is @{ML assume_tac}.
220  \<close>
221  val after_assumptions =
222      after_conjI |> assume_tac @{context} 1 |> Seq.hd
223                  |> assume_tac @{context} 1 |> Seq.hd;
224\<close>
225
226subsection "Finishing off"
227ML \<open>
228  \<comment> \<open>
229    We're done! Our proof state consists of just the original goal, with no
230    subgoals.  We can confirm this (and "unwrap" our goal from the special
231    protection set up by @{ML Goal.init}):
232  \<close>
233  val final_thm = Goal.finish @{context} after_assumptions;
234
235  \<comment> \<open>
236    Actually, we're not *quite* done. Our theorem has free variables, whereas a
237    global fact must not have free variables that don't refer to something in
238    the local context. This means we need to convert our free variables into
239    bound ones. Thankfully there's a utility for doing that conversion.
240  \<close>
241  \<comment> \<open>
242    TODO: the 'correct' way to do this is using @{ML "Variable.export"}, but
243    it's not clear how to actually use that here (what's the "destination"
244    context?).
245  \<close>
246  val final_thm = Thm.forall_intr_frees final_thm;
247
248  \<comment> \<open>
249    We can give our new thm a name so we can refer to it.
250  \<close>
251  val add_thm = Local_Theory.note ((@{binding my_cool_ML_thm}, []), [final_thm]) #> snd;
252
253  \<comment> \<open>
254    TODO: these are... magic incantations. What do they do?
255  \<close>
256  add_thm |> Named_Target.theory_map |> Theory.setup;
257\<close>
258thm my_cool_ML_thm
259
260section Combinators
261ML \<open>
262  \<comment> \<open>
263    Manually passing around these proof state thms, and fetching the first lazy
264    result from a tactic, is very annoying. However, there are utilities for
265    composing tactics.
266
267    We're going to construct a subgoal tactic that deals with the subgoal
268    @{term "A \<and> B \<Longrightarrow> B \<and> A"}.
269  \<close>
270
271  \<comment> \<open>
272    Here's one way to get a fact by name without using an antiquotation.
273  \<close>
274  fun get_thm name =
275      Facts.named name |> Proof_Context.get_fact @{context} |> hd;
276
277  val [iffI, conjI, conjunct1, conjunct2] =
278      ["iffI", "conjI", "conjunct1", "conjunct2"] |> map get_thm;
279
280  fun resolve thm = resolve_tac @{context} [thm];
281  fun eresolve thm = eresolve_tac @{context} [thm];
282
283  val solve_commute_conjunct_goal_tac =
284      resolve conjI
285      THEN' eresolve conjunct2
286      THEN' eresolve conjunct1;
287
288  \<comment> \<open>
289    After applying our tactic, we can confirm that the proof state is the same
290    as it was after the manual application of these steps (back at
291    @{ML after_conjunct1}).
292  \<close>
293  val result =
294      Goal.init goal_cterm
295      |> (HEADGOAL (resolve iffI)
296         THEN HEADGOAL (solve_commute_conjunct_goal_tac)) |> Seq.hd;
297
298  \<comment> \<open>
299    If we want to apply our subgoal tactic to both subgoals at once, we can
300    replace `HEADGOAL` with `ALLGOALS`. As expected, this will solve both
301    subgoals.
302  \<close>
303  val result =
304      Goal.init goal_cterm
305      |> (HEADGOAL (resolve iffI)
306         THEN ALLGOALS (solve_commute_conjunct_goal_tac)) |> Seq.hd;
307\<close>
308
309section "Tracing tactics"
310ML \<open>
311  \<comment> \<open>
312    The tactic @{ML print_tac} prints all the subgoals when it's invoked, then
313    passes the proof state through unchanged. Let's use it to follow what our
314    @{ML solve_commute_conjunct_goal_tac} is doing.
315  \<close>
316
317  \<comment> \<open>
318    `trace` wraps a subgoal-tactic with messages showing the state before and
319    after the tactic was applied (and also indicating which subgoal it's
320    applied to).  Note that the indicated subgoal might be *removed* in the
321    "after" state.
322  \<close>
323  fun trace msg tac =
324      let
325        fun msg_before i =
326            print_tac @{context} ("(subgoal " ^ Int.toString i ^ ") before " ^ msg);
327        val msg_after = K (print_tac @{context} ("after " ^ msg));
328      in
329        msg_before THEN' tac THEN' msg_after
330      end
331  val tracing_tac =
332      (trace "conjI" (resolve conjI))
333      THEN' (trace "conjunct2" (eresolve conjunct2))
334      THEN' (trace "conjunct1" (eresolve conjunct1));
335
336  \<comment> \<open>
337    The result is very verbose, but also very explicit about what changes and
338    when.
339  \<close>
340  val result =
341      Goal.init goal_cterm
342      |> (HEADGOAL (resolve iffI)
343         THEN ALLGOALS (tracing_tac)) |> Seq.hd;
344\<close>
345
346section "Proof States"
347ML \<open>
348  \<comment> \<open>
349    How is @{ML "Goal.init goal_cterm"} different to
350    @{ML "Thm.trivial goal_cterm"}?
351    
352    `Goal.init` "protects" the goal, which prevents most standard tactics from
353    changing it (this is good, because otherwise a tactic might suddenly change
354    *what you'll finally prove*).
355
356    In this tutorial, the section "Subgoal Restriction" goes through an example
357    of using this "protection" feature in a tactic.
358
359    This topic is covered in the Isabelle implementation manual, section 4.1
360    ("Goals").
361  \<close>
362
363  val bigger_cterm = @{cterm "A \<and> B \<Longrightarrow> B \<and> A"};
364  val bigger_goal_thm = Goal.init bigger_cterm;
365  val trivial_thm = Thm.trivial bigger_cterm;
366
367  \<comment> \<open>
368    `bigger_goal_thm` has one premise (a subgoal) and one conclusion (the
369    goal):
370
371    Subgoal                 Goal (protected)
372    vvvvvvvvvvvvvvvvvvv     vvvvvvvvvvvvvvvvvvv
373    (A /\ B ==> B /\ A) ==> (A /\ B ==> B /\ A)
374
375    Whereas `trivial_thm` has two premises (and hence two "subgoals"):
376
377    Subgoal 1               Subgoal 2  Goal (unprotected)
378    vvvvvvvvvvvvvvvvvvv     vvvvvv     vvvvvv
379    (A /\ B ==> B /\ A) ==> A /\ B ==> B /\ A
380
381    This means that tactics which specify what subgoal they're going to modify
382    can modify a part of the proof state that "should represent" the goal that
383    we want to prove.
384  \<close>
385\<close>
386
387section "Methods and tactics"
388ML \<open>
389  \<comment> \<open>
390    Dummy ML declarations to make the examples in the table work.
391  \<close>
392  val some_ctxt = @{context};
393  val some_thms = [];
394\<close>
395\<comment> \<open>
396  Here is a rough correspondence between methods and their tactic equivalents.
397  The first four "basic" tactics are documented in the Isabelle implementation
398  manual, section 4.2.1 ("Resolution and assumption tactics").
399
400  Method          | Tactic
401  ----------------+-------
402  @{method rule}  | @{ML resolve_tac}
403  @{method erule} | @{ML eresolve_tac}
404  @{method frule} | @{ML forward_tac}
405  @{method drule} | @{ML dresolve_tac}
406  ----------------+-----------------------------------
407  @{method subst} | "subst" ~ @{ML EqSubst.eqsubst_tac}
408                  | "subst (asm)" ~ @{ML EqSubst.eqsubst_asm_tac}
409  ----------------+--------------------------------------------------------------
410  @{method simp}  | "simp" ~ @{ML simp_tac} (only modifies conclusion of subgoal)
411                  |        ~ @{ML asm_simp_tac} (can use assumptions of subgoal, e.g. to do
412                  |                              proof by contradiction)
413                  | "simp only: some_thms" ~
414                  |      @{ML "simp_tac (clear_simpset some_ctxt addsimps some_thms)"}
415\<close>
416
417section "Method combinators and tactic combinators"
418\<comment> \<open>
419  These are documented in the Isabelle implementation manual, section 4.3.1
420  ("Combining tactics").
421
422  Method combinator | Tactic combinator | Subgoal tactic combinator
423  ------------------+-------------------+--------------------------
424  (a , b)           | @{ML THEN}        | @{ML THEN'}
425  (a | b)           | @{ML ORELSE}      | @{ML ORELSE'}
426  (a ; b)           | (none)            | @{ML THEN_ALL_NEW}
427  a +               | @{ML REPEAT1}     | (none)
428  a ?               | @{ML TRY}         | (none)
429  ------------------+-------------------+--------------------
430  a [n]             | (see the section on "Subgoal restriction")
431\<close>
432
433section "Subgoal restriction"
434ML \<open>
435  \<comment> \<open>
436    How do we stop a method or tactic from modifying certain subgoals?
437
438    The method combinator `[n]` restricts the given method to only modifying
439    the first n subgoals.  This works by using the same modification protection
440    that @{ML "Goal.init"} uses.
441
442    We usually see `[n]` used with methods that heavily modify proof state in
443    ways that are unsafe or hard to predict, such as @{method auto}.
444
445    The tactic combinator @{ML Goal.SELECT_GOAL} turns a general tactic into a
446    subgoal-targeted tactic, by restricting which subgoals the tactic can
447    modify (side note: there's also a combinator @{ML Goal.PREFER_GOAL} which
448    merely moves a specific subgoal to the "front").
449
450    We're going to write a subgoal-targeting version of `auto` *without* using
451    `SELECT_GOAL`, to learn how it works. To start, we're going to need to
452    understand how to "protect" subgoals. We'll begin with a proof state with
453    lots of subgoals for us to play with.
454  \<close>
455  val cterm = @{cterm "A \<or> B \<or> C \<or> D \<or> E \<Longrightarrow> X"};
456  val goal =
457      Goal.init cterm
458      |> HEADGOAL (REPEAT_ALL_NEW (eresolve_tac @{context} @{thms disjE}))
459      |> Seq.hd;
460
461  \<comment> \<open>
462    Our proof state has five subgoals:
463
464    Subgoal 1                                               Subgoal 5
465    vvvvvvvvv                                               vvvvvvvvv
466    (A ==> X) ==> (B ==> X) ==> (C ==> X) ==> (D ==> X) ==> (E ==> X) ==>
467
468    Goal
469    vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
470    (A \/ B \/ C \/ D \/ E ==> X)
471
472    In the same way that @{ML Goal.init} "protected" the final goal to prevent
473    us messing with it, we can use @{ML Goal.restrict} to "protect" some
474    subgoals so we can only modify the rest (hence "restricting" us).
475
476    In the abstract, `Goal.restrict i n thm` restricts the proof state to only
477    be able to modify subgoals i, i + 1, ..., i + (n - 1) (the n subgoals
478    starting at subgoal i). In detail, it accomplishes this by rotating the
479    first (i - 1) subgoals to the back of the subgoals list, then "protecting"
480    all but the first n subgoals.
481  \<close>
482  val restricted = goal |> Goal.restrict 2 2;
483  \<comment> \<open>
484    This proof state only has two subgoals; the others are "protected", and
485    can't be modified by most tactics.
486
487    Subgoal 2     Subgoal 3
488    vvvvvvvvv     vvvvvvvvv
489    (B ==> X) ==> (C ==> X) ==>
490
491    Protected "goal"
492    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
493     Protected     Protected     Protected     Doubly-protected
494     subgoal 4     subgoal 5     subgoal 1     actual goal
495     vvvvvvvvv     vvvvvvvvv     vvvvvvvvv     vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
496    ((D ==> X) ==> (E ==> X) ==> (A ==> X) ==> (A \/ B \/ C \/ D \/ E ==> X))
497  \<close>
498
499  \<comment> \<open>
500    To restore the previous state, we use @{ML Goal.unrestrict}. In the
501    abstract, `Goal.unrestrict i` "undoes" `Goal.restrict i n`. In detail, it
502    accomplishes this by first removing a layer of protection from the goal,
503    then rotating the last (i - 1) subgoals to the front.
504  \<close>
505  val unrestricted = restricted |> Goal.unrestrict 2;
506
507  \<comment> \<open>
508    Here, we check that the unrestricted restricted goal is the same as the
509    original goal.
510
511    This checks that the statements of the two thms are alpha-equivalent.
512  \<close>
513  @{assert} ((Thm.full_prop_of unrestricted) aconv (Thm.full_prop_of goal));
514
515  \<comment> \<open>
516    We're now ready to make a subgoal-targeting version of `auto_tac`.
517
518    This isn't equivalent to the `[n]` notation (if we wanted to apply auto to
519    the first i subgoals, instead of the ith subgoal, we'd replace
520    `Goal.restrict i 1` with `Goal.restrict 1 i`), but it's arguably more
521    useful.
522  \<close>
523  fun subgoal_auto_tac ctxt i =
524      PRIMITIVE (Goal.restrict i 1)
525      THEN (auto_tac ctxt)
526      THEN PRIMITIVE (Goal.unrestrict i);
527\<close>
528\<comment> \<open>
529  Let's check that it works.
530\<close>
531lemma "A = B \<and> B = C \<and> C = D \<and> D = E \<and> E = X \<Longrightarrow> A \<or> B \<or> C \<or> D \<or> E \<Longrightarrow> X"
532  apply (tactic \<open>HEADGOAL (REPEAT_ALL_NEW (eresolve_tac @{context} @{thms disjE}))\<close>)
533      apply (tactic \<open>subgoal_auto_tac @{context} 3\<close>) (* Only removes "C ==> X" case. *)
534     apply (tactic \<open>subgoal_auto_tac @{context} 4\<close>) (* Only removes "E ==> X" case. *)
535  oops
536ML \<open>
537  \<comment> \<open>
538    For reference, here's the version that uses SELECT_GOAL (the main
539    difference is that SELECT_GOAL handles the case where there's only one
540    subgoal).
541  \<close>
542  fun better_subgoal_auto_tac ctxt = Goal.SELECT_GOAL (auto_tac ctxt);
543\<close>
544lemma "A = B \<and> B = C \<and> C = D \<and> D = E \<and> E = X \<Longrightarrow> A \<or> B \<or> C \<or> D \<or> E \<Longrightarrow> X"
545  apply (tactic \<open>HEADGOAL (REPEAT_ALL_NEW (eresolve_tac @{context} @{thms disjE}))\<close>)
546      apply (tactic \<open>better_subgoal_auto_tac @{context} 3\<close>) (* Only removes "C ==> X" case. *)
547     apply (tactic \<open>better_subgoal_auto_tac @{context} 4\<close>) (* Only removes "E ==> X" case. *)
548  oops
549
550end
551