朱娜婓编译原理学习笔记
说明
笔记大部分内容来自参考资料[1], 看了B站上中科大华保健老师的编译原理课视频(参考资料[2]),补充完善了DFA的代码表示、Hopcroft 算法、文法重写、LL(1)算法、LR算法等内容
有许多知识是结合了自己的理解进行整理,所以可能会有错误之处
再往后因为时间问题就有点烂尾了...
文章末尾有北京工业大学2019年软件学院编译原理的考题回忆及朱娜斐老师划分的考点(recoded by @杰哥)
Keyword
- 中科大华宝健编译原理学习笔记
- 北京工业大学朱娜婓编译原理学习笔记
- 2019年北京工业大学软件学院编译原理期末考试试卷回忆版
什么是编译器
什么是编译器
编译器是一个程序,核心功能是把源代码翻译成目标代码
编译器的工作解释
源代码-->编译器的静态计算-->目标程序-->计算机的动态计算-->计算结果
静态计算
是指编译器只根据程序文本静态地分析(如做报错分析、优化分析),而不是真的拿 CPU 去执行
计算机
可能是一个 x86 的物理器(如对应 C 语言),也可能是 JVM java 虚拟机(如对应 java)。
编译器和解释器的比较
解释器也是一类处理程序的程序
区别在于:
-
编译器:输入源代码,输出一个可执行程序,但不去执行。
(存放在磁盘上等待被加载到内存中执行)
-
解释器:输入源代码,直接输出执行结果。
其实 JVM 就是一个解释器,而不是一个单纯的编译器。输入 java 字节码 bytecode ,然后直接输出执行结果,而不是输出汇编代码。
编译器简史
第一个编译器是Fortran语言的编译器
该编译器给计算机科学的发展产生了巨大的影响:
- 理论上:算法、数据结构、形式语言与自动机
- 实践上:软件工程、体系结构等
- 编译器架构
编译器内部结构
简述
编译器具有非常模块化的高层结构
编译器可以看做多个阶段构成的“流水线” 结构
编译器规模庞大,拆分模块容易实现和维护
一种没有优化的编译器结构
一种更复杂的编译器结构
编译器通常会被划分为两个部分(如下图):
- 前端:源代码生成中间代码,和源代码有关
- 后端:中间代码生成目标代码并优化,和目标代码有关
- 两者以抽象语法树 AST(Abstract Syntax Tree) 作为连接数据
[图片上传失败...(image-188606-1560422078662)]
一个简单的例子
背景一:现在我们设计一个叫做 Sum 的语言,特别简单,仅仅支持两种语法。第一是整形数字 n
,第二是加法表达式 e1 + e2
。举几个例子:
3
5 + 6
-
7 + 8 + 9
(加法要满足左结合性,即先计算7 + 8
) 7 + (8 + 9)
- 但不支持
7 + 8 * 9
,Sum 语言中没有乘法
背景二:有一个栈式计算机 Stack (后面会再次讲到),其中有一个操作数栈,只支持两条指令,push n
和 add
。之所以选择栈式计算机,第一是因为简单,第二是因为 JVM 就是采用了这种形式。其指令的详细解释例子如下:
-
push 3
将 3 压栈 -
push 4
将 4 压栈 -
add
将 3 和 4 出栈,然后做加法得到 7 ,再将 7 压栈。即将栈顶的两个元素都出栈,做加法,将结果再压栈
有了上述两个背景之后,接下来的任务是:将程序 1 + 2 + 3
编译到栈式计算机 Stack 。
第一个阶段是词法分析,先不管其中的原理是什么,总之词法分析会将 1 + 2 + 3
拆分为 1
+
2
+
3
这 5 个部分。(后面会提到词法分析的原理就是用正则表达式匹配)
第二阶段是语法分析,就是将词法分析拆分出来的内容,分析是否满足 Sum 语言的语法要求,即 n
或 e1 + e2
这种语法。
第三个阶段是语法树构造(有时算在语法分析阶段里) ,经过某些计算之后(可以看出是按中序遍历生成了二叉树),得到的抽象语法树如下图:
[图片上传失败...(image-d8b081-1560422078662)]
第四个阶段是根据抽象语法树做代码生成。首先,要满足加法的左结合性,对树进行遍历的时候就要优先遍历左子树,即后序遍历(左右根)。
在遍历树节点的过程中,如果遇到整数 n
就生成一条 push n
指令,如果遇到 +
就生成一条 add
指令。
接下来详细看一下这棵树的遍历过程:
- 第一步要访问的节点是
1
,生成push 1
,将 1 压栈 - 第二步要访问的节点是
2
,生成push 2
,将 2 压栈 - 第三步要访问的节点是
+
,生成add
,将 1 2 出栈,计算加法得到 3 ,将 3 压栈 (这里即体现了加法的左结合性) - 第四步要访问的节点是
3
,生成push 3
,将 3 压栈 - 第五步要访问的节点是
+
,生成add
,将 3 3 出栈,计算加法得到 6 ,将 6 压栈,完成
词法分析
词法分析简介
简介
从编译器内部结构得知,执行编译的第一个阶段就是词法分析。
词法分析的任务:将字符流转为记号流
字符流即源程序代码,记号流即编译器内部定义的数据结构、编码所识别出的词法单元
词法分析即将源程序代码与编译器内部定义的数据结构相对应
通俗来说,就是将源代码进行最细粒度的拆解,例如上面的例子将 1 + 2 + 3
拆分为 1
+
2
+
3
一样
一个例子
[图片上传失败...(image-58705d-1560422078662)]
如上图,从源代码到记号流(单词流)。
词法分析器会将源程序根据关键字、标识符(变量)、括号、引号、运算符、值(整数、字符串)等这些要素,将其从左到右拆分为若干个记号(单词),其中会忽略空格和换行等。上图中记号流输出的含义:
-
IF
关键字 -
LPAREN
RPAREN
左右括号 -
INDENT(x)
即标识符(变量),有一个属性x
,表示变量名 -
GR
即>
-
INT(t)
即int
类型值,属性是5
- 其他同理……
- 最后红色的
EOF
是结束符
根据上面的例子,可以总结出 token 其实有固定的形式,就可以定义其数据结构,如下图(本文中高级语言的示例,默认情况下都是 C 语言)
[图片上传失败...(image-61f347-1560422078662)]
理解了例子,定义了数据,接下来就要去探寻词法分析的实现算法,第一,手工构造;第二,自动生成 。
词法分析的手工构造法
手工构造即手写一个词法分析器,例如 GCC LLVM ,优点是利于掌控和优化细节,缺点是工作量大、易出错。手工构造法主要用到“转移图”这种数据结构,下面举两个例子说明。
[图片上传失败...(image-fafc05-1560422078662)]
上图的转移图模型,即可识别逻辑运算符,如 <=
<
<>
>=
>
。识别到第一个字符,就继续往下做分支判断,直到返回一个确定的运算符。
图中的 *
即一次回溯,即将当前的这个字符再返回到词法分析器重新进行分析。
例如 >1
,读到了 1
这个字符时,此时已经确定了运算符是 >
,而当前的 1
并不是运算符的一部分,因此将 1
再重新返回到词法分析器中重新进行分析。
[图片上传失败...(image-8d0828-1560422078662)]
上图是标识符(变量)的转移图模型,以及伪代码。其中 *
即一次回溯,跟上面一样。
关键字(如 class
if
for
等)是一种特殊的标识符,也满足标识符的规则。
要识别关键字,有两种解决方案:
- 继续扩展转移图的分支,识别到关键字走不通的分支逻辑,最后识别出关键字。
- (关键字表算法) 先识别所有的合法标识符,然后从已经识别出来的标识符中查找关键字。此时需要为该语言所有的关键字维护一个哈希表,如果数据结构合理(完美哈希),查询可以在
O(1)
复杂度内完成。
词法分析的自动生成技术
简介
所谓自动生成技术,就是有这样现成的工具(如 lex
flex
jlex
),输入一些声明式的规范,即可自动生成一个词法分析器。优点当然是简单快速,缺点就是无法控制细节。而这里的“声明式规范”,就是我们常见的正则表达式。下文的内容,就是如何用程序去解析正则表达式,如果你之前看过关于“正则表达式 原理”这类的文章,可能早就有了解了。
先说一下自动生成技术的几个阶段,专业术语后面都有解释:
- 正则表达式 -> NFA(
Thompson
算法) - NFA -> DFA(子集构造算法)
- (DFA的优化(
Hopcroft
最小化算法)) - DFA -> 词法分析代码,即完成自动生成
正则表达式
概念解释
正则表达式是一种数学上的概念,
首先它要有一个完整的字符集 Σ = {...}
要能涵盖程序所有的关键字、变量名、数字、运算符、括号、引号、特殊符号等
- 如 C 语言的这个字符集就是
ASC
编码,即 256 个字符 - 如 java 的字符集就是
unicode
编码,可能几万甚至十几万个字符集(因为 java 的变量名称并不仅限于英文、中文也可以作为变量)
然后只有以下几个基本的逻辑:
- 空串 是正则表达式
- 对于字符集中的任意单个字符是正则表达式
- 如果M和N是正则表达式,则以下也是正则表达式:
- 选择
M|N
两者取并集 - 连接
MN
是正则表达式,两者相连 - 闭包
M*
={ ,M,MM,MMM...} 称为“闭包”(和程序的闭包不一样),即可以有 0 或者若干个M
- 选择
- 以上随机组合,都是正则表达式,例如
a|(bc*)
这就是正则表达式的定义,而现代正则表达式这么多的语法,例如 [a-b]
?
+
等,都是后来扩展出的语法糖,即对基本规则的一种简写方式。
正则表达式例子
-
C语言中的关键字,例如if,while等,如何用正则表达式表示?
就是用
if
,while
表示,因为i
是字符集(C语言对应ASC字符集)的元素,f
是字符集中的元素,所以 它们连接形成if
是正则表达式同理可说明为什么其它关键字是正则表达式
-
C语言中的标识符:以字母或下划线开头,后跟零个或多个字母、数字或下划
线。如何用正则表达式表示?(a|b|c|...|z|A|B|...|Z|_ )(a|b|c|...|z|A|B|...|Z|_ |0|1|2...|9)*
如果用语法糖来表示的话就是[a-zA-Z_] [a-zA-Z_0-9]*
语法糖
有限状态自动机 FA
也称“有穷自动机”,是一种数学模型。
简单理解,就是输入一个字符串,输出这个字符串是否满足某个规则(true / false)。
FA(有限状态自动机) 实质上是带有边和节点的有向图。