-
Notifications
You must be signed in to change notification settings - Fork 343
Metaprogramming for dummies
This is an overview to the most common functions that are useful for writing tactics in Lean 4. We assume some knowledge of functional programming and a basic understanding of how metaprogramming works in Lean 4. The standard references are Lean 4 Metaprogramming book and Functional Programming in Lean, respectively.
The abstract Monad m
is built out of two operations, (>>=)
(also known as bind
) and pure
.
These have the signature (>>=) : m α → (α → m β) → m β
and pure : α → m α
.
Thinking about monads as types carrying around state, the bind operation takes a stateful variable a : m α
and function f : α → m β
,
'unpacks' a
, inserts it into f
and combines the state; and pure
just takes a value and 'packs' it into the monad.
In practice we don't use (>>=)
and pure
by hand, instead we use do
notation, which is very handy syntactic sugar for chains of (>>=)
(and in Lean 4 it also allows for automatic lifting of monads and we use return
, which differs from pure
in that it discards all code that would have run later, so that we can use if
branches and return
as in python.
Todo:
- Examples for
do
notation with corresponding(>>=)
version. - Example for lifting where the direction version is complicated.
We also mention some monadic higher order functions that are very useful to write efficient code:
-
Monad.join:
combine the state of
m (m α)
tom α
For iterating monadic functions over lists and arrays (to be found in the List
and Array
namespace, respectively):
-
mapM
: takes a monadic functionf : α → m β
and a list of inputsList α
and appliesf
to each input and returns a monadic list of outputsm (List β)
. -
filterM
: takes a monadic functionf : α → m Bool
and a list of inputsList α
and filters the input list byf
returningm (List α)
. -
foldlM
andfoldrM
: take a monadic functionf : s → α → m s
, an initial valuea : s
and aList α
and iteratesf
using the list in each step as parameters and returns the resultm s
. The difference betweenfoldlM
andfoldrM
is that direction in which the elements ofList α
are taken (left vs. right).
There are a lot of monads that are used in metaprogramming, but for tactics we can get away with using only two monads:
-
MetaM is the monad you want to use if you modify metavariables.
-
TacticM is the topmost monad for tactic-writing. You can get and set goals, etc
We also mention TermElabM
which is the monad for term elaboration and it sits in between MetaM
and TacticM
. Using the Quote4
(also called Qq
) library (see information on that below) one can do all elaboration of user-supplied expressions in TacticM
and 'internal' elaboration in MetaM
using Qq
.
Apart from the usual basic types of functional programming, Lean 4 has a lot of specialized types for metaprogramming. We will focus on the ones that are important for tactic-writing.
Lean has two different types for syntax: Lean.Syntax and Lean.TSyntax (typed syntax), where the later is a dependent type to allow compile-time checks of syntax. The kind of typed syntax is defined in Lean.SyntaxNodeKinds, which is just a List Lean.Name
.
- elabTerm
- elabTermForApply
- elabTermEnsuringType
Let's begin with a very simple example.
This example shows how to take a user input a
and add to the local context
the assumption [a].length = 1
.
The underlying principle is that we want to take some user input, embed it into the rich type-system that Lean has and then process it.
import Lean
open Lean Elab Tactic Term Meta
The following line of code, informs Lean that single_me <something>
is a tactic.
It also makes Lean aware that <something>
is a term
(or, with all the
namespaces, a Lean.Parser.Category.term
).
syntax "single_me " term : tactic
Thus, if we are in tactic mode and we write single_me whatever
, then Lean
knows that this is a tactic and that whatever
is a term
.
Let's test this out!
example : true := by
single_me whatever -- tactic 'tacticSingleMe_' has not been implemented
Sure, Lean has no idea what the tactic is supposed to do.
However, if you remove whatever
, leaving only singleMe
, then the error
message changes to expected term
: the tactic is not implemented, but Lean
knows that it should expect a term
.
If you tried with something else, say whoKnows
, Lean would have replied with
unknown tactic
.
Let's implement the single_me
tactic.
-- these are the "elaboration rules" for a tactic
elab_rules : tactic
-- if Lean finds `single_me [stuff]`, it is going to assign the label `x`
-- to [stuff] and it is going to assume that it is a `term`. This basically
-- affects what we can ask Lean to do with `x`.
| `(tactic| single_me $x:term) => do
-- at this point, the tactic state contains ``x: TSyntax `term``:
-- `x` "entered" the local context of our tactic development as a
-- "Typed Syntax" with `Category` `term`.
-- The next line essentially runs `have : [$x].length = 1 := by simp` in
-- the local context of where our tactic will be called.
evalTactic (← `(tactic| have : [$x].length = 1 := by simp))
-- Note that we are using `$x` to refer to the "quoted" variable `x`.
-- This is "our" input: we are going to type it in tactic mode, Lean
-- parses it and performs actions with it.
example : True := by
single_me 0 -- local context now contains `this: List.length [0] = 1`!
trivial
Great! We managed to add to the local context the hypothesis that the
list consisting of 0
has length 1.
In a similar vein, here are more examples of our new tactic:
example {α : Type} [Add α] {a b : α} : True := by
single_me a
-- local context now contains `this: List.length [a] = 1`
single_me 1 + 2
-- local context now contains `this: List.length [1 + 2] = 1`
single_me a + b
-- local context now contains `this: List.length [a + b] = 1`
single_me [a]
-- local context now contains `this: List.length [[a]] = 1`
single_me ∀ x : Int, x + 5 = 0
-- local context now contains `this: List.length [∀ x : Int, x + 5 = 0] = 1`
trivial
As you can see, the term
Category is fairly flexible: basically, anything
that looks like an Expr
ession will be allowed.
If we had used ident
instead of term
, then we would have been allowed
to use "identifiers", essentially the variables in our context. If we did
that, then in the final example single_me a
would have worked, but
nothing else would have.
withLocalDeclD
An expression is a general elaborated term in Lean. The most important types of expressions are the following:
Expr.isMVar
Expr.mvarId!
isBVar
isLambda
lambdaTelescope
isForall
forallTelescope
- isApp
- getAppFn
- getAppNumArgs
- getAppArgs
-
Expr.getAppFnArgs
: This is useful if you have an expression and you want to pattern-match
- Assignment
- type
Note about delayed assignments (and reference to meta-book)
open Lean.Elab.Tactic
getGoals
getMainGoal
setGoals
open Lean.Elab.Tactic
evalTactic
withMainContext
liftMetaTactic
Lean.MVarId.apply
Example:
- If no universe levels:
mvarId.apply (mkConst ``foo [])
- If known universe levels:
mvarId.apply (mkConst ``foo [u,v])
- Otherwise:
mvarId.apply (← mkConstWithFreshMVarLevels ``foo)
However, see also: Qq
.
- The easy stuff
- Qq (Qq is love, Qq is life).
If you want to build expressions, it's a good idea to use Qq
instead of manually building them from the constructors. The Q(α)
macro is a synonym of Expr
, with the (unchecked?) assertion that any e : Q(α)
typechecks to have type α
. And you can build elements of Q
using q
, and it will automatically, statically, fill in implicits. Instances are looked up in the context of the q
invocation.
Unfortunately, errors generated by Qq
are hard to understand.
-
dbg_trace s!"foo {var}"
is the most lowlevel way of debugging anything -- printsfoo
and whatevervar
is in the local context -
logInfo m!"foo {var}"
is slightly more sophisticated and only works in monads that support logging (in particular inMetaM
andTacticM
) -
trace[identifier] s!"foo {var}"
is the serious way of logging output. To declare a trace class invokeinitialize registerTraceClass identifier
and then activate the logging withset_option trace.identifier true
. Note that the logging does not work in the file where the trace class is defined.- However, note that
trace[debug]
is already registered in core, and can be convenient for, well, debugging! Activate it withset_option trace.debug true
.
- However, note that