Adding syntax for commands
Lean allows you to define custom syntax for a command. As with terms, this can be done in a simple way with macro, or in a more complex way with elab. The elab approach is more powerful, since it allows you to generate an expression from the syntax and perform checks during elaboration. In this recipe, we will show how to define custom syntax for commands using elab, which lets you specify both the syntax and its elaboration in one place.
"Hello World" command
We start with a simple example of a command that prints "Hello World". The following elab declaration tells Lean to parse #helloWorld as a command and explains what that command should do.
elab "#helloWorld" : command => do
logInfo "Hello World!"
#helloWorld
Here, logInfo s prints the string s in the InfoView.
Command for checking whether a proposition is solved by grind
We define a custom command that tests whether a proposition can be solved automatically by the grind tactic. The goal is to provide a small command-line-style tool that reports whether grind can close a goal.
Again, we can define the command directly with elab. In the declaration below, Lean parses inputs of the form #grindable? <term> as a command, and the elaborator says how that command should behave.
elab "#grindable?" t:term : command => do
Command.liftTermElabM do
try
withoutErrToSorry do
let tExpr ← elabTerm t none
let goal ← mkFreshExprMVar tExpr
Term.synthesizeSyntheticMVarsNoPostponing
let (goals,_) ← Elab.runTactic goal.mvarId!
(← `(tactic|grind))
if goals.isEmpty then
logInfo m!"{t} is grindable"
else
logInfo m!"grind failed with goals: {goals}"
catch _ =>
logInfo m!"{t} is not grindable"
#grindable? ∀ n : Nat, n + 0 = n -- grindable
#grindable? ∃ x : Nat, x > 100 -- not grindable
Let's break down the specific metaprogramming functions used in the elaborator above:
-
The call to
Command.liftTermElabMis needed because command elaboration happens in theCommandElabMmonad, while elaborating terms and running tactics uses the term elaboration machinery in theTermElabMmonad. -
Lean's elaborator sometimes turns elaboration errors into sorries.
withoutErrToSorryprevents that from happening, so we can catch the exceptions thrown while elaborating. -
We write a
try … catchblock and placewithoutErrToSorryinside thetryblock. -
Lean.Elab.Term.elabTermelaborates the user-provided proposition (i.e.,t) into an expression. -
Then
mkFreshExprMVarcreates a fresh metavariable goal whose type is given as an expression (i.e.,tExpr). -
Elab.runTacticruns the tacticgrindon that fresh goal and returns a tuple of typeList MVarId × Term.State. In this example, the first component is exactly the list of goals left open aftergrind, while the second component is the updated state of theTermElabMmonad, which we ignore with_. -
Finally, we inspect the remaining goals. If the list is empty, then
grindmanaged to prove the proposition completely.
If you prefer to separate the syntax declaration from the elaboration logic, Lean also lets you define the syntax first with syntax and then add elaboration rules with elab_rules.
syntax "#grindable'?" term : command
elab_rules : command
|`(command| #grindable'? $t:term ) => do
Command.liftTermElabM do
try
withoutErrToSorry do
let tExpr ← elabTerm t none
let goal ← mkFreshExprMVar tExpr
Term.synthesizeSyntheticMVarsNoPostponing
let (goals,_) ← Elab.runTactic goal.mvarId!
(← `(tactic|grind))
if goals.isEmpty then
logInfo m!"{t} is grindable"
else
logInfo m!"grind failed with goals: {goals}"
catch _ =>
logInfo m!"{t} is not grindable"
The elab_rules command lets us define elaboration rules by pattern matching on the command syntax that was parsed. Both styles are useful, but the direct elab form is often a good starting point for a compact one-off command, while syntax plus elab_rules is helpful when you want to separate the parser and elaborator more explicitly.