ch3.词法分析(2)——词法分析程序实现
词法分析程序简介
词法分析程序的工作
对由字符组成的源程序进行分析,将其识别为“单词”(具有独立意义的最小语法单位),再识别出与单词相关的属性(标识符、界限符、数等),最后再转换成长度上统一的标准形式——属性字,以供其他部分使用
词法分析时,注解、空格等非必要信息会被删除,而标识符会被录入符号表
例如,源程序:
begin M := 3.1415 / 4 * (d1 * d1 - d2 * d2) end
按顺序先后被识别为:
属性1,begin
属性2,M
属性5,:=
属性3,3.1415
……
词法分析输出结果为一个单词或单词串
词法分析程序的地位
将词法分析独立出来的好处(词法分析严格来讲是语法分析的一部分):针对词的特征单独研究这个方法
为每一个单词做一个结构,既关注属性,又关注属性的值
单词符号
单词形式表示为:<单词类别编码,单词自身值>
单词种别有两种标识方式:
1、一类一码:一个类别一个编码。例如,保留字一律编码为1,标识符一律编码为2,常数一律编码为3……
2、一符一码:一个单词符号一个编码。例如,if编码为3,then编码为4,else编码为5……
因为标识符和常数是有无穷多种的,所以即便是使用了一符一码的方式,在对标识符和常数进行编码时,也只能单独对其使用一类一码
单词符号的属性值是反映单词符号特征或特征的值,例如:标识符的符号表指针,常数的常数表指针等
单词的自身值可以存放索引:指向值的指针,可以保证所有单词都是同样大小的
标识符的语义信息可用一个机器字(机内符)来存放,如果信息过多,一个机内符存放不下,可在此机内符中存放部分语义信息,其余语义信息则存放在一个信息表区中。(https://wenku.baidu.com/view/0486459f51e79b89680226f3.html)
<单词机内符,单词自身值>
单词自身值可以为指向信息表的地址
符号表
符号表用于存放在程序中出现的各种单词符号及其语义属性,是编译程序进行各种语义检查(即语义分析)的重要依据,也是进行内存地址分配的依据
例如,<id, 1>中的1是位置信息,指向符号表的相应位置。符号表中可以找到详细信息
主程序开始时建立符号表,随着工作的深入,不断地向符号表加入信息/取出信息
符号表的内容包括:
1、标识符的名字
2、与标识符有关的信息:例如数组的信息向量表,过程或函数的参数的个数、类型、次序、是否允许递归等
3、基本内容:类型信息、存储类别(static、regist等)、地址码、层次信息(标识符所属分程序(过程)的静态层次)、行号信息(标识符在源程序中的行号(说明行与引用行))等
符号表可以使用线性符号表、树结构等各种数据结构实现
符号表的条目一般由名字栏、信息栏、连续的存储字(相应的指针或序号)组成
词法分析程序的设计
预处理
例如:删除注释、空格、回车换行符之类非必要信息
源程序不能一次性被读入时,会带来问题。例如定义了一个标识符name_of_student,但缓冲区大小正好只允许读到这个标识符的中间。
针对上述问题,可以使用双缓冲区进行解决:当对一个缓存区的扫描已经到底,而标识符还没有被读完的时候,可以将接下来的内容读入到另一个缓冲区中,并从那里开始继续向后扫描,直至识别出正在识别的标识符
单词符号的识别
因为如果只看当前扫描到了哪个位置,并不一定能准确判断正在扫描出来的是哪一个单词符号。例如,在扫描<=的第一个符号<时,可以认为这是<=的第一个字符,也可以认为这就是“小于”。这个时候,就需要提前向后看(超前搜索/预读),以此准确地判断出正在扫描的单词符号
要做到准确判断一个单词符号,至少需要超前搜索一位,超前搜索后,需要回退指针到上一个词结束的位置,以能够完整地识别这个单词(而不是多一个字符)
利用有穷自动机识别单词
最直接的思路:为每一个单词符号构建一个有穷自动机。但如果对if,while等关键字都单独构建DFA,则会导致工作量过于庞大,因此可以对有穷自动机进行合并,以精简程序
对于标识符和关键字这样的类型的区分:读出完整的单词后,去查关键字表,以判断其具体类型(例如:查到是关键字,查不到则是标识符)
DFA的选取问题:根据上述思路构造的DFA必然不止一个,在识别单词时,该选用哪一个DFA来处理下一个单词,就成为了一个问题。——解决方法:将开始状态合并为同一个,即得到了一个完整的自动机。这个FA可能是NFA,只需转换为DFA即可
状态转换图的实现:
1、从输入串中读一个字符
2、判明读入的字符与由此状态出发的哪条弧上的标记相匹配,便转至相匹配的那条弧所指向的状态
3、均不匹配时则失败(不能到达正常出口)
DFA的程序实现:
如图1所示,在开始状态时,单词name初始化为空串,随后开始向后读取字符。跳过若干个空格后,读到合法首字符时,便开始将读取到的字符加到name后面,随后继续向后看,直到读取到非法字符时,则表明读取结束。此时就得到了一个单词(name),在符号表中对name进行查询,判断其类型,再在相应的标识符表中进行填写,最后回退指针、返回这个单词
图1所示的过程的伪代码可以表示为:
if peek in letter # 如果下一位字符是字母
then while peek in letter or digit do # 每当下一位字符时字母或数字时
begin
name = name + peek; # 将下一位字符追加到name的末尾
read(peek) # 继续向后读一位
end; # 读取到非字母、非数字的字符时,结束循环
retract; # 回退指针
i = lookup(name); # 在符号表中查询name
if i != 0 then return(1, name) # 如果查询到了,则属于关键字
else return(2, name); # 否则,属于标识符
扫描程序构造方法:
1、对于无回路分叉结点,可以选用switch或if…else语句
2、对于含有回路的结点,可以使用由while和if语句构成的程序段
3、对于终态结点,使用return(code, value)语句,用于返回到调用者。其中,code为单词种别编码,value为单词属性值(或者无定义)
需要注意回退指针:搜索指示器回调一个字符