Up to now, we've used the manual part of Lean's tactic
facilities. In this chapter, we'll learn more about some of
Lean's powerful automation features, including
tactic combinators like try and repeat, decision procedures like lia,
and automatic simplification using simp.
Using these features together with Lean's metaprogramming facilities will enable us to make
some of our proofs startlingly short! Used properly, they can
also make proofs more maintainable and robust to changes in
underlying definitions.
Our motivating example will be the following proof, repeated with
just a few small changes from the IndProp chapter. We will
simplify this proof in several stages.
The lia tactic implements a decision procedure for linear integer
arithmetic: propositional formulas whose atoms are linear
constraints over the natural numbers and integers. This is exactly the
fragment of first-order logic (see the Logic chapter)
obtained by restricting connectives and quantifiers to arithmetic
building blocks.
If the goal is a universally quantified formula made out of
numeric constants, addition (+ and succ), subtraction (- and pred),
and multiplication by constants,
equality (= and ≠) and ordering (≤ and <), and
the logical connectives ∧, ∨, ¬, and →,
then invoking lia will either solve the goal, or fail because the
goal is actually false: within this fragment, lia is a complete
decision procedure. lia reasons about
the goal together with any hypotheses already in the local context: each
hypothesis is used exactly as if it had been written into the goal as an
antecedent with →.
Outside this fragment, lia can fail even when the goal is true —
for example, on n * n ≥ n, which multiplies two variables together rather
than a constant and a variable. Such a failure only means lia
couldn't decide the goal, not that the goal is false. Note that, when
failing, lia may mention another tactic, called grind.
This is another, more powerful tactic that subsumes lia, but we
will not use it here.
Anything in the goal or hypotheses that isn't built from these arithmetic
pieces — including an arbitrary proposition like x ∈ l — lia
simply treats as an opaque atom. So lia can also solve goals that
are purely propositional, with no arithmetic in them at all, as long as the
only way such atoms are combined is with ∧, ∨, ¬, and →.
In Induction, we saw how to use the <;> combinator in order to apply
the same tactic to every subgoal in a proof. As a reminder, consider this example,
where cases on b and c each leaves two subgoals that are discharged identically:
The <;> is not the only combinator that Lean has to offer.
In general, combinators allow us to build tactics out of smaller ones.
Getting used to them takes a little energy, but it lets us scale up to
more complex definitions and more interesting properties without
drowning in boring, repetitive detail.
The first such combinator we'll discuss is try. If t is a tactic,
then try t is a tactic that is just like t
except that, if t fails, try tsuccessfully does nothing at all
(rather than failing).
example{a:Prop}(h:a):a:=bya:Proph:a⊢ atryrfla:Proph:a⊢ a-- `rfl` would fail here, but `try` swallows it...exacthAll goals completed! 🐙-- ...so we can still finish some other way.example:1=1:=by⊢ 1=1tryrflAll goals completed! 🐙-- here `try rfl` just does `rfl`
There is not much reason to use try in completely manual proofs like
these, but it is very useful together with the <;> combinator.
Here, we can use the lia tactic to close some of these goals, but not all of them. So,
a more compact way to write this proof would be:
example{n}(h:Sillyn):n≠1:=byn:Nath:Sillyn⊢ n≠1caseshmk1n:Nath✝:n>1⊢ n≠1mk2n:Nath✝:1∈[]⊢ n≠1mk3n:Nath✝:∃m,n=m+2⊢ n≠1<;>mk1n:Nath✝:n>1⊢ n≠1mk2n:Nath✝:1∈[]⊢ n≠1mk3n:Nath✝:∃m,n=m+2⊢ n≠1tryliaAll goals completed! 🐙-- `lia` doesn't know that `1 ∈ []` is impossible,-- but we can use `contradiction`contradictionAll goals completed! 🐙
We can further simplify our Perm3.In example with try.
Note that try lia <;> try rw [...] <;> liadoesn't work because <;> short circuits.
A failure in the first lia prevents the rest of the sequence from
executing, meaning the try rw [...] never fires. (try lia <;> ... is parsed
try (lia <;> (...)), and it's the outermost try that catches the failure in this case.)
We'll see a solution to this problem further below.
The repeat combinator takes another tactic or parenthesized sequence of tactics
and keeps applying it until it fails.
Here is an example proving that 10 is in a long list using repeat:
example:10∈[1,2,3,4,5,6,7,8,9,10]:=by⊢ 10∈[1,2,3,4,5,6,7,8,9,10]repeatrw[List.mem_cons⊢ 10=1∨10∈[2,3,4,5,6,7,8,9,10]]⊢ 10=9∨10∈[10]⊢ 10=10∨10∈[]tryleft⊢ 10=10;rflAll goals completed! 🐙-- `try` makes this optional, which is necessary for the-- last repetition where `left; rfl` succeedstryright⊢ 10∈[10]
The tactic repeat t never fails: if the tactic t doesn't apply
to the original goal, then repeat tsucceeds without changing the
goal at all (i.e., it repeats zero times).
example:10∈[1,2,3,4,5,6,7,8,9,10]:=by⊢ 10∈[1,2,3,4,5,6,7,8,9,10]-- this is a no-oprepeatliaAll goals completed! 🐙repeatrw[List.mem_cons⊢ 10=1∨10∈[2,3,4,5,6,7,8,9,10]]⊢ 10=9∨10∈[10]⊢ 10=10∨10∈[]tryleft⊢ 10=10;rflAll goals completed! 🐙tryright⊢ 10∈[10]
The tactic repeat t does not have any upper bound on the
number of times it applies t. If t is a tactic that always
succeeds (and makes progress), then repeat t will loop
forever.
example(mn:Nat):m+n=n+m:=unsolved goalsmn:Nat⊢ m+n=n+mby/- Uncomment the next line to see the infinite loop occur. You will
then need to recomment it to make Lean listen to you again. -/-- repeat rewrite [Nat.add_comm]
Wait — did we just write an infinite loop in Lean?!?!
Sort of.
While evaluation in Lean's term language is guaranteed to
terminate, tactic evaluation is not. This does not affect Lean's
logical consistency, however, since the job of repeat and other
tactics is to guide Lean in constructing proofs; if the
construction process diverges (i.e., it does not terminate), this
simply means that we have failed to construct a proof at all, not
that we have constructed a bad proof.
Neither rfl nor left succeeds on this goal,
but lia does, so first stops after lia
and never tries induction. As with try,
first is most useful in combination with other combinators.
For example, we can rewrite our previous examples that used
repeat and try like so:
The first tactic here will attempt to close the goal with an application of
List.mem_cons_self, if it can, and otherwise apply List.mem_cons_of_mem to proceed to
checking the next element in the list. Note that the order here is important!
If we had instead written:
Here, when we reach the goal 10∈[10], instead of closing the goal with
List.mem_cons_self like before, we would instead first try apply List.mem_cons_of_mem,
which would also succeed. This leaves us with the goal 10∈[], which is of course false.
With first, we can solve the earlier issue with try where it would stop
executing the sequence on the first failure.
The simp tactic is Lean's simplifier. It is one of the most powerful
tools in the language, and it is used heavily in real Lean developments.
The tactic simplifies the target (the goal and/or one or more hypotheses)
by repeatedly rewriting it using a set of lemmas.
At each step it tries every lemma in its available set the way
first would, applies whichever one matches
via rw, and repeats until no lemma applies anywhere.
Like repeat, it fails outright if it never manages to apply a rewrite
("simp made no progress"), rather than succeeding as a
no-op the way trysimp would.
The simp tactic's available set of lemmas begins with a default set and can be extended
to include theorems labeled @[simp].
Indeed, the characterizing lemmas we've been writing for
our definitions all throughout this book are examples
of these simplification lemmas, or
simp lemmas as they're called by Lean programmers, only
we have refrained from annotating them as such (until now!).
namespacesimp_lemmas_example/- `add_zero` and `add_succ` are the `simp` lemmas for `+`. -/@[simp]theoremadd_zero(n:Nat):n+0=n:=byn:Nat⊢ n+0=nrflAll goals completed! 🐙@[simp]theoremadd_succ(nm:Nat):n+(m+1)=(n+m)+1:=byn:Natm:Nat⊢ n+(m+1)=n+m+1rflAll goals completed! 🐙
Instead of manually rewriting by the characterizing lemmas in the example below,
simp does it automatically.
The other way to extend simp's set of available lemmas is to list them
explicitly, by writing simp [<theorems>]. If you want simp to only use those,
you can use simp only [<theorems>]. As with rw, you can also supply a
definition to simp to simplify using that definition.
If you want to know what simp is doing, you can run simp?.
theoremadd_succ_nested_3(nm:Nat):n+(m+1+1)=(n+m+1)+1:=byn:Natm:Nat⊢ n+(m+1+1)=n+m+1+1Try this:[apply]simp only [add_succ, Nat.add_zero, Nat.add_left_cancel_iff]simp?All goals completed! 🐙endsimp_lemmas_example
In the InfoView, you will see
Try this:[apply]simp only [add_succ, Nat.add_zero, Nat.add_left_cancel_iff]
Click the [apply] button to replace simp? with
the suggested replacement. You should always do this for your final proof scripts, just
as was recommended for rw? and exact? in the
UsingLean chapter.
Interestingly, we can see for this example that simp used the Nat version
of add_zero, not our own, added above, and also pulled in Nat.add_left_cancel_iff,
which is not strictly needed. But the combination works, even if it is not minimal.
As with apply and rw, simp can also simplify
hypotheses — invoking simp as simp [<lemmas>] at h
runs the simplifier at hypothesis h.
exampleαx(l₁l₂l₃:Listα)(h₁:x∈l₁++l₂)(Variable name `h₂` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₂Note: This linter can be disabled with `set_option linter.unusedVariables false`h₂:x∈l₂++l₃):x∈l₁++l₃∨x∈l₂:=byα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁++l₂h₂:x∈l₂++l₃⊢ x∈l₁++l₃∨x∈l₂simpath₁α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂++l₃h₁:x∈l₁∨x∈l₂⊢ x∈l₁++l₃∨x∈l₂;simpath₂α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ x∈l₁++l₃∨x∈l₂;simpα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ (x∈l₁∨x∈l₃)∨x∈l₂;liaAll goals completed! 🐙
We could equally well have written
exampleαx(l₁l₂l₃:Listα)(h₁:x∈l₁++l₂)(Variable name `h₂` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₂Note: This linter can be disabled with `set_option linter.unusedVariables false`h₂:x∈l₂++l₃):x∈l₁++l₃∨x∈l₂:=byα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁++l₂h₂:x∈l₂++l₃⊢ x∈l₁++l₃∨x∈l₂simpat*α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ (x∈l₁∨x∈l₃)∨x∈l₂;liaAll goals completed! 🐙
If we want to mutually simplify everywhere, we can use simp_all, which
simplifies in all hypotheses and in the goal at the same time. The tactic simp_all
is not the same as simp at *. The latter
simplifies each target independently, whereas simp_all
additionally lets the (simplified) hypotheses simplify each other and the goal,
iterating to a joint fixpoint.
Here's an example that illustrates the difference:
example(ab:Nat)(h1:a=0)(h2:a+b=5):b=5:=bya:Natb:Nath1:a=0h2:a+b=5⊢ b=5`simp` made no progresssimpat*a:Natb:Nath1:a=0h2:a+b=5⊢ b=5
Because simp is such a powerful tactic, the Lean community has developed a number of
conventions surrounding appropriate usage. One such convention is around
terminalsimp usage.
A call to simp is considered terminal either when it is the last tactic used to close a
goal or when it is followed only by other automatic (also called "flexible") tactics like
simp or lia. In idiomatic Lean, all nonterminal uses of simp should
use the only qualifier and specify exactly which lemmas are being used to simplify.
Use of simp without only should only occur in terminal positions.
In our example from before, the use of simp is terminal (and therefore okay)
because it is followed only by other simps and lia:
exampleαx(l₁l₂l₃:Listα)(h₁:x∈l₁++l₂)(Variable name `h₂` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₂Note: This linter can be disabled with `set_option linter.unusedVariables false`h₂:x∈l₂++l₃):x∈l₁++l₃∨x∈l₂:=byα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁++l₂h₂:x∈l₂++l₃⊢ x∈l₁++l₃∨x∈l₂simpath₁α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂++l₃h₁:x∈l₁∨x∈l₂⊢ x∈l₁++l₃∨x∈l₂;simpath₂α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ x∈l₁++l₃∨x∈l₂;simpα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ (x∈l₁∨x∈l₃)∨x∈l₂;liaAll goals completed! 🐙
On the other hand, if we instead decline to use lia and solve the goal manually,
this example uses simp in a nonterminal position and is considered poor style:
exampleαx(l₁l₂l₃:Listα)(h₁:x∈l₁++l₂)(Variable name `h₂` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₂Note: This linter can be disabled with `set_option linter.unusedVariables false`h₂:x∈l₂++l₃):x∈l₁++l₃∨x∈l₂:=byα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁++l₂h₂:x∈l₂++l₃⊢ x∈l₁++l₃∨x∈l₂simpath₁α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂++l₃h₁:x∈l₁∨x∈l₂⊢ x∈l₁++l₃∨x∈l₂;simpath₂α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ x∈l₁++l₃∨x∈l₂;simpα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ (x∈l₁∨x∈l₃)∨x∈l₂casesh₁with|inlh=>inlα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₁⊢ (x∈l₁∨x∈l₃)∨x∈l₂leftinlα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₁⊢ x∈l₁∨x∈l₃;leftinlα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₁⊢ x∈l₁;exacthAll goals completed! 🐙|inrh=>inrα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₂⊢ (x∈l₁∨x∈l₃)∨x∈l₂rightinrα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₂⊢ x∈l₂;exacthAll goals completed! 🐙
Using simp this way is brittle because if we add new simp lemmas to our library,
this can change the way that our hypotheses and goals are simplified. Because our proof
after the simps relies on the precise structure of the goals and hypotheses, these
changes could cause the proof to break as the structure of the development evolves.
We can fix the style of this proof by changing the simps to specify which theorems
they are using to simplify:
exampleαx(l₁l₂l₃:Listα)(h₁:x∈l₁++l₂)(Variable name `h₂` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₂Note: This linter can be disabled with `set_option linter.unusedVariables false`h₂:x∈l₂++l₃):x∈l₁++l₃∨x∈l₂:=byα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁++l₂h₂:x∈l₂++l₃⊢ x∈l₁++l₃∨x∈l₂-- the * here targets all hypotheses and the goalsimponly[List.mem_append]at*α:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₁:x∈l₁∨x∈l₂h₂:x∈l₂∨x∈l₃⊢ (x∈l₁∨x∈l₃)∨x∈l₂casesh₁with|inlh=>inlα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₁⊢ (x∈l₁∨x∈l₃)∨x∈l₂leftinlα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₁⊢ x∈l₁∨x∈l₃;leftinlα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₁⊢ x∈l₁;exacthAll goals completed! 🐙|inrh=>inrα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₂⊢ (x∈l₁∨x∈l₃)∨x∈l₂rightinrα:Type u_1x:αl₁:Listαl₂:Listαl₃:Listαh₂:x∈l₂∨x∈l₃h:x∈l₂⊢ x∈l₂;exacthAll goals completed! 🐙
This usage of simp only is better because the addition of new simp lemmas won't
cause this proof to change.
Another rule around proper simp usage applies to the appropriate definition
of simp lemmas.
All of the theorems marked with the @[simp] attribute in a Lean library compose the simp set
for that library, and the result of simplifying an expression iteratively using all of the
theorems in the simp set is the simp normal form of that expression.
It's important for the stability of proofs using simp that all the theorems in the simp
set progress towards this normal form. Accordingly, library designers often first consider
what they want that normal form to look like, and then structure their theorem definitions
accordingly. As a simple example, the simp normal form for lists prefers to use the ++
notation instead of List.append, so there is a simp theorem List.append_eq
whose type is List.append_eq {α : Type u} {as bs : List α} : as.append bs = as ++ bs.
In this case, the simp normal form appears on the right, while the expression in need of
simplification appears on the left. We can thus think of this theorem as simplifying from left
to right. Not every simp lemma in the standard library has a simp normal form
on its right-hand side, but all make progress towards simp normal form when applied.
For our purposes, in this textbook and in later ones, we will take care to define our simp
lemmas such that they respect this left-to-right simplification behavior.
A final automated tactic to have in your toolkit is trivial,
which tries a number of different simple tactics
(such as rfl or contradiction)
to close the current goal. Some examples:
As a culminating exercise for this chapter and as practice using the automation techniques we
discussed above on a real proof,
we examine the theory of regular expressions,
eventually working up to a proof of the pumping lemma.
Regular expressions are a formal language for describing sets of
strings. Their syntax is defined as follows:
inductiveRegExp(α:Type):Typewhere|EmptySet|EmptyStr|Char(c:α)|App(r1r2:RegExpα)|Union(r1r2:RegExpα)|Star(r:RegExpα)derivingBEq,DecidableEq,Repr-- prevents printing dot-chained, method-call-style like r1.App r2attribute[pp_nodot]RegExp.CharRegExp.AppRegExp.UnionRegExp.StarnamespaceRegExp
Note that this definition is polymorphic: regular
expressions in RegExp α describe strings with characters drawn
from α — which in this exercise we represent as lists with
elements from α.
(Technical aside: we depart slightly from standard practice in
that we do not require the type α to be finite. This results in
a somewhat different theory of regular expressions, but the
difference is not significant for present purposes.)
We connect regular expressions and strings by defining when a
regular expression matches some string.
Informally, this looks as follows:
The regular expression EmptySet does not match any string.
If re₁ matches s₁, and re₂ matches s₂,
then App re₁ re₂ matches s₁ ++ s₂.
If at least one of re₁ and re₂ matches s,
then Union re₁ re₂ matches s.
Finally, if we can write some string s as the concatenation
of a sequence of strings s = s₁ ++ ... ++ sₖ, and the
expression re matches each one of the strings sᵢ,
then Star re matches s.
In particular, the sequence of strings may be empty, so
Star re always matches the empty string [] no matter what
re is.
We can easily translate this intuition into a set of rules,
where we write s =~ re to say that re matches s:
Notice that these rules are not quite the same as the
intuition that we gave at the beginning of the section. First, we
don't need to include a rule explicitly stating that no string is
matched by EmptySet; indeed, the syntax of inductive definitions
doesn't even allow us to give such a "negative rule." We just
don't happen to include any rule that would have the effect of
EmptySet matching some string.
Second, the intuition we gave for Union and Star corresponds
to two constructors each: mUnionL / mUnionR, and mStar0 /
mStarApp. The result is logically equivalent to the original
intuition but more convenient to use in Lean, since the recursive
occurrences of ExpMatch are given as direct arguments to the
constructors, making it easier to perform induction on evidence.
(The exercises below ask you
to prove that the constructors given in the inductive declaration
and the ones that would arise from a more literal transcription of
the intuition are indeed equivalent.)
Notice how the last example applies mApp to the string
[1] directly. Since the goal mentions [1, 2] instead of
[1] ++ [2], Lean wouldn't be able to figure out how to split
the string on its own.
Using inversion, we can also show that certain strings do not
match a regular expression:
We can define helper functions for writing down regular
expressions. The reg_exp_of_list function constructs a regular
expression that matches exactly the string that it receives as an
argument:
(Note the use of List.append_nil to change the goal of the theorem to
exactly the shape expected by mStarApp.)
The following lemmas show that the intuition about matching given
at the beginning of the section can be obtained from the formal
inductive definition.
The next lemma is stated in terms of the List.foldr function on lists:
if ss : List (List α) represents a sequence of
strings s₁, ..., sₙ, then List.foldr (· ++ ·) [] ss is the result of
concatenating them all together.
It turns out that the EmptyStr constructor is actually not
needed, since the regular expression matching the empty string can
also be defined from Star and EmptySet:
State and prove that this EmptyStr' definition matches exactly
the same strings as the EmptyStr constructor.
Since the definition of ExpMatch has a recursive
structure, we might expect that proofs involving regular
expressions will often require induction on evidence.
For example, suppose we want to prove the following intuitive
fact: if a string s is matched by a regular expression re,
then all elements of s must occur as character literals
somewhere in re.
To state this as a theorem, we first define a function reChars
that lists all characters that occur in a regular expression:
theoremin_re_match{α:Type}{s:Listα}{re:RegExpα}{x:α}(hmatch:s=~re)(hin:x∈s):x∈reCharsre:=byα:Types:Listαre:RegExpαx:αhmatch:s=~rehin:x∈s⊢ x∈re.reCharsinductionhmatchwith|mEmpty=>mEmptyα:Types:Listαre:RegExpαx:αhin:x∈[]⊢ x∈EmptyStr.reCharscontradictionAll goals completed! 🐙|mCharc=>mCharα:Types:Listαre:RegExpαx:αc:αhin:x∈[c]⊢ x∈(Charc).reCharssimponly[reChars]mCharα:Types:Listαre:RegExpαx:αc:αhin:x∈[c]⊢ x∈[c];assumptionAll goals completed! 🐙|mApp____ih₁ih₂=>mAppα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝h₂✝:s₂✝=~re₂✝ih₁:x∈s₁✝→x∈re₁✝.reCharsih₂:x∈s₂✝→x∈re₂✝.reCharshin:x∈s₁✝++s₂✝⊢ x∈(Appre₁✝re₂✝).reChars/- Something interesting happens in the `mApp` case. We obtain
_two_ induction hypotheses: one that applies when `x` occurs in
`s₁` (which is matched by `re₁`), and a second one that applies
when `x` occurs in `s₂` (matched by `re₂`). -/simponly[reChars,List.mem_append]at*mAppα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝h₂✝:s₂✝=~re₂✝ih₁:x∈s₁✝→x∈re₁✝.reCharsih₂:x∈s₂✝→x∈re₂✝.reCharshin:x∈s₁✝∨x∈s₂✝⊢ x∈re₁✝.reChars∨x∈re₂✝.reCharscaseshinwith|inlhin₁=>mApp.inlα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝h₂✝:s₂✝=~re₂✝ih₁:x∈s₁✝→x∈re₁✝.reCharsih₂:x∈s₂✝→x∈re₂✝.reCharshin₁:x∈s₁✝⊢ x∈re₁✝.reChars∨x∈re₂✝.reCharsleftmApp.inlα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝h₂✝:s₂✝=~re₂✝ih₁:x∈s₁✝→x∈re₁✝.reCharsih₂:x∈s₂✝→x∈re₂✝.reCharshin₁:x∈s₁✝⊢ x∈re₁✝.reChars;exactih₁hin₁All goals completed! 🐙|inrhin₂=>mApp.inrα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝h₂✝:s₂✝=~re₂✝ih₁:x∈s₁✝→x∈re₁✝.reCharsih₂:x∈s₂✝→x∈re₂✝.reCharshin₂:x∈s₂✝⊢ x∈re₁✝.reChars∨x∈re₂✝.reCharsrightmApp.inrα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝h₂✝:s₂✝=~re₂✝ih₁:x∈s₁✝→x∈re₁✝.reCharsih₂:x∈s₂✝→x∈re₂✝.reCharshin₂:x∈s₂✝⊢ x∈re₂✝.reChars;exactih₂hin₂All goals completed! 🐙|mUnionL__ih=>mUnionLα:Types:Listαre:RegExpαx:αs₁✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝ih:x∈s₁✝→x∈re₁✝.reCharshin:x∈s₁✝⊢ x∈(Unionre₁✝re₂✝).reCharssimponly[reChars,List.mem_append]mUnionLα:Types:Listαre:RegExpαx:αs₁✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝ih:x∈s₁✝→x∈re₁✝.reCharshin:x∈s₁✝⊢ x∈re₁✝.reChars∨x∈re₂✝.reChars;leftmUnionLα:Types:Listαre:RegExpαx:αs₁✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₁✝:s₁✝=~re₁✝ih:x∈s₁✝→x∈re₁✝.reCharshin:x∈s₁✝⊢ x∈re₁✝.reChars;exactihhinAll goals completed! 🐙|mUnionR__ih=>mUnionRα:Types:Listαre:RegExpαx:αs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₂✝:s₂✝=~re₂✝ih:x∈s₂✝→x∈re₂✝.reCharshin:x∈s₂✝⊢ x∈(Unionre₁✝re₂✝).reCharssimponly[reChars,List.mem_append]mUnionRα:Types:Listαre:RegExpαx:αs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₂✝:s₂✝=~re₂✝ih:x∈s₂✝→x∈re₂✝.reCharshin:x∈s₂✝⊢ x∈re₁✝.reChars∨x∈re₂✝.reChars;rightmUnionRα:Types:Listαre:RegExpαx:αs₂✝:Listαre₁✝:RegExpαre₂✝:RegExpαh₂✝:s₂✝=~re₂✝ih:x∈s₂✝→x∈re₂✝.reCharshin:x∈s₂✝⊢ x∈re₂✝.reChars;exactihhinAll goals completed! 🐙|mStar0=>mStar0α:Types:Listαre:RegExpαx:αre✝:RegExpαhin:x∈[]⊢ x∈(Starre✝).reCharssimpathinAll goals completed! 🐙|mStarApp____ih₁ih₂=>mStarAppα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre✝:RegExpαh₁✝:s₁✝=~re✝h₂✝:s₂✝=~Starre✝ih₁:x∈s₁✝→x∈re✝.reCharsih₂:x∈s₂✝→x∈(Starre✝).reCharshin:x∈s₁✝++s₂✝⊢ x∈(Starre✝).reChars/- Here again we get two induction hypotheses, and they illustrate
why we need induction on evidence for `ExpMatch`, rather than
induction on the regular expression `re`: the latter would only
provide an induction hypothesis for strings that match `re`, which
would not allow us to reason about the case `x ∈ s₂`. -/simponly[List.mem_append]athinmStarAppα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre✝:RegExpαh₁✝:s₁✝=~re✝h₂✝:s₂✝=~Starre✝ih₁:x∈s₁✝→x∈re✝.reCharsih₂:x∈s₂✝→x∈(Starre✝).reCharshin:x∈s₁✝∨x∈s₂✝⊢ x∈(Starre✝).reCharscaseshinwith|inlhin₁=>mStarApp.inlα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre✝:RegExpαh₁✝:s₁✝=~re✝h₂✝:s₂✝=~Starre✝ih₁:x∈s₁✝→x∈re✝.reCharsih₂:x∈s₂✝→x∈(Starre✝).reCharshin₁:x∈s₁✝⊢ x∈(Starre✝).reCharsexactih₁hin₁All goals completed! 🐙|inrhin₂=>mStarApp.inrα:Types:Listαre:RegExpαx:αs₁✝:Listαs₂✝:Listαre✝:RegExpαh₁✝:s₁✝=~re✝h₂✝:s₂✝=~Starre✝ih₁:x∈s₁✝→x∈re✝.reCharsih₂:x∈s₂✝→x∈(Starre✝).reCharshin₂:x∈s₂✝⊢ x∈(Starre✝).reCharsexactih₂hin₂All goals completed! 🐙
Exercise★(reNotEmpty) (Manually graded)
Write a recursive function reNotEmpty that tests whether a
regular expression matches some string. Prove that your function
is correct.
One potentially confusing feature of the induction tactic is
that it won't let you perform an induction over a term that
isn't sufficiently general. Here's an example:
example(α:Type)(s₁s₂:Listα)(re:RegExpα):s₁=~Starre→s₂=~Starre→s₁++s₂=~Starre:=byα:Types₁:Listαs₂:Listαre:RegExpα⊢ s₁=~Starre→s₂=~Starre→s₁++s₂=~Starreintroh₁α:Types₁:Listαs₂:Listαre:RegExpαh₁:s₁=~Starre⊢ s₂=~Starre→s₁++s₂=~Starre/- Now, just doing an `inversion` on `h₁` won't get us very far in
the recursive cases. (Try it!) So we need induction (on
evidence). We might try this, but Lean won't let us: -/Invalid target: Index in target's type is not a variable (consider using the `cases` tactic instead)Starreinductionh₁α:Types₁:Listαs₂:Listαre:RegExpαh₁:s₁=~Starre⊢ s₂=~Starre→s₁++s₂=~Starre
Invalid target: Index in target's type is not a variable (consider using the `cases` tactic instead)Starre
The problem here is that induction over a Prop hypothesis only
works properly with hypotheses that are "fully general," i.e.,
ones in which all the arguments are just variables, as opposed to more
specific expressions like Star re.
A possible, but awkward, way to solve this problem is "manually
generalizing" over the problematic expressions by adding
explicit equality hypotheses to the lemma:
The tactic generalize h : e = x causes Lean to (1) replace all
occurrences of the expression e by the variable x, and (2) add
an equation h : e = x to the context. Here's how we can use it
to show the above result:
Do not confuse generalize with the generalizing clause on
induction introduced in the Tactics chapter.
The generalizing clause would not help us here — induction on
s₁ =~ Star re would still fail because Star re is a compound expression,
not a bare variable.
Exercise★(exp_match_ex2) (Optional)
The MStar'' lemma below (combined with its converse, the
MStar' exercise above) shows that our definition of ExpMatch
for Star is equivalent to the informal one given previously.
One of the first really interesting theorems in the theory of
regular expressions is the so-called pumping lemma, which
states, informally, that any sufficiently long string s matching
a regular expression re can be "pumped" by repeating some middle
section of s an arbitrary number of times to produce a new
string also matching re. For the sake of simplicity, this
exercise considers a slightly weaker theorem than is usually
stated in courses on automata theory — hence the name
weak_pumping. The stronger one can be found below.
To get started, we need to define "sufficiently long." Since we
are working in a constructive logic, we actually need to be able
to calculate, for each regular expression re, a minimum length
for strings s to guarantee "pumpability."
Next, it is useful to define an auxiliary function that repeats a
string (appends it to itself) some number of times. Note
how we define simp lemmas for napp to go with its definition.
The (weak) pumping lemma itself says that, if s =~ re and if the
length of s is at least the pumping constant of re, then s
can be split into three substrings s₁ ++ s₂ ++ s₃ in such a way
that s₂ can be repeated any number of times and the result, when
combined with s₁ and s₃, will still match re.
Since s₂ is also guaranteed not to be the empty string, this gives us
a (constructive!) way to generate strings matching re that are
as long as we like.
This proof is quite long, so to make it more tractable we've
broken it up into a number of subproofs, which we then assemble
to prove the main lemma.
Your job is to complete the proofs of the helper lemmas; the main
lemma relies on these.
Now here is the usual version of the pumping lemma. In addition to
requiring that s₂ ≠ [], it also strengthens the result to
include the claim that s₁.length + s₂.length ≤ re.pumpingConstant.
We previously proved that ∀ l, Pal l → l = l.reverse.
The converse is also true, but significantly more difficult to prove, due
to the lack of evidence. Using the definition of Pal above, prove that
∀ l, l = l.reverse → Pal l
Source revision: 9cc9a7b, committed 2026-09-28 19:09 UTC