Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Change elaboration of `example`
Christian Merten (Feb 23 2025 at 19:01):
I want example
to add a declaration to the environment with an automatically generated name, so that I can access it later for further processing.
For this I tried the following very naive attempt: Essentially it is a macro that replaces example
by def
with an auto-generated name:
import Batteries
open Lean Elab Command
syntax (name := eExampleS) "eExample" " : "? term:10? " := " term:11 : command
macro_rules
| `(example := $r) => `(eExample := $r)
| `(example : $t := $r) => `(eExample : $t := $r)
def newLocalName : CommandElabM Name := do
let decls ← liftCoreM Batteries.Tactic.Lint.getDeclsInCurrModule
return s!"name_{(decls.filter fun name ↦ ¬ (name.isInternal)).size}".toName
def eExampleExpander : Syntax → CommandElabM Syntax
| `(eExample : $t := $r) => do
`(def $(mkIdent (← newLocalName)) : $t := $r)
| `(eExample := $r) => do
`(def $(mkIdent (← newLocalName)) := $r)
| _ => throwUnsupportedSyntax
@[command_elab eExampleS]
def eExampleElab : CommandElab := adaptExpander eExampleExpander
-- work
example : Nat := 3
example := 3
-- does not work
example (_ : False) : 1 = 1 := rfl
elab "#alldecls" : command => do
let decls ← liftCoreM Batteries.Tactic.Lint.getDeclsInCurrModule
logInfo s!"{decls.filter fun name ↦ ¬ (name.isInternal)}"
#alldecls
The issue is that this only works for a small subset of the syntax supported by the example
command. Certainly it is possible to extend this to eventually match all possible cases, but I would rather have a more principled solution that uses the example
parser directly. I found docs#Lean.Parser.Command.example, but I have no idea how to use this to write a macro / elaborator that does the above.
Christian Merten (Feb 23 2025 at 19:03):
(side note: I tried to get rid of the additional eExample
step with elab_rules
, but I could not figure out how to hook eExampleExpander
into that.)
Damiano Testa (Feb 23 2025 at 19:10):
What does this do that theorem
does not do? Is it simply giving the declaration a name that you do not know what it is?
Christian Merten (Feb 23 2025 at 19:13):
The usecase is the following: I am given a file with a bunch of example
s (and regular theorem
s, def
s) and I need to inspect all declarations including examples in this file. I could of course do some manual, text-based preprocessing to turn all example
s into def
s or theorem
s and then run the inspection, but I would rather just import a file at the top that changes how example
elaborates without touching the actual code.
Christian Merten (Feb 23 2025 at 19:14):
(This depends on my assumption that there is no way to inspect example
s, because they don't change the environment. If there is a way to do that directly, without changing the elaboration of example
, that would of course be better)
Damiano Testa (Feb 23 2025 at 19:16):
What kind of inspection do you want to do?
Damiano Testa (Feb 23 2025 at 19:17):
Also, you can take a look at Mathlib/Tactic/SuppressCompilation.lean
to see how suppress_compilation
parses example
s.
Christian Merten (Feb 23 2025 at 19:19):
Damiano Testa said:
What kind of inspection do you want to do?
I have a list of pairs of type signatures and a list of allowed axioms and I need to check if the input file contains a declaration for every entry in that list (i.e. a declaration with the given type signature) and if that declaration uses no other axioms besides the allowed axioms.
Christian Merten (Feb 23 2025 at 19:21):
Damiano Testa said:
Also, you can take a look at
Mathlib/Tactic/SuppressCompilation.lean
to see howsuppress_compilation
parsesexample
s.
Ah, thanks! This looks promising. I will report back if I could get it to work from there.
Christian Merten (Feb 23 2025 at 19:47):
Indeed, copy pasting from Mathlib/Tactic/SuppressCompilation.lean
worked:
import Batteries
import Lean
open Lean Elab Command
def newLocalName : CommandElabM Name := do
let decls ← liftCoreM Batteries.Tactic.Lint.getDeclsInCurrModule
return s!"name_{(decls.filter fun name ↦ ¬ (name.isInternal)).size}".toName
@[command_elab declaration]
def replaceExampleByDef : CommandElab := fun
| `($[$doc?:docComment]? $(attrs?)? $(vis?)? $[noncomputable]? $(unsafe?)?
$(recKind?)? example $sig:optDeclSig $val:declVal) => do
let name ← newLocalName
elabDeclaration <| ← `($[$doc?:docComment]? $(attrs?)? $(vis?)? noncomputable $(unsafe?)?
$(recKind?)? def $(mkIdent name) $sig:optDeclSig $val:declVal)
| _ => throwUnsupportedSyntax
example : Nat := 3
example := 3
example (_ : False) : 1 = 1 := rfl
elab "#alldecls" : command => do
let decls ← liftCoreM Batteries.Tactic.Lint.getDeclsInCurrModule
logInfo s!"{decls.filter fun name ↦ ¬ (name.isInternal)}"
#alldecls
Thanks again Damiano.
Damiano Testa (Feb 23 2025 at 19:49):
Looking quickly at the code above, you seem to have made all "new" declarations noncomputable
(which is a goal of suppress_compilation
), but maybe you want to preserve the same noncomputable
flag in your use-case?
Christian Merten (Feb 23 2025 at 19:51):
Oops, yes that was not intended, this should be correct:
@[command_elab declaration]
def replaceExampleByDef : CommandElab := fun
| `($[$doc?:docComment]? $(attrs?)? $(vis?)? $(noncomputable?)? $(unsafe?)?
$(recKind?)? example $sig:optDeclSig $val:declVal) => do
let name ← newLocalName
elabDeclaration <| ← `($[$doc?:docComment]? $(attrs?)? $(vis?)? $(noncomputable?)? $(unsafe?)?
$(recKind?)? def $(mkIdent name) $sig:optDeclSig $val:declVal)
| _ => throwUnsupportedSyntax
Kyle Miller (Feb 23 2025 at 19:56):
Instead of newLocalName
, there's mkAuxName `example 1
, which will generate a name like example_5
, guaranteed to be unused.
Damiano Testa (Feb 23 2025 at 20:05):
The matching can probably be simplified to
@[command_elab declaration]
def replaceExampleByDef : CommandElab := fun
| `($dms:declModifiers example $sig:optDeclSig $val:declVal) => do
let name ← newLocalName
elabDeclaration <| ← `($dms:declModifiers def $(mkIdent name) $sig:optDeclSig $val:declVal)
| _ => throwUnsupportedSyntax
Christian Merten (Feb 23 2025 at 20:22):
Yes, that works and is quite a bit more readable, thanks. While checking that things still work, I noticed that private
declarations (also def
) don't show up in my #alldecls
command from above, because they are filtered out by ¬ name.isInternal
.
Is there a more robust way to query all top level, user accessible declarations of a file, including private
ones? Just getDeclsInCurrModule
returns many auxiliary declarations.
Damiano Testa (Feb 23 2025 at 20:26):
Maybe you can look in Batteries/Tactic/OpenPrivate.lean
for private
? :smile:
Kyle Miller (Feb 23 2025 at 20:27):
Or maybe use Lean.isPrivateName
to unfilter out what's filtered out by isInternal
?
Kyle Miller (Feb 23 2025 at 20:28):
like Lean.isPrivateName n || !n.isInternal
Christian Merten (Feb 23 2025 at 20:33):
Kyle Miller said:
like
Lean.isPrivateName n || !n.isInternal
This is probably good enough for my purposes, but now contains all internal declarations of private declarations, because also _private.foo._cstage1
satisfies Lean.isPrivateName
.
Damiano Testa (Feb 23 2025 at 20:38):
Since you are already redefining the elaboration of some declarations, maybe you can redefine them all (like in suppress_compilation
) and, after each declaration has been elaborated append the type signature and axioms to an environment extension.
Damiano Testa (Feb 23 2025 at 20:39):
After that, the environment extension only contains what you added.
Christian Merten (Feb 23 2025 at 20:42):
That sounds like a good idea! I'll try it out.
Kyle Miller (Feb 23 2025 at 20:43):
@Christian Merten Ok, then theres docs#Lean.privateToUserName?
Something like let n' := (Lean.privateToUserName? n).getD n; !n'.isInternal
should work better.
Christian Merten (Feb 23 2025 at 20:51):
That works perfectly, thanks Kyle!
Christian Merten (Feb 24 2025 at 12:08):
Damiano Testa said:
Since you are already redefining the elaboration of some declarations, maybe you can redefine them all (like in
suppress_compilation
) and, after each declaration has been elaborated append the type signature and axioms to an environment extension.
I gave this a try. The main issue is that I can't figure out how to properly access the name of the added declaration. Right now I am doing this:
import Lean
import Batteries
open Lean Elab Command Batteries
def Lean.Name.isRelevant (name : Name) : Bool :=
!((Lean.privateToUserName? name).getD name).isInternal
@[command_elab declaration]
def appendDeclInfoDemo : CommandElab := fun
| `($dms:declModifiers example $sig:optDeclSig $val:declVal) => do
let declName ← mkAuxName `example 1
elabDeclaration <| ← `($dms:declModifiers def $(mkIdent declName) $sig:optDeclSig $val:declVal)
logInfo s!"added {declName}"
| stx => do
let allDeclsBefore := (← liftCoreM <| Tactic.Lint.getDeclsInCurrModule).filter Name.isRelevant
elabDeclaration stx
let allDeclsAfter := (← liftCoreM <| Tactic.Lint.getDeclsInCurrModule).filter Name.isRelevant
let newDecls := allDeclsAfter.filter (fun x ↦ x ∉ allDeclsBefore)
let declName : Name := newDecls.toList.head!
logInfo s!"added {declName}"
def foo (_ : False) : Nat := 3
example : Nat := 3
This seems to work, but is certainly not very robust. Is there a better way?
Damiano Testa (Feb 24 2025 at 13:31):
Here is a possible alternative approach:
import Lean
import Batteries
open Lean Elab Command Batteries
def Lean.Name.isRelevant (name : Name) : Bool :=
!((Lean.privateToUserName? name).getD name).isInternal
partial
def scanInfoTree : InfoTree → NameSet
| .context (.parentDeclCtx i) t => (scanInfoTree t).insert i
| .context _ t => scanInfoTree t
| .node _ ts => ts.foldl (·.union <| scanInfoTree ·) ∅
| .hole .. => ∅
def getAllNames (ts : PersistentArray InfoTree) : NameSet :=
ts.map scanInfoTree |>.foldl .union ∅
@[command_elab declaration]
def appendDeclInfoDemo : CommandElab := fun
| `($dms:declModifiers example $sig:optDeclSig $val:declVal) => do
let declName ← mkAuxName `example 1
elabDeclaration <| ← `($dms:declModifiers def $(mkIdent declName) $sig:optDeclSig $val:declVal)
let ts ← getInfoTrees
dbg_trace "Using `InfoTree`s: {(getAllNames ts).toArray}"
logInfo s!"added {declName}"
| stx => do
let allDeclsBefore := (← liftCoreM <| Tactic.Lint.getDeclsInCurrModule).filter Name.isRelevant
elabDeclaration stx
let ts ← getInfoTrees
dbg_trace "Using `InfoTree`s: {(getAllNames ts).toArray}"
let allDeclsAfter := (← liftCoreM <| Tactic.Lint.getDeclsInCurrModule).filter Name.isRelevant
let newDecls := allDeclsAfter.filter (fun x ↦ x ∉ allDeclsBefore)
let declName : Name := newDecls.toList.head!
logInfo s!"added {declName}"
def foo (_ : False) : Nat := 3
example : True := trivial
namespace X
example : True := trivial
end X
namespace Y
instance : ToString Nat where toString _ := ""
end Y
namespace _root_
theorem _root_.InRoot : True := trivial
theorem InRoot : True := trivial -- panic!
end _root_
Damiano Testa (Feb 24 2025 at 13:32):
I have not seen an example where the NameSet
that getAllNames
returns consists of more than a single name, but I honestly do not understand InfoTree
s enough to know whether this is just me not trying hard enough or whether it is a structural property.
Damiano Testa (Feb 24 2025 at 13:35):
If you can guarantee that the NameSet
consists of a single name, then you can avoid traversing all of the InfoTree
and simply return the first (non-anonymous?) name that it finds.
Damiano Testa (Feb 24 2025 at 13:37):
It also just occurred to me that the above will ignore entirely mutual
definitions: I do not know if this is something that is relevant in your use-case or not.
Christian Merten (Feb 24 2025 at 13:52):
Thanks, that looks quite a bit better, although I understand less what is going on :) Do I understand correctly that the (or one of the) PartialContextInfo
s will be of the form .parentDeclCtx parentDecl
where parentDecl
is the name of the added declaration? I am surprised by the fact that this information leaks out of the call to elabDeclaration
.
Christian Merten (Feb 24 2025 at 13:54):
Damiano Testa said:
It also just occurred to me that the above will ignore entirely
mutual
definitions: I do not know if this is something that is relevant in your use-case or not.
For now, ignoring mutual
defs is fine for my use case.
Damiano Testa (Feb 24 2025 at 13:58):
Christian Merten said:
Thanks, that looks quite a bit better, although I understand less what is going on :smile: Do I understand correctly that the (or one of the)
PartialContextInfo
s will be of the form.parentDeclCtx parentDecl
whereparentDecl
is the name of the added declaration? I am surprised by the fact that this information leaks out of the call toelabDeclaration
.
I think that this is correct, but I am not entirely sure. My vague understanding is that a parentDecl
in PartialContextInfo
contains the name of the declaration that is currently being elaborated. I would hope that such a node is always present when you are elaborating a declaration!
Christian Merten (Feb 24 2025 at 14:05):
So elabDeclaration
is updating the info tree with this information somewhere?
Damiano Testa (Feb 24 2025 at 14:05):
I think that everything that elaborates anything is expected to update the infotrees. I even think that if it does not, then it might be considered a bug!
Damiano Testa (Feb 24 2025 at 14:07):
The information that is stored in the infotrees is used, I think, by the hover information and if you hover over the name of a declaration, you see it's fully-qualified name. This suggests to me that some node of the infotree will contain that information.
Damiano Testa (Feb 24 2025 at 14:08):
I have a conjecture that any information that you may want to extract from lean code you can extract from the infotrees.
Christian Merten (Feb 24 2025 at 14:10):
Damiano Testa said:
I think that everything that elaborates anything is expected to update the infotrees. I even think that if it does not, then it might be considered a bug!
But this is up to the implementation of the specific elaborator and not automatically done by the elab
or @[command_elab]
infrastructure, right? Moving your
let ts ← getInfoTrees
dbg_trace "Using `InfoTree`s: {(getAllNames ts).toArray}"
before elabDeclaration
yields an empty output.
Damiano Testa (Feb 24 2025 at 14:22):
Ah, yes, I probably expressed myself badly: you should expect the infotrees to be updated by elabDeclaration
(or rather some of the inner elabXXX
that elabDeclaration
calls. There, I was piggy-backing the infotrees that elabDeclaration
updated. Until you call elabDeclaration
the infotrees for the declaration have not yet been created.
Damiano Testa (Feb 24 2025 at 14:24):
elabDeclaration
also takes care of updating the environment, which is what you were exploiting in your code: you were storing the declarations before elaboration and comparing them to the ones after.
Damiano Testa (Feb 24 2025 at 14:24):
In the "after" world, besides the change in the environment, there are also infotrees to play with.
Damiano Testa (Feb 24 2025 at 14:25):
When I talked about it being a bug not updating the infotrees, I meant that the low-level elabXXX
should do that for you, not that you should be doing that yourself.
Damiano Testa (Feb 24 2025 at 14:27):
(Also, do not take what I said too literally, since a lot about infotrees is obscure to me!)
Christian Merten (Feb 24 2025 at 14:30):
Thanks for the explanations, Damiano. That makes it a lot clearer.
Christian Merten (Apr 05 2025 at 17:10):
Kyle Miller said:
Instead of
newLocalName
, there'smkAuxName `example 1
, which will generate a name likeexample_5
, guaranteed to be unused.
This does not seem to interact well with namespaces, for example:
import Lean
open Lean Elab Command
@[command_elab declaration]
def replaceExampleByDef : CommandElab := fun
| `($dms:declModifiers example $sig:optDeclSig $val:declVal) => do
let name ← mkAuxName `example 1
elabDeclaration <| ← `($dms:declModifiers def $(mkIdent name) $sig:optDeclSig $val:declVal)
| _ => throwUnsupportedSyntax
namespace Foo
example : True := sorry
/--
error: 'Foo.example_1' has already been declared
-/
#guard_msgs in
example : True := sorry
end Foo
How do I make mkAuxName
namespace aware? My guess is that the check here does not first resolve the namespace of candidate
, hence it sees that example_1
does not exist, so it returns that name, which is then added as Foo.example_1
by elabDeclaration
.
(For now, I just went back to my naive newLocalName
, but I would like to know how to do it properly)
Kyle Miller (Apr 05 2025 at 17:33):
If you do let name ← `_root_ ++ mkAuxName `example 1
, I think it should work. That would have it always go into the root namespace.
Or, you could do something like
let baseName := (<- getCurrNamespace) ++ `example
let name <- mkAuxName (`_root_ ++ baseName) 1
if you want it to be inside the namespace.
Christian Merten (Apr 05 2025 at 17:46):
You probably meant let name := `_root_ ++ (← mkAuxName `example 1)
which works, thanks! The second one fails with the same error as above, though.
Kyle Miller (Apr 05 2025 at 17:48):
Yes, that was a mistake!
Kyle Miller (Apr 05 2025 at 17:50):
The second one should be
let baseName := (<- getCurrNamespace) ++ `example
let name := `_root_ ++ (<- mkAuxName baseName 1)
Christian Merten (Apr 05 2025 at 17:52):
Thanks!
Last updated: May 02 2025 at 03:31 UTC