INCOMING BOCHUM MATERIAL summarized by Claude (old/bochum-lf-updates/AltAuto.v): the
Bochum LF updates extend AltAuto's discussion of the sequencing
tactical with new material on Rocq's "local form with `..`":
T; [T1 .. | Tn]
which applies T1 to the first goal, Tn to the last, and T1 to all
goals in between (variants: T; [T1 | .. | Tn] applies nothing in
between; the `..` may also appear first, last, or alone). The new
material illustrates this by revisiting star_app from IndProp:
Lemma star_app'': forall T (s1 s2 : list T) (re : reg_exp T),
s1 =~ Star re ->
s2 =~ Star re ->
s1 ++ s2 =~ Star re.
Proof.
intros T s1 s2 re H1.
remember (Star re) as re' eqn:Eq.
induction H1
as [|x'|s1 re1 s2' re2 Hmatch1 IH1 Hmatch2 IH2
|s1 re1 re2 Hmatch IH|re1 s2' re2 Hmatch IH
|re''|s1 s2' re'' Hmatch1 IH1 Hmatch2 IH2];
[discriminate .. | intros H; apply H | idtac]. (* <=== *)
(* MStarApp *)
intros H1. rewrite <- app_assoc.
apply MStarApp.
+ apply Hmatch1.
+ apply IH2.
* apply Eq.
* apply H1.
Qed.
(first shown in its long form with all seven cases spelled out, then
shortened as above). Bochum also adds a QUIETSOLUTION alternate
solution to AltAuto's re_opt exercise that uses nested `..` lists
instead of `try`, and rewords the introduction of `T; T'` to say
simply that it is "equivalent to locally performing T' on all the
subgoals".
To incorporate: Lean has no direct analogue of the positional
`[T1 .. | Tn]` goal-selector list; the closest idioms are
case-labelled alternatives (`case ... =>`/`next`), `all_goals`,
and `first`. A future pass should decide whether to add a
parallel discussion here (e.g. using `star_app` below, proving the
six non-MStarApp cases uniformly) or to record the Rocq material
as intentionally unported.
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`inductiveSilly:Nat→Propwhere|mk1{n:Nat}(h:n>1):Sillyn|mk2{n:Nat}(h:1∈[]):Sillyn|mk3{n:Nat}(h:∃m,n=m+2):Sillynexample{n:Nat}(h:Sillyn):n≠1:=byn:Nath:Sillyn⊢ n≠1inversionhwith|mk1=>liaAll goals completed! 🐙|mk2=>contradictionAll goals completed! 🐙|mk3=>liaAll goals completed! 🐙
The try and <;> combinators used together allow you to apply a tactic to some,
but not all, goals...
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 repeat combinator can 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]
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.
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 using for rewriting are
good ones to give to simp, which is why they are also called simplification lemmas.
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.
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]
The simp ... at ... tactic simplifies in a hypothesis.
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.
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
Should we add something like the following, to explain conditional rewrites?
Recall from Tactics that rewriting with a theorem h₁ -> h₂ -> a = b uses
a = b and leaves h₁ and h₂ as new subgoals. We can include such theorems
in simp's available set, too, but it handles the premises differently:
it tries to discharge h₁, h₂ too, recursively, using simp again (by default),
and only fires the rewrite if it succeeds.
If it can't discharge a premise, it just doesn't use that lemma.
Claude tested three variants of this against the real toolchain (lake env lean).
The double_injective lemma from Tactics doesn't actually work here: its conclusion is the
bare variable n = m, which isn't a usable rewrite pattern (simp lemmas need a real
compound term on the left), so simp [double_injective] fails outright with "simp made
no progress" before it ever gets anywhere near the n.double = m.double premise.
A conditional lemma that does have a proper compound left-hand side, like
Nat.sub_add_cancel : n ≤ m -> m - n + n = m, tells a cleaner three-part story. With
hle : n ≤ m in context:
example (n m : Nat) (hle : n ≤ m) : m - n + n = m := by
simp [Nat.sub_add_cancel] -- fails: "simp made no progress"
example (n m : Nat) (hle : n ≤ m) : m - n + n = m := by
simp [Nat.sub_add_cancel, hle] -- succeeds
example (n m : Nat) (hle : n ≤ m) : m - n + n = m := by
simp_all [Nat.sub_add_cancel] -- succeeds, without naming hle
The first case shows the discharge step only consults the active simp set (default set
plus whatever's explicitly listed) -- not arbitrary context hypotheses -- so hle
sitting right there doesn't help; simp just skips the lemma, exactly the "no error, no
leftover subgoal" behavior claimed above. The second shows discharge working once hle
is added to the set. The third previews simp_all (introduced a bit further down this
chapter): it folds every hypothesis into the active set automatically, so it discharges
the premise without hle being named at all. That third point makes this example worth
placing right here rather than after simp_all -- it's a preview, and it ties the two
sections together.
Worth tying this convention explicitly back to the fixpoint framing suggested near the
top of this section: a terminal simp is safe precisely because nothing downstream
depends on its exact resulting term -- only on whether it helped close the goal. A
nonterminal simp is risky because whatever manual tactic comes next (cases h1 with |
inl h => ... in the example below) depends on the specific shape simp leaves behind,
and that shape isn't a stable contract -- it can change as the simp set grows, since
simp just runs to whatever fixpoint the current lemma set produces. Saying that
explicitly might make the "why" land harder than jumping straight to the
terminal/nonterminal rule.
Don't use simp without only unless you're closing a goal or following with a flexible
tactic. The example below breaks this rule:
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! 🐙
This usage of simp is brittle and can break due to upstream changes.
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! 🐙
Another rule around proper simp usage applies to the appropriate definition
of simp lemmas.
Appropriately defined simp lemmas simplify left to right.
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:
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 α.
Note to developers (Daniel Sainati @dsainati1)
CH: Do you mean here that this is different because the inductive type doesn't specify α is finite? In Lean the convention is for inductives not to carry Prop-valued typeclass assumptions, enforcing this only at the theorems that use them. So this could give off a slightly wrong impression.
DHS: @bcpierce00 What was the purpose of this aside in the original Rocq text? Does it make sense to keep here?
We connect regular expressions and strings by defining when a
regular expression matches some string.
Note to developers (Mike Hicks @mwhicks1)
It would be convenient to declare the variables below so that inline prose
throughout the rest of this section can use α, x, s, s₁, s₂, s₃,
ss, re, re₁, and re₂ without repeating their type annotations, but the
same problem described in Logic applies: an unused variable
is silently added to the local context in basically every proof from here on,
even when the theorem never mentions it, which makes theorem hover-overs in
the HTML book unusable. Until we have a way to declare variables visible only
for inline prose (rather than for every lean block), we leave this
commented out:
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:
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:
Naturally, proofs about ExpMatch 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:
theoremdeclaration uses `sorry`in_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₂`). -/sorryAll 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₂`. -/sorryAll goals completed! 🐙
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.
The remainder of this section in the full version of the chapter develops an extended
exercise on regular expressions, leading up to a proof of 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.