Skip to content

Commit bc9db78

Browse files
PatrickMassotfpvandoornkmill
committed
feat: cancel_denoms tactic (#3797)
Ports the tactic `CancelDenoms.derive` and the interactive tactic `cancel_denoms`. This tactic is needed to make `linarith` handle divisions by numbers, but this PR does not involve `linarith`. Co-authored-by: Floris van Doorn <fpvdoorn@gmail.com> Co-authored-by: Kyle Miller <kmill31415@gmail.com>
1 parent cab5256 commit bc9db78

File tree

7 files changed

+438
-11
lines changed

7 files changed

+438
-11
lines changed

Mathlib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,7 @@ import Mathlib.Tactic.Backtracking
17741774
import Mathlib.Tactic.Basic
17751775
import Mathlib.Tactic.ByContra
17761776
import Mathlib.Tactic.Cache
1777+
import Mathlib.Tactic.CancelDenoms
17771778
import Mathlib.Tactic.Cases
17781779
import Mathlib.Tactic.CasesM
17791780
import Mathlib.Tactic.Choose

Mathlib/Tactic/CancelDenoms.lean

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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?]?))

Mathlib/Tactic/Linarith/Datatypes.lean

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,6 @@ def mkSingleCompZeroOf (c : Nat) (h : Expr) : MetaM (Ineq × Expr) := do
402402
else do
403403
let tp ← inferType (← getRelSides (← inferType h)).2
404404
let cpos ← mkAppM ``GT.gt #[(← tp.ofNat c), (← tp.ofNat 0)]
405-
-- TODO There should be a def for this, rather than using `evalTactic`.
406-
let ex ← synthesizeUsing cpos (do evalTactic (←`(tactic| norm_num; done)))
405+
let ex ← synthesizeUsingTactic' cpos (← `(tactic| norm_num))
407406
let e' ← mkAppM iq.toConstMulName #[h, ex]
408407
return (iq, e')

Mathlib/Tactic/Linarith/Verification.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def addNegEqProofs : List Expr → MetaM (List Expr)
160160
def proveEqZeroUsing (tac : TacticM Unit) (e : Expr) : MetaM Expr := do
161161
let ⟨u, α, e⟩ ← inferTypeQ' e
162162
let _h : Q(Zero $α) ← synthInstanceQ q(Zero $α)
163-
synthesizeUsing q($e = 0) tac
163+
synthesizeUsing' q($e = 0) tac
164164

165165
/-! #### The main method -/
166166

Mathlib/Util/Qq.lean

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ open Lean Elab Tactic Meta
1414

1515
namespace Qq
1616

17-
/-- Analogue of `inferTypeQ`, but that gets universe levels right for our application. -/
17+
/-- Variant of `inferTypeQ` that yields a type in `Type u` rather than `Sort u`.
18+
Throws an error if the type is a `Prop` or if it's otherwise not possible to represent
19+
the universe as `Type u` (for example due to universe level metavariables). -/
1820
-- See https://leanprover.zulipchat.com/#narrow/stream/287929-mathlib4/topic/Using.20.60QQ.60.20when.20you.20only.20have.20an.20.60Expr.60/near/303349037
1921
def inferTypeQ' (e : Expr) : MetaM ((u : Level) × (α : Q(Type $u)) × Q($α)) := do
2022
let α ← inferType e
21-
let .sort u ← instantiateMVars (← whnf (← inferType α)) | unreachable!
22-
let some v := u.dec | throwError "not a type{indentExpr α}"
23+
let .sort u ← whnf (← inferType α) | throwError "not a type{indentExpr α}"
24+
let some v := (← instantiateLevelMVars u).dec | throwError "not a Type{indentExpr e}"
2325
pure ⟨v, α, e⟩
2426

2527
end Qq

0 commit comments

Comments
 (0)