Zulip Chat Archive

Stream: mathlib4

Topic: tactic by (boolean?) reflection example


Joachim Breitner (May 17 2023 at 17:17):

What is a good tactic whose code I could cargo cult from for a reflection-style normalizer or solver tactic?
I hope I am using the term correctly; I mean the approach where

  • I define a “normal” data type to describe the problem
  • a tactic that parses the goal state into this data type
  • a function that converts it back so that the tactic can replace the goal with that function applied to my custom data type
  • a function that solves or normalizes the value, with a proof that it’s correct
  • and then using that proof to conclude the tactic
    (Maybe this is called “boolean reflection”?)

Joachim Breitner (May 17 2023 at 17:40):

Some more googling may indicate that this style of boolean reflection isn’t actually a common technique in lean? Or is that just not the right term around here? I have also seen the name “small-scale reflection”.

James Gallicchio (May 17 2023 at 18:39):

maybe linarith? it's the only tactic I know that currently does something along those lines...

Eric Wieser (May 17 2023 at 18:41):

matrix.mulᵣ_eq was my attempt at this in Lean3, though the meta portion never got merged

Wojciech Nawrocki (May 17 2023 at 21:27):

@Joachim Breitner yeah, proof by reflection is almost not used at all in mathlib. This, afaik, is because historically the kernel didn't support it very well, i.e. reflection was slow. In mathlib3, the ring2 tactic proceeds by reflection. But it is more common to do the normalisation at the meta-level, and construct a proof of its correctness (e.g. using Eq.rec) as we go.

Mario Carneiro (May 18 2023 at 00:12):

Here's a simple tautology tactic using proof by reflection:

import Mathlib.Util.AtomM

inductive Formula : Type
  | var : Nat  Formula
  | not : Formula  Formula
  | imp : Formula  Formula  Formula
  deriving Inhabited

namespace Formula
def eval (V : List Bool) : Formula  Bool
  | .var n => V.getD n false
  | .not f => ¬ f.eval V
  | .imp f₁ f₂ => f₁.eval V  f₂.eval V

def eval' (F : List Prop) : Formula  Prop
  | .var n => F.getD n False
  | .not f => ¬ f.eval' F
  | .imp f₁ f₂ => f₁.eval' F  f₂.eval' F

def taut : Nat  List Bool  Formula  Bool
  | 0, V, f => f.eval V
  | n+1, V, f => f.taut n (false::V) && f.taut n (true::V)

theorem eval_map (V : List Bool) (f : Formula) : f.eval' (V.map (· = true))  f.eval V := by
  induction f <;> simp [eval', eval, *, List.getD]
  rename_i n
  induction V generalizing n <;> simp [List.get?]
  · simp [Option.getD]
  · match n with
    | 0 => simp [List.get?, Option.getD]
    | n+1 => simp [List.get?, *]

theorem exists_val :  F : List Prop,  V : List Bool, F = V.map (· = true)
  | [] => ⟨[], rfl
  | p::F => by
    let V, e := exists_val F
    by_cases h : p
    · exact true::V, by simp [e, List.map, h]⟩
    · exact false::V, by simp [e, List.map, h]⟩

theorem eval_of_taut :
     (V W : List Bool) (f : Formula), f.taut V.length W  f.eval (V.reverseAux W)
  | [], W, f => by simp [taut]
  | v::V, W, f => by
    simp [taut]
    intro h₁ h₂
    have := eval_of_taut V (v::W) f
    cases v
    · exact this h₁
    · exact this h₂

theorem eval'_of_taut (F : List Prop) (f : Formula) (h : f.taut F.length []) : f.eval' F := by
  let V, e := exists_val F
  subst e
  rw [List.length_map] at h
  rw [eval_map]
  have : _  f.eval V.reverse.reverse := eval_of_taut V.reverse [] f
  rw [List.length_reverse, List.reverse_reverse] at this
  exact this h

end Formula

open Mathlib.Tactic Qq

abbrev imp (p q : Prop) : Prop := p  q -- workaround for QQ bug?
partial def parseExpr (e : Q(Prop)) : AtomM Q(Formula) := do
  match e with
  | ~q(¬$p) => return q(.not $( parseExpr p))
  | ~q(imp $p $q) => return q(.imp $( parseExpr p) $( parseExpr q))
  | ~q($e) => have n : Q() := Lean.mkNatLit ( AtomM.addAtom e); return q(.var $n)

open Lean Elab Tactic
elab "taut" : tactic => liftMetaTactic fun g => do
  let some (ty : Q(Prop))  checkTypeQ ( g.getType) (q(Prop) : Q(Type)) | failure
  let (f, atoms)  AtomM.run .default do pure ( parseExpr ty, ( get).atoms)
  have F : Q(List Prop) := atoms.foldr (fun (a : Q(Prop)) ls => q($a :: $ls)) q([])
  let pf : Q(($f).taut ($F).length [] = true) := (q(Eq.refl true) : Expr)
  g.assign q(Formula.eval'_of_taut $F $f $pf)
  pure []

example (p q : Prop) : p  q  p := by taut
example : (¬1 = 2  1 = 2)  1 = 2 := by taut
example : ¬p  ¬p := by taut

Mario Carneiro (May 18 2023 at 00:16):

I have also seen the name “small-scale reflection”.

This is not small-scale reflection. Boolean reflection in this style is "large-scale reflection", while ssreflect deals with using reflection to perform rewrites and other small reductions in the course of a proof. Large-scale reflection is characterized by having a single kernel proof goal like myFunc = true which does almost all of the actual proof work.

Mario Carneiro (May 18 2023 at 00:17):

@Joachim Breitner

Joachim Breitner (May 18 2023 at 10:04):

Cool, thanks for that example! Not too much boiler-plate code it seems.


Last updated: Dec 20 2023 at 11:08 UTC