从零开始的语法制导器:词法分析(一)
编译原理恰好学到第二章,作业要求是参照龙书自己写一个语法制导器。目前项目已经完成了第一版,不过有些地方仍然有待改进,自己也不是特别满意,未来可能会重构,在复习与查漏的同时也供大家参考。
项目源码(第一版)
https://github.com/LabinNovenki/MySDT
什么是语法制导
我们要解决的第一个问题:理解什么是语法制导?
以下定义摘自维基
语法制导翻译是指一种源语言代码的翻译完全由语法分析器驱动的编译器的实现方法。
一个常见的语法制导翻译方法是将输入字符串通过把相应的动作附加到每一条语法规则上的方法翻译为一连串的动作。
因此,对于一个基于某语法的字符串的解析会产生一个对于规则的应用序列。语法制导分析提供了一种将语义附加到任何一种语法上的简单方法。
上面的定义可能有些晦涩难懂,那么我们来看一下语法制导的英文原文Syntax-directed translation(简称SDT),Syntax是句法的意思,英文释义the arrangement of words and phrases to create well-formed sentences in a language.指的是将单词组织起来成句的方法,它规定了每个单词怎样排列组合才能形成一个合法的句子。Syntax-directed顾名思义就是以句法为导向的翻译器。与自然语言一样,当我说“我爱你”时,你接受到的是一个合法的句子,因为这个句子以主+谓+宾的形式被合法地组织到了一起。同样的,我们的目的正是让我们的机器“读懂”我们输入的文本想要表达的是什么意思,只不过这次我们对机器说的不是自然语言,而是编程语言。
计算机领域的学习中,当你无法从字面理解一个专业名词时,寻求其英文释义往往是值得尝试的手段。
语法制导器的模块
龙书中的一张图很好地说明了语法制导器的工作流程:
词法分析用于将输入的字符串分隔成一个个词法单元,并且将这个词法单元序列交由给语法分析器作出解释,语法分析将从左向右(但也不一定,依赖于具体的文法定义)扫描阅读词法单元,检查语法错误,最后由语义分析根据语句所表达的意思作出相应的操作。
在本文中将着重讲词法分析的部分
语法定义
我们目标中的语法制导器支持以下功能:
1)支持浮点数和整数两种类型,整数可以转换成浮点;浮点数不可以转换成整数;
2)每个语句需要以“;”结束,以“#”表示全部输入的结束;
3)变量需要先声明再使用;
4)变量名可以是由数字和字母组成,但首字符必须是字母。
5)每个表达式中使用的变量需要在之前已经有赋值。
6)输出语句使用write(a),输出并换行,其中a为int类型或者float类型的变量名。
关键词 | 说明 |
---|---|
float | 定义一个浮点型变量 |
int | 定义一个整型变量 |
write | 输出一个变量的值 |
词法单元
一般来说,一个词法单元由终结符号与属性值组成,终结符号指的是一个语言中无法再被继续推导的符号,不是终结符的都是非终结符。非终结符可理解为一个可拆分元素,而终结符是不可拆分的最小元素。而词法单元的属性则会根据终结符号的不同代表不同的含义。
当终结符号为if,while这样的关键词时,一般来说词法单元属性并不重要,如果终结符号为id,option(分别代表着标识符与操作符)这样可以由多个词素表示的终结符号的属性一般是词素的字符串变量。有时词法单元还包含了这个词法单元出现的行号,来输出相关的编译信息。
我们用一个不那么恰当的例子来说明语法单元:
词法分析语句
我爱你
得到的词法单元序列:
<主语 ,我> <谓语,爱> <宾语,你>
DFA与NFA
下面我们来介绍词法分析中最为重要的两个概念:确定的有穷自动机 Deterministic Finite Automata(DFA) 与不确定的有穷自动机Nondeterministic Finite Automata(NFA).
初学者可以简单地将自动机理解为一个状态转化图,它对于每一个输入只能简单地回答是或否。
DFA与NFA最大的区别在于NFA可以以空串作为标号,而DFA不能。并且对于所有输入符号,对于DFA的每个状态来说,有且只有一条离开该状态、以该符号位标号的边。
DFA与NFA可以互相地进行转化,这在数学上可以被证明。一般来说NFA更有利于人类的理解,因此我们设计的时候一般采用NFA的写法,而DFA更有利于计算机的处理。在龙书中提到的词法分析器自动化生成工具LEX中,这两者被结合起来使用以达到更高的转换效率。
设计一个有穷自动机
输入字母表 Σ = {alpha, digit, option}
其中alpha代表26个字母
digit代表0~9十个数字字符
option代表+, -, *, /, =, %操作符
我们目标是,标识符id必须由字母开头,必须只由数字与字母组成,那么其正则表达式为 alpha (alpha | digit)*
识别标识符的自动机可以如下图所示:
可以发现的是,由于空串的存在,我们在扫描到一个字符后无法确定该自动机是处于结束输入的2状态还是处于等待下一个alpha/digit输入的1状态,这也是为何称其为不确定有穷自动机的原因。
我们可以为这样的NFA创建一个表格:
状态 | alpha | digit | else | 空串 |
---|---|---|---|---|
0 | {1, 2} | ERROR | ERROR | ERROR |
1 | {1} | {1} | ERROR | {2} |
2 | ERROR | ERROR | ERROR | ERROR |
我们来手动地将上述NFA转换成DFA:
状态 | alpha | digit |
---|---|---|
{0} | {1, 2} | ERROR |
{1, 2} | {1, 2} | {1, 2} |
将{0}编码为状态A, {1, 2}编码为B,其中当读取到最后一个字符时,如果是状态B,则自动机识别出一个合法的表示符。
上述表格中的ERROR并不代表出错,而是说明检测到的词法单元并不是一个标识符,根据语法定义需要进行下一步的检测。
在程序编写的过程中,我们只需要记住自动机当前的状态,并且根据输入对应地查找上述表格即可实现。现在你知道为什么我们的程序更喜欢DFA了。
有关NFA向DFA的转化也能够通过相应算法自动化地实现。
先写这么多,主要是介绍词法分析相应的基本概念吧,具体的实现在项目中的lexcial.cpp/lexcial.h有代码。由于只是个简单的语法制导翻译器,因此实现非常简单,不需要借助工具就能够自主设计。