Zulip Chat Archive

Stream: metaprogramming / tactics

Topic: Calling an `Expr` that has type `MetaM` in a `MetaM` program


Théophile (Sep 24 2025 at 21:23):

Hello!

I am currently writing a tactic, which will encounter some expressions, and I would like to allow users to associate these expressions with a MetaM function that my tactic could call. For example, users could say something like "when you encounter the term f x y z, use the metaprogram m_f: MVarId -> Expr -> MetaM Unit", and my tactic would internally call m_f.

For example something like

class HasMetaprogram {a} (x: a) where
  prog: Lean.MVarId  Lean.Expr  Lean.MetaM Unit

def f (n: Nat): Nat := n+n

instance {n}: HasMetaprogram (f (10*n)) where
  prog mvarid expr := do
    -- do something smart that assign mvarid
    -- `expr` is the expression of `f (10*n)` that may be useful
    mvarid.assign (.const ``Unit.unit [])

Now onto the question: given an expression f (10*n), I can easily get the Expr which has type Lean.MVarId → Lean.Expr → Lean.MetaM Unit, but how can I "call" this expression within my tactic?

Théophile (Sep 24 2025 at 21:24):

This is a bit of a #xy, so here is the actual problem I am trying to solve (but it is a bit long to explain).

In my development, I have defined many functions and corresponding theorems, and I wrote a tactic which, among many things, lookup for the theorem corresponding to a given function call. To this end, I am using Lean's typeclass mechanism as a database of theorems, in a fashion similar to this code:

class Theorem (x: a) (property: outParam (a  Prop)) where
  pf: property x

def f (n: Nat): Nat := n+n

instance: Theorem (f n) (fun res => res = 2*n) where
  pf := by grind [f]

then, when my tactic would encounter the expression f n, it invokes typeclass synthesis on Theorem (f n) ?property, which instantiates the metavariable ?property to fun res => res = 2*n, and uses this afterward.

In reality, things are slightly more complex, I sometimes need to associate a function with a class of theorems. This class of theorem is parametrized by another type, as shown in this code:

class Theorem (x: a) (y: b) (property: outParam (a  Prop)) where
  pf: property x

def f (n: Nat): Nat := n+n

-- the property here is a bit silly, but I do have real usecases for this in my full development
instance: Theorem (f n) (y: Nat) (fun res => res = 2*n + 0*y) where
  pf := by grind [f]

I then rely on users to provide this value y when using my tactic, which will synthetize an instance of Theorem (f n) y ?property

Now onto my question: although in some cases, the chosen value of y is a crucial argument in the proof, in some other cases, there is only one sensible value to choose for y. For example, it may be than when I encounter f n, I know there should be in the context h: p n y0, and in this case y0 is the only sensible choice for y. This value is easily found by using docs#Lean.MVarId.assumption on the goal p n ?y. In other words, I can write a MetaM that will find this value of y automatically.

I would therefore like users to be able to provide MetaM function that will find such y, which is what I asked in my first message. But maybe there is a different way toward this goal?

Aaron Liu (Sep 24 2025 at 21:34):

What happens when you get something like

instance {n}: HasMetaprogram (f (n + 1)) where
  prog mvarid expr := do
    -- do something smart that assign mvarid
    -- `expr` is the expression of `f (n + 1)` that may be useful
    if n == 0 then mvarid.assign (.const ``Unit.unit [])
    else mvarid.assign (.app (.app (.const ``id [1])
      (.const ``Unit [])) (.const ``Unit.unit []))

Aaron Liu (Sep 24 2025 at 21:38):

then n is not determined at runtime

Aaron Liu (Sep 24 2025 at 21:38):

maybe you can do something like what norm_num does

Théophile (Sep 24 2025 at 21:40):

Ah, indeed I would not be happy if provided such a metaprogram. In real scenarios I only need the Expr version of n, not its actual value. But I understand this hints toward the fact what I want may not be possible (at least not in the form of typeclass)

Théophile (Sep 24 2025 at 21:43):

I could do the following instead:

def prog_f_n_plus_1 (mvarid: MVarId) (e: Expr): MetaM Unit := do
    -- do something smart that assign mvarid
    -- `expr` is the expression of `f (n + 1)` that may be useful
    mvarid.assign (.const ``Unit.unit [])

instance {n}: HasMetaprogram (f (n + 1)) where
  prog mvarid expr := prog_f_n_plus_1

Then I can get the Name corresponding to prog_f_n_plus_1, and I don't have such dependency problem.

Théophile (Sep 24 2025 at 21:49):

maybe you can do something like what norm_num does

Ah, maybe like this snippet of code?

/-- Read a `norm_num` extension from a declaration of the right type. -/
def mkNormNumExt (n : Name) : ImportM NormNumExt := do
  let { env, opts, .. }  read
  IO.ofExcept <| unsafe env.evalConstCheck NormNumExt opts ``NormNumExt n

(NormNumExt does contain a MetaM program)

Aaron Liu (Sep 24 2025 at 22:02):

Théophile said:

maybe you can do something like what norm_num does

Ah, maybe like this snippet of code?

/-- Read a `norm_num` extension from a declaration of the right type. -/
def mkNormNumExt (n : Name) : ImportM NormNumExt := do
  let { env, opts, .. }  read
  IO.ofExcept <| unsafe env.evalConstCheck NormNumExt opts ``NormNumExt n

(NormNumExt does contain a MetaM program)

yeah like that

Théophile (Sep 25 2025 at 13:53):

I got something to work using docs#Lean.evalConstCheck, thanks!!


Last updated: Dec 20 2025 at 21:32 UTC