src/tools/shrink_proof.lean (86 lines of code) (raw):
/-
Copyright (c) 2022 OpenAI. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author(s): Daniel Selsam
Simple, barebones heuristic proof-minimization.
-/
import util.io
import data.list.basic
import util.tactic
/--
Consider the following proof:
```
have h1 : ... := ...,
have h2 : ... h1 ... := ...,
have h3 : ... h1 ... := ...,
have h4 : ... h2 ... := ...,
exact h3
```
Here `h2` and `h4` can be safely removed.
We detect removable `have`s by proceeding in reverse order.
For a given `have` statement, we can tell if it appears in the resulting proof
by checking the proof term in the metavariable context of the final tactic state.
-/
meta def strip_unused_haves (ts_final : tactic_state) (steps : list (tactic_state × string × tactic_state)) : io (list (tactic_state × string × tactic_state)) := do {
⟨bad_idx, resume_goals⟩ ← steps.enum.reverse.mfirst (λ ⟨idx, ⟨ts1, action, ts2⟩⟩, do {
if action.to_list.take 5 = "have ".to_list then do {
gs1 ← io.run_tac ts1 tactic.get_goals,
gs2 ← io.run_tac ts2 tactic.get_goals,
(goal_with_assumption, all_in_one) ← do {
-- `have` step with proof provided
if gs1.length = gs2.length ∧ gs1.drop 1 = gs2.drop 1 then pure (gs2.head, tt)
-- `have` step without proof provided
else if gs1.length + 1 = gs2.length ∧ gs1.drop 1 = gs2.drop 2 then pure (gs2.nth_le 1 sorry, ff)
-- something weird, e.g. `have h : <prop>; swap`
else io.fail "weird `have`"
},
assumption ← io.run_tac ts2 $ do {
tactic.set_goals [goal_with_assumption],
hs ← tactic.local_context,
if hs_nnil : hs ≠ [] then pure (hs.last hs_nnil) else tactic.fail "weird `have`"
},
proof_of_goal_with_assumption ← io.run_tac ts_final (tactic.instantiate_mvars goal_with_assumption),
if not (expr.occurs assumption proof_of_goal_with_assumption) then pure (idx, if all_in_one then gs2 else gs2.drop 1) else io.fail "`have` step is used"
} else
io.fail "not a `have` step"
}
),
goals_per_step : list (list expr) ← steps.mmap (λ ⟨ts1, action, ts2⟩, io.run_tac ts1 tactic.get_goals),
let resume_idx : ℕ := goals_per_step.enum.find_index (λ idx__goals, idx__goals.fst > bad_idx ∧ idx__goals.snd = resume_goals),
if resume_idx = steps.length then io.fail "weird `have`, possible swap" else pure (steps.take bad_idx ++ steps.drop resume_idx)
}
meta def rerun_proof (ts_orig : tactic_state) : tactic_state → list string → io (list (tactic_state × string × tactic_state))
| ts1 [] := io.run_tac ts_orig (do {
-- confirm that the proof has finished
[g] ← tactic.get_goals,
tgt ← tactic.infer_type g,
tactic.write ts1,
pf ← tactic.get_assignment g >>= tactic.instantiate_mvars,
validate_proof tgt pf <|> (tactic.trace "failed to validate" >> tactic.fail "failed to validate"),
pure []
})
| ts1 (action::actions) := do
result_with_string ← io.run_tac ts1 (get_tac_and_capture_result action 5000),
match result_with_string with
-- The tactic application was successful.
| interaction_monad.result.success _ ts2 := do {
rest ← rerun_proof ts2 actions,
pure $ (ts1, action, ts2) :: rest
}
| _ := io.fail "failed to rerun proof"
end
meta def shrink_proof : list (tactic_state × string × tactic_state) → io (list (tactic_state × string × tactic_state))
| [] := io.fail "shrink_proof called on empty proof"
| steps@(step::rest) := do
let ts_final := (steps.last sorry).snd.snd,
io.run_tac ts_final tactic.done,
⟨fewer_steps, success⟩ ← io.catch (do steps ← strip_unused_haves ts_final steps, pure (steps, tt)) (λ e, pure (steps, ff)),
let ts_start := step.fst,
if success then do {
io.catch (do {
steps ← rerun_proof ts_start ts_start $ fewer_steps.map (λ ⟨_, action, _⟩, action),
shrink_proof steps
}) (λ e, pure steps)
}
else
pure steps