|
| 1 | +/- |
| 2 | +Copyright (c) 2020 Robert Y. Lewis. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Robert Y. Lewis |
| 5 | +-/ |
| 6 | + |
| 7 | +import Mathlib.Tactic.NormNum |
| 8 | +import Mathlib.Util.SynthesizeUsing |
| 9 | +import Mathlib.Data.Tree |
| 10 | +import Mathlib.Util.Qq |
| 11 | + |
| 12 | +/-! |
| 13 | +# A tactic for canceling numeric denominators |
| 14 | +
|
| 15 | +This file defines tactics that cancel numeric denominators from field Expressions. |
| 16 | +
|
| 17 | +As an example, we want to transform a comparison `5*(a/3 + b/4) < c/3` into the equivalent |
| 18 | +`5*(4*a + 3*b) < 4*c`. |
| 19 | +
|
| 20 | +## Implementation notes |
| 21 | +
|
| 22 | +The tooling here was originally written for `linarith`, not intended as an interactive tactic. |
| 23 | +The interactive version has been split off because it is sometimes convenient to use on its own. |
| 24 | +There are likely some rough edges to it. |
| 25 | +
|
| 26 | +Improving this tactic would be a good project for someone interested in learning tactic programming. |
| 27 | +-/ |
| 28 | + |
| 29 | +open Lean Parser Tactic Mathlib Meta NormNum Qq |
| 30 | + |
| 31 | +initialize registerTraceClass `CancelDenoms |
| 32 | + |
| 33 | +namespace CancelDenoms |
| 34 | + |
| 35 | +/-! ### Lemmas used in the procedure -/ |
| 36 | + |
| 37 | +theorem mul_subst {α} [CommRing α] {n1 n2 k e1 e2 t1 t2 : α} |
| 38 | + (h1 : n1 * e1 = t1) (h2 : n2 * e2 = t2) (h3 : n1 * n2 = k) : k * (e1 * e2) = t1 * t2 := by |
| 39 | + rw [← h3, mul_comm n1, mul_assoc n2, ← mul_assoc n1, h1, |
| 40 | + ← mul_assoc n2, mul_comm n2, mul_assoc, h2] |
| 41 | +#align cancel_factors.mul_subst CancelDenoms.mul_subst |
| 42 | + |
| 43 | +theorem div_subst {α} [Field α] {n1 n2 k e1 e2 t1 : α} |
| 44 | + (h1 : n1 * e1 = t1) (h2 : n2 / e2 = 1) (h3 : n1 * n2 = k) : k * (e1 / e2) = t1 := by |
| 45 | + rw [← h3, mul_assoc, mul_div_left_comm, h2, ← mul_assoc, h1, mul_comm, one_mul] |
| 46 | +#align cancel_factors.div_subst CancelDenoms.div_subst |
| 47 | + |
| 48 | +theorem cancel_factors_eq_div {α} [Field α] {n e e' : α} |
| 49 | + (h : n * e = e') (h2 : n ≠ 0) : e = e' / n := |
| 50 | + eq_div_of_mul_eq h2 <| by rwa [mul_comm] at h |
| 51 | +#align cancel_factors.cancel_factors_eq_div CancelDenoms.cancel_factors_eq_div |
| 52 | + |
| 53 | +theorem add_subst {α} [Ring α] {n e1 e2 t1 t2 : α} (h1 : n * e1 = t1) (h2 : n * e2 = t2) : |
| 54 | + n * (e1 + e2) = t1 + t2 := by simp [left_distrib, *] |
| 55 | +#align cancel_factors.add_subst CancelDenoms.add_subst |
| 56 | + |
| 57 | +theorem sub_subst {α} [Ring α] {n e1 e2 t1 t2 : α} (h1 : n * e1 = t1) (h2 : n * e2 = t2) : |
| 58 | + n * (e1 - e2) = t1 - t2 := by simp [left_distrib, *, sub_eq_add_neg] |
| 59 | +#align cancel_factors.sub_subst CancelDenoms.sub_subst |
| 60 | + |
| 61 | +theorem neg_subst {α} [Ring α] {n e t : α} (h1 : n * e = t) : n * -e = -t := by simp [*] |
| 62 | +#align cancel_factors.neg_subst CancelDenoms.neg_subst |
| 63 | + |
| 64 | +theorem cancel_factors_lt {α} [LinearOrderedField α] {a b ad bd a' b' gcd : α} |
| 65 | + (ha : ad * a = a') (hb : bd * b = b') (had : 0 < ad) (hbd : 0 < bd) (hgcd : 0 < gcd) : |
| 66 | + (a < b) = (1 / gcd * (bd * a') < 1 / gcd * (ad * b')) := by |
| 67 | + rw [mul_lt_mul_left, ← ha, ← hb, ← mul_assoc, ← mul_assoc, mul_comm bd, mul_lt_mul_left] |
| 68 | + · exact mul_pos had hbd |
| 69 | + · exact one_div_pos.2 hgcd |
| 70 | +#align cancel_factors.cancel_factors_lt CancelDenoms.cancel_factors_lt |
| 71 | + |
| 72 | +theorem cancel_factors_le {α} [LinearOrderedField α] {a b ad bd a' b' gcd : α} |
| 73 | + (ha : ad * a = a') (hb : bd * b = b') (had : 0 < ad) (hbd : 0 < bd) (hgcd : 0 < gcd) : |
| 74 | + (a ≤ b) = (1 / gcd * (bd * a') ≤ 1 / gcd * (ad * b')) := by |
| 75 | + rw [mul_le_mul_left, ← ha, ← hb, ← mul_assoc, ← mul_assoc, mul_comm bd, mul_le_mul_left] |
| 76 | + · exact mul_pos had hbd |
| 77 | + · exact one_div_pos.2 hgcd |
| 78 | +#align cancel_factors.cancel_factors_le CancelDenoms.cancel_factors_le |
| 79 | + |
| 80 | +theorem cancel_factors_eq {α} [LinearOrderedField α] {a b ad bd a' b' gcd : α} (ha : ad * a = a') |
| 81 | + (hb : bd * b = b') (had : 0 < ad) (hbd : 0 < bd) (hgcd : 0 < gcd) : |
| 82 | + (a = b) = (1 / gcd * (bd * a') = 1 / gcd * (ad * b')) := by |
| 83 | + rw [← ha, ← hb, ← mul_assoc bd, ← mul_assoc ad, mul_comm bd] |
| 84 | + ext; constructor |
| 85 | + · rintro rfl |
| 86 | + rfl |
| 87 | + · intro h |
| 88 | + simp only [← mul_assoc] at h |
| 89 | + refine' mul_left_cancel₀ (mul_ne_zero _ _) h |
| 90 | + apply mul_ne_zero |
| 91 | + apply div_ne_zero |
| 92 | + all_goals apply ne_of_gt ; first | assumption | exact zero_lt_one |
| 93 | +#align cancel_factors.cancel_factors_eq CancelDenoms.cancel_factors_eq |
| 94 | + |
| 95 | +/-! ### Computing cancelation factors -/ |
| 96 | + |
| 97 | +/-- |
| 98 | +`findCancelFactor e` produces a natural number `n`, such that multiplying `e` by `n` will |
| 99 | +be able to cancel all the numeric denominators in `e`. The returned `Tree` describes how to |
| 100 | +distribute the value `n` over products inside `e`. |
| 101 | +-/ |
| 102 | +partial def findCancelFactor (e : Expr) : ℕ × Tree ℕ := |
| 103 | + match e.getAppFnArgs with |
| 104 | + | (``HAdd.hAdd, #[_, _, _, _, e1, e2]) | (``HSub.hSub, #[_, _, _, _, e1, e2]) => |
| 105 | + let (v1, t1) := findCancelFactor e1 |
| 106 | + let (v2, t2) := findCancelFactor e2 |
| 107 | + let lcm := v1.lcm v2 |
| 108 | + (lcm, .node lcm t1 t2) |
| 109 | + | (``HMul.hMul, #[_, _, _, _, e1, e2]) => |
| 110 | + let (v1, t1) := findCancelFactor e1 |
| 111 | + let (v2, t2) := findCancelFactor e2 |
| 112 | + let pd := v1 * v2 |
| 113 | + (pd, .node pd t1 t2) |
| 114 | + | (``HDiv.hDiv, #[_, _, _, _, e1, e2]) => |
| 115 | + -- If e2 is a rational, then it's a natural number due to the simp lemmas in `deriveThms`. |
| 116 | + match isNatLit e2 with |
| 117 | + | some q => |
| 118 | + let (v1, t1) := findCancelFactor e1 |
| 119 | + let n := v1 * q |
| 120 | + (n, .node n t1 <| .node q .nil .nil) |
| 121 | + | none => (1, .node 1 .nil .nil) |
| 122 | + | (``Neg.neg, #[_, _, e]) => findCancelFactor e |
| 123 | + | _ => (1, .node 1 .nil .nil) |
| 124 | + |
| 125 | +def synthesizeUsingNormNum (type : Expr) : MetaM Expr := do |
| 126 | + try |
| 127 | + synthesizeUsingTactic' type (← `(tactic| norm_num)) |
| 128 | + catch e => |
| 129 | + throwError "Could not prove {type} using norm_num. {e.toMessageData}" |
| 130 | + |
| 131 | +/-- |
| 132 | +`mkProdPrf α sα v tr e` produces a proof of `v*e = e'`, where numeric denominators have been |
| 133 | +canceled in `e'`, distributing `v` proportionally according to the tree `tr` computed |
| 134 | +by `findCancelFactor`. |
| 135 | +-/ |
| 136 | +partial def mkProdPrf (α : Q(Type u)) (sα : Q(Field $α)) (v : ℕ) (t : Tree ℕ) |
| 137 | + (e : Q($α)) : MetaM Expr := do |
| 138 | + let amwo ← synthInstanceQ q(AddMonoidWithOne $α) |
| 139 | + match t, e with |
| 140 | + | .node _ lhs rhs, ~q($e1 + $e2) => do |
| 141 | + let v1 ← mkProdPrf α sα v lhs e1 |
| 142 | + let v2 ← mkProdPrf α sα v rhs e2 |
| 143 | + mkAppM ``CancelDenoms.add_subst #[v1, v2] |
| 144 | + | .node _ lhs rhs, ~q($e1 - $e2) => do |
| 145 | + let v1 ← mkProdPrf α sα v lhs e1 |
| 146 | + let v2 ← mkProdPrf α sα v rhs e2 |
| 147 | + mkAppM ``CancelDenoms.sub_subst #[v1, v2] |
| 148 | + | .node _ lhs@(.node ln _ _) rhs, ~q($e1 * $e2) => do |
| 149 | + let v1 ← mkProdPrf α sα ln lhs e1 |
| 150 | + let v2 ← mkProdPrf α sα (v / ln) rhs e2 |
| 151 | + have ln' := (← mkOfNat α amwo <| mkRawNatLit ln).1 |
| 152 | + have vln' := (← mkOfNat α amwo <| mkRawNatLit (v/ln)).1 |
| 153 | + have v' := (← mkOfNat α amwo <| mkRawNatLit v).1 |
| 154 | + let ntp : Q(Prop) := q($ln' * $vln' = $v') |
| 155 | + let npf ← synthesizeUsingNormNum ntp |
| 156 | + mkAppM ``CancelDenoms.mul_subst #[v1, v2, npf] |
| 157 | + | .node _ lhs (.node rn _ _), ~q($e1 / $e2) => do |
| 158 | + -- Invariant: e2 is equal to the natural number rn |
| 159 | + let v1 ← mkProdPrf α sα (v / rn) lhs e1 |
| 160 | + have rn' := (← mkOfNat α amwo <| mkRawNatLit rn).1 |
| 161 | + have vrn' := (← mkOfNat α amwo <| mkRawNatLit <| v / rn).1 |
| 162 | + have v' := (← mkOfNat α amwo <| mkRawNatLit <| v).1 |
| 163 | + let ntp : Q(Prop) := q($rn' / $e2 = 1) |
| 164 | + let npf ← synthesizeUsingNormNum ntp |
| 165 | + let ntp2 : Q(Prop) := q($vrn' * $rn' = $v') |
| 166 | + let npf2 ← synthesizeUsingNormNum ntp2 |
| 167 | + mkAppM ``CancelDenoms.div_subst #[v1, npf, npf2] |
| 168 | + | t, ~q(-$e) => do |
| 169 | + let v ← mkProdPrf α sα v t e |
| 170 | + mkAppM ``CancelDenoms.neg_subst #[v] |
| 171 | + | _, _ => do |
| 172 | + have v' := (← mkOfNat α amwo <| mkRawNatLit <| v).1 |
| 173 | + let e' ← mkAppM ``HMul.hMul #[v', e] |
| 174 | + mkEqRefl e' |
| 175 | + |
| 176 | +/-- Theorems to get expression into a form that `findCancelFactor` and `mkProdPrf` |
| 177 | +can more easily handle. These are important for dividing by rationals and negative integers. -/ |
| 178 | +def deriveThms : List Name := |
| 179 | + [``div_div_eq_mul_div, ``div_neg] |
| 180 | + |
| 181 | +/-- Helper lemma to chain together a `simp` proof and the result of `mkProdPrf`. -/ |
| 182 | +theorem derive_trans [Mul α] {a b c d : α} (h : a = b) (h' : c * b = d) : c * a = d := h ▸ h' |
| 183 | + |
| 184 | +/-- |
| 185 | +Given `e`, a term with rational division, produces a natural number `n` and a proof of `n*e = e'`, |
| 186 | +where `e'` has no division. Assumes "well-behaved" division. |
| 187 | +-/ |
| 188 | +def derive (e : Expr) : MetaM (ℕ × Expr) := do |
| 189 | + trace[CancelDenoms] "e = {e}" |
| 190 | + let eSimp ← simpOnlyNames (config := Simp.neutralConfig) deriveThms e |
| 191 | + trace[CancelDenoms] "e simplified = {eSimp.expr}" |
| 192 | + let (n, t) := findCancelFactor eSimp.expr |
| 193 | + let ⟨u, tp, e⟩ ← inferTypeQ' eSimp.expr |
| 194 | + let stp ← synthInstance q(Field $tp) |
| 195 | + try |
| 196 | + let pf ← mkProdPrf tp stp n t eSimp.expr |
| 197 | + trace[CancelDenoms] "pf : {← inferType pf}" |
| 198 | + let pf' ← |
| 199 | + if let some pfSimp := eSimp.proof? then |
| 200 | + mkAppM ``derive_trans #[pfSimp, pf] |
| 201 | + else |
| 202 | + pure pf |
| 203 | + return (n, pf') |
| 204 | + catch E => do |
| 205 | + throwError "CancelDenoms.derive failed to normalize {e}.\n{E.toMessageData}" |
| 206 | + |
| 207 | +/-- |
| 208 | +`findCompLemma e` arranges `e` in the form `lhs R rhs`, where `R ∈ {<, ≤, =}`, and returns |
| 209 | +`lhs`, `rhs`, and the `cancel_factors` lemma corresponding to `R`. |
| 210 | +-/ |
| 211 | +def findCompLemma (e : Expr) : Option (Expr × Expr × Name) := |
| 212 | + match e.getAppFnArgs with |
| 213 | + | (``LT.lt, #[_, _, a, b]) => (a, b, ``cancel_factors_lt) |
| 214 | + | (``LE.le, #[_, _, a, b]) => (a, b, ``cancel_factors_le) |
| 215 | + | (``Eq, #[_, a, b]) => (a, b, ``cancel_factors_eq) |
| 216 | + | (``GE.ge, #[_, _, a, b]) => (b, a, ``cancel_factors_le) |
| 217 | + | (``GT.gt, #[_, _, a, b]) => (b, a, ``cancel_factors_lt) |
| 218 | + | _ => none |
| 219 | + |
| 220 | +/-- |
| 221 | +`cancelDenominatorsInType h` assumes that `h` is of the form `lhs R rhs`, |
| 222 | +where `R ∈ {<, ≤, =, ≥, >}`. |
| 223 | +It produces an Expression `h'` of the form `lhs' R rhs'` and a proof that `h = h'`. |
| 224 | +Numeric denominators have been canceled in `lhs'` and `rhs'`. |
| 225 | +-/ |
| 226 | +def cancelDenominatorsInType (h : Expr) : MetaM (Expr × Expr) := do |
| 227 | + let some (lhs, rhs, lem) := findCompLemma h | throwError "cannot kill factors" |
| 228 | + let (al, lhs_p) ← derive lhs |
| 229 | + let ⟨u, α, _⟩ ← inferTypeQ' lhs |
| 230 | + let _ ← synthInstanceQ q(LinearOrderedField $α) |
| 231 | + let amwo ← synthInstanceQ q(AddMonoidWithOne $α) |
| 232 | + let (ar, rhs_p) ← derive rhs |
| 233 | + let gcd := al.gcd ar |
| 234 | + have al := (← mkOfNat α amwo <| mkRawNatLit al).1 |
| 235 | + have ar := (← mkOfNat α amwo <| mkRawNatLit ar).1 |
| 236 | + have gcd := (← mkOfNat α amwo <| mkRawNatLit gcd).1 |
| 237 | + let al_pos : Q(Prop) := q(0 < $al) |
| 238 | + let ar_pos : Q(Prop) := q(0 < $ar) |
| 239 | + let gcd_pos : Q(Prop) := q(0 < $gcd) |
| 240 | + let al_pos ← synthesizeUsingNormNum al_pos |
| 241 | + let ar_pos ← synthesizeUsingNormNum ar_pos |
| 242 | + let gcd_pos ← synthesizeUsingNormNum gcd_pos |
| 243 | + let pf ← mkAppM lem #[lhs_p, rhs_p, al_pos, ar_pos, gcd_pos] |
| 244 | + let pf_tp ← inferType pf |
| 245 | + return ((findCompLemma pf_tp).elim default (Prod.fst ∘ Prod.snd), pf) |
| 246 | + |
| 247 | +end CancelDenoms |
| 248 | + |
| 249 | +/-- |
| 250 | +`cancel_denoms` attempts to remove numerals from the denominators of fractions. |
| 251 | +It works on propositions that are field-valued inequalities. |
| 252 | +
|
| 253 | +```lean |
| 254 | +variable [LinearOrderedField α] (a b c : α) |
| 255 | +
|
| 256 | +example (h : a / 5 + b / 4 < c) : 4*a + 5*b < 20*c := by |
| 257 | + cancel_denoms at h |
| 258 | + exact h |
| 259 | +
|
| 260 | +example (h : a > 0) : a / 5 > 0 := by |
| 261 | + cancel_denoms |
| 262 | + exact h |
| 263 | +``` |
| 264 | +-/ |
| 265 | +syntax (name := cancelDenoms) "cancel_denoms" (ppSpace location)? : tactic |
| 266 | + |
| 267 | +open Elab Tactic |
| 268 | + |
| 269 | +def cancelDenominatorsAt (fvar: FVarId) : TacticM Unit := do |
| 270 | + let t ← instantiateMVars (← fvar.getDecl).type |
| 271 | + let (new, eqPrf) ← CancelDenoms.cancelDenominatorsInType t |
| 272 | + liftMetaTactic' fun g => do |
| 273 | + let res ← g.replaceLocalDecl fvar new eqPrf |
| 274 | + return res.mvarId |
| 275 | + |
| 276 | +def cancelDenominatorsTarget : TacticM Unit := do |
| 277 | + let (new, eqPrf) ← CancelDenoms.cancelDenominatorsInType (← getMainTarget) |
| 278 | + liftMetaTactic' fun g => g.replaceTargetEq new eqPrf |
| 279 | + |
| 280 | +def cancelDenominators (loc : Location) : TacticM Unit := do |
| 281 | + withLocation loc cancelDenominatorsAt cancelDenominatorsTarget |
| 282 | + (λ _ => throwError "Failed to cancel any denominators") |
| 283 | + |
| 284 | +elab "cancel_denoms" loc?:(location)? : tactic => do |
| 285 | + cancelDenominators (expandOptLocation (Lean.mkOptionalNode loc?)) |
| 286 | + Lean.Elab.Tactic.evalTactic (← `(tactic| try norm_num [← mul_assoc] $[$loc?]?)) |
0 commit comments