正则表达式(RE)、有限自动机(FA)和词法分析(LA)

正则表达式(RE, Regular Expression)是一种用于描述语言特性的方法,使用 RE 描述或者生成的语言被称为正则语言(RL, Regular Language),而与 RE 等价的被称为有限自动机(FA,Finite Automata)的方法则适合识别正则语言。

RE 和 FA 可以用于编译器的词法分析器(LA, Lexical Analysis)。词法分析的任务是将字符流转为特定语言的单词流,并且将每个单词根据当前语言的特性归类到到某个语法范畴,也即词类,例如 new 是关键字、_subset 是标识符等。正则表达式可以用于程序语言中有效单词的符号表示,RE 这种符号表示还需要转换为可以使用计算机程序实现的形式化方法,有限自动机就是这样的方法。词法分析器实现的流图如下:

Thompson算法
子集构造算法
Hopcroft最小化算法
模拟DFA
RE
NFA
DFA
最小DFA
词法分析程序

正则表达式 RE

语言(language)是由字符串(string)组成的集合,字符串是符号(symbol)的有限序列,符号来自有限字母表(alphabet)。可以使用 RE 来描述语言的字符串的集合。一个 RE 是由三个基本操作构建而成:

  • 可选(alternation):对于两个正则表达式 M M M N N N,可选操作( | )形成一个新的正则表达式 M ∣ N M | N MN
  • 联接(concatenation):对于两个正则表达式 M M M N N N,联接操作( ⋅ \cdot )形成一个新的正则表达式 M ⋅ N M \cdot N MN,也可以写成 M N MN MN
  • 重复(repeatition):对于给定正则表达式 M M M,它的克林闭包(Kleene closure)是 M ∗ M^* M,表示一个字符串是由 M 中字符串经零次或多次的联接运算而来。

另外,还有一个特殊的 RE:空字符串( ϵ \epsilon ϵ)。通过使用符号和可选、连结、重复、克林闭包运算以及空串 ϵ \epsilon ϵ,构建出新的可以用于表示程序语言中单词的正则表达式。正则表达式还提供了语法糖可以简化正则表达式。例如:

  • [ a b c d ] [abcd] [abcd]表示 ( a ∣ b ∣ c ∣ d ) ( a | b | c | d ) (abcd)
  • [ b − g ] [b-g] [bg] 表示 [ b c d e f g ] [bcdefg] [bcdefg] [ a − z A − Z ] [a-zA-Z] [azAZ] 表示字符集。
  • M ? M? M? 表示 ( M ∣ ϵ M | \epsilon Mϵ), M + M^+ M+ 表示( M ⋅ M ∗ M \cdot M^* MM)。

我们再来看下 C 语言中常见单词的正则表达式。例如:

  • C 语言的标识符,一般可以是字母或者下划线开头,然后紧跟字母或者数字,可以表示为 ( _ ∣ [ a − z A − Z ] ) ( _ ∣ [ a − z A − Z ] ∣ [ 0 − 9 ] ) ∗ ( \_ | [a-zA-Z] )( \_ | [a-zA-Z] | [0-9] )^* (_∣[azAZ])(_∣[azAZ][09]),如果限定字符串长度为10,则可以表示为 ( _ ∣ [ a − z A − Z ] ) ( _ ∣ [ a − z A − Z ] ∣ [ 0 − 9 ] ) 9 (\_ | [a-zA-Z] )( \_ | [a-zA-Z] | [0-9] )^9 (_∣[azAZ])(_∣[azAZ][09])9

  • 无符号整数,正则表达式为 0 ∣ [ 1 − 9 ] [ 0 − 9 ] ∗ 0 | [1-9][0-9]^* 0∣[19][09]

有限自动机 FA

正则表达式可以很方便地表示程序语言中的单词,但还需要一种可以使用计算机实现的形式化表示方法——有限自动机(FA,Finite Automata)。

例如我们有这么一段程序用于识别关键字 new:
在这里插入图片描述
代码的右侧给出其状态转移图,是有限自动机(或称有限状态机)的图形化表示,其形式化表示可以采用一个五元组 ( S , Σ , δ , s 0 , S A ) (S, \Sigma, \delta, s_0, S_A) (S,Σ,δ,s0,SA)

  • S S S 是状态机中有限状态的集合。
  • Σ \Sigma Σ 是状态机中有限字母表。转移图的边上字母的集合。
  • δ \delta δ 是转移函数。
  • s 0 s_0 s0 是初始状态。 s 0 ∈ S s_0 \in S s0S
  • S A S_A SA 是接受状态。 S A ∈ S S_A \in S SAS。在状态转移图上用双层圆圈表示。

例如:

S = { s 0 , s 1 , s 2 , s 3 } S = \{s_0, s_1, s_2, s_3\} S={s0,s1,s2,s3}
Σ = { n , e , w } \Sigma = \{n, e, w \} Σ={n,e,w}
δ = { s 0 → n s 1 , s 1 → e s 2 , s 2 → w s 3 } \delta = \{s_0 \stackrel{n} {\rightarrow} s_1, s_1 \stackrel{e} {\rightarrow} s_2, s_2 \stackrel{w} {\rightarrow} s_3 \} δ={s0ns1,s1es2,s2ws3}
s 0 = s 0 s_0 = s_0 s0=s0
S A = s 3 S_A = s_3 SA=s3

FA 分为确定有限自动机(DFA, Deterministic Finite Automata)和非确定有限状态机(NFA, Non-Deterministic Finite Automata)。

  • DFA:FA 中每个状态对于任一输入字符都只有唯一可能的转移。或者说转移函数为单值。DFA 不允许 ϵ \epsilon ϵ 转移。 ϵ \epsilon ϵ 转移是指针对空串输入 ϵ \epsilon ϵ 的转移,不会改变输入流中的读写位置。
  • DFA:FA 中状态对同一字符输入可能有多种转移,并且允许 ϵ \epsilon ϵ 转移。

例如, a ∗ a^* a a b ab ab 的 DFA 分别为:

使用 ϵ \epsilon ϵ 合并两个 DFA,形成一个 NFA,也即 a ∗ a b a^{*}ab aab

NFA 和 DFA 在表达能力上是等价的。任何 DFA 都是 NFA 的一个特例,或者说任何 NFA 都可以通过一个 DFA 来模拟。例如上述 NFA 可以通过下面的 DFA 来模拟:

RE 转换为 NFA:Tompson 算法

Tompson 算法是一种将 RE 转换为 NFA 的简单方法,它模拟了 RE 的联接、选择和克林闭包等基本运算的效果。例如:

这种转换方法适用于任意的 NFA。用同样的方法构造 a ( b ∣ c ) ∗ a(b | c)^* a(bc) 如下:

其实,对于简单的 RE,我们可以直接人工构造出其 NFA。例如 a ( b ∣ c ) ∗ a(b | c)^* a(bc),我们可以很简单的画出其 NFA 如下:

可见,通过 Thompson 算法构建的 NFA 比我们人工构建 NFA 多了很多不必要的 ϵ \epsilon ϵ 转移,在下面的阶段会消除他们。

NFA 转换为 DFA:子集构造法

与 NFA 相比,计算机模拟 DFA 的执行要简单很多,因而从 NFA 构造 DFA 是必要的。从前面介绍的 NFA 和 DFA 的区别可以知道,要想将 NFA 转换为 DFA,需要解决以下两种情况:

  1. 消除 ϵ \epsilon ϵ 闭包

状态 s 1 s_1 s1 s 2 s_2 s2 s 3 s_3 s3 完全可以合并,这样就可以消除 ϵ \epsilon ϵ 边。这里定义 ϵ \epsilon ϵ 闭包:

ϵ − c l o s u r e { S } \epsilon-closure\{S\} ϵclosure{S} 为从集合 S 中的状态出发,无需接收任何输入字符,即只通过 ϵ \epsilon ϵ 边就可达到的状态组成的集合。

  1. 消除不确定的状态

状态 s 1 s_1 s1 s 2 s_2 s2 s 3 s_3 s3 合并到一起,状态 s 0 s_0 s0 接收输入 n 0 n_0 n0 只有一条确定的转移状态。

综合以上,这里介绍一种被称为子集构造的算法,它以 NFA ( N , Σ , δ N , n 0 , N A ) (N,\Sigma, \delta_N, n_0, N_A) (N,Σ,δN,n0,NA) 为输入,得到一个 DFA ( D , Σ , δ D , d 0 , D A ) (D, \Sigma, \delta_D, d_0, D_A) (D,Σ,δD,d0,DA)。其算法如下图所示:
在这里插入图片描述
这里, D e l t a ( q , c ) Delta(q,c) Delta(q,c) 表示从状态 q q q 读入字符 c c c 能够到达的边的集合。下面给出算法迭代的例子:

对于 a ( b ∣ c ) ∗ a(b | c)^* a(bc) 的NFA:

使用子集构造算法迭代如下:

初始化:
ϵ − c l o s u r e { s 0 } = { s 0 } \epsilon-closure\{s_0\} = \{s_0\} ϵclosure{s0}={s0}
q 0 ← s 0 q_0 \leftarrow s_0 q0s0
Q ← s 0 Q \leftarrow s_0 Qs0
w l ← s 0 wl \leftarrow s_0 wls0

第一轮迭代:
{ s 0 } \{s_0\} {s0} w l wl wl 中去除,此时 w l wl wl 为空。

对于输入字符 a a a D e l t a ( { s 0 } , a ) = { s 1 } Delta(\{s_0\}, a) = \{s_1\} Delta({s0},a)={s1},则 t ← ϵ − c l o s u r e { D e l t a ( { s 0 } , a ) } = { s 1 , s 8 , s 6 , s 2 , s 4 , s 9 } t\leftarrow\epsilon-closure\{Delta(\{s_0\}, a) \}=\{s_1, s_8,s_6,s_2,s_4,s_9\} tϵclosure{Delta({s0},a)}={s1,s8,s6,s2,s4,s9}。记 d 0 = { s 0 } , d 1 = { s 1 , s 8 , s 6 , s 2 , s 4 , s 9 } d_0= \{s_0\},d_1=\{s_1, s_8,s_6,s_2,s_4,s_9\} d0={s0}d1={s1,s8,s6,s2,s4,s9}。此时,得到部分 DFA 如下。由于 t t t 中包括了原 NFA 的接受状态 s 9 s_9 s9,因而 d 1 d_1 d1 也是一个接受状态。 t = d 1 t=d_1 t=d1 不在 Q 中,将 d 1 d_1 d1 加入 Q Q Q w l wl wl 中,则 Q = { d 0 , d 1 } Q=\{d_0,d_1\} Q={d0,d1} w l = { d 1 } wl=\{d_1\} wl={d1}

对于输入字符 b b b c c c,状态 s 0 s_0 s0 不会发生转移状态。

第二轮迭代:
d 1 d_1 d1 w l wl wl 中删除,此时 w l wl wl 为空。

对于输入字符 a a a d 1 d_1 d1 中没有状态可以读入 a a a 发生转移。

对于输入字符 b b b d 1 d_1 d1 中只有 s 2 s_2 s2 可以发生状态转移,则 t ← ϵ − c l o s u r e { D e l t a ( { s 2 } , b ) = { s 3 , s 7 , s 6 , s 2 , s 4 , s 9 } } t\leftarrow\epsilon-closure\{Delta(\{s_2\}, b)=\{s_3,s_7,s_6,s_2,s_4,s_9\}\} tϵclosure{Delta({s2},b)={s3,s7,s6,s2,s4,s9}},记 d 2 = { s 3 , s 7 , s 6 , s 2 , s 4 , s 9 } d_2=\{s_3,s_7,s_6,s_2,s_4,s_9\} d2={s3,s7,s6,s2,s4,s9}。此时,得到的DFA如下图。 t = d 2 t=d_2 t=d2 不在 Q Q Q 中,将 d 2 d_2 d2 加入 Q Q Q w l wl wl 中。此时 Q = { d 0 , d 1 , d 2 } Q=\{d_0,d_1,d_2\} Q={d0,d1,d2} w l = { d 2 } wl=\{d_2\} wl={d2}

对于输入字符 c c c d 1 d_1 d1 中只有 s 4 s_4 s4 可以发生状态转移,则 t ← ϵ − c l o s u r e { D e l t a ( { s 4 } , c ) = { s 5 , s 7 , s 6 , s 2 , s 4 , s 9 } } t\leftarrow\epsilon-closure\{Delta(\{s_4\}, c)=\{s_5,s_7,s_6,s_2,s_4,s_9\}\} tϵclosure{Delta({s4},c)={s5,s7,s6,s2,s4,s9}},记 d 3 = { s 5 , s 7 , s 6 , s 2 , s 4 , s 9 } d_3=\{s_5,s_7,s_6,s_2,s_4,s_9\} d3={s5,s7,s6,s2,s4,s9}。此时,得到的DFA如下图。 t = d 3 t=d_3 t=d3 不在 Q 中,将 d 3 d_3 d3 加入 Q Q Q w l wl wl 中。此时 Q = { d 0 , d 1 , d 2 , d 3 } Q=\{d_0,d_1,d_2,d_3\} Q={d0,d1,d2,d3} w l = { d 2 , d 3 } wl=\{d_2,d_3\} wl={d2,d3}

第三轮迭代:
d 2 d_2 d2 w l wl wl 中删除,此时 w l = { d 3 } wl=\{d_3\} wl={d3}

对于输入字符 a a a d 2 d_2 d2 中没有状态可以发生转移。

对于输入字符 b b b d 2 d_2 d2 中只有 s 2 s_2 s2 可以发生状态转移,则 t ← ϵ − c l o s u r e { D e l t a ( { s 2 } , b ) = d 2 t\leftarrow\epsilon-closure\{Delta(\{s_2\}, b)=d_2 tϵclosure{Delta({s2},b)=d2,此时,得到的DFA如下图。 t = d 2 t=d_2 t=d2 已经在 Q Q Q 中。

对于输入字符 c c c d 2 d_2 d2 中只有 s 4 s_4 s4 可以发生状态转移,则 t ← ϵ − c l o s u r e { D e l t a ( { s 4 } , c ) = d 3 t\leftarrow\epsilon-closure\{Delta(\{s_4\}, c)=d_3 tϵclosure{Delta({s4},c)=d3,此时,得到的DFA如下图。 t = d 3 t=d_3 t=d3 已经在 Q Q Q 中。

第四论迭代:
d 3 d_3 d3 w l wl wl 中删除,此时 w l wl wl 为空。

对于输入字符 a a a d 3 d_3 d3 中没有状态可以发生转移。

对于输入字符 b b b d 3 d_3 d3 中只有 s 2 s_2 s2 可以发生状态转移,则 t ← ϵ − c l o s u r e { D e l t a ( { s 2 } , b ) = d 2 t\leftarrow\epsilon-closure\{Delta(\{s_2\}, b)=d_2 tϵclosure{Delta({s2},b)=d2,此时,得到的DFA如下图。 t = d 2 t=d_2 t=d2 已经在 Q Q Q 中。

对于输入字符 c c c d 3 d_3 d3 中只有 s 4 s_4 s4 可以发生状态转移,则 t ← ϵ − c l o s u r e { D e l t a ( { s 4 } , c ) = d 3 t\leftarrow\epsilon-closure\{Delta(\{s_4\}, c)=d_3 tϵclosure{Delta({s4},c)=d3,此时,得到的DFA如下图。 t = d 3 t=d_3 t=d3 已经在 Q Q Q 中。

至此。算法迭代结束。NFA 转换为了 DFA。

最小 DFA: Hopcroft 算法

到这里我们得到的 DFA 可能还存在冗余的状态,需要进一步处理得到最小化的 DFA。我们用到等价类划分的思想,如果两个状态等价,则他们属于同一个等价类,只需要保留其中之一即可。而两个状态对于任意输入字符串都产生同样的行为则二者等价。最小化 DFA 的 Hopcroft 算法如下图:
在这里插入图片描述
该算法的核心思想为:假设 d i d_i di d j d_j dj 都在集合 p s p_s ps 中,对 ∀ c ∈ Σ \forall c \in \Sigma cΣ d i → c d x d_i \stackrel{c} {\rightarrow} d_x dicdx d j → c d y d_j \stackrel{c} {\rightarrow} d_y djcdy,如果 d x , d y ∈ p t d_x,d_y \in p_t dx,dypt,则二者属于等价类。如果有任一状态 d k ∈ p s d_k \in p_s dkps d k → c d z d_k \stackrel{c} {\rightarrow} d_z dkcdz,而 d z ∉ p t d_z \notin p_t dz/pt,则 d k d_k dk d i 、 d j d_i、d_j didj 不属于等价类,也就需要将 d k d_k dk p t p_t pt 中分离出来。

该算法初始化将所有状态划分为两个状态集合,一个是所有的接收状态集合,另一个是它的补集。

对于 a ( b ∣ c ) ∗ a(b | c)^* a(bc) 的 DFA:

初始划分为 p 1 = { { d 0 } } p_1=\{\{d_0\}\} p1={{d0}} p 2 = { { d 1 , d 2 , d 3 } } p_2=\{\{d_1,d_2,d_3\}\} p2={{d1,d2,d3}} p 1 p_1 p1 只有一个状态,没法再拆分。对于 p 2 p_2 p2,输入字符 a a a p 2 p_2 p2 的状态都不会发生转移;输入字符 b b b c c c p 2 p_2 p2 中的状态都转回 p 2 p_2 p2;因此, Σ \Sigma Σ 中任意符号都不会导致 p 2 p_2 p2 发生拆分。

词法分析器实现方案

到目前为止,已经实现了从 RE 构建了一个 NFA,构建了一个 DFA 模拟 NFA,并且最小化了 DFA。需要进一步将最小 DFA 转换为可执行代码,即产生一个词法分析器。

从概念上将,DFA 是一个有向图,可以有多种实现策略,比如转移表(类似邻接矩阵)、哈希表、跳转表等。这里 有一个转移表实现的实例:

对于从 RE 转化为最小 DFA,当前已经有工具(Lex,Flex)可以自动化来完成。也就是说,编译器开发者先为词法分析器构建语法范畴的 RE,并将这些 RE 输入给词法分析器生成器,生成器会为每个 RE 构建一个 NFA,并使用 ϵ \epsilon ϵ 将各个 NFA 联接合并,再转换为 DFA,并最小化 DFA。最后,词法分析器生成器再将 DFA 转换为可执行代码。

当前商用(GCC、LLVM)的编译器很多是使用手工实现的词法分析器,精心编写的手工词法分析器会比词法分析器生成器生成的词法分析器更加高效,并且可以控制细节。

参考:

  • Engineering a compiler, Second Edition
  • Modern Compiler Implementation in C
  • https://mooc.study.163.com/learn/1000002001?tid=1000003000#/learn/content
  • 13
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您设计一个简洁的图灵机表达式的语法。这个语法可以描述一个图灵机的行为,类似于正则表达式有限自动机的描述。 首先,我们需要定义一些基本的符号和操作: 1. 状态符号:用来表示图灵机的状态,可以是任意字符串,但不能包含逗号或分号。例如:A, B, C, q0, q1, q2, ... 2. 输入符号:用来表示图灵机的输入字符,可以是任意字符串,但不能包含逗号或分号。例如:0, 1, a, b, c, ... 3. 空格符号:表示空格字符,通常用 "_" 表示。 4. 转移符号:用来表示图灵机的转移关系,可以是任意字符串,但不能包含逗号或分号。例如:->, -->, =>, ... 5. 分隔符:用来分隔不同的转移关系或状态。我们使用分号 ";" 作为分隔符。 接下来,我们定义一些操作符来描述图灵机的行为: 1. 连接操作符:用来连接两个图灵机状态。例如:A B,表示从状态 A 到状态 B。 2. 循环操作符:用来表示一个状态的自环。例如:A*,表示状态 A 自环。 3. 选择操作符:用来表示多个状态之间的选择。例如:A | B,表示从状态 A 或状态 B 转移。 4. 重复操作符:用来表示多个状态的重复。例如:A{2,4},表示状态 A 连续出现 2 到 4 次。 5. 反转义操作符:用来表示特殊字符的转义。例如:\->,表示转义 "->" 符号。 最后,我们可以使用这些符号和操作符来描述一个图灵机。例如,下面是一个描述一个简单的二进制加法器的图灵机: q0 0 -> q0 0 R ; q0 1 -> q1 1 R ; q0 _ -> q3 _ L ; q1 0 -> q2 1 R ; q1 1 -> q1 0 R ; q1 _ -> q2 1 R ; q2 0 -> q1 0 R ; q2 1 -> q2 1 R ; q2 _ -> q3 1 L ; q3 0 -> q3 0 L ; q3 1 -> q3 1 L ; q3 _ -> q4 _ R 这个表达式表示了一个从状态 q0 开始,对输入的二进制数进行加法运算的图灵机。它的描述包含了每个状态对不同输入字符的转移关系,以及最终状态的输出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值