feat(Computability/Languages): regular languages have matching regular expressions through Kleene’s Algorithm - #887
feat(Computability/Languages): regular languages have matching regular expressions through Kleene’s Algorithm#887chiyunhsu wants to merge 95 commits into
Conversation
Added regex_of_dfa' to avoid equiv with Fin n
…into IsRegularIffRegex
I personally prefer to have the list of lemmas given to grind in order of application. I have not seen adding a whole by tactic into grind before. I think it might be too much golfing
RegularExpresions.lean has been saved in SummerResearch2026
ctchou
left a comment
There was a problem hiding this comment.
This is the first batch of my comments. I will have more in the future.
My general point is that by defining FLTS.execution, you can express directly the concepts in the textbook proof, rather than using bespoke recursive definitions. This allows you to leverage the large number of results about lists in mathlib.
| section PathSupp | ||
|
|
||
| variable {State : Type*} | ||
|
|
||
| /-- `PathSupp s xs` is the set of states that can be reached from state `s` by reading | ||
| the string `xs`, not including the starting state and the ending state. -/ | ||
| def PathSupp (flts : FLTS State Symbol) : State → List Symbol → Set State | ||
| | _, [] | _, [_] => ∅ | ||
| | s, a :: x => {flts.tr s a} ∪ PathSupp flts (flts.tr s a) x | ||
|
|
||
| theorem pathSupp_empty_iff_empty_or_char {flts : FLTS State Symbol} {s : State} {xs : List Symbol} : | ||
| PathSupp flts s xs = ∅ ↔ xs = [] ∨ (∃ a : Symbol, xs = [a]) := by | ||
| match xs with | ||
| | [] | [_] => grind [PathSupp] | ||
| | x :: y :: ys => | ||
| have : flts.tr s x ∈ PathSupp flts s (x :: y :: ys) := by grind [PathSupp] | ||
| grind | ||
|
|
||
| /-- If `xs` is nonempty, then the interior states of the run that starts at `s` and reads `a :: xs` | ||
| consist of the state reached after reading `a` as well as the interior states of the run that starts | ||
| at `flts.tr s a` and reads `xs`. -/ | ||
| theorem pathSupp_head {flts : FLTS State Symbol} {s : State} {a : Symbol} {xs : List Symbol} | ||
| (hxs : xs ≠ []) : PathSupp flts s (a :: xs) = | ||
| {flts.tr s a} ∪ PathSupp flts (flts.tr s a) xs := by | ||
| grind [PathSupp] | ||
|
|
||
| theorem pathSupp_append {flts : FLTS State Symbol} {s : State} {xs ys : List Symbol} | ||
| (hxs : xs ≠ [] ∧ ys ≠ []) : PathSupp flts s (xs ++ ys) = | ||
| {flts.mtr s xs} ∪ PathSupp flts s xs ∪ PathSupp flts (flts.mtr s xs) ys := by | ||
| induction xs generalizing s with | ||
| | nil => grind [PathSupp] | ||
| | cons a xs ih => | ||
| rw [List.cons_append, pathSupp_head (by simp [hxs.2])] | ||
| by_cases hx : xs = [] | ||
| · grind [PathSupp] | ||
| · grind [pathSupp_head hx] | ||
|
|
||
| end PathSupp |
There was a problem hiding this comment.
Instead of defining this specialized notion PathSupp, I think you should define the following general notion:
namespace Cslib.FLTS
def execution (flts : FLTS State Label) (s : State) : List Label → List State
| [] => [s]
| μ :: μs => s :: flts.execution (flts.tr s μ) μs
end Cslib.FLTS
which you can place in the file FLTS/Basic.lean. So far as I can tell, you need PathSupp only to define BddPath in the following expression:
∀ i ∈ PathSupp p.toFLTS p.start xs, i < p.bound
whch you can express as:
let ss := p.toFLTS.execution p.start xs
∀ k, ∀ _ : 0 < k ∧ k + 1 < ss.length, ss[k] < p.bound
Other expressions involving membership in PathSupp can be similarly translated.
I make this suggestion because execution is a cleaner notion which will have simpler properties and can have more general applications in the future. The above expression is also a more direct translation of the informal proof. Lean's grind tactic is very good at reasoning about simple properties of natural numbers, so you shouldn't have any problem working with list indices.
Also, this whole section should be in the namespace Cslib.FLTS.
There was a problem hiding this comment.
BTW, you may want to consider doing the proof using NFA rather than DFA. (Dexter Kozen's textbook has the proof.) LTS.Execution is already defined and a number of theorems proved about it. I suspect there may be less "noise" in that setup.
There was a problem hiding this comment.
I agree with the suggestion of using Execution than PathSupp.
I am now contemplating on the reasons whether NFA approach would be more preferable than DFA approach.
More preferble:
LTS.Executionis defined. Defining FLTS.Execution duplicates the code (unless there might other usages? I feel that FLTS.Execution would not be huge.)- Explicit NFA -> Regex rather than implicit NFA -> DFA -> Regex as currently is (but is it needed?)
Less preferable:
- The current
LTS/Execution.leanhas no lemmas about bounding the states. Everything should be rewritten and strengthened, so back to the previous question, is explicit NFA -> Regex so preferable? And would this strengthened (probably longer codes) worth not defining FLTS.Execution?
I am all ears to hear what you think before we start to rewrite the codes for better applications in the future.
There was a problem hiding this comment.
It seems to me that the crux of the proof is to formalize Figure 3.3 of Hopcroft et al's textbook. Given an execution in the sense of LTS.Execution and the additional assumption that no state in the execution is > k, you can obtain the indices of the occurrences of k using Nat.nth p, where the predicate p is something like:
fun j => ∃ _ : 0 < j ∧ j + 1 < ss.length, ss[j] = k
Then using those indices you can extract the relevant segments of ss and xs using List.extract (which degenerates to List.take and List.drop for the first and last of them). Then you need to show that those extracted segments are LTS.Execution in their own right and have the necessary properties to make them members of the various R_ij^(k) sets. But there are a lot of results about Nat.nth, List.extract, LTS.Execution, etc in mathlib and cslib that you can leverage and Lean's grind tactic is very good at reasoning about list indices.
Alternatively, you can continue to use concepts like splitFirst and splitLast. Here I would suggest that instead of splitting the list directly, you use List.findIdx to find the index of the occurrence of k and then split the list using List.take and List.drop with that index. Your theorems like split{First,Last}_append are then just instances of the existing theorem List.take_append_drop.
There was a problem hiding this comment.
Thanks for making more detailed suggestions. To clarify, it sounds like there are multiple routes to change the code:
- Define new
FLTS.Executionto replace the role ofPathSupp. - Use existing
LTS.Executionto replace the role ofPathSupp. This will involveFLTS.toLTS. - Upgrade the PR to prove Kleene's algorithm for NFA -> Regex. Use existing
LTS.Execution.
Which route are you suggesting us to take?
There was a problem hiding this comment.
Number 3. I think the determinism of FLTS is a distraction.
The hard part of the proof is to identify the occurrences of k in an execution and reason about them. It doesn't really matter whether the execution is from a deterministic or nondeterministic machine. There just happens to be more infrastructure already developed for the latter in cslib.
| /-- Starting at state `i`, the function `splitLast` sends a string to its longest prefix | ||
| ending at state `k`. | ||
| If the string ends at state `k`, then `splitLast` returns the original string. | ||
| If the string never passes through state `k` (starting state can be `k`), | ||
| then `splitLast` returns the empty string. -/ | ||
| def splitLast (flts : FLTS (Fin n) Symbol) (i k : Fin n) : List Symbol → List Symbol | ||
| | [] => [] | ||
| | a :: x => if (splitLast flts (flts.tr i a) k x = []) ∧ flts.tr i a ≠ k then [] | ||
| else a :: splitLast flts (flts.tr i a) k x |
There was a problem hiding this comment.
See my comment about splitFirst below.
| then `splitFirst` returns the original string. -/ | ||
| def splitFirst (flts : FLTS (Fin n) Symbol) (i k : Fin n) : List Symbol → List Symbol | ||
| | [] => [] | ||
| | a :: x => if flts.tr i a = k then [a] else a :: splitFirst flts (flts.tr i a) k x |
There was a problem hiding this comment.
Using the notion of FLTS.execution mentioned above, I think splitFirst can be defined more directly without recursion: just use List.findIdx or its variants to search for k in FLTS.execution and then use the returned index to extract the sub-list via List.take. For splitFirstCompl, use List.drop instead of List.take. Mathlib has a lot of theorems about List.{findIdx,take,drop} which I suspect can simplify many of your proofs or even make some of your theorems unnecessary.
For splitLast[Compl], reverse FLTS.execution before applying List.findIdx.
Alternatively, Nat.nth and Nat.count let you talk about the occurrences of k directly:
https://leanprover-community.github.io/mathlib4_docs/Mathlib/Data/Nat/Nth.html
We introduce
IsRegular.iff_regex, a language is regular if and only if it matches a regular expression. It is a combination of the existingIsRegular.regex, a language matching a regular expression is regular and our new direction. The new direction is proven through Kleene’s algorithm.It is the result that was anticipated in PR#846 by @ctchou.
The pull request introduces a new file,
KleeneAlgorithm.lean. In it, we proved a language defined by a DFA onFin nwith one accepting state has a matching regular expression. The algorithm applies induction on a bound which restricts which interior states that a run may pass through.Implemented collaboratively by Brooke Gill and Chi-Yun Hsu