Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Qq context in run_tac
Mirek Olšák (Jun 26 2025 at 01:44):
I thought it could be qute, if the run_tac script directly obtained context from the proof state in the form of expressions, or even better quoted expressions. I could get bare expressions working. I thought I could also get quoted expressions by replacing q(Expr) with q(Quoted $t) but that just breaks everything. Am I missing something simple, or is there a deeper difficulty?
import Lean
import Qq
open Lean Meta Elab Tactic Qq
elab "run_taq" e:doSeq : tactic => do
let lctx ← getLCtx
let mut metaCtx := LocalContext.empty
let mut fs : Array Expr := #[]
for decl in lctx do
if decl.kind != default then continue
let fid : FVarId := decl.fvarId -- reuse the same fvarId, hopefully ok
let f : Expr := (Expr.fvar fid)
let t : Expr := decl.type
let metaDecl := LocalDecl.ldecl
metaCtx.size fid decl.userName
q(Expr) q($f)
true default
metaCtx := metaCtx.addDecl metaDecl
fs := fs.push f
let typeExpr := (mkApp (Lean.mkConst ``TacticM) (Lean.mkConst ``Unit))
let tacExpr : Expr ← withLCtx metaCtx #[] do
let body ← Term.elabTerm (← `(discard do $e)) typeExpr
mkLetFVars fs body
let tac ← unsafe evalExpr (TacticM Unit) typeExpr tacExpr
tac
example (a b : Nat) (h : a = b) : True := by
run_taq
have a : Q(Nat) := a
logInfo "a"
have b : Q(Nat) := b
have h : Q($a = $b) := h
have p : Q(Prop) := q($a = $b)
let t ← inferType h
logInfo t
logInfo a
logInfo h
logInfo p
logInfo <| toString p
logInfo <| toString <| ← isDefEq p t
trivial
Mirek Olšák (Jun 27 2025 at 10:32):
Ok, I was a bit too naive -- Qq builds references to external variables differently than just by quoting an Expr. Now I am thinking of using Qq.Impl.quoteExpr with carefully setting the exprBackSubst, I will let know when I get it working.
Eric Wieser (Jun 27 2025 at 10:44):
To check I understand, you want run_tacq to create a meta-time context with quoted copies of each variable?
Eric Wieser (Jun 27 2025 at 10:46):
I think you need to insert a have rather than a let
Eric Wieser (Jun 27 2025 at 11:30):
Ultimately though I think this is a great idea, though probably this makes more sense as a by_elabQ in MetaM Q(expected_type)
Mirek Olšák (Jun 27 2025 at 11:43):
Eric Wieser said:
I think you need to insert a
haverather than alet
That might turn out to be necessary, I was experimenting with cdecl instead of ldecl too. The have analogue to mkLetFVars, is just mkLambdaFVars & mkAppN?
Mirek Olšák (Jun 27 2025 at 11:43):
But first, I need to quote the expression correctly.
Mirek Olšák (Jun 27 2025 at 11:53):
Eric Wieser said:
Ultimately though I think this is a great idea, though probably this makes more sense as a
by_elabQinMetaM Q(expected_type)
Sure, by_elab could have this feature too, so it makes sense to write a code generally to support both cases. Originally, I just wanted it for more convenient experiments in TacticM.
Eric Wieser (Jun 27 2025 at 11:54):
Here's a cdecl version:
elab "run_taq" e:doSeq : tactic => do
let lctx ← getLCtx
let mut metaCtx := LocalContext.empty
let mut fs : Array Expr := #[]
let mut vals : Array Expr := #[]
for decl in lctx do
if decl.kind != default then continue
let fid : FVarId ← mkFreshFVarId
let f : Expr := (Expr.fvar fid)
let t : Expr := decl.type
let metaDecl := LocalDecl.cdecl
metaCtx.size fid decl.userName
q(Expr)
default default
metaCtx := metaCtx.addDecl metaDecl
fs := fs.push f
vals := vals.push (.fvar decl.fvarId)
let tacExpr : Expr ← withLCtx metaCtx #[] do
let body ← Term.elabTerm (← `(discard do $e)) q(TacticM Unit)
mkLambdaFVars fs body
let tacExpr := mkLambda `fvars default q(Array Expr) <|
let arg : Q(Array Expr) := Expr.bvar 0
mkAppN tacExpr (fs.mapIdx fun (i : Nat) _ => q($arg[$i]!))
Lean.logInfo tacExpr
let tac ← unsafe evalExpr (Array Expr → TacticM Unit) q(Array Expr → TacticM Unit) tacExpr
tac vals
Eric Wieser (Jun 27 2025 at 11:55):
Note that you'll want to translate lets in the goal state into ldecls, but you certainly want to go through the Qq internals to get there
Eric Wieser (Jun 27 2025 at 11:57):
(edit: fixed above)
Eric Wieser (Jun 27 2025 at 11:59):
Certainly to make Quoted $t work you need to back-substitute t with the variables you've created so far
Robin Arnez (Jun 27 2025 at 12:38):
Also, use mkLocalDecl instead of addDecl
Robin Arnez (Jun 27 2025 at 12:51):
Here is my implementation of by_elabq:
import Lean
import Qq
open Lean Meta Elab Tactic Qq
elab "by_elabq" e:doSeq : tactic => withMainContext do
let lctx ← getLCtx
let mut metaCtx := LocalContext.empty
let mut assignments : Array Expr := ∅
let mut levelSubst : Std.HashMap Level Expr := ∅
for nm in (← Term.getLevelNames) do
let fid ← mkFreshFVarId
let metaDecl := LocalDecl.cdecl
metaCtx.size fid nm
q(Level) .default .default
metaCtx := metaCtx.addDecl metaDecl
levelSubst := levelSubst.insert (.param nm) (.fvar fid)
assignments := assignments.push (toExpr (Level.param nm))
let mut exprSubst : Std.HashMap Expr Qq.Impl.ExprBackSubstResult := ∅
for decl in lctx do
if decl.kind != .default then continue
let fid ← mkFreshFVarId
let type ← instantiateMVars decl.type
let quotedType ← (Qq.Impl.quoteExpr type).run {
exprBackSubst := exprSubst,
levelBackSubst := levelSubst
mayPostpone := false
}
metaCtx := metaCtx.mkLocalDecl fid decl.userName
(mkApp (mkConst ``Quoted) quotedType) decl.binderInfo
assignments := assignments.push (toExpr (Expr.fvar decl.fvarId))
if decl.isLet then
let eqFid ← mkFreshFVarId
let level ← getLevel type
let quotedLevel ← (Qq.Impl.quoteLevel (← instantiateLevelMVars level)).run {
exprBackSubst := exprSubst,
levelBackSubst := levelSubst
mayPostpone := false
}
let quotedValue ← (Qq.Impl.quoteExpr (← instantiateMVars decl.value)).run {
exprBackSubst := exprSubst,
levelBackSubst := levelSubst
mayPostpone := false
}
metaCtx := metaCtx.mkLocalDecl eqFid (← mkFreshUserName `eq)
(mkApp4 (mkConst ``QuotedDefEq) quotedLevel quotedType (.fvar fid) quotedValue)
assignments := assignments.push
(mkApp4 (mkConst ``QuotedDefEq.unsafeIntro) quotedLevel quotedType (.fvar fid) quotedValue)
exprSubst := exprSubst.insert (.fvar decl.fvarId) (.quoted (.fvar fid))
let goalType ← instantiateMVars (← (← getMainGoal).getType)
let quotedGoal ← (Qq.Impl.quoteExpr goalType).run {
exprBackSubst := exprSubst,
levelBackSubst := levelSubst
mayPostpone := false
}
let quotedGoalType := mkApp (mkConst ``TacticM) (mkApp (mkConst ``Quoted) quotedGoal)
let tacExpr : Expr ← withLCtx metaCtx #[] do
let body ← Term.elabTermAndSynthesize (← `(do $e)) quotedGoalType
let res ← metaCtx.foldrM (init := (assignments, body)) fun decl (assignments, body) => do
let value := assignments.back!
let result := .letE decl.userName decl.type value (body.abstract #[.fvar decl.fvarId]) false
return (assignments.pop, result)
return res.2
unless tacExpr.hasMVar do
let typeExpr := (mkApp (Lean.mkConst ``TacticM) (Lean.mkConst ``Expr))
let tac ← unsafe evalExpr (TacticM Expr) typeExpr tacExpr
let val ← tac
(← getMainGoal).assign val
example (a b : Nat) (h : a = b) : True := by
by_elabq
return q(trivial)
Robin Arnez (Jun 27 2025 at 12:52):
When you look at the expected type you get something like
u : Level
α : Q(Type u)
a b : Q(«$α»)
h : Q(«$a» = «$b»)
⊢ TacticM Q(True)
Robin Arnez (Jun 27 2025 at 12:52):
for
example {α : Type u} (a b : α) (h : a = b) : True := by
by_elabq
return q(trivial)
Eric Wieser (Jun 27 2025 at 13:27):
This is really excellent Robin!
Eric Wieser (Jun 27 2025 at 13:29):
I think probably this makes sense to Pr to Qq, since it relies on implementation details of Qq itself?
Eric Wieser (Jun 27 2025 at 13:29):
As a nit, things like (mkApp (Lean.mkConst ``TacticM) (Lean.mkConst ``Expr)) can be golfed to q(TacticM Expr)
Aaron Liu (Jun 27 2025 at 13:30):
That's great, but what about non-finishing tactics?
Eric Wieser (Jun 27 2025 at 13:31):
I think probably the signature of those should be
u : Level
α : Q(Type u)
a b : Q(«$α»)
h : Q(«$a» = «$b»)
⊢ Q(True) -> TacticM Unit
(where the Q(True) is the goal metavariable to be assigned)
Eric Wieser (Jun 27 2025 at 13:32):
by_elabq should probably live in TermElabM or even MetaM. I think there's probably some inspiration to be had from docs#Lean.Elab.Tactic.liftMetaFinishingTactic and docs#Lean.Elab.Tactic.liftMetaTactic for what the API should look like
Eric Wieser (Jun 27 2025 at 13:33):
Really what we're doing is providing a Qq-ified version of those
Robin Arnez (Jun 27 2025 at 13:54):
Hmm so then the idea is to use it like this?
example {α : Type u} (a b : α) (h : a = b) : True := by
by_elabq fun goal => do
let _ ← assertDefEqQ goal q(trivial)
return []
Robin Arnez (Jun 27 2025 at 14:11):
Oh wow no there is a bug with this (the fact that it doesn't throw an error)
Robin Arnez (Jun 27 2025 at 14:12):
Screenshot 2025-06-27 161210.png
It doesn't assign the meta-variable but pretends to have done so
Robin Arnez (Jun 27 2025 at 14:15):
oh wait no proof irrelevance
Robin Arnez (Jun 27 2025 at 14:15):
weird stuff
Notification Bot (Jun 27 2025 at 14:31):
Mirek Olšák has marked this topic as resolved.
Mirek Olšák (Jun 27 2025 at 14:42):
Thanks! I agree with Eric that if this gets into Qq, it will be great. My understanding is that there should be two elaborators -- by_elabq, and run_tacq. The first runs in TermElabM allowing something like
def f (x : Bool) : Rat :=
by_elabq return q(if $x then 2 else 3)
and the second would run in TacticM and ignores its return value, like run_tac allowing something like
example (a b : Nat) (h : a = b) : True := by
by_elabq
let p := q($a = $b)
let t ← inferType h
logInfo <| toString (← isDefEq t p)
logInfo <| toString (← isDefEq h.ty p)
trivial
I think with your example, I would manage to write it, but I am happy to leave it for you.
Aaron Liu (Jun 27 2025 at 14:46):
Robin Arnez said:
Screenshot 2025-06-27 161210.png
It doesn't assign the meta-variable but pretends to have done so
assertDefEqQ won't assign synthetic opaque metavariables
Notification Bot (Jun 27 2025 at 14:50):
Eric Wieser has marked this topic as unresolved.
Eric Wieser (Jun 27 2025 at 14:52):
One difference with run_tac is that run_tacq only runs on the first goal
Aaron Liu (Jun 27 2025 at 14:52):
Maybe just pass the goal list?
Eric Wieser (Jun 27 2025 at 14:52):
But the hypotheses only exist in a single goal
Eric Wieser (Jun 27 2025 at 14:54):
I think perhaps we don't need new "context-switching" elaborators at all, and should instead have a with_contextQ% => and with_contextQ% goal => elaborators that can be used inside by_elab and run_tac?
Robin Arnez (Jun 27 2025 at 16:13):
There is no goal state context though when elaborating the body of run_tac or by_elab
Mirek Olšák (Jun 27 2025 at 19:27):
I feel that making run_tac to choose the goal for its context is a bit over-complicated. When I want another goal, I can run case', or rotate_left. Even the Q-annotated goal is not a must-have feature, but I understand its beauty.
Mirek Olšák (Jun 28 2025 at 00:24):
I made the PR
https://github.com/leanprover-community/quote4/pull/88
Eric Wieser (Jun 28 2025 at 00:28):
I was unclear; the goal above was intended to be the variable name introduced to hold the goal state, not as a selector to choose the goal
Aaron Liu (Jun 28 2025 at 00:29):
Eric Wieser said:
I was unclear; the
goalabove was intended to be the variable name introduced to hold the goal state, not as a selector to choose the goal
oh I thought it was a selector to choose the goal
Eric Wieser (Jun 28 2025 at 00:32):
Either way, easier to comment on the PR with what I actually meant
Eric Wieser (Jun 29 2025 at 20:15):
This is now available in mathlib master; thanks @Mirek Olšák and @Robin Arnez!
Eric Wieser (Jun 30 2025 at 08:59):
https://github.com/leanprover-community/quote4/pull/90 has a small follow-up, since I couldn't remember the name of this API during review
Last updated: Dec 20 2025 at 21:32 UTC