Skip to content

Commit 26cb699

Browse files
vasnesterovplp127
andcommitted
feat(Tactic/Order): translate linear orders to Int (#26580)
It was [pointed out](https://leanprover.zulipchat.com/#narrow/channel/287929-mathlib4/topic/tactic.20for.20partial.20orders/near/515897754) that `order` is not complete for linear orders with lattice operations (while it remains complete for linear orders without lattice operations and for general lattices without assuming linearity). The problem for linear orders with lattice operations is NP-hard, but it can be translated from an arbitrary type to `Int` and then solved using a smart and efficient procedure (such as `omega`). This PR implements such a translation within the `order` tactic. Co-authored-by: Aaron Liu <aaronliu2008@outlook.com> Co-authored-by: Aaron Liu <aaronliu2008@outlook.com>
1 parent 984810d commit 26cb699

File tree

7 files changed

+335
-43
lines changed

7 files changed

+335
-43
lines changed

Mathlib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6456,6 +6456,7 @@ import Mathlib.Tactic.Order.CollectFacts
64566456
import Mathlib.Tactic.Order.Graph.Basic
64576457
import Mathlib.Tactic.Order.Graph.Tarjan
64586458
import Mathlib.Tactic.Order.Preprocessing
6459+
import Mathlib.Tactic.Order.ToInt
64596460
import Mathlib.Tactic.PNatToNat
64606461
import Mathlib.Tactic.PPWithUniv
64616462
import Mathlib.Tactic.Peel

Mathlib/Tactic.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ import Mathlib.Tactic.Order.CollectFacts
222222
import Mathlib.Tactic.Order.Graph.Basic
223223
import Mathlib.Tactic.Order.Graph.Tarjan
224224
import Mathlib.Tactic.Order.Preprocessing
225+
import Mathlib.Tactic.Order.ToInt
225226
import Mathlib.Tactic.PNatToNat
226227
import Mathlib.Tactic.PPWithUniv
227228
import Mathlib.Tactic.Peel

Mathlib/Tactic/Order.lean

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Authors: Vasilii Nesterov
66
import Mathlib.Tactic.ByContra
77
import Mathlib.Tactic.Order.CollectFacts
88
import Mathlib.Tactic.Order.Preprocessing
9+
import Mathlib.Tactic.Order.ToInt
910
import Mathlib.Tactic.Order.Graph.Basic
1011
import Mathlib.Tactic.Order.Graph.Tarjan
1112
import Mathlib.Util.ElabWithoutMVars
@@ -216,28 +217,6 @@ def updateGraphWithNltInfSup (g : Graph) (idxToAtom : Std.HashMap Nat Expr)
216217
break
217218
return g
218219

219-
/-- Supported order types: linear, partial, and preorder. -/
220-
inductive OrderType
221-
| lin | part | pre
222-
deriving BEq
223-
224-
instance : ToString OrderType where
225-
toString
226-
| .lin => "linear order"
227-
| .part => "partial order"
228-
| .pre => "preorder"
229-
230-
/-- Find the "best" instance of an order on a given type. A linear order is preferred over a partial
231-
order, and a partial order is preferred over a preorder. -/
232-
def findBestOrderInstance (type : Expr) : MetaM <| Option OrderType := do
233-
if (← synthInstance? (← mkAppM ``LinearOrder #[type])).isSome then
234-
return some .lin
235-
if (← synthInstance? (← mkAppM ``PartialOrder #[type])).isSome then
236-
return some .part
237-
if (← synthInstance? (← mkAppM ``Preorder #[type])).isSome then
238-
return some .pre
239-
return none
240-
241220
/-- Necessary for tracing below. -/
242221
local instance : Ord (Nat × Expr) where
243222
compare x y := compare x.1 y.1
@@ -248,27 +227,46 @@ def orderCore (only? : Bool) (hyps : Array Expr) (negGoal : Expr) (g : MVarId) :
248227
let TypeToAtoms ← collectFacts only? hyps negGoal
249228
for (type, (idxToAtom, facts)) in TypeToAtoms do
250229
let some orderType ← findBestOrderInstance type | continue
251-
let facts : Array AtomicFact ← match orderType with
252-
| .pre => preprocessFactsPreorder facts
253-
| .part => preprocessFactsPartial facts idxToAtom
254-
| .lin => preprocessFactsLinear facts idxToAtom
255230
trace[order] "Working on type {← ppExpr type} ({orderType})"
256231
let atomsMsg := String.intercalate "\n" <| Array.toList <|
257232
← idxToAtom.toArray.sortDedup.mapM
258233
fun ⟨idx, atom⟩ => do return s!"#{idx} := {← ppExpr atom}"
259234
trace[order] "Collected atoms:\n{atomsMsg}"
260235
let factsMsg := String.intercalate "\n" (facts.map toString).toList
261236
trace[order] "Collected facts:\n{factsMsg}"
262-
let mut graph ← Graph.constructLeGraph idxToAtom.size facts idxToAtom
263-
graph ← updateGraphWithNltInfSup graph idxToAtom facts
237+
let facts ← replaceBotTop facts idxToAtom
238+
let processedFacts : Array AtomicFact ← preprocessFacts facts idxToAtom orderType
239+
let factsMsg := String.intercalate "\n" (processedFacts.map toString).toList
240+
trace[order] "Processed facts:\n{factsMsg}"
241+
let mut graph ← Graph.constructLeGraph idxToAtom.size processedFacts
242+
graph ← updateGraphWithNltInfSup graph idxToAtom processedFacts
264243
if orderType == .pre then
265-
let some pf ← findContradictionWithNle graph idxToAtom facts | continue
244+
let some pf ← findContradictionWithNle graph idxToAtom processedFacts | continue
266245
g.assign pf
267246
return
268-
else
269-
let some pf ← findContradictionWithNe graph idxToAtom facts | continue
247+
if let some pf ← findContradictionWithNe graph idxToAtom processedFacts then
270248
g.assign pf
271249
return
250+
-- if fast procedure failed and order is linear, we try `omega`
251+
if orderType == .lin then
252+
let ⟨u, type⟩ ← getLevelQ' type
253+
let instLinearOrder ← synthInstanceQ q(LinearOrder $type)
254+
-- Here we only need to translate the hypotheses,
255+
-- since the goal will remain to derive `False`.
256+
let (_, factsNat) ← translateToInt type instLinearOrder idxToAtom facts
257+
let factsExpr : Array Expr := factsNat.filterMap fun factNat =>
258+
match factNat with
259+
| .eq _ _ proof => some proof
260+
| .ne _ _ proof => some proof
261+
| .le _ _ proof => some proof
262+
| .nle _ _ proof => some proof
263+
| .lt _ _ proof => some proof
264+
| .nlt _ _ proof => some proof
265+
| _ => none
266+
try
267+
Omega.omega factsExpr.toList g
268+
return
269+
catch _ => pure ()
272270
throwError ("No contradiction found.\n\n" ++
273271
"Additional diagnostic information may be available using " ++
274272
"the `set_option trace.order true` command.")

Mathlib/Tactic/Order/Graph/Basic.lean

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,12 @@ namespace Graph
4040
def addEdge (g : Graph) (edge : Edge) : Graph :=
4141
g.modify edge.src fun edges => edges.push edge
4242

43-
/-- Constructs a directed `Graph` using `≤` facts. It also creates edges from `⊥`
44-
(if present) to all vertices and from all vertices to `⊤` (if present). -/
45-
def constructLeGraph (nVertexes : Nat) (facts : Array AtomicFact)
46-
(idxToAtom : Std.HashMap Nat Expr) : MetaM Graph := do
43+
/-- Constructs a directed `Graph` using `≤` facts. It ignores all other facts. -/
44+
def constructLeGraph (nVertexes : Nat) (facts : Array AtomicFact) : MetaM Graph := do
4745
let mut res : Graph := Array.replicate nVertexes #[]
4846
for fact in facts do
4947
if let .le lhs rhs proof := fact then
5048
res := res.addEdge ⟨lhs, rhs, proof⟩
51-
else if let .isTop idx := fact then
52-
for i in [:nVertexes] do
53-
if i != idx then
54-
res := res.addEdge ⟨i, idx, ← mkAppOptM ``le_top #[none, none, none, idxToAtom.get! i]⟩
55-
else if let .isBot idx := fact then
56-
for i in [:nVertexes] do
57-
if i != idx then
58-
res := res.addEdge ⟨idx, i, ← mkAppOptM ``bot_le #[none, none, none, idxToAtom.get! i]⟩
5949
return res
6050

6151
/-- State for the DFS algorithm. -/

Mathlib/Tactic/Order/Preprocessing.lean

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,47 @@ lemma le_of_not_lt_le {α : Type u} [Preorder α] {x y : α} (h1 : ¬(x < y)) (h
2929

3030
end Lemmas
3131

32+
/-- Supported order types: linear, partial, and preorder. -/
33+
inductive OrderType
34+
| lin | part | pre
35+
deriving BEq
36+
37+
instance : ToString OrderType where
38+
toString
39+
| .lin => "linear order"
40+
| .part => "partial order"
41+
| .pre => "preorder"
42+
43+
/-- Find the "best" instance of an order on a given type. A linear order is preferred over a partial
44+
order, and a partial order is preferred over a preorder. -/
45+
def findBestOrderInstance (type : Expr) : MetaM <| Option OrderType := do
46+
if (← synthInstance? (← mkAppM ``LinearOrder #[type])).isSome then
47+
return some .lin
48+
if (← synthInstance? (← mkAppM ``PartialOrder #[type])).isSome then
49+
return some .part
50+
if (← synthInstance? (← mkAppM ``Preorder #[type])).isSome then
51+
return some .pre
52+
return none
53+
54+
/-- Replaces facts of the form `x = ⊤` with `y ≤ x` for all `y`, and similarly for `x = ⊥`. -/
55+
def replaceBotTop (facts : Array AtomicFact) (idxToAtom : Std.HashMap Nat Expr) :
56+
MetaM <| Array AtomicFact := do
57+
let mut res : Array AtomicFact := #[]
58+
let nAtoms := idxToAtom.size
59+
for fact in facts do
60+
match fact with
61+
| .isBot idx =>
62+
for i in [:nAtoms] do
63+
if i != idx then
64+
res := res.push <| .le idx i (← mkAppOptM ``bot_le #[none, none, none, idxToAtom.get! i])
65+
| .isTop idx =>
66+
for i in [:nAtoms] do
67+
if i != idx then
68+
res := res.push <| .le i idx (← mkAppOptM ``le_top #[none, none, none, idxToAtom.get! i])
69+
| _ =>
70+
res := res.push fact
71+
return res
72+
3273
/-- Preprocesses facts for preorders. Replaces `x < y` with two equivalent facts: `x ≤ y` and
3374
`¬ (y ≤ x)`. Replaces `x = y` with `x ≤ y`, `y ≤ x` and removes `x ≠ y`. -/
3475
def preprocessFactsPreorder (facts : Array AtomicFact) : MetaM <| Array AtomicFact := do
@@ -115,5 +156,13 @@ def preprocessFactsLinear (facts : Array AtomicFact) (idxToAtom : Std.HashMap Na
115156
res := res.push fact
116157
return res
117158

159+
/-- Preprocesses facts for order of `orderType` using either `preprocessFactsPreorder` or
160+
`preprocessFactsPartial` or `preprocessFactsLinear`. -/
161+
def preprocessFacts (facts : Array AtomicFact) (idxToAtom : Std.HashMap Nat Expr)
162+
(orderType : OrderType) : MetaM <| Array AtomicFact :=
163+
match orderType with
164+
| .pre => preprocessFactsPreorder facts
165+
| .part => preprocessFactsPartial facts idxToAtom
166+
| .lin => preprocessFactsLinear facts idxToAtom
118167

119168
end Mathlib.Tactic.Order

Mathlib/Tactic/Order/ToInt.lean

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/-
2+
Copyright (c) 2025 Vasilii Nesterov. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Vasilii Nesterov
5+
-/
6+
import Batteries.Data.List.Pairwise
7+
import Mathlib.Tactic.Order.CollectFacts
8+
import Batteries.Tactic.GeneralizeProofs
9+
import Mathlib.Util.Qq
10+
11+
/-!
12+
# Translating linear orders to ℤ
13+
14+
In this file we implement the translation of a problem in any linearly ordered type to a problem in
15+
`ℤ`. This allows us to use the `omega` tactic to solve it.
16+
17+
While the core algorithm of the `order` tactic is complete for the theory of linear orders in the
18+
signature (`<`, `≤`),
19+
it becomes incomplete in the signature with lattice operations `⊓` and `⊔`. With these operations,
20+
the problem becomes NP-hard, and the idea is to reuse a smart and efficient procedure, such as
21+
`omega`.
22+
23+
## TODO
24+
25+
Migrate to `grind` when it is ready.
26+
-/
27+
28+
namespace Mathlib.Tactic.Order.ToInt
29+
30+
variable {α : Type*} [LinearOrder α] {n : ℕ} (val : Fin n → α)
31+
32+
/-- The main theorem asserting the existence of a translation.
33+
We use `Classical.chooose` to turn this into a value for use in the `order` tactic,
34+
see `toInt`.
35+
-/
36+
theorem exists_translation : ∃ tr : Fin n → ℤ, ∀ i j, val i ≤ val j ↔ tr i ≤ tr j := by
37+
let li := List.ofFn val
38+
let sli := li.mergeSort
39+
have (i : Fin n) : ∃ j : Fin sli.length, sli[j] = val i := by
40+
apply List.get_of_mem
41+
rw [List.Perm.mem_iff (List.mergeSort_perm _ _)]
42+
simp [li]
43+
use fun i ↦ (this i).choose
44+
intro i j
45+
simp only [Fin.getElem_fin, Int.ofNat_le]
46+
by_cases h_eq : val i = val j
47+
· simp [h_eq]
48+
generalize_proofs _ hi hj
49+
rw [← hi.choose_spec, ← hj.choose_spec] at h_eq
50+
conv_lhs => rw [← hi.choose_spec, ← hj.choose_spec]
51+
have := List.sorted_mergeSort (l := li) (le := fun a b ↦ decide (a ≤ b))
52+
(by simpa using Preorder.le_trans) (by simpa using LinearOrder.le_total)
53+
rw [List.pairwise_iff_get] at this
54+
refine ⟨fun h ↦ ?_, fun h ↦ ?_⟩
55+
· contrapose! h
56+
exact lt_of_le_of_ne (by simpa using (this hj.choose hi.choose (by simpa)))
57+
(fun h ↦ h_eq (h.symm))
58+
· simpa using this hi.choose hj.choose (by apply lt_of_le_of_ne h; contrapose! h_eq; simp [h_eq])
59+
60+
/-- Auxiliary definition used by the `order` tactic to transfer facts in a linear order to `ℤ`. -/
61+
noncomputable def toInt (k : Fin n) : ℤ :=
62+
(exists_translation val).choose k
63+
64+
variable (i j k : Fin n)
65+
66+
theorem toInt_le_toInt : toInt val i ≤ toInt val j ↔ val i ≤ val j := by
67+
simp [toInt, (exists_translation val).choose_spec]
68+
69+
theorem toInt_lt_toInt : toInt val i < toInt val j ↔ val i < val j := by
70+
simpa using (toInt_le_toInt val j i).not
71+
72+
theorem toInt_eq_toInt : toInt val i = toInt val j ↔ val i = val j := by
73+
simp [toInt_le_toInt, le_antisymm_iff]
74+
75+
theorem toInt_ne_toInt : toInt val i ≠ toInt val j ↔ val i ≠ val j := by
76+
simpa using (toInt_eq_toInt val i j).not
77+
78+
theorem toInt_nle_toInt : ¬toInt val i ≤ toInt val j ↔ ¬val i ≤ val j := by
79+
simpa using toInt_lt_toInt val j i
80+
81+
theorem toInt_nlt_toInt : ¬toInt val i < toInt val j ↔ ¬val i < val j := by
82+
simpa using toInt_le_toInt val j i
83+
84+
theorem toInt_sup_toInt_eq_toInt :
85+
toInt val i ⊔ toInt val j = toInt val k ↔ val i ⊔ val j = val k := by
86+
simp [le_antisymm_iff, sup_le_iff, le_sup_iff, toInt_le_toInt]
87+
88+
theorem toInt_inf_toInt_eq_toInt :
89+
toInt val i ⊓ toInt val j = toInt val k ↔ val i ⊓ val j = val k := by
90+
simp [le_antisymm_iff, inf_le_iff, le_inf_iff, toInt_le_toInt]
91+
92+
open Lean Meta Qq
93+
94+
/-- Given an array `atoms : Array α`, create an expression representing a function
95+
`f : Fin atoms.size → α` such that `f n` is defeq to `atoms[n]` for `n : Fin atoms.size`. -/
96+
def mkFinFun {u : Level} {α : Q(Type $u)} (atoms : Array Q($α)) : MetaM Expr := do
97+
if h : atoms.isEmpty then
98+
return q(Fin.elim0 : Fin 0 → $α)
99+
else
100+
let rarray := RArray.ofArray atoms (by simpa [Array.size_pos_iff] using h)
101+
let rarrayExpr : Q(RArray $α) ← rarray.toExpr α (fun x ↦ x)
102+
haveI m : Q(ℕ) := mkNatLit atoms.size
103+
return q(fun (x : Fin $m) ↦ ($rarrayExpr).get x.val)
104+
105+
/-- Translates a set of values in a linear ordered type to `ℤ`,
106+
preserving all the facts except for `.isTop` and `.isBot`. These facts are filtered at the
107+
preprocessing step. -/
108+
def translateToInt {u : Lean.Level} (type : Q(Type u)) (inst : Q(LinearOrder $type))
109+
(idxToAtom : Std.HashMap ℕ Q($type))
110+
(facts : Array AtomicFact) :
111+
MetaM <| Std.HashMap ℕ Q(ℤ) × Array AtomicFact := do
112+
haveI nE : Q(ℕ) := mkNatLitQ idxToAtom.size
113+
haveI finFun : Q(Fin $nE → $type) :=
114+
← mkFinFun (Array.ofFn fun (n : Fin idxToAtom.size) => idxToAtom[n]!)
115+
let toFinUnsafe : ℕ → Q(Fin $nE) := fun k =>
116+
haveI kE := mkNatLitQ k
117+
haveI heq : decide ($kE < $nE) =Q true := ⟨⟩
118+
q(⟨$kE, of_decide_eq_true $heq⟩)
119+
return Prod.snd <| facts.foldl (fun (curr, map, facts) fact =>
120+
match fact with
121+
| .eq lhs rhs prf =>
122+
(curr, map, facts.push (
123+
haveI lhsFin := toFinUnsafe lhs
124+
haveI rhsFin := toFinUnsafe rhs
125+
haveI prfQ : Q($finFun $lhsFin = $finFun $rhsFin) := prf
126+
.eq lhs rhs q((toInt_eq_toInt $finFun $lhsFin $rhsFin).mpr $prfQ)
127+
))
128+
| .ne lhs rhs prf =>
129+
(curr, map, facts.push (
130+
haveI lhsFin := toFinUnsafe lhs
131+
haveI rhsFin := toFinUnsafe rhs
132+
haveI prfQ : Q($finFun $lhsFin ≠ $finFun $rhsFin) := prf
133+
.ne lhs rhs q((toInt_ne_toInt $finFun $lhsFin $rhsFin).mpr $prfQ)
134+
))
135+
| .le lhs rhs prf =>
136+
(curr, map, facts.push (
137+
haveI lhsFin := toFinUnsafe lhs
138+
haveI rhsFin := toFinUnsafe rhs
139+
haveI prfQ : Q($finFun $lhsFin ≤ $finFun $rhsFin) := prf
140+
.le lhs rhs q((toInt_le_toInt $finFun $lhsFin $rhsFin).mpr $prfQ)
141+
))
142+
| .lt lhs rhs prf =>
143+
(curr, map, facts.push (
144+
haveI lhsFin := toFinUnsafe lhs
145+
haveI rhsFin := toFinUnsafe rhs
146+
haveI prfQ : Q($finFun $lhsFin < $finFun $rhsFin) := prf
147+
.lt lhs rhs q((toInt_lt_toInt $finFun $lhsFin $rhsFin).mpr $prfQ)
148+
))
149+
| .nle lhs rhs prf =>
150+
(curr, map, facts.push (
151+
haveI lhsFin := toFinUnsafe lhs
152+
haveI rhsFin := toFinUnsafe rhs
153+
haveI prfQ : Q(¬$finFun $lhsFin ≤ $finFun $rhsFin) := prf
154+
.nle lhs rhs q((toInt_nle_toInt $finFun $lhsFin $rhsFin).mpr $prfQ)
155+
))
156+
| .nlt lhs rhs prf =>
157+
(curr, map, facts.push (
158+
haveI lhsFin := toFinUnsafe lhs
159+
haveI rhsFin := toFinUnsafe rhs
160+
haveI prfQ : Q(¬$finFun $lhsFin < $finFun $rhsFin) := prf
161+
.nlt lhs rhs q((toInt_nlt_toInt $finFun $lhsFin $rhsFin).mpr $prfQ)
162+
))
163+
| .isBot _
164+
| .isTop _ => (curr, map, facts)
165+
| .isSup lhs rhs val =>
166+
haveI lhsFin := toFinUnsafe lhs
167+
haveI rhsFin := toFinUnsafe rhs
168+
haveI valFin := toFinUnsafe val
169+
haveI heq : max («$finFun» «$lhsFin») («$finFun» «$rhsFin») =Q «$finFun» «$valFin» := ⟨⟩
170+
(curr + 1, map.insert curr q(toInt $finFun $lhsFin ⊔ toInt $finFun $rhsFin),
171+
(facts.push (.isSup lhs rhs curr)).push (.eq curr val
172+
q((toInt_sup_toInt_eq_toInt $finFun $lhsFin $rhsFin $valFin).mpr $heq)
173+
)
174+
)
175+
| .isInf lhs rhs val =>
176+
haveI lhsFin := toFinUnsafe lhs
177+
haveI rhsFin := toFinUnsafe rhs
178+
haveI valFin := toFinUnsafe val
179+
haveI heq : min («$finFun» «$lhsFin») («$finFun» «$rhsFin») =Q «$finFun» «$valFin» := ⟨⟩
180+
(curr + 1, map.insert curr q(toInt $finFun $lhsFin ⊓ toInt $finFun $rhsFin),
181+
(facts.push (.isInf lhs rhs curr)).push (.eq curr val
182+
q((toInt_inf_toInt_eq_toInt $finFun $lhsFin $rhsFin $valFin).mpr $heq)
183+
)
184+
))
185+
(idxToAtom.size, idxToAtom.map fun k _ =>
186+
haveI kFin := toFinUnsafe k
187+
q(toInt $finFun $kFin), Array.emptyWithCapacity idxToAtom.size)
188+
189+
end Mathlib.Tactic.Order.ToInt
190+
191+
export Mathlib.Tactic.Order.ToInt (translateToInt)

0 commit comments

Comments
 (0)