龙书阅读笔记

星期五, 十月 06, 2006

2.6 LEXICAL ANALYSIS

我们现在应该在翻译器中加入一个词法分析器,读取并把输入转换成token流。回忆Section 2.2中的一个文法定义,一种语言的语句由token字符串组成。组成一个单独的token的一个输入字符序列叫做一个Lexeme。一个词法分析器可以把一个解析器和token的lexeme表现形式隔离开来。我们开始列出某些函数,词法分析器需要执行的。

Removal of White Space and Comments

上一节中的表达式翻译器看到输入的每一个字符,所以额外的字符,例如空白,将导致失败。许多语言,允许“空白”(空格、tab和新行)出现在token之间。注释也通常被解析器和翻译器忽略,所以他们也可以被对待为空白。

如果空白被词法分析器排除,那么解析器将不需要考虑它们。不然就需要修改文法,加入对空白的处理,但非常难以实现。

Constants

任何时候当一个数字出现在表达式中时,看起来在这个出现的位置允许一个任意数目的整数常量是应该的。因为一个整数常量是一个数字序列,整数常量可以被允许要么向表达式文法中加入过程,要么为这样的常量创建一个token。通常,收集数字为整数的任务是词法分析器的,因为数字可以被对待为一个单一的单元,在翻译过程中。

让num是代表整数的token。当一个数字序列出现在输入流中,词法分析器将把num传递给parser.整数的值作为num token的一个属性一起传送过去。逻辑上,词法分析器可以同时把token和属性传输过去。如果,我们写一个token和他的属性为一个tuple,用<>,那么输入
31 + 28 + 59

将被翻译为tuples:
<num, 31> <+, > <num, 28> <+, > <num, 59>

+ 这个token没有任何属性。tuple的第二个元素,属性,在解析的过程中不承担任何角色,但是在翻译时需要

Recognizing Identifiers and Keywords

语言使用标识符作为变量、数组和函数的名字。一种语言的文法通常把一个标识符对待为一个token。基于这样一个文法的解析器想看到相同的文法,例如id,每次在输入中看到一个标识符的时候。例如,输入:
count = count + increment; (2.15)

会被词法分析器转换为token流:

id = id + id ; (2.16)

这个token流用作解析。

当谈论输入行(2.15)的词法分析时,有必要区分token id和 与这个token实例相关的lexeme- count 和increment之间的区别。翻译器需要知道(2.16)中,前两个id实例的lexeme 是count,第三个id实例是lexeme increment.


当一个lexeme形成一个标识符在输入中被看到的时候,某些机制被需要去决定这个lexeme是否在以前被看倒过。第一章介绍过的符号表就是这样的一种机制。lexeme保存在符号表中,指向这个符号表项的一个指针变成这个token id的一个属性。

许多语言使用固定的字符串例如begin, end ,if等等,作为标点符号,或者指定一定的构造。这个字符串,称为关键字keywords,一般满足形成标识符的规则,因此需要有一种机制决定一个lexeme是一个关键字还是一个标识符。如果关键字是保留的,reserved,那么比较容易解决,也就是说,它们不能被用作为标识符。那么一个字符串只有不是关键字的时候才能作为一个标识符使用。

隔离token的问题也会发生,如果出现在lexeme中的相同的字符可以出现在多个token中,例如Pascal的< , <=和<>。高效识别这些token的技术将在Chapter3中讨论。

Interface to the Lexical Analyzer

当一个词法分析器被插入到解析器和输入流之间的时候,他可以象图2.25一样和两个交互。



它从输入读取字符,把它们分组为lexeme,然后把lexemes形成的token和他们的属性传递给编译器的后面阶段。在这样的情况下,词法分析器必须在决定返回给解析器什么token之前,预先读取某些字符。例如,一个Pascal的词法分析器必须预先读取,在它看到字符>之后。如果下一个字符是=,那么字符序列>=是形成"大于等于"操作符的lexeme。如果不是的情况下,它必须把这个字符放回到输入,因为它应该是下一个lexeme的开始字符。

词法分析器和解析器形成一个生产者-消费者对。procedure-consumer pair.词法分析器生产token,解析器消费它们。被生成的token可以放在一个token缓冲区中,直到它们被消费。两者之间的交互只受缓冲区大小的约束,因为词法分析器不能前进,当缓冲区满的时候,解析器也不能前进,如果buffer是空的。通常,buffer只包含一个token。此时,交互可以通过让词法分析器成为被解析器调用的过程简单地实现,按需返回token。

读取和退回字符通常用一个输入缓冲区来实现。每次,一个字符块被读入到缓冲区中,一个指针跟踪输入中已经被分析的部分。回退一个字符只需要指针向后移动即可。输入字符也可能用于错误报告而进行保存,因为需要给出错误出现的输入文本位置。单单出于效率考虑,对输入字符进行缓存也是有意义的。获取一块


A Lexical Analyzer

我们现在为2.5节的表达式翻译器构造一个初级的词法分析器。词法分析器的目的是为了允许空白和数字出现在表达式中。下一节中,我们会扩展词法分析器允许使用标识符。


Fig. 2,26 建议词法分析器如何来实现Fig2.25中的交互,写作C语言的函数lexan。例程getchar()和ungetc来自包含文件<stdio.h>小心处理输入缓冲;lexan通过调用getchar和ungetc实现相应的读取和退回输入字符。如果c声明为一个字符,下面的语句

c = getchar(); ungetc(c, stdin);

等于让输入没有任何变化。


如果实现的语言不允许从函数返回数据结构,那么token和他们的属性必须单独传输。lexan函数返回token的一个整数编码。一个字符的token可以是该字符的证书编码。一个token,例如num,因此可以编码为任何大于字符编码的整数,例如256。为了允许编码容易改变,我们用一个符号常量NUM来引用num的整数编码。在Pascal中,NUM和编码之间的关系可以用const声明,在C中,NUM可以用一个define语句。

#define NUM 256

函数lexan返回NUM当一个数字序列在输入中被看到时。一个全局变量tokenval的值被设置为数字序列的值。因此,如果7后面是一个6,那么tokenval的值就是整数值76,
允许数字出现在表达式中需要对Fig 2.19的文法做一个改变。我们可以用非终结符factor替换单个数字,并且介入下面的产生式和语义动作:
factor → (expr)
| num {print(num.val)}
Fig2.27 中factor的C代码是上面产生式的直接实现。

factor() {
if (lookahead = '(') {
match('(');expr();match(')');
} else if (lookahead == NUM) {
printf(" %d ", tokenval);match(NUM);
}
else error();
}

Fig. 2.27 C code for factor when operands can be numbers.

当lookahead等于NUM的时候,属性值num.val已经给了全局变量tokenval.打印这个值的动作是通过标准函数printf实现的。

#include <stdio.h>
#include <ctype.h>
int lineno = 1;
int tokenval = NONE;

int lexan() {
int t;
while(1) {
t = getchar();
if(t == ' ' || t == '\t')
;
else if(t == '\n')
lineno = lineno + 1;
else if(isdigit(t))
{
tokenval = t - '0';
t = getchar();
while(isdigit(t)) {
tokenval = tokenval*10+ t - '0';
t = getchar();
}
ungetc(t, stdin);
return NUM;
} else {
tokenval = NONE;
return t;
}

}
}
Fig. 2.28


lexan()函数的实现如图2.28所示。每次第8-18行的while语句被执行,一个字符就被第9行读入。如果字符是一个空格或者tab,那么没有token返回到解析器,我们只是继续进行循环。如果字符是一个新行,全局变量lineno被增加,跟踪输入中的行数,但是也不会返回token。错误信息提供行号可以帮助找到错误。

读取一系列数字的代码是14-23行。,isdigit(t)判断一个进入的字符是否一个数字。如果是的话,那么它的整数值被给于t-'0'对ASCII码和EBCDIC而言。对于其它字符集,转换的方式可能会有所不同。在2.9节中,我们把这个词法分析器合并到我们的表达式翻译器中。

没有评论: