CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

| Download

Try doing some basic maths questions in the Lean Theorem Prover. Functions, real numbers, equivalence relations and groups. Click on README.md and then on "Open in CoCalc with one click".

Project: Xena
Views: 18536
License: APACHE
/-
Copyright (c) 2017 Jeremy Avigad. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Jeremy Avigad, Jesse Michael Han
-/

import logic.basic tactic.core

/-!
# The `finish` family of tactics

These tactics do straightforward things: they call the simplifier, split conjunctive assumptions,
eliminate existential quantifiers on the left, and look for contradictions. They rely on ematching
and congruence closure to try to finish off a goal at the end.

The procedures *do* split on disjunctions and recreate the smt state for each terminal call, so
they are only meant to be used on small, straightforward problems.

## Main definitions

We provide the following tactics:

  `finish`  -- solves the goal or fails
  `clarify` -- makes as much progress as possible while not leaving more than one goal
  `safe`    -- splits freely, finishes off whatever subgoals it can, and leaves the rest

All accept an optional list of simplifier rules, typically definitions that should be expanded.
(The equations and identities should not refer to the local context.)

## Implementation notes

The variants `ifinish`, `iclarify`, and `isafe` try to restrict to intuitionistic logic. But the
`done` tactic leaks classical logic:

```
example {P : Prop} : ¬¬P → P :=
by using_smt (do smt_tactic.intros, smt_tactic.close)
```

They also do not work well with the current heuristic instantiation method used by `ematch`.
So they are left here mainly for reference.
-/

declare_trace auto.done
declare_trace auto.finish

namespace tactic

/- call (assert n t) with a fresh name n. -/
meta def assert_fresh (t : expr) : tactic expr :=
do n ← get_unused_name `h none,
   assert n t

/- call (assertv n t v) with a fresh name n. -/
meta def assertv_fresh (t : expr) (v : expr) : tactic expr :=
do h ← get_unused_name `h none,
   assertv h t v

namespace interactive

meta def revert_all := tactic.revert_all

end interactive

end tactic

open tactic expr

namespace auto

/- Utilities -/

meta def whnf_reducible (e : expr) : tactic expr := whnf e reducible

-- stolen from interactive.lean
meta def add_simps : simp_lemmas → list name → tactic simp_lemmas
| s []      := return s
| s (n::ns) := do s' ← s.add_simp n, add_simps s' ns

/-
  Configuration information for the auto tactics.
-/
@[derive decidable_eq, derive inhabited]
structure auto_config : Type :=
(use_simp := tt)           -- call the simplifier
(classical := tt)          -- use classical logic
(max_ematch_rounds := 20)  -- for the "done" tactic

/-
  Preprocess goal.

  We want to move everything to the left of the sequent arrow. For intuitionistic logic,
  we replace the goal p with ∀ f, (p → f) → f and introduce.
-/

theorem by_contradiction_trick (p : Prop) (h : ∀ f : Prop, (p → f) → f) : p :=
h p id

meta def preprocess_goal (cfg : auto_config) : tactic unit :=
do repeat (intro1 >> skip),
   tgt ← target >>= whnf_reducible,
   if (¬ (is_false tgt)) then
     if cfg.classical then
       (mk_mapp ``classical.by_contradiction [some tgt]) >>= apply >> intro1 >> skip
     else
       (mk_mapp ``decidable.by_contradiction [some tgt, none] >>= apply >> intro1 >> skip) <|>
       applyc ``by_contradiction_trick >> intro1 >> intro1 >> skip
   else
     skip

/-
  Normalize hypotheses. Bring conjunctions to the outside (for splitting),
  bring universal quantifiers to the outside (for ematching). The classical normalizer
  eliminates a → b in favor of ¬ a ∨ b.

  For efficiency, we push negations inwards from the top down. (For example, consider
  simplifying ¬ ¬ (p ∨ q).)
-/

section

universe u
variable  {α : Type u}
variables (p q : Prop)
variable  (s : α → Prop)

local attribute [instance, priority 10] classical.prop_decidable
theorem not_not_eq : (¬ ¬ p) = p := propext not_not
theorem not_and_eq : (¬ (p ∧ q)) = (¬ p ∨ ¬ q) := propext not_and_distrib
theorem not_or_eq : (¬ (p ∨ q)) = (¬ p ∧ ¬ q) := propext not_or_distrib
theorem not_forall_eq : (¬ ∀ x, s x) = (∃ x, ¬ s x) := propext not_forall
theorem not_exists_eq : (¬ ∃ x, s x) = (∀ x, ¬ s x) := propext not_exists
theorem not_implies_eq : (¬ (p → q)) = (p ∧ ¬ q) := propext not_imp

theorem classical.implies_iff_not_or : (p → q) ↔ (¬ p ∨ q) := imp_iff_not_or

end

def common_normalize_lemma_names : list name :=
[``bex_def, ``forall_and_distrib, ``exists_imp_distrib, ``or.assoc, ``or.comm, ``or.left_comm,
  ``and.assoc, ``and.comm, ``and.left_comm]

def classical_normalize_lemma_names : list name :=
common_normalize_lemma_names ++ [``classical.implies_iff_not_or]

-- optionally returns an equivalent expression and proof of equivalence
private meta def transform_negation_step (cfg : auto_config) (e : expr) :
  tactic (option (expr × expr)) :=
do e ← whnf_reducible e,
   match e with
   | `(¬ %%ne) :=
      (do ne ← whnf_reducible ne,
      match ne with
      | `(¬ %%a)      := if ¬ cfg.classical then return none
                         else do pr ← mk_app ``not_not_eq [a],
                            return (some (a, pr))
      | `(%%a ∧ %%b)  := do pr ← mk_app ``not_and_eq [a, b],
                            return (some (`(¬ %%a ∨ ¬ %%b), pr))
      | `(%%a ∨ %%b)  := do pr ← mk_app ``not_or_eq [a, b],
                            return (some (`(¬ %%a ∧ ¬ %%b), pr))
      | `(Exists %%p) := do pr ← mk_app ``not_exists_eq [p],
                            `(%%_ = %%e') ← infer_type pr,
                            return (some (e', pr))
      | (pi n bi d p) := if ¬ cfg.classical then return none
                         else if p.has_var then do
                            pr ← mk_app ``not_forall_eq [lam n bi d (expr.abstract_local p n)],
                            `(%%_ = %%e') ← infer_type pr,
                            return (some (e', pr))
                         else do
                            pr ← mk_app ``not_implies_eq [d, p],
                            `(%%_ = %%e') ← infer_type pr,
                            return (some (e', pr))
      | _             := return none
      end)
    | _        := return none
  end

-- given an expr 'e', returns a new expression and a proof of equality
private meta def transform_negation (cfg : auto_config) : expr → tactic (option (expr × expr)) :=
λ e, do
  opr ← transform_negation_step cfg e,
  match opr with
  | (some (e', pr)) := do
    opr' ← transform_negation e',
    match opr' with
    | none              := return (some (e', pr))
    | (some (e'', pr')) := do pr'' ← mk_eq_trans pr pr',
                              return (some (e'', pr''))
    end
  | none            := return none
  end

meta def normalize_negations (cfg : auto_config) (h : expr) : tactic unit :=
do t ← infer_type h,
   (_, e, pr) ← simplify_top_down ()
                   (λ _, λ e, do
                       oepr ← transform_negation cfg e,
                       match oepr with
                       | (some (e', pr)) := return ((), e', pr)
                       | none            := do pr ← mk_eq_refl e, return ((), e, pr)
                       end)
                   t,
   replace_hyp h e pr,
   skip

meta def normalize_hyp (cfg : auto_config) (simps : simp_lemmas) (h : expr) : tactic unit :=
(do h ← simp_hyp simps [] h, try (normalize_negations cfg h)) <|>
try (normalize_negations cfg h)

meta def normalize_hyps (cfg : auto_config) : tactic unit :=
do simps ← if cfg.classical then
             add_simps simp_lemmas.mk classical_normalize_lemma_names
           else
             add_simps simp_lemmas.mk common_normalize_lemma_names,
   local_context >>= monad.mapm' (normalize_hyp cfg simps)

/-
  Eliminate existential quantifiers.
-/

-- eliminate an existential quantifier if there is one
meta def eelim : tactic unit :=
do ctx ← local_context,
   first $ ctx.map $ λ h,
     do t ← infer_type h >>= whnf_reducible,
        guard (is_app_of t ``Exists),
        tgt ← target,
        to_expr ``(@exists.elim _ _ %%tgt %%h) >>= apply,
        intros,
        clear h

-- eliminate all existential quantifiers, fails if there aren't any
meta def eelims : tactic unit := eelim >> repeat eelim

/-
  Substitute if there is a hypothesis x = t or t = x.
-/

-- carries out a subst if there is one, fails otherwise
meta def do_subst : tactic unit :=
do ctx ← local_context,
   first $ ctx.map $ λ h,
     do t ← infer_type h >>= whnf_reducible,
        match t with
        | `(%%a = %%b) := subst h
        | _            := failed
        end

meta def do_substs : tactic unit := do_subst >> repeat do_subst

/-
  Split all conjunctions.
-/

-- Assumes pr is a proof of t. Adds the consequences of t to the context
-- and returns tt if anything nontrivial has been added.
meta def add_conjuncts : expr → expr → tactic bool :=
λ pr t,
let assert_consequences := λ e t, mcond (add_conjuncts e t) skip (assertv_fresh t e >> skip) in
do t' ← whnf_reducible t,
   match t' with
   | `(%%a ∧ %%b) :=
     do e₁ ← mk_app ``and.left [pr],
        assert_consequences e₁ a,
        e₂ ← mk_app ``and.right [pr],
        assert_consequences e₂ b,
        return tt
  | `(true) :=
     do return tt
  | _ := return ff
end

-- return tt if any progress is made
meta def split_hyp (h : expr) : tactic bool :=
do t ← infer_type h,
   mcond (add_conjuncts h t) (clear h >> return tt) (return ff)

-- return tt if any progress is made
meta def split_hyps_aux : list expr → tactic bool
| []        := return ff
| (h :: hs) := do b₁ ← split_hyp h,
                  b₂ ← split_hyps_aux hs,
                  return (b₁ || b₂)

-- fail if no progress is made
meta def split_hyps : tactic unit := local_context >>= split_hyps_aux >>= guardb

/-
  Eagerly apply all the preprocessing rules.
-/

meta def preprocess_hyps (cfg : auto_config) : tactic unit :=
do repeat (intro1 >> skip),
   preprocess_goal cfg,
   normalize_hyps cfg,
   repeat (do_substs <|> split_hyps <|> eelim /-<|> self_simplify_hyps-/)

/-
  The terminal tactic, used to try to finish off goals:
  - Call the contradiction tactic.
  - Open an SMT state, and use ematching and congruence closure, with all the universal
    statements in the context.

  TODO(Jeremy): allow users to specify attribute for ematching lemmas?
-/

meta def mk_hinst_lemmas : list expr → smt_tactic hinst_lemmas
| []        := -- return hinst_lemmas.mk
               do get_hinst_lemmas_for_attr `ematch
| (h :: hs) := do his ← mk_hinst_lemmas hs,
                  t ← infer_type h,
                  match t with
                  | (pi _ _ _ _) :=
                    do t' ← infer_type t,
                       if t' = `(Prop) then
                          (do new_lemma ← hinst_lemma.mk h,
                             return (hinst_lemmas.add his new_lemma)) <|> return his
                       else return his
                  | _ := return his
                  end

private meta def report_invalid_em_lemma {α : Type} (n : name) : smt_tactic α :=
fail format!"invalid ematch lemma '{n}'"

private meta def add_hinst_lemma_from_name (md : transparency) (lhs_lemma : bool) (n : name)
  (hs : hinst_lemmas) (ref : pexpr) : smt_tactic hinst_lemmas :=
do p ← resolve_name n,
   match p with
   | expr.const n _ := (do h ← hinst_lemma.mk_from_decl_core md n lhs_lemma,
                           tactic.save_const_type_info n ref, return $ hs.add h) <|>
                       (do hs₁ ← smt_tactic.mk_ematch_eqn_lemmas_for_core md n,
                           tactic.save_const_type_info n ref, return $ hs.merge hs₁) <|>
                        report_invalid_em_lemma n
   | _              := (do e ← to_expr p, h ← hinst_lemma.mk_core md e lhs_lemma,
                        try (tactic.save_type_info e ref), return $ hs.add h) <|>
                        report_invalid_em_lemma n
   end

private meta def add_hinst_lemma_from_pexpr (md : transparency) (lhs_lemma : bool) (hs : hinst_lemmas)
  : pexpr → smt_tactic hinst_lemmas
| p@(expr.const c [])          := add_hinst_lemma_from_name md lhs_lemma c hs p
| p@(expr.local_const c _ _ _) := add_hinst_lemma_from_name md lhs_lemma c hs p
| p                          := do new_e ← to_expr p, h ← hinst_lemma.mk_core md new_e lhs_lemma,
                                   return $ hs.add h

private meta def add_hinst_lemmas_from_pexprs (md : transparency) (lhs_lemma : bool)
  (ps : list pexpr) (hs : hinst_lemmas) : smt_tactic hinst_lemmas :=
list.mfoldl (add_hinst_lemma_from_pexpr md lhs_lemma) hs ps

/--
`done` first attempts to close the goal using `contradiction`. If this fails, it creates an
SMT state and will repeatedly use `ematch` (using `ematch` lemmas in the environment,
universally quantified assumptions, and the supplied lemmas `ps`) and congruence closure.
-/
meta def done (ps : list pexpr) (cfg : auto_config := {}) : tactic unit :=
do when_tracing `auto.done (trace "entering done" >> trace_state),
   contradiction <|>
   (solve1 $
     (do revert_all,
         using_smt
         (do smt_tactic.intros,
             ctx ← local_context,
             hs ← mk_hinst_lemmas ctx,
             hs' ← add_hinst_lemmas_from_pexprs reducible ff ps hs,
             smt_tactic.iterate_at_most cfg.max_ematch_rounds
               (smt_tactic.ematch_using hs' >> smt_tactic.try smt_tactic.close))))
/-
  Tactics that perform case splits.
-/
@[derive decidable_eq, derive inhabited]
inductive case_option
| force        -- fail unless all goals are solved
| at_most_one  -- leave at most one goal
| accept       -- leave as many goals as necessary

private meta def case_cont (s : case_option) (cont : case_option → tactic unit) : tactic unit :=
do match s with
   | case_option.force := cont case_option.force >> cont case_option.force
   | case_option.at_most_one :=
       -- if the first one succeeds, commit to it, and try the second
       (mcond (cont case_option.force >> return tt) (cont case_option.at_most_one) skip) <|>
       -- otherwise, try the second
       (swap >> cont case_option.force >> cont case_option.at_most_one)
   | case_option.accept := focus [cont case_option.accept, cont case_option.accept]
   end

-- three possible outcomes:
--   finds something to case, the continuations succeed ==> returns tt
--   finds something to case, the continutations fail ==> fails
--   doesn't find anything to case ==> returns ff
meta def case_hyp (h : expr) (s : case_option) (cont : case_option → tactic unit) : tactic bool :=
do t ← infer_type h,
   match t with
   | `(%%a ∨ %%b) := cases h >> case_cont s cont >> return tt
   | _            := return ff
   end

meta def case_some_hyp_aux (s : case_option) (cont : case_option → tactic unit) :
  list expr → tactic bool
| []      := return ff
| (h::hs) := mcond (case_hyp h s cont) (return tt) (case_some_hyp_aux hs)

meta def case_some_hyp (s : case_option) (cont : case_option → tactic unit) : tactic bool :=
local_context >>= case_some_hyp_aux s cont

/-
  The main tactics.
-/

/--
  `safe_core s ps cfg opt` negates the goal, normalizes hypotheses
  (by splitting conjunctions, eliminating existentials, pushing negations inwards,
  and calling `simp` with the supplied lemmas `s`), and then tries `contradiction`.

  If this fails, it will create an SMT state and repeatedly use `ematch`
  (using `ematch` lemmas in the environment, universally quantified assumptions,
  and the supplied lemmas `ps`) and congruence closure.

  `safe_core` is complete for propositional logic. Depending on the form of `opt`
  it will:

  - (if `opt` is `case_option.force`) fail if it does not close the goal,
  - (if `opt` is `case_option.at_most_one`) fail if it produces more than one goal, and
  - (if `opt` is `case_option.accept`) ignore the number of goals it produces.
-/
meta def safe_core (s : simp_lemmas × list name) (ps : list pexpr) (cfg : auto_config) : case_option → tactic unit :=
λ co, focus1 $
do when_tracing `auto.finish (trace "entering safe_core" >> trace_state),
   if cfg.use_simp then do
     when_tracing `auto.finish (trace "simplifying hypotheses"),
     simp_all s.1 s.2 { fail_if_unchanged := ff },
     when_tracing `auto.finish (trace "result:" >> trace_state)
   else skip,
   tactic.done <|>
   do when_tracing `auto.finish (trace "preprocessing hypotheses"),
      preprocess_hyps cfg,
      when_tracing `auto.finish (trace "result:" >> trace_state),
      done ps cfg <|>
        (mcond (case_some_hyp co safe_core)
          skip
          (match co with
            | case_option.force       := done ps cfg
            | case_option.at_most_one := try (done ps cfg)
            | case_option.accept      := try (done ps cfg)
            end))

/--
  `clarify` is `safe_core`, but with the `(opt : case_option)`
  parameter fixed at `case_option.at_most_one`.
-/
meta def clarify (s : simp_lemmas × list name) (ps : list pexpr)
  (cfg : auto_config := {}) : tactic unit := safe_core s ps cfg case_option.at_most_one

/--
  `safe` is `safe_core`, but with the `(opt : case_option)`
  parameter fixed at `case_option.accept`.
-/
meta def safe (s : simp_lemmas × list name) (ps : list pexpr)
  (cfg : auto_config := {}) : tactic unit := safe_core s ps cfg case_option.accept

/--
  `finish` is `safe_core`, but with the `(opt : case_option)`
  parameter fixed at `case_option.force`.
-/
meta def finish (s : simp_lemmas × list name) (ps : list pexpr)
  (cfg : auto_config := {}) : tactic unit := safe_core s ps cfg case_option.force

/--
  `iclarify` is like `clarify`, but in some places restricts to intuitionistic logic.
  Classical logic still leaks, so this tactic is deprecated.
-/
meta def iclarify (s : simp_lemmas × list name) (ps : list pexpr)
  (cfg : auto_config := {}) : tactic unit := clarify s ps {classical := ff, ..cfg}

/--
  `isafe` is like `safe`, but in some places restricts to intuitionistic logic.
  Classical logic still leaks, so this tactic is deprecated.
-/
meta def isafe (s : simp_lemmas × list name) (ps : list pexpr)
  (cfg : auto_config := {}) : tactic unit := safe s ps {classical := ff, ..cfg}

/--
  `ifinish` is like `finish`, but in some places restricts to intuitionistic logic.
  Classical logic still leaks, so this tactic is deprecated.
-/
meta def ifinish (s : simp_lemmas × list name) (ps : list pexpr) (cfg : auto_config := {}) : tactic unit :=
  finish s ps {classical := ff, ..cfg}

end auto

/- interactive versions -/

open auto

namespace tactic
namespace interactive

open lean lean.parser interactive interactive.types

local postfix `?`:9001 := optional
local postfix *:9001 := many

/--
  `clarify [h1,...,hn] using [e1,...,en]` negates the goal, normalizes hypotheses
  (by splitting conjunctions, eliminating existentials, pushing negations inwards,
  and calling `simp` with the supplied lemmas `h1,...,hn`), and then tries `contradiction`.

  If this fails, it will create an SMT state and repeatedly use `ematch`
  (using `ematch` lemmas in the environment, universally quantified assumptions,
  and the supplied lemmas `e1,...,en`) and congruence closure.

  `clarify` is complete for propositional logic.

  Either of the supplied simp lemmas or the supplied ematch lemmas are optional.

  `clarify` will fail if it produces more than one goal.
-/
meta def clarify (hs : parse simp_arg_list) (ps : parse (tk "using" *> pexpr_list_or_texpr)?)
  (cfg : auto_config := {}) : tactic unit :=
do s ← mk_simp_set ff [] hs,
   auto.clarify s (ps.get_or_else []) cfg

/--
  `safe [h1,...,hn] using [e1,...,en]` negates the goal, normalizes hypotheses
  (by splitting conjunctions, eliminating existentials, pushing negations inwards,
  and calling `simp` with the supplied lemmas `h1,...,hn`), and then tries `contradiction`.

  If this fails, it will create an SMT state and repeatedly use `ematch`
  (using `ematch` lemmas in the environment, universally quantified assumptions,
  and the supplied lemmas `e1,...,en`) and congruence closure.

  `safe` is complete for propositional logic.

  Either of the supplied simp lemmas or the supplied ematch lemmas are optional.

  `safe` ignores the number of goals it produces, and should never fail.
-/
meta def safe (hs : parse simp_arg_list) (ps : parse (tk "using" *> pexpr_list_or_texpr)?)
  (cfg : auto_config := {}) : tactic unit :=
do s ← mk_simp_set ff [] hs,
   auto.safe s (ps.get_or_else []) cfg

/--
  `finish [h1,...,hn] using [e1,...,en]` negates the goal, normalizes hypotheses
  (by splitting conjunctions, eliminating existentials, pushing negations inwards,
  and calling `simp` with the supplied lemmas `h1,...,hn`), and then tries `contradiction`.

  If this fails, it will create an SMT state and repeatedly use `ematch`
  (using `ematch` lemmas in the environment, universally quantified assumptions,
  and the supplied lemmas `e1,...,en`) and congruence closure.

  `finish` is complete for propositional logic.

  Either of the supplied simp lemmas or the supplied ematch lemmas are optional.

  `finish` will fail if it does not close the goal.
-/
meta def finish (hs : parse simp_arg_list) (ps : parse (tk "using" *> pexpr_list_or_texpr)?)
  (cfg : auto_config := {}) : tactic unit :=
do s ← mk_simp_set ff [] hs,
   auto.finish s (ps.get_or_else []) cfg

/--
  `iclarify` is like `clarify`, but only uses intuitionistic logic.
-/
meta def iclarify (hs : parse simp_arg_list) (ps : parse (tk "using" *> pexpr_list_or_texpr)?)
  (cfg : auto_config := {}) : tactic unit :=
do s ← mk_simp_set ff [] hs,
   auto.iclarify s (ps.get_or_else []) cfg

/--
  `isafe` is like `safe`, but only uses intuitionistic logic.
-/
meta def isafe (hs : parse simp_arg_list) (ps : parse (tk "using" *> pexpr_list_or_texpr)?)
  (cfg : auto_config := {}) : tactic unit :=
do s ← mk_simp_set ff [] hs,
   auto.isafe s (ps.get_or_else []) cfg

/--
  `ifinish` is like `finish`, but only uses intuitionistic logic.
-/
meta def ifinish (hs : parse simp_arg_list) (ps : parse (tk "using" *> pexpr_list_or_texpr)?)
  (cfg : auto_config := {}) : tactic unit :=
do s ← mk_simp_set ff [] hs,
   auto.ifinish s (ps.get_or_else []) cfg

end interactive
end tactic