Zulip Chat Archive
Stream: lean4
Topic: Commenting tactic
Patrick Massot (Aug 05 2022 at 15:19):
I'm still doing meta-programming exercises with a view towards teaching. It would be nice to have tactics that react to the tactic state using some prerecorded reactions (stored in an environment extension say). The teacher would write somewhere
RegisterMessage exercise1 (n : Nat) (m : Nat) : n + m = m + n => "Congratulations on introducing variables"
and in the student would write
lemma exercise1 : ∀ n m : Nat, n + m = m + n := by
comment -- Nothing happens
intros k l
comment -- triggers `logInfo "Congratulations on introducing variables"`
Ideally the comment
tactic would be invoked automatically after each tactic invocation. I know how to register a simple environment extension, and I can parse the syntax of my RegisterMessage command. But then I'm stuck when I try to create a metavariable context that I could compare to the main goal in the comment
tactic. Note that this comparison is also a non-trivial question since I don't want the order of declarations or the user facing names of free variables to be considered relevant. But this comparison question comes after understanding how to create a metavariable with a context outside of a proving context (I know this sentence isn't clear but I'm indeed confused about what needs to be done). I tried to read the definition of the def
command but there are too many layers and too much generality and I got lost.
Henrik Böving (Aug 05 2022 at 15:21):
so basically you want to have a map from current tactic state => String (per theorem) and use the comment tactic to access that map and print a message about the current state if there is one registered yes?
Patrick Massot (Aug 05 2022 at 15:22):
Using strings would be easy but you would miss lots of cases. Notice that in my example I carefully chose stupid variable names in my intro
line so that matching strings wouldn't work (the RegisterMessage
command uses n
and m
but I introduce k
and l
).
Henrik Böving (Aug 05 2022 at 15:23):
No no I mean a map from the tactic state to strings (i.e. your messages) so that you can give it your current tactic state, it will try to look it up in said map and if it finds an entry print the string that is associated with the state.
Patrick Massot (Aug 05 2022 at 15:25):
Yes, except that, AFAIK there is no data structure called "tactic state". I'm stuck one creating such a synthetic tactic state in my RegisterMessage
command.
Henrik Böving (Aug 05 2022 at 15:28):
Yes I'm talking about a logical concept such as a tactic state, although one could argue that a meta variable + its context essentially is the tactic state sind it consists of the free variables + the type of the meta variable.
Either way I do not know how to do this from the top of my head but iirc there were efforts to produce a tactic testing framework back in the ICERM22 after party right? They must have implemented a function that can compare tactic states already (since the test framework tests the tactic state before and after a tactic was run) so I would suggest you could try to look into how it was done there maybe.
Patrick Massot (Aug 05 2022 at 15:29):
Maybe I don't need to create such a synthetic tactic state, simply store the Syntax elements given to the registering command and then compare the actual tactic state to this data.
Patrick Massot (Aug 05 2022 at 15:30):
Henrik Böving said:
Yes I'm talking about a logical concept such as a tactic state, although one could argue that a meta variable + its context essentially is the tactic state sind it consists of the free variables + the type of the meta variable.
Do you know how to create such a pair "meta variable + its context" out of nowhere?
Sebastian Ullrich (Aug 05 2022 at 15:39):
I'm not sure you'll want to carry around a metavar context between commands. After elaborating the RegisterMessage
arguments, you can pack them into an Expr
as in ∀ (n : Nat) (m : Nat), n + m = m + n
alongside the information that the first two binders are the context.
Patrick Massot (Aug 05 2022 at 15:42):
Oh that's clever. I thought about reverting all variables but I didn't want to loose the information about what was introduced.
Sebastian Ullrich (Aug 05 2022 at 15:42):
Lean does something like that in a lot of places internally :)
Patrick Massot (Aug 05 2022 at 15:45):
Are there any API functions supporting this?
Sebastian Ullrich (Aug 05 2022 at 16:04):
You'll want docs4#mkForallFVars and docs4#forallBoundedTelescope in the other direction
Patrick Massot (Aug 07 2022 at 11:14):
I returned to this idea but, unsurprisingly, I'm having issues. Reproducing is a bit painful because you need to create the environment extension in a different file. In order to reproduce that issue I don't know an easier solution than creating a lake project. You can do mkdir messages && cd messages && echo "leanprover/lean4:nightly-2022-08-06" > lean-toolchain && lake init messages && mkdir Messages
and create a file messages/Messages/EnvExtensions.lean
containing
import Lean
open Lean
/-- A goal message is a string indexed by a level name, an expression and a number of necessary intros. -/
def GoalMessageEntry := Name × Expr × Nat × String
/-- Environment extension supporting goal driven messages. -/
initialize goalMessageExt : SimplePersistentEnvExtension GoalMessageEntry (Array GoalMessageEntry) ←
registerSimplePersistentEnvExtension {
name := `goal_message
addEntryFn := Array.push
addImportedFn := Array.concatMap id
}
and a file messages/Messages/Issue.lean
containing:
import Lean
import Messages.EnvExtensions
set_option autoImplicit false
open Lean Meta Elab Command Term
declare_syntax_cat mydecl
syntax "(" ident ":" term ")" : mydecl
def getIdent : TSyntax `mydecl → Ident
| `(mydecl| ($n:ident : $_t:term)) => n
| _ => default
def getType : TSyntax `mydecl → Term
| `(mydecl| ($_n:ident : $t:term)) => t
| _ => default
def mkGoalSyntax (s : Term) : List (Ident × Term) → MacroM Term
| (n, t)::tail => do return (← `(∀ $n : $t, $(← mkGoalSyntax s tail)))
| [] => return s
def mkGoalExpr (e : Expr) : List (Name × Expr) → Expr
| (n, t)::tail => .forallE n t (mkGoalExpr e tail) BinderInfo.default
| [] => e
elab "Message" decls:mydecl* ":" goal:term "=>" msg:str : command => do
let g ← liftMacroM $ mkGoalSyntax goal (decls.map (λ decl => (getIdent decl, getType decl))).toList
let g ← liftTermElabM none $ elabTerm g none
modifyEnv (goalMessageExt.addEntry · ((← getCurrNamespace), g, decls.size, msg.getString))
Message (n : Nat) (m : Nat) : n + m = m + n => "Well done!"
open Tactic
/-- Get the list of free variables occuring in the expression (with repetitions). -/
def Lean.Expr.getFVars : Expr → List FVarId
| .app fn arg => (getFVars fn) ++ (getFVars arg)
| .fvar Id => [Id]
| .lam _ bt body .. => getFVars bt ++ getFVars body
| .forallE _ bt body .. => getFVars bt ++ getFVars body
| .letE _ t v b .. => getFVars t ++ getFVars v ++ getFVars b
| .mdata _ e => getFVars e
| .proj _ _ e => getFVars e
| _ => []
/-- Returns the type of the goal after reverting all free variables in the order
where they appear in the goal type. -/
def normalizedRevertExpr (goal : MVarId) : MetaM Expr := do
let (_, new) ← goal.revert (← goal.getType).getFVars.toArray true
return (← new.getType)
elab "comment" : tactic => do
let tgt ← getMainGoal
let tgt_ctx_size := (← tgt.getDecl).lctx.size
let normalized_tgt ← normalizedRevertExpr tgt
for (_name, msg_goal, nb, d) in goalMessageExt.getEntries (← getEnv) do
let msg_mvar ← mkFreshExprMVar msg_goal MetavarKind.syntheticOpaque
let (_, msg_mvar) ← msg_mvar.mvarId!.introNP nb
let msg_ctx_size := (← msg_mvar.getDecl).lctx.size
if tgt_ctx_size ≠ msg_ctx_size then continue
let normalized_msg ← normalizedRevertExpr msg_mvar
if (← isDefEq normalized_tgt normalized_msg) then logInfo d
example : ∀ n m : Nat, n + m = m + n := by
comment
intros n m
revert n
revert m
intros m n
comment
rw [Nat.add_comm]
Patrick Massot (Aug 07 2022 at 11:18):
Recall the goal is to be able to recognize a tactic state even if the free variables are permuted in the context. In the example above, the goal is to recognize the context n m : Nat ⊢ n + m = m + n
even if it appears as m n : Nat ⊢ n + m = m + n
(note the free variables order). The comparison should also discard free variable names differences, so k l : Nat ⊢ k + l = l + k
should match as well. My idea to compare to such contexts is to list all free variables in the target expression, here n + m = m + n
and revert all those variables in the order they appear then I test whether the resulting goals are defeq. In the above code I first do a cheap check comparing the context sizes to fail the comparison early in case they don't match.
Patrick Massot (Aug 07 2022 at 11:19):
It sort of works except that all those manipulation actually alter the goal and the above example has an error on the last line saying there is no remaining goal to solve.
Patrick Massot (Aug 07 2022 at 11:21):
Note that I'm interested in both a fix to my code and a completely different much better way to achieve my goal.
Mario Carneiro (Aug 07 2022 at 11:24):
the easy fix to the fact that your manipulations alter the goal is to roll back the state once you're done
Mario Carneiro (Aug 07 2022 at 11:25):
wrap the whole thing in withoutModifyingState
Patrick Massot (Aug 07 2022 at 11:53):
Do you mean
elab "comment" : tactic => do
withoutModifyingState do
let tgt ← getMainGoal
let tgt_ctx_size := (← tgt.getDecl).lctx.size
let normalized_tgt ← normalizedRevertExpr tgt
for (_name, msg_goal, nb, d) in goalMessageExt.getEntries (← getEnv) do
let msg_mvar ← mkFreshExprMVar msg_goal MetavarKind.syntheticOpaque
let (_, msg_mvar) ← msg_mvar.mvarId!.introNP nb
let msg_ctx_size := (← msg_mvar.getDecl).lctx.size
if tgt_ctx_size ≠ msg_ctx_size then continue
let normalized_msg ← normalizedRevertExpr msg_mvar
if (← isDefEq normalized_tgt normalized_msg) then logInfo d
This indeed suppresses the error message but it also suppresses the success message :sad:
Mario Carneiro (Aug 07 2022 at 11:57):
You can have the body return the list of messages you want to report, and then do them outside the withoutModifyingState
. Alternatively, you could try using saveState
and restore
directly
Mario Carneiro (Aug 07 2022 at 12:06):
elab "comment" : tactic => do
let s ← saveState
let tgt ← getMainGoal
let tgt_ctx_size := (← tgt.getDecl).lctx.size
let normalized_tgt ← normalizedRevertExpr tgt
let mut messages := #[]
for (_name, msg_goal, nb, d) in goalMessageExt.getEntries (← getEnv) do
let msg_mvar ← mkFreshExprMVar msg_goal MetavarKind.syntheticOpaque
let (_, msg_mvar) ← msg_mvar.mvarId!.introNP nb
let msg_ctx_size := (← msg_mvar.getDecl).lctx.size
if tgt_ctx_size ≠ msg_ctx_size then continue
let normalized_msg ← normalizedRevertExpr msg_mvar
if (← isDefEq normalized_tgt normalized_msg) then
messages := messages.push d
s.restore
for d in messages do logInfo d
I'm not sure why s.restore
is backtracking the trace state; the documentation says it shouldn't
Sebastian Ullrich (Aug 07 2022 at 12:16):
Diagnostic messages are not part of the trace state, if you mean that
Patrick Massot (Aug 07 2022 at 13:12):
Thank you very much Mario! Do you have other comment about my approach? Do you see a better way to do it? I know I should store a normalized version in the environment extension instead of recomputing it endlessly, but I didn't want to optimize this before having a working version.
Patrick Massot (Aug 07 2022 at 13:19):
Actually precomputing the normalized expression is not as easy as I thought because there is no MonadLiftT MetaM CommandElabM
.
Patrick Massot (Aug 07 2022 at 13:24):
I still have issues navigating the monads of meta-programming in Lean 4. Do we have a cheat-sheet displaying all relations between them? I mean both extension relations and lifting capabilities.
Patrick Massot (Aug 07 2022 at 13:26):
There must be a way to go from CommandElabM Unit
to MetaM
since declaration commands can state and prove theorems.
Henrik Böving (Aug 07 2022 at 13:46):
Its not as easy as you think though, CommandElabM has no direct way to execute a MetaM computation since it can only access an Environment and doesn't have all the other stuff MetaM needs included. The MetaM executing elaborators basically create new either empty or customly filled states and contexts for the MetaM monad.
Since it is not entirely obvious what these initial states and contexts should be though (at least not generally for all use cases) these MonadLift instances do not exist.
You can see this yourself if you go through the definitions of the monad stack, a CommandElabM does not have all the info a MetaM needs
Patrick Massot (Aug 07 2022 at 13:50):
I understand that, but the def
and theorem
commands somehow do this, right?
Patrick Massot (Aug 07 2022 at 13:50):
I guess we're back to the question that started this thread: how to create an artificial context for a goal outside a proving context?
Sebastian Ullrich (Aug 07 2022 at 14:08):
MetaM.run'
defaults to an empty local and mvar context, which I assume is what you want in this case. Then you're in CoreM
, which you can lift with Command.liftCoreM
.
Leonardo de Moura (Aug 07 2022 at 14:15):
@Patrick Massot
The def
and theorem
commands are implemented using
runTermElabM
. I added documentation and examples for liftTermElabM
and runTermElabM
:
https://github.com/leanprover/lean4/commit/e236eeba870c64178805c2d5a4d6727f9b329fcb
Leonardo de Moura (Aug 07 2022 at 14:17):
Here is where runTermElabM
is used to implement def
and theorem
:
https://github.com/leanprover/lean4/blob/master/src/Lean/Elab/MutualDef.lean#L876
Leonardo de Moura (Aug 07 2022 at 14:25):
BTW, I will remove the unnecessary declName?
parameter. New commit coming soon.
Leonardo de Moura (Aug 07 2022 at 14:35):
https://github.com/leanprover/lean4/commit/413db56b89459d7328cee7abd669f9b99e7f4b37
Patrick Massot (Aug 07 2022 at 14:36):
Thank you very much for all this!
Patrick Massot (Aug 07 2022 at 14:39):
I can confirm that liftTermElab
works for me.
Mario Carneiro (Aug 07 2022 at 14:41):
Would it be an option to make it a liftM
instance now? Or is that being deliberately avoided because of the dummy state issues
Leonardo de Moura (Aug 07 2022 at 14:53):
@Mario Carneiro I think it is problematic because
- The lift is not a cheap operation
- It matters where the lift is inserted. Example:
import Lean
open Lean Meta Elab Command Term
def ex1 : CommandElabM Expr := liftTermElabM do
let x ← mkFreshTypeMVar
inferType x
def ex2 : CommandElabM Expr := do
let x ← liftTermElabM <| mkFreshTypeMVar
liftTermElabM <| inferType x
#eval ex1 -- Ok
#eval ex2 -- Error unknown metavariable
Mario Carneiro (Aug 07 2022 at 14:54):
does liftM prefer the second interpretation? (EDIT: yes)
Mario Carneiro (Aug 07 2022 at 14:58):
The names liftTermElabM
and runTermElabM
are not very clearly different though. I wouldn't be able to guess which is which without looking them up
Leonardo de Moura (Aug 07 2022 at 14:58):
Mario Carneiro said:
The names
liftTermElabM
andrunTermElabM
are not very clearly different though. I wouldn't be able to guess which is which without looking them up
We now have the docstrings :)
Leonardo de Moura (Aug 07 2022 at 14:59):
liftTermElabM
has (now) the same signature of other liftM
methods.
Mario Carneiro (Aug 07 2022 at 15:01):
How about runTermElabMWithContext
?
Mario Carneiro (Aug 07 2022 at 15:02):
and renaming liftTermElabM
to runTermElabM
Leonardo de Moura (Aug 07 2022 at 15:03):
I didn't like the new names :big_smile:
Mario Carneiro (Aug 07 2022 at 15:03):
Although the signature of liftTermElabM
is like other liftM
s, I am convinced by your argument that it's not really a lift
Mario Carneiro (Aug 07 2022 at 15:04):
especially liftTermElabM (a >> b) != liftTermElabM a >> liftTermElabM b
Last updated: Dec 20 2023 at 11:08 UTC