import LoVe.LoVelib
import Mathlib.Tactic.Linarith

namespace LoVe

open Qq

Proof by reflection

YOu may have noticed that we can't prove anything about the tactics we write. But there's a middle ground: sometimes with a bit of meta "wrapper code," we can turn proofs about syntax-like operations into actual proof terms.

The general strategy looks like this:

The idea is that the goal left after applying your correctness theorem can be proved by computation.

This is commonly used for evaluation or normalization functions. ring, for example, can be implemented by defining the syntax of ring expressions and verifying a normalization algorithm: if ring_syntax : Type, interp {α : Type} [ring α] : ring_syntax → α, normalize : ring_syntax → ring_syntax, then ∀ r1 r2 : ring_syntax, interp r1 = interp r2 ↔ normalize r1 = normalize r2.

(We often only need one direction of the bi-implication: ∀ r1 r2 : ring_syntax, normalize r1 = normalize r2 → interp r1 = interp r2)

The meta code looks at a goal c + a*b = b*a + c, constructs ring_syntax objects r1 and r2 representing both sides, and changes the goal to showing that normalize r1 = normalize r2. This can be proved by refl.

inductive bexpr
| atom : Bool → bexpr
| and : bexpr → bexpr → bexpr
| or : bexpr → bexpr → bexpr
| imp : bexpr → bexpr → bexpr
| not : bexpr → bexpr

open bexpr

def interp : bexpr → Prop
| (atom true) => True
| (atom false) => False
| (bexpr.and a b) => interp a ∧ interp b
| (bexpr.or a b) => interp a ∨ interp b
| (imp a b) => interp a → interp b
| (bexpr.not b) => ¬ interp b

def normalize : bexpr → Bool
| (atom b) => b
| (bexpr.and a b) => normalize a && normalize b
| (bexpr.or a b) => normalize a || normalize b
| (imp a b) => (! (normalize a)) || normalize b
| (bexpr.not b) => ! (normalize b)

theorem normalize_correct (b : bexpr) : normalize b = true ↔ interp b :=
by
  induction b with
  | atom a => cases' a <;> simp [normalize, interp]
  | and a b iha ihb => simp [normalize, interp, *]
  | or a b iha ihb => simp [normalize, interp, *]
  | not b ih =>
    simp [normalize, interp]
    rw [Bool.eq_false_iff]
    tauto
  | imp a b iha ihb =>
    simp [normalize, interp, ← iha, ← ihb]
    rw [Bool.eq_false_iff]
    tauto

open Lean Meta Tactic Elab Tactic

partial def bexprOfExpr (e : Q(Prop)) : MetaM Expr :=
match e with
| ~q(True) => mkAppM ``bexpr.atom #[mkConst ``true]
| ~q(False) => mkAppM ``bexpr.atom #[mkConst ``false]
| ~q($a ∧ $b) => do
  let a' ← bexprOfExpr a
  let b' ← bexprOfExpr b
  mkAppM ``bexpr.and #[a', b']
| ~q($a ∨ $b) => do
  let a' ← bexprOfExpr a
  let b' ← bexprOfExpr b
  mkAppM ``bexpr.or #[a', b']
| Expr.forallE _ a b _  => do
  let a' ← bexprOfExpr a
  let b' ← bexprOfExpr b
  mkAppM ``bexpr.imp #[a', b']
| ~q(¬ $a) => do
  let a' ← bexprOfExpr a
  mkAppM ``bexpr.not #[a']
| _ => failure

def changeGoal : TacticM Unit := withMainContext <| do
  let t ← getMainTarget
  let t' : Q(bexpr) ← bexprOfExpr t
  let gl ← getMainGoal
  let gls ← gl.apply q(Iff.mp (normalize_correct $t'))
  setGoals gls

elab "change_goal" : tactic => changeGoal

-- normalize (or (imp (atom true) (atom true)) (atom false)) = true
example : (True → True) ∨ False := by
  change_goal
  rfl

example (p : Prop) : (p → p) ∨ False := by
  sorry
  -- change_goal
  -- rfl

You could imagine doing the same with, say, a SAT solver.

Modify bexpr to cover all propositional formulas: atom : ℕ → bexpr. Add an argument to interp: dict : ℕ → Prop assigning atoms to propositions. normalize becomes is_tautology : bexpr → bool. normalize_correct becomes is_tautology_correct (b : bexpr) : is_tautology b = tt ↔ ∀ dict, interp dict b bexpr_of_expr will also have to return a dictionary.

Linear arithmetic

We've used the tactic linarith to prove statements that follow from linear (in)equalities in the local context.

For convenience we will work over a linear ordered ring, although this makes sense with weaker assumptions.

An expression is linear in variables x₁ ... xₙ if it is a polynomial of degree at most one: that is, it is a sum of products cᵢ * xᵢ, where each cᵢ is a coefficient.

The key point: variables are not multiplied together. 5*x + 3*y is linear, but 5*x*y is not.

An (in)equality is linear if it is of the form a ⋈ b, where ⋈ is <, ≤ =, ≥, or >, and a and b are linear expressions.

example (ε : ℚ) (h1 : ε > 0) : ε / 2 + ε / 3 + ε / 7 < ε :=
by linarith

example (g v V c h : ℚ) (h1 : h = 0) (h2 : v = V) (h3 : V > 0) (h4 : g > 0)
         (h5 : 0 ≤ c) (h6 : c < 1) :
  v ≤ V :=
by linarith

example (N : ℕ) (n : ℕ) (A : ℚ) (l : ℚ) (Hirrelevant : n > N)
        (h : A - l ≤ -(A - l)) (h_1 : ¬A ≤ -A) (h_2 : ¬l ≤ -l) (h_3 : -(A - l) < 1) :
  A < l + 1 :=
by linarith

We're interested in the problem of deciding whether a system of linear inequalities is satisfiable:

Given h1 : a₁¹x₁ + a₂¹x₂ + ... + aₙ¹xₙ ⋈¹ 0 h2 : a₁²x₁ + a₂²x₂ + ... + aₙ²xₙ ⋈² 0 h3 : a₁³x₁ + a₂³x₂ + ... + aₙ³xₙ ⋈³ 0 ... hk : a₁ᵏx₁ + a₂ᵏx₂ + ... + aₙᵏxₙ ⋈ᵏ 0

is there an assignment of variables to values x₁ ↦ v₁, x₂ ↦ v₂, ..., xₙ ↦ vₙ that makes each of h1 ... hk true?

Note: if we can decide this, we can decide implications like in the examples above. For a linear inequality h, the implication (h1 ∧ h2 ∧ ... ∧ hk) → h is valid exactly when the system h1, h2, ... hk, ¬ h is unsatisfiable.

Verified decision procedures

To implement a tactic like linarith, we need to do more than just decide these kinds of statements: we need to prove that our decisions are correct.

What are our options here?

Proof by reflection

We talked about proof by reflection earlier today. To do this, we'd have to represent the syntax of linear expressions as a datatype, define our decision procedure in Lean, and prove that it's correct with respect to an interpretation function.

This is probably doable! But executing our decision procedure would happen in Lean's (slow, reliable) kernel.

"Prove-as-we-go"

The simple tactics we implemented just updated the goal and context directly. Presumably, some decision procedures here will make intermediate claims: they'll start with h1 ... hk, derive new facts from those, derive new facts from those, ... We could imagine a tactic that just fills up the local context with new conclusions, until it proves the goal or terminates. (Details depending on the algorithm.)

Is this efficient? It depends on how much unnecessary work the tactic does...

Certificate checking

Our input is h1 : a₁¹x₁ + a₂¹x₂ + ... + aₙ¹xₙ ⋈¹ 0 h2 : a₁²x₁ + a₂²x₂ + ... + aₙ²xₙ ⋈² 0 h3 : a₁³x₁ + a₂³x₂ + ... + aₙ³xₙ ⋈³ 0 ... hk : a₁ᵏx₁ + a₂ᵏx₂ + ... + aₙᵏxₙ ⋈ᵏ 0 and our goal is to prove false. We can assume that each ⋈ⁱ is <, ≤, or =.

Suppose we had an oracle that produced a list of nonnegative coefficients c₁ ... cₖ such that

We could then prove false by summing together the scaled linear expressions; this sum must be both equal to and less than 0.

This list of coefficients c₁ ... cₖ is a certificate for the unsatisfiability of the system of inequalities.

Efficiently finding such a certificate can be very hard. But given such a certificate, it's easy to turn it into a proof term.

Mathlib's linarith tactic

linarith in mathlib follows the certificate checking approach.

Certificate generation is the computationally expensive part of this process.

Note, we don't need to assume anything about it: the certificate procedure can be an oracle.

There are lots and lots of algorithms out there for doing this. Simplex is a familiar and efficient technique (but can be hard to implement efficiently in a pure functional language.)

Fourier-Motzkin elimination is another.

Start with h1 : a₁¹x₁ + a₂¹x₂ + ... + aₙ¹xₙ ⋈¹ 0 h2 : a₁²x₁ + a₂²x₂ + ... + aₙ²xₙ ⋈² 0 h3 : a₁³x₁ + a₂³x₂ + ... + aₙ³xₙ ⋈³ 0 ... hk : a₁ᵏx₁ + a₂ᵏx₂ + ... + aₙᵏxₙ ⋈ᵏ 0

Pick a variable to eliminate: let's say xₙ.

Partition h1...hk into three sets pos, zero, neg: pos = {hi | aₙⁱ > 0} zero = {hi | aₙⁱ = 0} neg = {hi | aₙⁱ < 0}

Set aside zero; xₙ has already been eliminated from these.

For each pair of hp ∈ pos and hn ∈ neg:

After this process, zero is a set of comparisons that do not contain xₙ.

Theorem: over a dense linear ordered field, the initial set {h1, ... hk} is satisfiable iff the new set zero is satisfiable.

So, by successively eliminating all of the variables from our initial system, we end up with a list of comparisons 0 ⋈ⁱ 0. If any ⋈ⁱ is <, we've found a contradiction. Tracing the history of how this comparison was derived, we can compute how many copies of each original hi were added to derive it. This produces the certificate we wanted.

But: nothing about the rest of linarith assumes that this is the algorithm used to generate certificates. It just needs a valid certificate in the right format.

We can use external software such as Mathematica to generate these: perhaps more efficiently, perhaps not.

Mathlib's linarith is modular: all of these oracles exist and can be used!

Nonlinear arithmetic

You can imagine problems that are just outside this framework.

example (x y : ℚ) : 0 ≤ x*x + y*y := by
  -- linarith
  sorry

Turns out: these problems are also decidable!

(Old result by Tarski; modern algorithms like cylindrical algebraic decomposition)

But algorithms are very slow, and hard to produce proofs.

Hard to separate proof search from the ultimate proof: the justification is that the entire search was carried out correctly.

But for tactic implementations, we don't necessarily need decision procedures.

example (x y : ℚ) : 0 ≤ x*x*x*x + y*y*y*y :=
by nlinarith

end LoVe