Skip to content

feat(Computability/Languages): regular languages have matching regular expressions through Kleene’s Algorithm - #887

Open
chiyunhsu wants to merge 96 commits into
leanprover:mainfrom
chiyunhsu:IsRegularIffRegex
Open

chiyunhsu wants to merge 96 commits into
leanprover:mainfrom
chiyunhsu:IsRegularIffRegex

Conversation

@chiyunhsu

@chiyunhsu chiyunhsu commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

We introduce IsRegular.iff_regex, a language is regular if and only if it matches a regular expression. It is a combination of the existing IsRegular.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 on Fin n with 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

@ctchou ctchou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Cslib/Computability/Languages/KleeneAlgorithm.lean Outdated
Comment thread Cslib/Computability/Languages/KleeneAlgorithm.lean Outdated
Comment thread Cslib/Computability/Languages/KleeneAlgorithm.lean Outdated
Comment thread Cslib/Computability/Languages/KleeneAlgorithm.lean Outdated

@ctchou ctchou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can benefit from having the following functions about List:

namespace List

variable {α : Type*}

def AllButFirstLast (p : α → Bool) (as : List α) : Prop :=
  ∀ i, ∀ _ : 0 < i ∧ i + 1 < as.length, p as[i]

def findIdxButFirst (p : α → Bool) (as : List α) : ℕ :=
  as.tail.findIdx p + 1

def revFindIdxButLast (p : α → Bool) (as : List α) : ℕ :=
  as.length - as.reverse.findIdxButFirst p

end List

Then not only a lot of your definitions can be simplified (see the inline comments), but you can state and prove properties about the above List functions without distractions.

I have not looked into the proofs in detail.

Comment on lines +59 to +81
/-- A Bounded Path (`BddPathLTS`) has states `Fin n` and accepts strings (lists of symbols)
starting with state `start` and ending with state `last`
with the interior states less than `bound`. -/
structure BddPathLTS (n : ℕ) (Symbol : Type*) extends LTS (Fin n) Symbol where
/-- The start state of the path. -/
start : Fin n
/-- The last state of the path. -/
last : Fin n
/-- The bound for interior states of the path. -/
bound : ℕ

instance : Acceptor (BddPathLTS n Symbol) Symbol where
Accepts (p : BddPathLTS n Symbol) (xs : List Symbol) :=
∃ ss : List (Fin n),
p.toLTS.Execution p.start xs p.last ss ∧
∀ idx, ∀ _ : 0 < idx ∧ idx + 1 < ss.length, ss[idx] < p.bound
-- ∀ s ∈ ss.tail.dropLast, s < p.bound

theorem language_bddpath_eq_nfa (lts : LTS (Fin n) Symbol) (i j : Fin n) {k : ℕ} (hk : n ≤ k) :
language (BddPathLTS.mk lts i j k) =
language (NA.FinAcc.mk {Tr := lts.Tr, start := {i}} {j}) := by
simp [language, Accepts]
grind

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole block of code can be replaced by:

namespace Cslib.LTS

variable {Symbol : Type*} {n : ℕ}

def BddExec (lts : LTS (Fin n) Symbol) (bound : ℕ)
    (start : Fin n) (xs : List Symbol) (last : Fin n) (ss : List (Fin n)) : Prop :=
  lts.Execution start xs last ss ∧ ss.AllButFirstLast (· < bound)

def bddLang (lts : LTS (Fin n) Symbol) (bound : ℕ) (start last : Fin n) : Language Symbol :=
  { xs | ∃ ss, lts.BddExec bound start xs last ss }

end Cslib.LTS

Note that your main proof will refer to different starts, lasts, and bounds in the same statement, so it is better to have them as explicit parameters rather than as fields of structures which you need to construct and destruct all the time.

Accepts (p : BddPathLTS n Symbol) (xs : List Symbol) :=
∃ ss : List (Fin n),
p.toLTS.Execution p.start xs p.last ss ∧
∀ idx, ∀ _ : 0 < idx ∧ idx + 1 < ss.length, ss[idx] < p.bound

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nitpick: use i instead of idx. I understand that you want to use i, j, k etc to denote states from Fin n. But I think it is clearer to keep denoting the latter using s, t, r, etc.

Comment on lines +128 to +138
/-- Starting at state `i`, the function `splitFirst` sends a string to its shortest prefix
ending at state `k`. -/
def splitFirst {lts : LTS (Fin n) Symbol} {i j : Fin n} {xs : List Symbol} {ss : List (Fin n)}
(_ : LTS.Execution lts i xs j ss) (k : Fin n) : List Symbol :=
xs.take (ss.tail.findIdx (· = k) + 1)

/-- Starting at state `i`, the function `splitFirstCompl` sends a string to its longest suffix
starting at state `k`. -/
def splitFirstCompl {lts : LTS (Fin n) Symbol} {i j : Fin n} {xs : List Symbol} {ss : List (Fin n)}
(_ : LTS.Execution lts i xs j ss) (k : Fin n) : List Symbol :=
xs.drop (ss.tail.findIdx (· = k) + 1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in general a good idea to include only those parameters which are actually needed:

def splitFirst (xs : List Symbol) (ss : List (Fin n)) (k : Fin n) : List Symbol :=
  xs.take (ss.findIdxButFirst (· = k))

def splitFirstCompl (xs : List Symbol) (ss : List (Fin n)) (k : Fin n) : List Symbol :=
  xs.drop (ss.findIdxButFirst (· = k))

The same comment applies to SplitLast[Compl] above.


open List

section splitLast

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is probably better to put the splitLast section after the splitFirst section, because the latter seems easier to reason about than the former.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants