Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Macros that introduce non-level parameters
Anthony Vandikas (Aug 07 2025 at 20:16):
The Type* macro in mathlib introduces a fresh level metavariable and then converts it to a parameter using Elab.Term.levelMVarToParam. Is there any way to achieve the same thing for normal (non-level) parameters?
For instance, I'd like to write a macro HOM* such that
def foo {Hom : HOM*} (x : Hom A B) := ...
expands to
def foo.{u} {Hom : Obj → Obj → Type u} (x : Hom A B) := ...
which then becomes (via autoImplicits)
def foo.{u} {Obj : Type v} {A B : Obj} {Hom : Obj → Obj → Type u} (x : Hom A B) := ...
so that I may avoid declaring Obj whenever possible. Such a thing might also be useful for implementing a crude version of RFC: reimplementing class abbrev #8279.
Kyle Miller (Aug 07 2025 at 20:27):
This works, so long as Obj isn't a constant that's already in scope:
macro "HOM*" : term => do
let o := mkIdent `Obj
`($o → $o → Type _)
example (hom : HOM*) : True := by
/-
Obj : Sort u_1
hom : Obj → Obj → Type u_2
⊢ True
-/
sorry
Kyle Miller (Aug 07 2025 at 20:28):
The mkIdent prevents the identifier from being made hygienic, and autoImplicits checks to see that identifiers are not hygienic (I think because that's a proxy for "written by the user")
Anthony Vandikas (Aug 07 2025 at 21:42):
Thank you! This was exactly what I needed to proceed.
Aside, I needed the names to be unique as I sometimes need to deal with more than one HOM*. I achieved this by re-implementing this using the elab command and generating the names based off a global counter.
open Lean Elab.Term in
elab "HOM[*]" : term => do
let e ← getEnv
let n := objCounter.getState e
setEnv (objCounter.setState e (n + 1))
let o := mkIdent (.str .anonymous ("Obj" ++ n.toSubscriptString))
elabTerm (←`($o → $o → Type _)) .none
In another file:
initialize objCounter : Lean.EnvExtension Nat ← Lean.registerEnvExtension (pure 0)
Robin Arnez (Aug 07 2025 at 21:47):
Do you need to refer to the name of the Obj? If yes then it feels like this is a bit weird (a global counter? won't that throw off everything if you add HOM[*] somewhere?), if no then `(Obj → Obj → Type*) would also do the trick
Anthony Vandikas (Aug 07 2025 at 21:50):
I don't need to refer to the name. When I do, I have an alternate macro HOM[Obj] which is just equivalent to Obj → Obj → Type*.
if no then
`(Obj → Obj → Type*)would also do the trick
If I do that, I get unknown identifier 'Obj✝' at the use site.
Robin Arnez (Aug 07 2025 at 21:58):
I'm confused; you say you sometimes have more than one; how do you distinguish?
Robin Arnez (Aug 07 2025 at 22:00):
I mean at the use site; I suppose you won't just x : Obj_12754
Robin Arnez (Aug 07 2025 at 22:01):
Oh wait you mean that hygienic identifiers don't get auto-implicits? That might be the case
Robin Arnez (Aug 07 2025 at 22:08):
Hmm you're right it might not be as easy to make something like this if you don't do what you did
Kyle Miller (Aug 07 2025 at 22:17):
If you're using a term elaborator, then you have access to the local context, and you can avoid using a global counter by instead finding an unused name.
Kyle Miller (Aug 07 2025 at 22:21):
Robin Arnez said:
Oh wait you mean that hygienic identifiers don't get auto-implicits? That might be the case
Kyle Miller said:
autoImplicits checks to see that identifiers are not hygienic
In particular it checks that the name is an atomic string. (isValidAutoBoundImplicitName)
Last updated: Dec 20 2025 at 21:32 UTC