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