My story is quite long, and my code for what I actually wanted to do is also quite long. So first I will describe what I have tried with the formula in the title, and then I will describe how I encountered something like this in practice, putting my actual code at the very bottom.
I understand that maybe I should have done what I want to do in an entirely different way: I have no prior experience with Isabelle (or even much experience with functional programming languages like Haskell), so my code is probably non-idiomatic.
What I have done with it
First, I understand that everything in the section can be proved with by presburger
(as try
told me). That doesn't work with my actual problem, though, and it probably shouldn't, because there I deal with custom datatypes containing integers, instead of integers themselves.
In attempts to reduce this into an minimal complete verifiable example, I noticed that even simple formulas like "∃(x::nat). (x ≠ 2)"
are hard to prove for Isabelle. However, I thought something concrete like x = 1
may help. Indeed,
lemma "∃(x::nat). (x ≠ 2 ∧ x = 1)"
by auto
worked easily, but
lemma "∃(x::nat). (x ≠ 2 ∧ (x ≠ 2 ⟶ x = 1))"
by auto
still fails. I thought maybe I should put the concrete part first, but when I tried
lemma "∃(x::nat). ((x ≠ 2 ⟶ x = 1) ∧ x ≠ 2)"
by auto
the by auto
line stayed purple in the jEdit, which I believe indicates an infinite loop.
How I encountered it
I am trying to use Isabelle to make a model for Shogi, and ultimately prove the uniqueness of solution for some Shogi problems. When I tried to prove that a simple configuration is a checkmate, Isabelle was stuck at (for context, I had datatype coord = Coord int int
):
∃dst. dst ≠ Coord 5 3 ∧
(dst ≠ Coord 5 3 ⟶
dst ≠ Coord 5 2 ∧
(dst ≠ Coord 5 2 ⟶
(dst = Coord 5 1 ⟶
(∃src. src ≠ Coord 5 3 ∧ (src ≠ Coord 5 3 ⟶ src = Coord 5 2))) ∧
dst = Coord 5 1))
I know this looks like a mess. Let me slightly tweak the indent to make it hopefully easier to parse for human eyes.
∃dst. dst ≠ Coord 5 3 ∧
(
dst ≠ Coord 5 3 ⟶
dst ≠ Coord 5 2 ∧
(
dst ≠ Coord 5 2 ⟶
(
dst = Coord 5 1 ⟶
(∃src. src ≠ Coord 5 3 ∧ (src ≠ Coord 5 3 ⟶ src = Coord 5 2))
) ∧ dst = Coord 5 1
)
)
Hopefully it can be seen that src = Coord 5 2
satisfies the single line with src
, and then dst = Coord 5 1
satisfies the whole thing.
I tried to put this as a lemma and prove it by auto
--- of course that doesn't work. I finally tried by try
, and it told me:
"cvc4": Try this: by (smt coord.inject) (866 ms)
OK, that worked. But I am really confused because I don't even know what smt
is.
Regardless, that method (after applying auto
) successfully proved that Player 2 is in check. I tried to use the same method to prove that it's a checkmate (which, at the very least, amounts to proving that in several configurations Player 2 is still in check), but that resulted in an error too:
Solver z3: Solver "z3" failed -- enable tracing using the "smt_trace" option for details
Even sledgehammer
didn't help me.
Since checks and checkmates are in the very basic rules of Shogi problems, I think I need to be able to recognize them automatically before I can do anything nontrivial. I am open to having to write down and prove a few lemmas manually, but now I believe that I might be doing something fundamentally wrong.
My code
If there is anything I need to clarify, please ask in the comments.
theory Shogi
imports Main "HOL-Library.Multiset"
begin
datatype coord = Coord int int
datatype vector = Vector int int
datatype piece_type = King | Gold
datatype move = Move coord coord | Drop piece_type coord
datatype player = Sente | Gote
type_synonym on_board = "(coord, player * piece_type) map"
datatype board = Board on_board "(player * piece_type) multiset" player
fun opponent :: "player ⇒ player" where
"opponent Sente = Gote" |
"opponent Gote = Sente"
fun diff :: "coord ⇒ coord ⇒ vector" where
"diff (Coord x1 y1) (Coord x0 y0) = Vector (x1 - x0) (y1 - y0)"
fun negate :: "vector ⇒ vector" where
"negate (Vector x y) = (Vector (-x) (-y))"
fun to_hand :: "(player * piece_type) option ⇒ (player * piece_type) multiset" where
"to_hand None = {#}" |
"to_hand (Some (owner, piece)) = {#((opponent owner), piece)#}"
fun make_move :: "board ⇒ move ⇒ board" where
"make_move (Board on_board in_hand to_move) (Move src dst) =
(Board
(on_board(src := None, dst := (on_board src)))
(in_hand + (to_hand (on_board dst)))
(opponent to_move)
)" |
"make_move (Board on_board in_hand to_move) (Drop piece pos) =
(Board
(on_board(pos := Some (to_move, piece)))
(in_hand - {#(to_move, piece)#})
(opponent to_move)
)"
fun is_on_board :: "coord ⇒ bool" where
"is_on_board (Coord file rank) = (1 ≤ file ∧ file ≤ 9 ∧ 1 ≤ rank ∧ rank ≤ 9)"
fun movement_vector :: "piece_type ⇒ vector ⇒ bool" where
"movement_vector King (Vector x y) = ((x, y) ∈ {
(1, -1), (0, -1), (-1, -1),
(1, 0), (-1, 0),
(1, 1), (0, 1), (-1, 1)})" |
"movement_vector Gold (Vector x y) = ((x, y) ∈ {
(1, -1), (0, -1), (-1, -1),
(1, 0), (-1, 0),
(0, 1) })"
fun negated_movement_vector :: "piece_type ⇒ vector ⇒ bool" where
"negated_movement_vector piece vector = movement_vector piece (negate vector)"
fun absolute_movement_vector :: "player ⇒ piece_type ⇒ vector ⇒ bool" where
"absolute_movement_vector Sente = movement_vector" |
"absolute_movement_vector Gote = negated_movement_vector"
fun is_legal_for_piece :: "(player * piece_type) option ⇒ player ⇒ on_board
⇒ coord ⇒ coord ⇒ bool" where
"is_legal_for_piece None _ _ _ _ = False" |
"is_legal_for_piece (Some (owner, piece)) to_move on_board src dst =
(owner = to_move ∧ absolute_movement_vector owner piece (diff dst src))"
fun get_owner :: "(player * piece_type) option ⇒ player option" where
"get_owner None = None" | "get_owner (Some (owner, piece)) = Some owner"
fun is_legal_physically :: "board ⇒ move ⇒ bool" where
"is_legal_physically (Board on_board in_hand to_move) (Move src dst) =
(
(is_on_board dst) ∧
(get_owner (on_board dst)) ≠ Some to_move ∧
(is_legal_for_piece (on_board src) to_move on_board src dst)
)" |
"is_legal_physically (Board on_board in_hand to_move) (Drop piece pos) =
((is_on_board pos) ∧ ((on_board pos) = None))"
fun is_in_check :: "on_board ⇒ player ⇒ bool" where
"is_in_check on_board player =
(∃dst src.
(on_board dst) = Some (player, King) ∧
(is_legal_for_piece (on_board src) (opponent player) on_board src dst)
)"
fun is_legal_board :: "board ⇒ bool" where
"is_legal_board (Board on_board in_hand to_move) =
(¬(is_in_check on_board (opponent to_move)))"
fun is_legal :: "board ⇒ move ⇒ bool" where
"is_legal board move =
(
(is_legal_physically board move) ∧
(is_legal_board (make_move board move))
)"
fun is_mate :: "board ⇒ bool" where "is_mate board = (¬(∃move. (is_legal board move)))"
fun is_checkmate :: "board ⇒ bool" where
"is_checkmate (Board on_board in_hand to_move) = (
(is_mate (Board on_board in_hand to_move)) ∧ (is_in_check on_board to_move))"
abbreviation board1 :: on_board where "board1 ≡ (map_of [
((Coord 5 1), (Gote, King)),
((Coord 5 2), (Sente, Gold)),
((Coord 5 3), (Sente, King))
])"
lemma "∃dst. dst ≠ Coord 5 3 ∧
(dst ≠ Coord 5 3 ⟶
dst ≠ Coord 5 2 ∧
(dst ≠ Coord 5 2 ⟶
(dst = Coord 5 1 ⟶
(∃src. src ≠ Coord 5 3 ∧ (src ≠ Coord 5 3 ⟶ src = Coord 5 2))) ∧
dst = Coord 5 1))"
by (smt coord.inject)
lemma "is_in_check board1 Gote"
apply auto
apply (smt coord.inject)
done
lemma "is_checkmate (Board board1 {#} Gote)"
apply auto
apply sledgehammer (* Both "cvc4" and "vampire" timed out *)
apply (smt coord.inject) (* Solver z3: Solver "z3" failed -- enable tracing using the "smt_trace" option for details *)
apply presburger (* Failed to apply proof method *)
end