Zulip Chat Archive
Stream: lean4
Topic: recall command
Johan Commelin (Jun 16 2023 at 04:13):
For expository purposes, I think it's very nice if you can sometimes "recall" a definition. How hard would it be to have a recall
command that behaves as follows
recall HasFDerivAtFilter (f : E → F) (f' : E →L[𝕜] F) (x : E) (L : Filter E) :=
(fun x' => f x' - f x - f' (x' - x)) =o[L] fun x' => x' - x
and the only thing it will do is roughly a #check
. So it checks that HasFDerivAtFilter
exists, and that the type that is written down after recall
is defeq to the existing type in the library.
Johan Commelin (Jun 16 2023 at 04:15):
Sorry, it should be a bit more than a #check
. Because for def
s it shouldn't just check that the types are defeq, but also the terms.
When recalling a theorem/lemma statement, it is of course enough that the types are defeq.
Johan Commelin (Jun 16 2023 at 04:15):
When recalling a theorem statement, recalling the proof should be optional.
Damiano Testa (Jun 16 2023 at 05:44):
Should the order of hypotheses matter?
Damiano Testa (Jun 16 2023 at 05:47):
In your example, for instance, if I recall
ed x
before f
, I would imagine that you would still want the check to pass, right?
Johan Commelin (Jun 16 2023 at 05:48):
No, it doesn't have to be sophisticated like that.
Johan Commelin (Jun 16 2023 at 05:49):
It should really be a way to recall a definition or theorem that will be used in some file that gives an expository account of some maths in Lean.
Damiano Testa (Jun 16 2023 at 05:49):
A possible approach to the question of exact recalling is to interpret recall
as theorem
, catch the existing declaration
error, check defeq of the current and the found declaration and report the correct error/message.
Johan Commelin (Jun 16 2023 at 05:49):
In fact, I think recall
may be quite pedantic about how well the statement matches the "original"
Johan Commelin (Jun 16 2023 at 05:50):
What I find important is that I can use the exact same name as the original.
Johan Commelin (Jun 16 2023 at 05:50):
So recall
shouldn't attempt to add any new decl to the environment
Damiano Testa (Jun 16 2023 at 05:51):
So you would really like something like: if you printedtyped the output of #check
up to defeq, it should pass, otherwise fail?
Johan Commelin (Jun 16 2023 at 05:55):
yes, but with def
s I don't just want to check the types but also the terms
Johan Commelin (Jun 16 2023 at 05:55):
recall Complex.exp : \C \to \C := id
should give an error.
Johan Commelin (Jun 16 2023 at 05:55):
Or recall Real.pi : \R := 0
Damiano Testa (Jun 16 2023 at 05:57):
Ok, these would be all things that you can enforce after you have extracted the actual declaration from the environment. I do not have much experience in doing this, but I am up for trying to implement it!
Siddhartha Gadgil (Jun 16 2023 at 05:58):
I can also try this. What would be nice is to also have a code action after a #check
that can turn it into a recall.
Damiano Testa (Jun 16 2023 at 05:59):
One more thing: in your initial recall example, there were no Type
or instances: I was assuming that they were present thanks to a previous variable
command. Is this the case or you really just want a sublist of the arguments?
Damiano Testa (Jun 16 2023 at 05:59):
Siddhartha, I had the same idea!
Johan Commelin (Jun 16 2023 at 06:02):
@Damiano Testa That's right, they were present thanks to an implicit variable
command.
Kyle Miller (Jun 16 2023 at 06:03):
@Johan Commelin
syntax "recall " ident ppIndent(optDeclSig) declVal : command
open Lean Elab Command in
elab_rules : command
| `(recall $id $sig:optDeclSig $val) => withoutModifyingEnv <| do
let name ← mkAuxName id.getId 1
let id' := mkIdentFrom id name
elabCommand <| ← `(def $id':declId $sig:optDeclSig $val)
elabCommand <| ← `(example : @$id = @$id' := rfl)
recall id (x : α) : α := x
recall id (x : α) : ℕ := 0
/-
type mismatch
@id_1
has type
{α : Sort ?u.8944} → α → ℕ : Type ?u.8944
but is expected to have type
{α : Sort ?u.8943} → α → α : Sort (imax (?u.8943 + 1) ?u.8943)
-/
Kyle Miller (Jun 16 2023 at 06:04):
def foo := 22
recall foo := 22
recall foo := 23
/-
type mismatch
rfl
has type
foo = foo : Prop
but is expected to have type
foo = foo_1 : Prop
-/
Johan Commelin (Jun 16 2023 at 06:04):
Nice! And can I recall theorem statements without giving a proof?
Kyle Miller (Jun 16 2023 at 06:06):
Ah, not yet
Scott Morrison (Jun 16 2023 at 06:10):
Presumably just recall foo
should generate Try this: recall foo := 22
.
Damiano Testa (Jun 16 2023 at 06:12):
Or even already #check foo
should have the option.
Johan Commelin (Jun 16 2023 at 06:12):
Or otherwise a recall?
command could do that.
Kyle Miller (Jun 16 2023 at 06:16):
New version:
syntax "recall " ident ppIndent(optDeclSig) (declVal)? : command
open Lean Elab Command in
elab_rules : command
| `(recall $id $sig:optDeclSig $val) => withoutModifyingEnv <| do
let id' := mkIdentFrom id (← mkAuxName id.getId 1)
elabCommand <| ← `(noncomputable def $id':declId $sig:optDeclSig $val)
elabCommand <| ← `(example : @$id = @$id' := rfl)
| `(recall $id $sig:optDeclSig) => withoutModifyingEnv <| do
let mySorry := mkIdentFrom id (← mkAuxName `mySorry 1)
let id' := mkIdentFrom id (← mkAuxName id.getId 1)
elabCommand <| ← `(axiom $mySorry {α : Sort _} : α)
elabCommand <| ← `(noncomputable def $id':declId $sig:optDeclSig := $mySorry)
elabCommand <| ← `(example : type_of% @$id' := @$id)
recall id (x : α) : α := x
recall id (x : α) : ℕ := 0
/-
type mismatch
@id_1
has type
{α : Sort ?u.8944} → α → ℕ : Type ?u.8944
but is expected to have type
{α : Sort ?u.8943} → α → α : Sort (imax (?u.8943 + 1) ?u.8943)
-/
recall id (_x : α) : α
def foo := 22
recall foo := 22
recall foo : ℕ
recall foo := 23
/-
type mismatch
rfl
has type
foo = foo : Prop
but is expected to have type
foo = foo_1 : Prop
-/
recall Nat.add_comm (n m : Nat) : n + m = m + n
Kyle Miller (Jun 16 2023 at 06:19):
The way the no-value case works is that it creates the temporary definition using a custom sorry axiom (otherwise def
reports "uses sorry" as a warning), and then there's a check that the type of this temporary definition is correct.
Kyle Miller (Jun 16 2023 at 06:21):
Both cases are using the def
command itself so that you get all the usual binder and value elaboration rules without any extra work.
Kyle Miller (Jun 16 2023 at 06:24):
One problem with this is that it doesn't give an error if your recollection of the binder types is wrong:
recall Nat.add_comm (n m : Nat) : n + m = m + n
recall Nat.add_comm {n m : Nat} : n + m = m + n
Mac Malone (Jun 18 2023 at 20:28):
Here is a funny error message:
axiom bar : Nat
recall bar := 5
/-
type mismatch
rfl
has type
bar = bar : Prop
but is expected to have type
bar = bar_1 : Prop
-/
Mac Malone (Jun 18 2023 at 20:32):
(I was thinking that the proper answer was that bar has no definitional value, but maybe saying it is not equal is sufficient?)
Mac Malone (Jun 18 2023 at 20:46):
Anyway, I stumbled onto this topic today and having recently learned how Lean elaborates the headers of mutual definitions, I wanted to give a more nuts and bolts version of @Kyle Miller's solution a try. The end result does not use sorry and elides the implementation details from its error messages. So, here is what I came up with:
import Lean
import Std.Tactic.OpenPrivate
open private elabHeaders from Lean.Elab.MutualDef
syntax "recall " ident ppIndent(optDeclSig) (declVal)? : command
open Lean Meta Elab Command Term in
elab_rules : command
| `(recall $id $sig:optDeclSig $[$val?]?) => withoutModifyingEnv do
let some info := (← getEnv).find? id.getId
| throwError "unknown constant '{id}'"
let id' := mkIdentFrom id (← mkAuxName id.getId 1)
if let some val := val? then
let some infoVal := info.value?
| throwErrorAt val "constant '{id}' has no defined value"
elabCommand <| ← `(noncomputable def $id':declId $sig:optDeclSig $val)
let some newInfo := (← getEnv).find? id'.getId | return -- def already threw
runTermElabM fun _ => do
let mvs ← newInfo.levelParams.mapM fun _ => mkFreshLevelMVar
let newType := newInfo.type.instantiateLevelParams newInfo.levelParams mvs
unless (← isDefEq info.type newType) do
throwTypeMismatchError none info.type newInfo.type (mkConst id.getId)
let newVal := newInfo.value?.get!.instantiateLevelParams newInfo.levelParams mvs
unless (← isDefEq infoVal newVal) do
throwErrorAt val "value mismatch{indentD id.getId}\nhas value{indentExpr newVal}\nbut is expected to have value{indentExpr infoVal}"
else
let (binders, type?) := expandOptDeclSig sig
let views := #[{
declId := id', binders, type?, value := .missing,
ref := ← getRef, kind := .theorem, modifiers := {}
: DefView}]
runTermElabM fun _ => do
let elabView := (← elabHeaders views)[0]!
unless (← isDefEq info.type elabView.type) do
throwTypeMismatchError none info.type elabView.type (mkConst id.getId)
recall id (x : α) : α := x
recall id (x : α) : Nat := 0
/-
type mismatch
id
has type
{α : Sort u_1} → α → Nat : Type u_1
but is expected to have type
{α : Sort u} → α → α : Sort (imax (u + 1) u)
-/
recall id (_x : α) : α
def foo := 22
recall foo := 22
recall foo : Nat
recall foo := 23
/-
value mismatch
foo
has value
23
but is expected to have value
22
-/
recall Nat.add_comm (n m : Nat) : n + m = m + n
recall Nat.add_comm {n m : Nat} : n + m = m + n
axiom bar : Nat
recall bar := bar
/- constant 'bar' has no defined value -/
Scott Morrison (Jun 18 2023 at 22:33):
LGTM! PR?
Johan Commelin (Jun 19 2023 at 16:51):
@Mac Nice! That looks cool.
Johan Commelin (Jun 19 2023 at 16:53):
The thing with binder types is a tidbit unfortunate, but absolutely not a deal breaker, I think
Mac Malone (Jun 20 2023 at 01:22):
Scott Morrison said:
LGTM! PR?
Were should something like this be PR'd to? Std or Mathlib?
Scott Morrison (Jun 20 2023 at 01:25):
I would say Mathlib
. There is both higher reviewing bandwidth, and more inclination towards "are we sure we really want this? let's find out!". Usually we wait for a specific reason (or Mario just wanting to) to move things up to Std.
Mac Malone (Jun 20 2023 at 01:30):
@Scott Morrison Perhaps surprisingly, this would actually be my first PR to Mathlib. I believe I have learned through osmosis here on the Zulip channel that PR's to mathlib are bit weird in that they generally come from branches on the repository rather than forks. Do I have that right, and how exactly does that work?
Scott Morrison (Jun 20 2023 at 01:39):
@Mac, yes, that's right. Our CI works better from a branch of the main repo. I've just sent an invite giving your write access to the mathlib4 repo.
Scott Morrison (Jun 20 2023 at 01:40):
(And yes, this is lame, and ideas for fixing it are welcome! I think the main issue is that CI needs to upload oleans to the global cache. :-)
Mac Malone (Jun 20 2023 at 03:25):
PR is up: !4#5278 (@Scott Morrison, I tagged you in case I made any mistakes in the process.)
Scott Morrison (Jun 20 2023 at 03:29):
Looks great. Left a query.
Scott Morrison (Jun 20 2023 at 05:16):
Ah -- one more thing, @Mac: you should add the awaiting-review
label, or otherwise it won't make it onto the #queue4 and might never get merged. :-)
Scott Morrison (Jun 20 2023 at 05:16):
The other useful label to for authors to frequently add is awaiting-CI
, which will automatically be removed again once you get a green tick.
Mac Malone (Jun 20 2023 at 05:18):
@Scott Morrison Thanks and thanks again for all your help!
Patrick Massot (Jul 19 2023 at 12:34):
@Mac I tried to use the recall
command today and the version where users give only an identifier very rarely succeeds. For instance in:
recall add_assoc {G : Type _} [AddSemigroup G] (a b c : G) : a + b + c = a + (b + c)
recall add_assoc
The first call succeeds but not the second one which says
Error updating: Error fetching goals: Rpc error: InternalError: incorrect number of universe levels add_assoc.
Patrick Massot (Jul 19 2023 at 12:35):
recall Nat.add_assoc
works but this is pretty much the only kind of example where I've able to get an answer.
Jannis Limperg (Jul 19 2023 at 12:37):
The error message makes me suspect that recall
turns the name add_assoc
into an Expr.const
, but without specifying appropriate universe levels (should probably be level metavariables). Nat.add_assoc
doesn't have universe levels, so there it works.
Patrick Massot (Jul 19 2023 at 12:42):
Yes, this is exactly what it sounds like.
Mac Malone (Jul 19 2023 at 12:42):
@Patrick Massot @Jannis Limperg Is right, I forgot to instantiate the level params in the identifier-only branch. Working on a fix now.
Patrick Massot (Jul 19 2023 at 12:42):
Thanks!
Mac Malone (Jul 19 2023 at 13:21):
Hmm, having worked on this somewhat. I am not sure my original diagnosis was correct. Instantiating the level params does not seem to fix the issue, and further debugging reveals the error only appears in interactive mode. Thus, the server seems to assuming some invariant that I am violating that the normal lean frontend does not.
Mac Malone (Jul 19 2023 at 13:37):
Found the problem: it had nothing to do with main recall command logic, was actual the term info node that was added to the info tree for go-to-def that was causing the error. Apparently the server checks that it has the right number of level parameters and I had not been providing them.
Mac Malone (Jul 19 2023 at 13:39):
I am, however, confused why this error only occurred on the ident-only branch and not the normal one.
Mac Malone (Jul 19 2023 at 14:01):
PR: #6004
Mac Malone (Jul 19 2023 at 14:14):
As an aside, #guard_msgs
seems broken in VSCode (i.e., it errors there with what appears to be the same message, but not on the CLI).
Kyle Miller (Jul 19 2023 at 14:18):
@Mac What are you reporting about #guard_msgs
?
Mac Malone (Jul 19 2023 at 14:50):
@Kyle Miller #guard_msgs
is broken in Windows VSCode. That is, it reports a mismatch when there is none. For instance, I opened up e.g. mathlib's test/Explode.lean
in VSCode and all the #guard_msgs
commands error. I think the problem may be due to the embedded expression interactivity metadata because simple string messages like the last two #guard_msgs
test cases in test/Recall.lean
work correctly.
Kyle Miller (Jul 19 2023 at 14:51):
It's fine in VS Code on Linux. I wonder what the difference between Linux and Windows might be?
Kyle Miller (Jul 19 2023 at 14:52):
Maybe line endings need to be normalized? The test/Recall.lean
examples both have no newlines.
Kyle Miller (Jul 19 2023 at 14:53):
Are you saying that the tests pass in the CLI on Windows? Is this any sort of unix-like environment you're working in?
Mac Malone (Jul 19 2023 at 15:52):
@Kyle Miller Yeah, line endings could definitely be it. My CLI is MSYS2 (so Unix-like).
Mac Malone (Jul 19 2023 at 15:53):
Yep, went and checked -- it was the line endings. Likely an artifact of Git autocrlf
.
Mac Malone (Jul 22 2023 at 00:18):
The fix PR seems to be idling (#6004). @Patrick Massot, is there anything I should do to get eyes on it, or is this just the normal waiting game?
Johan Commelin (Jul 22 2023 at 05:35):
Meta PRs usually sit in the queue for a bit longer... there are fewer people qualified to review them.
Patrick Massot (Jul 22 2023 at 10:04):
I merged the PR since having no error message is better. But actually it made me realize the version giving only a name does nothing useful at all. I wrote a crappy version of what I had in mind in this case:
import Mathlib.Analysis.Calculus.ContDiff
open Lean Std Tactic TryThis
/-- Dumb version of `addSuggestion` which suggest to write a String instead of
valid syntax. -/
def Std.Tactic.TryThis.addTextSuggestion (ref : Syntax) (msg : String := "") : MetaM Unit := do
logInfoAt ref m!"Try this: {msg}"
if let some range := ref.getRange? then
let map ← getFileMap
let start := findLineStart map.source range.start
let body := map.source.findAux (· ≠ ' ') range.start start
let text := Format.prettyExtra msg
(indent := (body - start).1) (column := (range.start - start).1)
let stxRange := ref.getRange?.getD range
let stxRange :=
{ start := map.lineStart (map.toPosition stxRange.start).line
stop := map.lineStart ((map.toPosition stxRange.stop).line + 1) }
let range := map.utf8RangeToLspRange range
let json := Json.mkObj [("suggestion", text), ("range", toJson range), ("info", "")]
Widget.saveWidgetInfo ``tryThisWidget json (.ofRange stxRange)
/-- Successively perform all string replacements in order. Slow. -/
def String.replace_list (s : String) : List (String × String) → String
| [] => s
| h::tail => (s.replace h.1 h.2).replace_list tail
/-- Try to remove distracting bits of the output of `#check`. This is
really a workaround which should be replaced by something smarter. -/
def String.fix_decl (s : String) : String :=
s.replace_list [(".{u_1}",""), (".{u_1, u_2}",""), (".{u_1, u_2, u_3}",""),
("Type u_1", "Type _"), ("Type u_2", "Type _"), ("Type u_3", "Type _"),
("inst✝ : ", ""), ("inst✝¹ : ", ""), ("inst✝² : ", ""), ("inst✝³ : ", ""),
("inst✝⁴ : ", ""), ("inst✝⁵ : ", ""), ("inst✝⁶ : ", ""), ("inst✝⁷ : ", ""),
("inst✝⁸ : ", ""), ("inst✝⁹ : ", "")]
namespace Mathlib.Tactic.Recall
/--
The `recall` command redeclares a previous definition for illustrative purposes.
This can be useful for files that give an expository account of some theory in Lean.
The syntax of the command mirrors `def`, so all the usual bells and whistles work.
```
recall List.cons_append (a : α) (as bs : List α) : (a :: as) ++ bs = a :: (as ++ bs) := rfl
```
Also, one can leave out the body.
```
recall Nat.add_comm (n m : Nat) : n + m = m + n
```
The command verifies that the new definition type-checks and that the type and value
provided are definitionally equal to the original declaration. However, this does not
capture some details (like binders), so the following works without error.
```
recall Nat.add_comm {n m : Nat} : n + m = m + n
```
-/
syntax (name := recall) "recall " ident ppIndent(optDeclSig) (declVal)? : command
open Lean Meta Elab Command Term
open private elabHeaders from Lean.Elab.MutualDef
elab_rules : command
| `(recall%$tk $id $sig:optDeclSig $[$val?]?) => withoutModifyingEnv do
let declName := id.getId
let some info := (← getEnv).find? declName
| throwError "unknown constant '{declName}'"
let declConst : Expr := mkConst declName <| info.levelParams.map Level.param
discard <| liftTermElabM <| addTermInfo id declConst
let (binders, _type?) := expandOptDeclSig sig
if binders[0].isMissing && val?.isNone then -- User didn't provide any info
let foo ← liftTermElabM <| PrettyPrinter.ppSignature declName
liftTermElabM <| addTextSuggestion tk s!"recall {(toString foo.fmt).fix_decl}"
let newId := mkIdentFrom id (← mkAuxName declName 1)
if let some val := val? then
let some infoVal := info.value?
| throwErrorAt val "constant '{declName}' has no defined value"
elabCommand <| ← `(noncomputable def $newId $sig:optDeclSig $val)
let some newInfo := (← getEnv).find? newId.getId | return -- def already threw
liftTermElabM do
let mvs ← newInfo.levelParams.mapM fun _ => mkFreshLevelMVar
let newType := newInfo.type.instantiateLevelParams newInfo.levelParams mvs
unless (← isDefEq info.type newType) do
throwTypeMismatchError none info.type newInfo.type declConst
let newVal := newInfo.value?.get!.instantiateLevelParams newInfo.levelParams mvs
unless (← isDefEq infoVal newVal) do
let err :=
m!"value mismatch{indentExpr declConst}\nhas value{indentExpr newVal}\n" ++
m!"but is expected to have value{indentExpr infoVal}"
throwErrorAt val err
else
let (binders, type?) := expandOptDeclSig sig
let views := #[{
declId := newId, binders, type?, value := .missing,
ref := ← getRef, kind := default, modifiers := {}
: DefView}]
liftTermElabM do
let elabView := (← elabHeaders views)[0]!
unless (← isDefEq info.type elabView.type) do
throwTypeMismatchError none info.type elabView.type declConst
recall add_assoc
There are many crappy things here. First the "Try this" replacement doesn't really work because it does not remove the identifier. This is probably easy to fix. More importantly, the two String functions at top are really dumb.
Patrick Massot (Jul 22 2023 at 10:05):
Note the new code in the recall
command itself is only
if binders[0].isMissing && val?.isNone then -- User didn't provide any info
let foo ← liftTermElabM <| PrettyPrinter.ppSignature declName
liftTermElabM <| addTextSuggestion tk s!"recall {(toString foo.fmt).fix_decl}"
Last updated: Dec 20 2023 at 11:08 UTC