lean4-htt/tests/lean/run/partial_fixpoint.lean
Joachim Breitner 24cb133eb2
feat: explicit defeq attribute (#8419)
This PR introduces an explicit `defeq` attribute to mark theorems that
can be used by `dsimp`. The benefit of an explicit attribute over the
prior logic of looking at the proof body is that we can reliably omit
theorem bodies across module boundaries. It also helps with intra-file
parallelism.

If a theorem is syntactically defined by `:= rfl`, then the attribute is
assumed and need not given explicitly. This is a purely syntactic check
and can be fooled, e.g. if in the current namespace, `rfl` is not
actually “the” `rfl` of `Eq`. In that case, some other syntax has be
used, such as `:= (rfl)`. This is also the way to go if a theorem can be
proved by `defeq`, but one does not actually want `dsimp` to use this
fact.

The `defeq` attribute will look at the *type* of the declaration, not
the body, to check if it really holds definitionally. Because of
different reduction settings, this can sometimes go wrong. Then one
should also write `:= (rfl)`, if one does not want this to be a defeq
theorem. (If one does then this is currently not possible, but it’s
probably a bad idea anyways).

The `set_option debug.tactic.simp.checkDefEqAttr true`, `dsimp` will
warn if could not apply a lemma due to a missing `defeq` attribute.

With `set_option backward.dsimp.useDefEqAttr.get false` one can revert
to the old behavior of inferring rfl-ness based on the theorem body.

Both options will go away eventually (too bad we can’t mark them as
deprecated right away, see #7969)

Meta programs that generate theorems (e.g. equational theorems) can use
`inferDefEqAttr` to set the attribute based on the theorem body of the
just created declaration.

This builds on #8501 to update Init to `@[expose]` a fair amount of
definitions that, if not exposed, would prevent some existing `:= rfl`
theorems from being `defeq` theorems. In the interest of starting
backwards compatible, I exposed these function. Hopefully many can be
un-exposed later again.

A mathlib adaption branch exists that includes both the meta programming
fixes and changes to the theorems (e.g. changing `:= by rfl` to `:=
rfl`).

With the module system there is now no special handling for `defeq`
theorem bodies, because we don’t look at the body anymore. The previous
hack is removed. The `defeq`-ness of the theorem needs to be checked in
the context of the theorem’s *type*; the error message contains a hint
if the defeq check fails because of the exported context.
2025-06-06 18:40:06 +00:00

401 lines
11 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

def monadic (x : Nat) : Option Unit := monadic (x + 1)
partial_fixpoint
def loop (x : Nat) : Unit := loop (x + 1)
partial_fixpoint
def monadic0 : Option Unit := monadic0
partial_fixpoint
def loop0 : Unit := loop0
partial_fixpoint
/--
info: equations:
@[defeq] theorem loop.eq_1 : ∀ (x : Nat), loop x = loop (x + 1)
-/
#guard_msgs in
#print equations loop
/-- error: unknown constant 'loop.induct' -/
#guard_msgs in
#check loop.induct
def find (P : Nat → Bool) (x : Nat) : Option Nat :=
if P x then
some x
else
find P (x +1)
partial_fixpoint
/--
info: equations:
theorem find.eq_1 : ∀ (P : Nat → Bool) (x : Nat), find P x = if P x = true then some x else find P (x + 1)
-/
#guard_msgs in
#print equations find
/--
error: failed to compile definition 'notinhabited' using `partial_fixpoint`, could not prove that the type
(n : Nat) → Nat → Fin n
is nonempty.
This process uses multiple strategies:
- It looks for a parameter that matches the return type.
- It tries synthesizing 'Inhabited' and 'Nonempty' instances for the return type, while making every parameter into a local 'Inhabited' instance.
- It tries unfolding the return type.
If the return type is defined using the 'structure' or 'inductive' command, you can try adding a 'deriving Nonempty' clause to it.
-/
#guard_msgs in
def notinhabited (n m : Nat) : Fin n :=
notinhabited n (m+1)
partial_fixpoint
/--
error: failed to compile definition 'notinhabited_mutual1' using `partial_fixpoint`, could not prove that the type
(n : Nat) → Nat → Fin n
is nonempty.
This process uses multiple strategies:
- It looks for a parameter that matches the return type.
- It tries synthesizing 'Inhabited' and 'Nonempty' instances for the return type, while making every parameter into a local 'Inhabited' instance.
- It tries unfolding the return type.
If the return type is defined using the 'structure' or 'inductive' command, you can try adding a 'deriving Nonempty' clause to it.
-/
#guard_msgs in
mutual
def notinhabited_mutual1 (n m : Nat) : Fin n :=
notinhabited_mutual2 n (m+1)
partial_fixpoint
def notinhabited_mutual2 (n m : Nat) : Fin n :=
notinhabited_mutual1 n (m+1)
partial_fixpoint
end
/--
error: Could not prove 'notTailRec1' to be monotone in its recursive calls:
Cannot eliminate recursive call `notTailRec1 (n + 1)` enclosed in
notTailRec1 (n + 1) - 1
-/
#guard_msgs in
def notTailRec1 (n : Nat) := notTailRec1 (n + 1) - 1
partial_fixpoint
/--
error: Could not prove 'notTailRec2' to be monotone in its recursive calls:
Cannot eliminate recursive call `notTailRec2 n (m + 1)` enclosed in
notTailRec2 n (m + 1) - 1
-/
#guard_msgs in
def notTailRec2 (n m : Nat) := notTailRec2 n (m + 1) - 1
partial_fixpoint
/--
error: Could not prove 'notTailRec3' to be monotone in its recursive calls:
Cannot eliminate recursive call `notTailRec3 (m + 1) n` enclosed in
notTailRec3 (m + 1) n - 1
-/
#guard_msgs in
def notTailRec3 (n m : Nat) := notTailRec3 (m + 1) n - 1
partial_fixpoint
/--
error: Could not prove 'notTailRec4a' to be monotone in its recursive calls:
Cannot eliminate recursive call `notTailRec4b (m + 1) n` enclosed in
notTailRec4b (m + 1) n - 1
-/
#guard_msgs in
mutual
def notTailRec4a (n m : Nat) := notTailRec4b (m + 1) n - 1
partial_fixpoint
def notTailRec4b (n m : Nat) := notTailRec4a (m + 1) n - 1
partial_fixpoint
end
def fib (n : Nat) := go 0 0 1
where
go i fip fi :=
if i = n then
fi
else
go (i + 1) fi (fi + fip)
partial_fixpoint
local instance (b : Bool) [Nonempty α] [Nonempty β] : Nonempty (if b then α else β) := by
split <;> assumption
def dependent1 (b : Bool) (n : Nat) : if b then Nat else Bool
:= dependent1 b (n + 1)
partial_fixpoint
def dependent2 (b : Bool) (n : Nat) : if b then Nat else Bool :=
if b then dependent2 b (n + 1) else dependent2 b (n + 1)
partial_fixpoint
def dependent2' (n : Nat) (b : Bool) : if b then Nat else Bool :=
if b then dependent2' (n + 1) b else dependent2' (n + 2) b
partial_fixpoint
def dependent2'' (n : Nat) (b : Bool) : if b then Nat else Bool :=
if _ : b then dependent2'' (n + 1) b else dependent2'' (n + 2) b
partial_fixpoint
local instance (b : Bool) [Nonempty α] [Nonempty β] : Nonempty (cond b α β) := by
cases b <;> assumption
def dependent3 (b : Bool) (n : Nat) : cond b Nat Bool :=
match b with
| true => dependent3 true (n + 1)
| false => dependent3 false (n + 2)
partial_fixpoint
mutual
def mutualUnary1 (n : Nat) : Unit := mutualUnary2 (n + 1)
partial_fixpoint
def mutualUnary2 (n : Nat) : Unit := mutualUnary1 (n + 1)
partial_fixpoint
end
mutual
def mutual1 (n m : Nat) : Unit := mutual2 (m + 1) n
partial_fixpoint
def mutual2 (n m : Nat) : Unit := mutual1 (m + 1) n
partial_fixpoint
end
mutual
def dependent2''a (n : Nat) (b : Bool) : if b then Nat else Bool :=
if _ : b then dependent2''a (n + 1) b else dependent2''b (n + 2) b
partial_fixpoint
def dependent2''b (n : Nat) (b : Bool) : if b then Nat else Bool :=
if _ : b then dependent2''a (n + 1) b else dependent2''b (n + 2) b
partial_fixpoint
end
/--
info: equations:
theorem dependent2''b.eq_1 : ∀ (n : Nat) (b : Bool),
dependent2''b n b = if x : b = true then dependent2''a (n + 1) b else dependent2''b (n + 2) b
-/
#guard_msgs in #print equations dependent2''b
/--
info: dependent2''b.eq_unfold :
dependent2''b = fun n b => if x : b = true then dependent2''a (n + 1) b else dependent2''b (n + 2) b
-/
#guard_msgs in #check dependent2''b.eq_unfold
def computeLfp' {α : Type u} [DecidableEq α] (f : αα) (x : α) : α :=
let next := f x
if x ≠ next then
computeLfp' f next
else
x
partial_fixpoint
/--
info: equations:
theorem computeLfp'.eq_1.{u} : ∀ {α : Type u} [inst : DecidableEq α] (f : αα) (x : α),
computeLfp' f x = if x ≠ f x then computeLfp' f (f x) else x
-/
#guard_msgs in #print equations computeLfp'
/--
error: Could not prove 'computeLfp'''' to be monotone in its recursive calls:
Cannot eliminate recursive call `computeLfp''' f next` enclosed in
id (computeLfp''' f next)
-/
#guard_msgs in
def computeLfp''' {α : Type u} [DecidableEq α] (f : αα) (x : α) : α :=
have next := f x
if x ≠ next then
id $ computeLfp''' f next -- NB: Error message should use correct variable name
else
x
partial_fixpoint
def whileSome (f : α → Option α) (x : α) : α :=
match f x with
| none => x
| some x' => whileSome f x'
partial_fixpoint
/--
info: equations:
theorem whileSome.eq_1.{u_1} : ∀ {α : Type u_1} (f : α → Option α) (x : α),
whileSome f x =
match f x with
| none => x
| some x' => whileSome f x'
-/
#guard_msgs in #print equations whileSome
def ack : (n m : Nat) → Option Nat
| 0, y => some (y+1)
| x+1, 0 => ack x 1
| x+1, y+1 => do ack x (← ack (x+1) y)
partial_fixpoint
/--
info: equations:
theorem ack.eq_1 : ∀ (x : Nat), ack 0 x = some (x + 1)
theorem ack.eq_2 : ∀ (x_2 : Nat), ack x_2.succ 0 = ack x_2 1
theorem ack.eq_3 : ∀ (x_2 y : Nat),
ack x_2.succ y.succ = do
let __do_lift ← ack (x_2 + 1) y
ack x_2 __do_lift
-/
#guard_msgs in #print equations ack
/--
info: ack.eq_def (x✝ x✝¹ : Nat) :
ack x✝ x✝¹ =
match x✝, x✝¹ with
| 0, y => some (y + 1)
| x.succ, 0 => ack x 1
| x.succ, y.succ => do
let __do_lift ← ack (x + 1) y
ack x __do_lift
-/
#guard_msgs in #check ack.eq_def
/--
info: ack.eq_unfold :
ack = fun x x_1 =>
match x, x_1 with
| 0, y => some (y + 1)
| x.succ, 0 => ack x 1
| x.succ, y.succ => do
let __do_lift ← ack (x + 1) y
ack x __do_lift
-/
#guard_msgs in #check ack.eq_unfold
/--
error: Could not prove 'WrongMonad.ack' to be monotone in its recursive calls:
Cannot eliminate recursive call `ack (x + 1) y` enclosed in
do
let __do_lift ← ack (x✝ + 1) y✝
ack x✝ __do_lift
Tried to apply 'Lean.Order.monotone_bind', but failed.
Possible cause: A missing `Lean.Order.MonoBind` instance.
Use `set_option trace.Elab.Tactic.monotonicity true` to debug.
-/
#guard_msgs in
def WrongMonad.ack : (n m : Nat) → Id Nat
| 0, y => pure (y+1)
| x+1, 0 => ack x 1
| x+1, y+1 => do ack x (← ack (x+1) y)
partial_fixpoint
structure Tree where cs : List Tree
def Tree.rev (t : Tree) : Option Tree := do
Tree.mk (← t.cs.reverse.mapM (Tree.rev ·))
partial_fixpoint
def Tree.rev' (t : Tree) : Option Tree := do
let mut cs := []
for c in t.cs do
cs := (← c.rev') :: cs
return Tree.mk cs
partial_fixpoint
-- These tests check that the user's variable names are preserved in the goals
/--
error: Could not prove 'VarName.computeLfp' to be monotone in its recursive calls:
Cannot eliminate recursive call `computeLfp f next` enclosed in
id (computeLfp f next)
-/
#guard_msgs in
def VarName.computeLfp {α : Type u} [DecidableEq α] (f : α → Option α) (x : α) : Option α := do
let next ← f x
if x ≠ next then
id $ computeLfp f next --NB: Error message should use correct variable name
else
x
partial_fixpoint
opaque mentionsH : ¬ b → αα := fun _ x => x
/--
error: Could not prove 'VarName.dite' to be monotone in its recursive calls:
Cannot eliminate recursive call `dite (n + 2) b` enclosed in
mentionsH this_is_my_h (dite (n + 2) b)
-/
#guard_msgs in
def VarName.dite (n : Nat) (b : Bool) : if b then Nat else Bool :=
if this_is_my_h : b then dite (n + 1) b else mentionsH this_is_my_h (dite (n + 2) b)
partial_fixpoint
/--
error: Could not prove 'Tree.rev_bad' to be monotone in its recursive calls:
Cannot eliminate recursive call `Tree.rev_bad my_name` enclosed in
id my_name.rev_bad
-/
#guard_msgs in
def Tree.rev_bad (t : Tree) : Option Tree := do
Tree.mk (← t.cs.reverse.mapM (fun my_name => id (Tree.rev_bad my_name)))
partial_fixpoint
/--
error: Could not prove 'Tree.rev''' to be monotone in its recursive calls:
Cannot eliminate recursive call `Tree.rev'' my_name` enclosed in
id (if my_idx < 0 then some my_name else my_name.rev'')
-/
#guard_msgs in
def Tree.rev'' (t : Tree) : Option Tree := do
Tree.mk (← t.cs.reverse.toArray.mapFinIdxM
(fun my_idx my_name _ => id (if my_idx < 0 then my_name else Tree.rev'' my_name))).toList
partial_fixpoint
/--
error: Could not prove 'Tree.rev'''' to be monotone in its recursive calls:
Cannot eliminate recursive call `Tree.rev''' my_tree.cs.toArray` enclosed in
ts.reverse.mapFinIdxM fun my_idx my_tree x =>
id
(if my_idx < 0 then my_tree
else do
let ts ← rev''' my_tree.cs.toArray
{ cs := ts.toList })
Tried to apply 'Lean.Order.Array.monotone_mapFinIdxM', but failed.
Possible cause: A missing `Lean.Order.MonoBind` instance.
Use `set_option trace.Elab.Tactic.monotonicity true` to debug.
-/
#guard_msgs in
def Tree.rev''' (ts : Array Tree) : Id (Array Tree) := do
ts.reverse.mapFinIdxM
(fun my_idx my_tree _ => id (if my_idx < 0 then my_tree else (Tree.rev''' my_tree.cs.toArray) >>= (fun ts => ⟨ts.toList⟩)))
partial_fixpoint
def List.findIndex (xs : List α) (p : α → Bool) : Option Nat := match xs with
| [] => none
| x::ys =>
if p x then
some 0
else
(· + 1) <$> List.findIndex ys p
partial_fixpoint
-- Applicative operator idioms
def app (n m : Nat) : Option Nat := (· + ·) <$> app (n - 1) m <*> app n (m - 1)
partial_fixpoint
def app' (n m : Nat) : Option Nat := pure (· + ·) <*> app' (n - 1) m <*> app' n (m - 1)
partial_fixpoint
def app'' {α} (n : Nat) : Option (αα) := do
let _n ← app'' (n+1) <*> pure 5
pure id
partial_fixpoint