Zulip Chat Archive

Stream: metaprogramming / tactics

Topic: How `intro` works with mvars


Chase Norman (Oct 26 2025 at 21:06):

The tactic intro x makes it seem like we assigned the main goal to fun x => ?M for some new metavariable ?M which contains x in the local context. This metavariable is the goal state after the tactic. However, it appears this is not possible, as x would need to be a bvar for ?M, not an fvar. Here's an experiment I ran with mkLetFVars, which is used in the implementation of intro:

import Lean
open Lean Meta Expr

#eval (do
  let goal  mkFreshExprMVar (mkConst ``Nat)
  withLetDecl `test (mkConst ``Nat) ( mkFreshExprMVar (mkConst ``Nat)) fun fvar => do
    let inner  mkFreshExprMVar (mkConst ``Nat)
    dbg_trace inner
    -- ?_uniq.22524
    goal.mvarId!.assign ( mkLambdaFVars #[fvar] (usedLetOnly := false) inner)
    dbg_trace ( instantiateMVars goal)
    -- let test : Nat := ?_uniq.22522; ?_uniq.22525
    inner.mvarId!.assign fvar
    dbg_trace ( instantiateMVars goal)
    -- let test : Nat := ?_uniq.22522; ?_uniq.22525
)

Rather than assigning goal to a let definition with body inner, it creates a new metavariable that does not respond to updates in inner. This is sensible, but it implies that the tactics after intro have to be evaluated first, before the mkLambdaFVars is applied. This is surprising to me, as I thought tactics were run in order.

My main question is: is it possible to create a function that assigns a metavariable to a let definition of other metavariables, where the latter has an extended local context, and where assignments of those metavariables would be propagated?

Aaron Liu (Oct 26 2025 at 21:11):

this is the delayed assignment thing

Aaron Liu (Oct 26 2025 at 21:11):

tactics are run in order I'm pretty sure

Aaron Liu (Oct 26 2025 at 21:12):

it's just delayed assigned mvars are weird

Aaron Liu (Oct 26 2025 at 21:12):

Chase Norman said:

My main question is: is it possible to create a function that assigns a metavariable to a let definition of other metavariables, where the latter has an extended local context, and where assignments of those metavariables would be propagated?

can you give some pseudocode of what you want to assign

Chase Norman (Oct 26 2025 at 21:14):

import Lean
open Lean Meta Expr

def assignLet (mvar : MVarId) (name : Name) (type : Expr) : MetaM (MVarId × MVarId) := do
  let value  mkFreshExprMVar type
  withLetDecl name type value fun fvar => do
    let body  mkFreshExprMVar ( mvar.getType)
    mvar.assign ( mkLambdaFVars #[fvar] (usedLetOnly := false) value)
    return (body.mvarId!, value.mvarId!)

Chase Norman (Oct 26 2025 at 21:14):

I want this but something that would actually work

Chase Norman (Oct 26 2025 at 21:16):

Just edited it to return the new mvars

Chase Norman (Oct 26 2025 at 21:16):

The idea would be that then you could present these mvars to a user, and they would be assigned later

Aaron Liu (Oct 26 2025 at 21:25):

ok now I'm confused this mvar is weird

Aaron Liu (Oct 26 2025 at 21:28):

the mvar structure has some questionable design choices

Aaron Liu (Oct 26 2025 at 21:28):

I think you assigned an mvar that was already assigned and that's why it wasn't updating

Aaron Liu (Oct 26 2025 at 21:29):

specifically, the mkLambdaFVars in your first example assigned inner

Aaron Liu (Oct 26 2025 at 21:29):

so when you assigned it again it broke the thing

Chase Norman (Oct 26 2025 at 21:31):

That appears to be true. I want inner to be just like what intro does, unassigned and presentable to a user for assignment.

Aaron Liu (Oct 26 2025 at 21:31):

look it's already assigned

import Lean
open Lean Meta Expr

#eval (do
  let goal  mkFreshExprMVar (mkConst ``Nat)
  withLetDecl `test (mkConst ``Nat) ( mkFreshExprMVar (mkConst ``Nat)) fun fvar => do
    let inner  mkFreshExprMVar (mkConst ``Nat)
    dbg_trace inner
    -- ?_uniq.22524
    goal.mvarId!.assign ( mkLambdaFVars #[fvar] (usedLetOnly := false) inner)
    dbg_trace ( instantiateMVars goal)
    -- let test : Nat := ?_uniq.22522; ?_uniq.22525
    dbg_trace  inner.mvarId!.isAssigned
    -- true
)

Aaron Liu (Oct 26 2025 at 21:31):

if you want it to not be assigned then maybe making it synthetic opaque will do something

Aaron Liu (Oct 26 2025 at 21:32):

since that's what tactics work on

Aaron Liu (Oct 26 2025 at 21:33):

docs#Lean.MetavarKind.syntheticOpaque

Chase Norman (Oct 26 2025 at 21:34):

This is progress:

let test : Nat := ?_uniq.2026; ?_uniq.2029 test
let test : Nat := ?_uniq.2026; test

Chase Norman (Oct 26 2025 at 21:34):

I guess you can't expect the top to read just ?inner because that breaks the Expr invariant

Aaron Liu (Oct 26 2025 at 21:36):

wait what's the expr invariant

Chase Norman (Oct 26 2025 at 21:36):

That test should be bound in the body

Chase Norman (Oct 26 2025 at 21:37):

Meaning any mvar that uses it has to be passed it as an argument

Chase Norman (Oct 26 2025 at 21:37):

Is there a way to instantiateMVars that also greedily instantiates delayed mvars? It would be nice to be able to show the partial assignment, even though there are still mvars around

Aaron Liu (Oct 26 2025 at 21:39):

nope can't find anything

Aaron Liu (Oct 26 2025 at 21:39):

there is a pp option pp.mvars.delayed

Aaron Liu (Oct 26 2025 at 21:39):

it seems to be doing something

Chase Norman (Oct 26 2025 at 21:40):

I see no effect


Last updated: Dec 20 2025 at 21:32 UTC