Compare commits

...

25 Commits

Author SHA1 Message Date
Joachim Breitner
f905b76fae Update test 2024-11-12 10:49:25 +01:00
Joachim Breitner
62ae4ea2e5 More renaming, split file 2024-11-12 10:47:27 +01:00
Joachim Breitner
135e8ece56 float_match → lift_match 2024-11-12 10:19:32 +01:00
Joachim Breitner
46939d894e Merge commit '811d8fb3c0' into joachim/float-match 2024-11-12 10:17:41 +01:00
Joachim Breitner
e9ee459244 Update src/Lean/Meta/Match/Float.lean 2024-11-06 13:58:55 +01:00
Joachim Breitner
e808c96719 Treat if-then-else like a matcher 2024-11-06 11:31:47 +01:00
Joachim Breitner
bf933f7961 Merge branch 'master' of github.com:leanprover/lean4 into joachim/float-match 2024-11-06 10:51:24 +01:00
Joachim Breitner
719625928a Docstrings 2024-11-06 10:48:15 +01:00
Joachim Breitner
10ade6973f Minimize imports 2024-11-06 10:35:47 +01:00
Joachim Breitner
9b6b198714 More tests 2024-11-06 10:20:46 +01:00
Joachim Breitner
e23c131993 Float out of the let-value 2024-11-06 10:11:06 +01:00
Joachim Breitner
bc763304d9 float_match conv and regular tactic 2024-11-05 17:03:06 +01:00
Joachim Breitner
2148f334f1 Do not search arbitrarily deep 2024-11-05 12:53:24 +01:00
Joachim Breitner
bda38cca69 Test case-of-case 2024-11-05 12:34:40 +01:00
Joachim Breitner
e9894121c5 Float through more than one application at a time
needed to float out of ite
2024-11-05 12:33:15 +01:00
Joachim Breitner
de4dd49069 Experiment with ite 2024-11-05 12:15:57 +01:00
Joachim Breitner
221551e6d3 Merge branch 'master' of github.com:leanprover/lean4 into joachim/float-match 2024-11-05 11:38:16 +01:00
Joachim Breitner
7b1921e23b Limit floating out of ite-branches 2024-11-05 11:38:10 +01:00
Joachim Breitner
e4676ed31f More tests 2024-11-04 15:50:16 +01:00
Joachim Breitner
0ef4b9a2ec Shuffle modules 2024-11-04 14:41:22 +01:00
Joachim Breitner
ca68d14502 Add simproc 2024-11-04 11:34:55 +01:00
Joachim Breitner
907c85001d More whnf, cannot hurt 2024-11-02 13:34:48 +01:00
Joachim Breitner
5bce34edb4 Actually prove the lemmas 2024-11-02 13:19:56 +01:00
Joachim Breitner
5ac35e7a2d Tweaks 2024-11-02 00:02:26 +01:00
Joachim Breitner
77ea192b23 feat: match_1.float theorems 2024-11-01 23:59:24 +01:00
7 changed files with 617 additions and 1 deletions

View File

@@ -325,4 +325,31 @@ syntax (name := conv) "conv" (" at " ident)? (" in " (occs)? term)? " => " convS
/-- `norm_cast` tactic in `conv` mode. -/
syntax (name := normCast) "norm_cast" : conv
/--
Lifts out `match` expressions, or, equivalently, pushes function applications into the
branches of a match. For example it can rewrite
```
f (match o with | some x => x + 1 | none => 0)
```
to
```
match o with | some x => f (x + 1) | none => f 0
```
For the purposes of this tactic, `if-then-else` expressions are treated like `match` expressions.
It can only lift matches with a non-dependent motive, no extra arguments and when the context
(here `fun x => f x`) is type-correct.
It lifts the first eligible match it finds to the top. To lift less far (e.g. into the
left-hand side of an equality) focus on the desired position first (e.g. using `lhs` and `rhs`).
Also see the `liftMatch` simproc for use in the simplifier.
-/
syntax (name := liftMatch) "lift_match" : conv
@[inherit_doc liftMatch]
macro (name := _root_.Lean.Parser.Tactic.liftMatch) "lift_match" : tactic =>
`(tactic| conv => lift_match)
end Lean.Parser.Tactic.Conv

View File

@@ -9,6 +9,7 @@ import Lean.Meta.Match.Match
import Lean.Meta.Match.CaseValues
import Lean.Meta.Match.CaseArraySizes
import Lean.Meta.Match.MatchEqs
import Lean.Meta.Match.Lift
namespace Lean

View File

@@ -0,0 +1,99 @@
/-
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Joachim Breitner
-/
prelude
import Lean.Meta.Match.MatcherApp.Basic
import Lean.Meta.Match.MatchEqsExt
import Lean.Elab.Tactic.Meta
open Lean Meta Elab Term
namespace Lean.Meta
def deriveMatchLift (name : Name) : MetaM Unit := do
mapError (f := (m!"Cannot construct match lifter:{indentD ·}")) do
let some info getMatcherInfo? name | throwError "getMatcherInfo? failed"
-- Fail early if splitter cannot be generated
try
discard <| Match.getEquationsFor name
catch _ =>
throwError "Could not construct splitter for {name}"
let cinfo getConstInfo name
let (u, v, us, us', levelParams) := if let some upos := info.uElimPos? then
let u := mkLevelParam `u
let v := mkLevelParam `v
let lps := (List.range cinfo.levelParams.length).map (Name.mkSimple s!"u_{·+1}")
let us := lps.map mkLevelParam
let us := us.set upos u
let us' := us.set upos v
let lps := [`u, `v] ++ lps.eraseIdx upos
(u, v, us, us', lps)
else
let lps := cinfo.levelParams
let us := lps.map mkLevelParam
(0, 0, us, us, lps)
let matchf := .const name us
let matchType inferType matchf
let type forallBoundedTelescope matchType info.numParams fun params matchType => do
let matchType whnf matchType
unless matchType.isForall do throwError "resual type {matchType} of {.ofConstName name} not a forall"
withLocalDecl `α .implicit (.sort u) fun α => do
withLocalDecl .implicit (.sort v) fun β => do
withLocalDeclD `f ( mkArrow α β) fun f => do
let motive forallTelescope matchType.bindingDomain! fun xs _ => mkLambdaFVars xs α
let motive' forallTelescope matchType.bindingDomain! fun xs _ => mkLambdaFVars xs β
let matchType instantiateForall matchType #[motive]
forallBoundedTelescope matchType info.numDiscrs fun discrs matchType => do
forallBoundedTelescope matchType info.altNumParams.size fun alts _ => do
let lhs := mkAppN (.const name us) (params ++ #[motive] ++ discrs ++ alts)
let lhs := .app f lhs
let alts' alts.mapM fun alt => do
let altType inferType alt
forallTelescope altType fun ys _ =>
if ys.size == 1 && altType.bindingDomain!.isConstOf ``Unit then
mkLambdaFVars ys (mkApp f (mkApp alt (.const ``Unit.unit [])))
else
mkLambdaFVars ys (mkApp f (mkAppN alt ys))
let rhs := mkAppN (.const name us') (params ++ #[motive'] ++ discrs ++ alts')
let type mkEq lhs rhs
mkForallFVars (#[α,β,f] ++ params ++ discrs ++ alts) type
let value mkFreshExprSyntheticOpaqueMVar type
TermElabM.run' do withoutErrToSorry do
runTactic value.mvarId! ( `(Parser.Term.byTactic| by intros; split <;> rfl)).raw .term
let value instantiateMVars value
let decl := Declaration.thmDecl { name := name ++ `lifter, levelParams, type, value }
addDecl decl
def isMatchLiftName (env : Environment) (name : Name) : Bool :=
if let .str p "lifter" := name
then
(getMatcherInfoCore? env p).isSome
else
false
def mkMatchLifterApp (f : Expr) (matcherApp : MatcherApp) : MetaM (Option Expr) := do
let some (α, β) := ( inferType f).arrow? |
trace[lift_match] "Cannot lift match: {f} is dependent"
return none
let lifterName := matcherApp.matcherName ++ `lifter
let _ realizeGlobalName lifterName
let lifterArgs := #[α,β,f] ++ matcherApp.params ++ matcherApp.discrs ++ matcherApp.alts
-- using mkAppOptM to instantiate the level params
let e mkAppOptM lifterName (lifterArgs.map some)
return some e
builtin_initialize
registerReservedNamePredicate isMatchLiftName
registerReservedNameAction fun name => do
if isMatchLiftName ( getEnv) name then
let .str p _ := name | return false
MetaM.run' <| deriveMatchLift p
return true
return false
Lean.registerTraceClass `lift_match

View File

@@ -5,7 +5,7 @@ Authors: Leonardo de Moura, Joachim Breitner
-/
prelude
import Lean.Meta.Match
import Lean.Meta.Match.MatchEqs
import Lean.Meta.InferType
import Lean.Meta.Check
import Lean.Meta.Tactic.Split

View File

@@ -41,3 +41,4 @@ import Lean.Meta.Tactic.FunInd
import Lean.Meta.Tactic.Rfl
import Lean.Meta.Tactic.Rewrites
import Lean.Meta.Tactic.Grind
import Lean.Meta.Tactic.LiftMatch

View File

@@ -0,0 +1,177 @@
/-
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Joachim Breitner
-/
prelude
import Init.Simproc
import Lean.Meta.Match.MatcherApp.Basic
import Lean.Meta.Match.Lift
import Lean.Meta.KAbstract
import Lean.Elab.Tactic.Conv.Basic
/-!
This module implements the `liftMatch` simpproc and the `lift_match` conv tactic.
-/
open Lean Meta Elab Term
namespace Lean.Meta
section LiftMatch
inductive MatcherOrIteApp where
| matcher (matcherApp : MatcherApp)
| ite (dite : Bool) (α P inst t e : Expr)
private def _root_.Lean.Expr.constLams? : Expr Option Expr
| .lam _ _ b _ => constLams? b
| e => if e.hasLooseBVars then none else some e
def MatcherOrIteApp.motive? : MatcherOrIteApp -> Option Expr
| matcher matcherApp => matcherApp.motive.constLams?
| ite _ α _ _ _ _ => some α
def mkMatchOrIteLiftApp (f : Expr) (moi : MatcherOrIteApp) : MetaM (Option Expr) := do
match moi with
| .matcher matcherApp => mkMatchLifterApp f matcherApp
| .ite dite α P inst t e =>
let some (_, β) := ( inferType f).arrow? |
trace[lift_match] "Cannot lift match: {f} is dependent"
return none
let lifterName := if dite then ``apply_dite else ``apply_ite
let e mkAppOptM lifterName #[some α, some β, some f, some P, some inst, some t, some e]
return some e
/--
Like matchMatcherApp, but also matches ite/dite.
-/
private def matchMatcherOrIteApp? (e : Expr) : MetaM (Option MatcherOrIteApp) := do
match_expr e with
| ite α P inst t e => return some (.ite false α P inst t e)
| dite α P inst t e => return some (.ite true α P inst t e)
| _ =>
if let some matcherApp matchMatcherApp? e then
if matcherApp.remaining.isEmpty then
return .some (.matcher matcherApp)
return none
/--
Finds the first possible liftable match (or (d)ite).
-/
private partial def findMatchToLift? (e : Expr) (far : Bool) (depth : Nat := 0) : MetaM (Option (Expr × MatcherOrIteApp)) := do
if !far && depth > 1 then
return none
if e.isApp then
if depth > 0 then
if let some matcherApp matchMatcherOrIteApp? e then
return some (e, matcherApp)
let args := e.getAppArgs
if e.isAppOf ``ite then
-- Special-handling for if-then-else:
-- * We do not want to lift out of the branches.
-- * We want to be able to lift out of
-- @ite ([ ] = true) (instDecidableEqBool [ ] true) t e
-- but doing it one application at a time does not work due to the dependency.
-- So to work around this, we do not bump the depth counter here.
if h : args.size > 1 then
if let some r findMatchToLift? args[1] far depth then
return some r
else
for a in args do
if let some r findMatchToLift? a far (depth + 1) then
return some r
if e.isLet then
if let some r findMatchToLift? e.letValue! far (depth + 1) then
return some r
return none
/--
In `e`, try to find a `match` expression or `ite` application that we can lift
to the root. Returns the new expression `s` and a proof of `e = s`.
-/
def findAndLiftMatch (e : Expr) (far : Bool) : MetaM (Option (Expr × Expr)) := do
unless far do
-- In the simproc: We could, but for now we do not lift out of props
if Meta.isProp e then return none
let some (me, matcherApp) findMatchToLift? e far| return none
-- We do not handle dependent motives
let some α := matcherApp.motive? |
trace[lift_match] "Cannot lift match: motive depends on targets"
return none
-- Using kabstract, rather than just abstracting over the single occurrence of `me` in `e` with
-- helps if later arguments depend on the abstracted argument,
-- in particular with ``ite's `Decidable c` parameter
let f := (mkLambda `x .default α ( kabstract e me)).eta
-- Abstracting over the argument can result in a type incorrect `f` (like in `rw`)
unless ( isTypeCorrect f) do
trace[lift_match] "Cannot lift match: context is not type correct"
return none
let some proof mkMatchOrIteLiftApp f matcherApp | return none
let type inferType proof
let some (_, _, rhs) := type.eq?
| throwError "lift_match: Unexpected non-equality type:{indentExpr type}"
return some (rhs, proof)
end LiftMatch
end Lean.Meta
/-!
The simproc tactic
-/
section Simproc
/--
Lifts out `match` expressions, or, equivalently, pushes function applications into the
branches of a match. For example it can rewrite
```
f (match o with | some x => x + 1 | none => 0)
```
to
```
match o with | some x => f (x + 1) | none => f 0
```
For the purposes of this simproc, `if-then-else` expressions are treated like `match` expressions.
It can only lift matches with a non-dependent motive, no extra arguments and when the context
(here `fun x => f x`) is type-correct and is not a proposition.
It is recommended to enable this simproc only selectively, and not by default, as it looks for
match expression to lift at every step of the simplifier.
Also see the `conv`-tactic `lift_match`.
-/
builtin_simproc_decl liftMatch (_) := fun e => do
let some (rhs, proof) findAndLiftMatch (far := false) e | return .continue
return .visit { expr := rhs, proof? := some proof }
end Simproc
/-!
The conv tactic
-/
namespace Lean.Elab.Tactic.Conv
@[builtin_tactic Lean.Parser.Tactic.Conv.liftMatch]
def evalLiftMatch : Tactic := fun _ => do
let mvarId getMainGoal
mvarId.withContext do
let lhs getLhs
let some (rhs, proof) findAndLiftMatch (far := true) lhs
| throwError "cannot find match to lift"
updateLhs rhs proof
end Lean.Elab.Tactic.Conv

View File

@@ -0,0 +1,311 @@
import Lean.Elab.Command
import Lean.Meta.Match.MatchEqsExt
def test1 : Nat Nat
| 0 => 0
| _+1 => 42
-- set_option pp.match false
/--
info: test1.match_1.lifter.{u, v} {α : Sort u} {β : Sort v} (f : α → β) (x✝ : Nat) (h_1 : Unit → (fun x => α) 0)
(h_2 : (n : Nat) → (fun x => α) n.succ) :
f
(match x✝ with
| 0 => h_1 ()
| n.succ => h_2 n) =
match x✝ with
| 0 => f (h_1 ())
| n.succ => f (h_2 n)
-/
#guard_msgs in
#check test1.match_1.lifter
def test2 (α β) : α β γ (β α) γ
| .inl x, y => .inr x, y
| .inr x, y => .inl x, y
set_option pp.proofs true in
/--
info: test2.match_1.lifter {α β : Prop} (f : α → β) {γ : Prop} (α✝ β✝ : Prop) (x✝ : α✝ β✝) (x✝¹ : γ)
(h_1 : ∀ (x : α✝) (y : γ), (fun x x => α) (Or.inl x) y) (h_2 : ∀ (x : β✝) (y : γ), (fun x x => α) (Or.inr x) y) :
f
(match x✝, x✝¹ with
| Or.inl x, y => h_1 x y
| Or.inr x, y => h_2 x y) =
match x✝, x✝¹ with
| Or.inl x, y => f (h_1 x y)
| Or.inr x, y => f (h_2 x y)
-/
#guard_msgs in
#check test2.match_1.lifter
-- This fails if there is no splitter theorem for a match
/--
error: Failed to realize constant Nat.lt_or_gt_of_ne.match_1.lifter:
Cannot construct match lifter:
Could not construct splitter for Nat.lt_or_gt_of_ne.match_1
---
error: Failed to realize constant Nat.lt_or_gt_of_ne.match_1.lifter:
Cannot construct match lifter:
Could not construct splitter for Nat.lt_or_gt_of_ne.match_1
---
error: unknown identifier 'Nat.lt_or_gt_of_ne.match_1.lifter'
-/
#guard_msgs in
#check Nat.lt_or_gt_of_ne.match_1.lifter
-- A typical example
theorem List.filter_map' (f : β α) (l : List β) : filter p (map f l) = map f (filter (p f) l) := by
induction l <;> simp [filter, *, liftMatch]
-- Using the lift_match conv tactic
theorem List.filter_map'' (f : β α) (l : List β) : filter p (map f l) = map f (filter (p f) l) := by
induction l
· simp
· simp only [filter]
conv => rhs; lift_match
simp only [Function.comp_apply, map_cons, *]
-- Using the lift_match tactic
-- This works in principle, but isn't very useful, because the simplifier, even with
-- `(contextual := true)`, does not simplify the duplicated match target in the alternatives.
-- Looks like that would require congruence theorems for matchers.
/-- warning: declaration uses 'sorry' -/
#guard_msgs in
theorem List.filter_map''' (f : β α) (l : List β) : filter p (map f l) = map f (filter (p f) l) := by
induction l
· simp
· simp only [filter]
lift_match
simp (config := {contextual := true}) [*]
-- fail
sorry
-- A simple example
example (o : Option Bool) :
(match o with | some b => b | none => false)
= !(match o with | some b => !b | none => true) := by
simp [liftMatch]
-- Can float out of ite-condition
/--
error: tactic 'fail' failed
o : Option Bool
P : Nat → Prop
⊢ P
(match o with
| some b => if b = true then 1 else 2
| none => if True then 1 else 2)
-/
#guard_msgs in
example (o : Option Bool) (P : Nat Prop):
P (if (match o with | some b => b | none => true) then 1 else 2) := by
simp only [liftMatch]
fail
-- Cannot lift out of ite-branch
example (b : Bool) (o : Option Bool) (P : Bool Prop) (abort : b, P b):
P (if b then (match o with | some b => b | none => true) else b) := by
fail_if_success simp only [liftMatch]
apply abort
-- Can float out of a match target (aka case-of-case)
/--
error: tactic 'fail' failed
o : Option Bool
P : Nat → Prop
⊢ P
(match o with
| some b =>
match b with
| true => 1
| false => 2
| none => 1)
-/
#guard_msgs in
example (o : Option Bool) (P : Nat Prop):
P (match (match o with | some b => b | none => true) with | true => 1 | false => 2) := by
simp only [liftMatch]
fail
-- Dependent motive; must not rewrite
set_option trace.lift_match true in
/-- info: [lift_match] Cannot lift match: motive depends on targets -/
#guard_msgs in
example (o : Option Bool) (motive : Bool Type) (P : {b : Bool} motive b Prop)
(f : (x : Bool) motive x) (g : {x : Bool} motive x motive x)
(abort : b (x : motive b), P x) :
P (g (match (motive := b, motive b.isSome) o with | some _ => f true | none => f false)) := by
fail_if_success simp [liftMatch]
apply abort
-- Dependent context; must not rewrite
set_option trace.lift_match true in
/-- info: [lift_match] Cannot lift match: f is dependent -/
#guard_msgs in
example (o : Option Bool) (motive : Bool Type) (P : {b : Bool} motive b Prop)
(f : (x : Bool) motive x)
(abort : b (x : motive b), P x) :
P (f (match (motive := _, Bool) o with | some b => b | none => false)) := by
fail_if_success simp [liftMatch]
apply abort
-- Context depends on the concrete value of the match, must not rewrite
set_option trace.lift_match true in
/-- info: [lift_match] Cannot lift match: context is not type correct -/
#guard_msgs in
example (o : Option Bool)
(f : (x : Bool) (h : x = (match o with | some b => b | none => false)) Bool)
(abort : (P : Prop), P) :
f (match (motive := _, Bool) o with | some b => b | none => false) rfl = true := by
fail_if_success simp [liftMatch]
apply abort
-- Can float out of a let (Only relevant with zeta := false)
/--
error: tactic 'fail' failed
o : Option Bool
P : Bool → Prop
⊢ P
(match o with
| some b =>
let b := b;
!b
| none =>
let b := true;
!b)
-/
#guard_msgs in
example (o : Option Bool) (P : Bool Prop):
P (let b := match o with | some b => b | none => true; !b) := by
simp -zeta only [liftMatch]
fail
/-
This following code tries to create all float theorems for all matches found in the environment.
-/
-- At the time of writing, the following two quite large matches fail by running out of heartbeat
-- #check Lean.Expr.name?.match_1.float
-- #check Lean.Meta.reduceNat?.match_1.float
/-
open Lean Meta in
run_meta do
for es in (Match.Extension.extension.toEnvExtension.getState (← getEnv)).importedEntries do
for e in es do
-- Let's not look at matchers that eliminate to Prop only
if e.info.uElimPos?.isNone then continue
withCurrHeartbeats do
tryCatchRuntimeEx do
let hasSplitter ← try
discard <| Lean.Meta.Match.getEquationsFor e.name
pure true
catch _ => pure false
if hasSplitter then
let floatName := e.name ++ `float
unless (← hasConst floatName) do
executeReservedNameAction floatName
fun ex =>
logInfo m!"Failed to handle {e.name}:{ex.toMessageData}"
-/
-- Testing if-then-else
/--
error: tactic 'fail' failed
P : Nat → Prop
f : Nat → Nat
b : Bool
⊢ P (if b = true then f 1 else f 2)
-/
#guard_msgs in
example (P : Nat Prop) (f : Nat Nat) (b : Bool) :
P (f (if b then 1 else 2)) := by
simp only [liftMatch]
fail
-- Dependent f
/--
error: simp made no progress
---
info: [lift_match] Cannot lift match: f is dependent
-/
#guard_msgs in
set_option trace.lift_match true in
example (P : {n : Nat} Fin n Prop) (f : (n : Nat) Fin n) (b : Bool) :
P (f (if b then 1 else 2)) := by
simp only [liftMatch]
fail
-- Somewhat dependent f, but abstracting still succeeds
/--
error: tactic 'fail' failed
P : Nat → Prop
f : (n : Nat) → DecidableEq (Fin n) → Nat
b : Bool
⊢ P (if b = true then f 1 inferInstance else f 2 inferInstance)
-/
#guard_msgs in
example (P : Nat Prop) (f : (n : Nat) DecidableEq (Fin n) Nat) (b : Bool) :
P (f (if b then 1 else 2) inferInstance) := by
simp only [liftMatch]
fail
-- Testing dependent if-then-else
/--
error: tactic 'fail' failed
P : Nat → Prop
f : Nat → Nat
b : Bool
⊢ P (if h : b = true then f 1 else f 2)
-/
#guard_msgs in
example (P : Nat Prop) (f : Nat Nat) (b : Bool) :
P (f (if h : b then 1 else 2)) := by
simp only [liftMatch]
fail
-- Dependent f
/--
error: simp made no progress
---
info: [lift_match] Cannot lift match: f is dependent
-/
#guard_msgs in
set_option trace.lift_match true in
example (P : {n : Nat} Fin n Prop) (f : (n : Nat) Fin n) (b : Bool) :
P (f (if h : b then 1 else 2)) := by
simp only [liftMatch]
fail
-- Somewhat dependent f, but abstracting still succeeds
/--
error: tactic 'fail' failed
P : Nat → Prop
f : (n : Nat) → DecidableEq (Fin n) → Nat
b : Bool
⊢ P (if h : b = true then f 1 inferInstance else f 2 inferInstance)
-/
#guard_msgs in
example (P : Nat Prop) (f : (n : Nat) DecidableEq (Fin n) Nat) (b : Bool) :
P (f (if h : b then 1 else 2) inferInstance) := by
simp only [liftMatch]
fail