一、编译概述
计算机科学研究方向众多,我们不可能精通所有,但是如果明白了本质,我们就能进行交叉学习,理顺思路,也就不至于觉得某项技术特别难,无从下手。
最原始的计算机是靠机器代码(Machine Code)来工作的,而计算机使用的是我们熟知的冯诺依曼体系结构,计算模型就是图灵机。冯诺依曼结构决定了计算机的控制,存储和运算单元,而图灵机则揭示了计算机是怎么进行运算的。说得简单一点,图灵机就是如下图的一串序列:
我们在此忽略图灵机的其他细节和各种理论,简言之,输入计算机的就是一串01序列,加上一些开始,接受,中止的状态来执行代码。
但是为了开发方便,提高效率,更多的易于理解的编程语言诞生了,我们用类似于人类语言一样,带有特定语法规则的符号和标记来简化和约束代码。可是不管怎样,我们至少目前改变不了计算机的工作方式,我们必须能够把代码转换成机器码才能让计算机执行,这样一个必然产生必然需要使用的工具就是编译器。
为了完成语言编译的工作,需要六个主要步骤:词法分析,语法分析,语义分析,代码优化,代码生成。前三个步骤一般又称为编译器前端工作
词法分析(Scanning):这个过程的输入是我们的源代码,它的工作是对我们编程语言规则的第一次也是最简单解析,也就是把关键字、变量、常量等单词读取并区分开来,最终得到一个Token table。
语法分析(Syntax Analysis):这个过程接受词法分析得到的符号表,根据编程语言的语法规则,将符号按照规则组合成语句,并检测输入是否符合文法。一般是上下文无关文法。例如一个赋值语句是否完整。
语义分析(Semantic Analysis):这个是语言的一个逻辑分析过程,比如将数组赋值给了一个整数变量。
二、词法分析
本篇文章主要将词法分析中的过程,在编程语言中,变量和关键字,一般都是字母,数字或者下划线,所有单词都是按照一定的规则构成,可以用正则表达式来描述。例如[0-9],[a-zA-Z],s*等等。
所有的这样的正则序列都可以用不确定有限状态自动机(NFA)来验证,也就是说我们输入的源程序,可以通过NFA来判断能不能被编译器接受。由正则语言到NFA的转换是直观的,简单的,但是由于机器更接近于确定性有限状态自动机这种更为严格的定义形式,所以我们需要把NFA转换为DFA,最后完成词法分析。
因此在词法分析中,最关键的步骤是NFA到DFA的转换。
三、NFA to DFA
NFA: 由一系列的状态和转移函数组成,并且允许epsilon-转换,而且某个状态可能转移到两个或者更多的下一状态,由此你不知道在遇到此状态时,到底该选择哪一个进行转换,这就是为什么叫做不确定性有限状态自动机。
DFA也是由状态和转移函数组成,但是不允许epsilon-转换,而且每个状态只能转移到一个确定的下一状态。
下面通过例子说明转换过程:
这是一个NFA
转换的第一步,起始状态是这么定义的:
epsilon-closure(0)={0,1,2,4,7}=A,A就是DFA的起始状态
这里转移路径只有两种,通过a或者b,于是第二步
DFA_Move(A,a)= epsilon-closure(NFA_Move(A,a))=epsilon-closure({3,8})={1,3,6,7,8}=B
这就得到了DFA的第二个状态
接下来DFA_Move(A,b)= epsilon-closure(NFA_Move(A,b))=epsilon-closure({5})={1,5,8}=C
这是DFA的第三个状态
A的状态转换已经结束,但是由于新增添了B,C状态,我们要按照A之前的方式,继续转换B和C
DFA_Move(B,a)= epsilon-closure(NFA_Move(B,8}=B,这是一个到自身的转换
DFA_Move(B,b)= epsilon-closure(NFA_Move(B,b))=epsilon-closure({5,9})={1,9}=D,增加了新状态D
接着DFA_Move(C,a)= B, DFA_Move(C,b)= C
DFA_Move(D,a)= B, DFA_Move(D,b)= E
DFA_Move(E,a)= B, DFA_Move(E,b)= C
到此,所有的状态都转换完毕。得到的DFA如下图所示:
最后我们发现,A-C的转换还可以进行简化,我们必须得到一个最简的DFA:
结束!