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