自己动手实现编译器理论篇 (一) 词法分析

711 阅读4分钟

词法分析

编译器的执行流程

编译器执行可以分为8个阶段:词法分析、语法分析、语意分析、中间代码生成、指令选择、代码优化、寄存器分配、代码生成。其中前4个阶段构成编译器的前端frontend,剩余阶段组成了编译器的后端backend。如下图所示: image-20230414144058013-1454469.png 编译器的前端与机器无关,后端执行的结果是特定系统架构下的汇编代码。为了生成可执行程序,汇编代码还要经过汇编器、链接器。整个程序的执行可以用下面这张图来说明: image-20230414234604616.png

词法分析的作用

词法分析和NLP里的命名实体识别技术类似,都是把输入字符映射到有意义的代表物(Token)上,和人类语言相比,编程语言的规则语法过于简单,所以我们用基于规则的字符匹配算法就可以搞定这一步,并不需要模型算法技术。当然,在词法分析中,我们还可以滤除冗余信息(空格,换行符等),所以词法分析可以看作是从原始信息中提取关键信息的一步。对于source code中的词法结构,我们应该如何描述呢?正则表达式是一种工具,有描述工具之后就需要实现它,对应的实现工具就是有限自动机(finite automata)。

有限自动机

定义

有限自动机是一个五元组(S,Σ,δ,s0,SA)(S,\Sigma,\delta,s_0,S_A):

  • SS 是一个有限状态集合,包含一个错误状态ses_e
  • Σ\Sigma 是一个有限字母集合
  • δ(s,c)\delta(s,c) 叫做转移函数,对于sScΣ,δ(s,c)S\forall s\in S \vee \forall c \in \Sigma ,\delta(s,c) \in S ,状态转移可以写为sicδ(si,c)s_i\stackrel{c}{\rightarrow}\delta(s_i,c)
  • s0Ss_0\in S 叫做起始状态
  • SASS_A \subseteq S 是接受状态的集合

有限自动机也可以用图来表示,假设我们要识别new,not,while这三个关键词,程序伪代码如下:

c = NextChar()
if (c == 'n'):
    c = NextChar()
    if(c == 'e')
        c = NextChar()
        if(c=='t'):
            return 'accept state'
	else if (c == 'o'):
        c = NextChar()
        if (c == 't'):
            return 'accept state'
else if(c=='w'):
    c = NextChar()
    if(c == 'h'):
        c = NextChar()
        if(c =='i'):
            c = NextChar()
            if(c=='l'):
                c = NextChar()
                if(c=='e'):
                    return 'accept state'
return 'unaccept state'

用状态图表示:

状态图

我们说状态机FA(S,Σ,δ,s0,SA)FA(S,\Sigma,\delta,s_0,S_A) 接受字符串xx当且仅当如下条件成立:

δ(δ(...δ(δ(δ(s0,x1),x2),x3)...,xn1),xn)SA\delta(\delta(...\delta(\delta(\delta(s_0,x_1),x_2),x_3)...,x_{n-1}),x_n)\in S_A

有没有一种等价的方式来描述上述的状态图呢?答案是有!正则表达式regular expression

正则表达式

定义

正则表达式在给定字母表上描述了字符集合,包括空字符。这样的字符集合称之为language。对于给定的正则表达式r,language表示为L(r).正则表达式定义了三种运算:

  • Union,RS:={xxR or xS}R|S:=\{x|x\in R \text{ or } x\in S\}
  • Concatenation,RS:={xyxR and yS}RS:=\{xy|x\in R\text{ and } y\in S\}
  • Closure,R:=i=0RiR^*:=\bigcup_{i=0}^{\infty}R^i

特别地,R+:=i=1Ri=RRR^+:=\bigcup_{i=1}^{\infty}R^i=RR^*,[0…9]=(0|1|2|3|4|5|6|7|8|9)

例子

  • 标识符identifier:([a...z][A...z])([a...z][A...Z][0...9])([a...z]|[A...z])([a...z]|[A...Z]|[0...9])^*

  • 非负整数:0[1...9][0...9]0|[1...9][0...9]^*or[0...9]+[0...9]^+

  • 非负实数: [0...9]+(ϵ.[0...9])E(ϵ+)(0...9)+[0...9]^{+}(\epsilon|.[0...9]^*)E(\epsilon|+|-)(0...9)^+

  • 字符串:“(( ^((“|\n) n) ))^*

  • 单行注释:\\(^\n)\n

  • 多行注释:\*(^*)**/

确定性有限自动机DFA

对于任意输入字母c,δ(s,c)\delta(s,c)的输出是唯一确定的

非确定性有限自动机NFA

对于任意输入字母c,δ(s,c)\delta(s,c)的输出可以有多个,并且包含空跳(不消费字母即可进行状态跳转)δ(s,ϵ)\delta(s,\epsilon)

DFA与NFA的等价性

对于有N个状态的NFA,我们总可以用至多2N2^N个状态节点构建DFA;对于DFA,本身就是NFA的一种特例,所以二者等价。

正则表达式到NFA

Thompson Construction

由于正则表达式在前面定义的三种运算上是封闭的,所以我们可以定义NFA的这三种等价运算:

abab

image-20230416010125181.png

aba|b

image-20230416010209372.png

aa^*

image-20230416010351166.png

例子 a(bc)a(b|c)^*

构建单独的DFA

image-20230416010849256.png

构建bcb|c

image-20230416011025019.png

构建(bc)(b|c)^*

image-20230416011124622.png

构建a(bc)a(b|c)^*

image-20230416011321949.png

DFA的等价表示

image-20230416011430187.png

对比DFA和NFA我们可以发现:NFA包含很多冗余的状态节点及状态转移,因此利用NFA构建DFA是必要的。

NFA到DFA

Subset Construction算法

变量解释:

  • ϵclosure(sa)\epsilon-closure(s_a):当前状态的所有空跳后继状态集合

  • n0n_0:start state

  • qq:迭代状态变量

  • QQ:DFA state set

算法描述

image-20230416143108667.png

算法分析:

对于NFA的N个状态,路径排列是2N2^N ,在while循环中注意到,Q是单调递增的,且worklist中t一定是不同的(相同地不会加入Q和worklist),当Q中元素数量达到最大时,add操作不会执行,所以算法一定会终止,不会陷入死循环。

例子

image-20230416151530550.png

image-20230416151559591.png

DFA