龙书阅读笔记

星期日, 十月 01, 2006

2.5 A TRANSLATOR FOR SIMPLE EXPRESSIONS

使用上面三节中的技术,我们可以构造一个语法导向的翻译器,以一个可工作C程序的形式,把算术表达式翻译成后缀形式。为了让开始的程序更小,我们开始的表达式只包含用加减号分开的数字。语言将在后面两节得到扩展,以包含数字,标识符和其他操作符。因为表达式在很多语言中出现,因此值得详细研究他们的翻译。

expr → expr + term {print('+')}
expr → expr - term {print('-')}
expr → term
term → 0 {print('0')}
term → 1 {print('1')}
term → 2 {print('2')}
.....
term → 9 {print('9')}

Fig. 2.19 Initial specification of infix-to-postfix translator

一个语法导向的translation scheme 通常能够作为一个翻译器的规格。我们使用Fig 2.19中的scheme(重复自Fig. 2.13)将被执行的翻译的定义。作为一种通常情况,一个给定scheme的下部文法必须被修改,在它能够作为一个predictive parser解析之前。特别,Fig 2.19中scheme之下的文法是左递归的,我们已经知道,一个predictive parser不能处理一个左递归文法。通过排除左递归,我们能够获得一个文法,适合在predictive 递归下降的翻译器中使用。

Abstract and Concrete Syntax

一种有用的考虑一个输入字符串的翻译的起点是一棵abstract syntax tree抽象语法树,其中每一个节点代表一个操作符,节点的子节点代表操作数。相反,一个解析树叫做concrete syntax tree具体语言树,下部的文法称为语言的concrete syntax具体语法。抽象语法树,或者简单地叫做语法树,和解析树不同,因为形式的表面不同,对于翻译并不重要,不会出现在语法树中。


例如,9-5+2的语法树如Fig. 2.20所示。因为+和-具有相同的优先级别,而相同优先级别的操作符从左到右计算,因此9-5组成一个子表达式。把Fig. 2.20和对应的解析树Fig. 2.2比较,我们注意到语法树把一个操作符和一个内部节点关联,而不是操作符变成子节点之一。

对一个translation scheme而言,它所基于的文法的解析树和语言树越接近越好。用Fig. 2.19 的文法对子表达式的分组类似于他们在语法树中的分组。不幸的是,Fig 2.19中的文法是左递归的,因此不适合predictive解析。看起来有点冲突,另一方面,我们需要一个文法能够方便解析,而另一方面要容易解析需要一个很大不同的文法。最明显的解决方案是排除左递归。但是,就象下面的例子一样,这种排除需要非常小心才能完成。

Example 2.9 下面的文法不适合用来把表达式翻译成后缀形式,即使它确实生成了Fig 2.19文法的相同语言,并且可以用来进行递归下降解析。

expr → term rest
rest → + expr | - expr | ε
term → 0 | 1 | ... | 9

该文法的问题是rest → + expr 和rest - expr 生成的操作符的操作数不明显。选择下面的任意一个,从expr.t翻译rest.t方法都是不可接受的。
rest → - expr {rest.t := '-' || expr.t} (2.12)
rest → - expr {rest.t := expr.t || '-' } (2.13)

(我们只显示了减法操作符的产生式和语义动作)。从9-5的翻译应该是95-。但是,如果我们使用(2.12)的动作,那么减法符号将在expr.t之前出现,因此9-5错误地在翻译后依旧保持9-5。
另一方面,如果我们使用(2.13),那么操作符将一致地移到右端,那么9-5+2将错误地翻译成为952+-(正确的翻译应该是95-2+)。

Adapting the Translation Scheme

Fig. 2.18画出的左递归排除也可以运用到包含语义动作的产生式。我们扩展5.5中的转换,把合成属性考虑在内。该技术把产生式A → Aα | Aβ | ϒ 转换成
Α → ϒR
R → αR | βR | ε
当语义动作嵌入在产生式中时,我们可以在转换的过程中执行它们。这里,如果我们让A = expr , α =+ term {prnt('+')}, β = - term {print('-')}, ϒ = term, 那么上述的转换将产生translation scheme (2.14)。

expr → term rest
rest → + term { print('+') } rest | - term {print('-')} rest | ε
term → 0 {print('0') } (2.14)
term → 1 {print('1') }
................
term → 9 {print('9') }

Fig. 2-21展示了翻译过程:


Procedures for the Nonterminals expr, term and rest

我们现在用语法导向的translation scheme(2.14) 实现一个C编写的翻译器。翻译器的核心是Fig. 2.22 的C代码,包括expr,term和rest函数。这些函数实现(2.14)中对应的非终结符。

expr() {
term(); rest();
}

rest() {
if (lookahead == '+') {
match('+'); term(); putchar('+');rest();
} else if (lookahead == '-') {
match('-'); term(); putchar('-'); rest();
} else
;
}

term() {
if(isdigit(lookahead))
{
putchar(lookahead); match(lookahead);
} else
error();
}

Fig. 2.22. Functions for the nonterminals expr, rest and term

函数match,将在后面给出。

非终结符的函数模拟产生式的右边。例如,expr→ term rest通过在函数expr()中调用term()和rest()实现。

另外一个例子,函数rest()使用rest的第一个产生式(2.14),如果lookahead符号是一个加号,而如果是一个减号,则利用第二个产生式,缺省情况使用产生式rest → ε.第一个产生式对应rest()函数的第一个if条件,第2个产生式对应第2个条件,因为第3个产生式右边是ε,所以rest()的最后一个else什么也不做。


term的10个产生式生成10个数字。在Fig, 2.22 中,isdigit例程测试lookahead符号是否为数字。数字被打印,然后match。不然则发生错误(由于match以后会发生lookahead的改变,因此match在putchar之后)。

Optimizing the Translator

某些递归调用可以用循环代替。当一个过程体中的最后一个语句是对相同过程的一个递归调用时,这个调用叫做尾递归tail recursive。例如,rest()的第4行和第7行是tail recursive。

我们可以用循环替换tail recursive来加速一个程序。对一个没有参数的过程,一个tail-recursive 调用可以用一个jump到过程开始替换。rest的代码可以重写如下:

rest() {
L: if (lookahead == '+' ) {
match('+'); term(); putchar('+'); goto L;
}
else if (lookahead == '-') {
match('-');term();putchar('-');goto L;
}
else ;
}

只要lookahead符号是一个加号或者减号,过程rest匹配符号。掉用term去匹配一个数字,然后重复这一过程。注意因为match每次调用的时候都移去符号,循环只发生在改变符号的数字的序列。如果改变发生在Fig. 2.22,唯一需要留下的对rest的调用就是expr的第3行。因此两个函数可以继承为一个,如Fig 2.23所示。在C中,一个语句stmt可以重复执行通过:

while(1) stmt

因为1永远为真。我们可以用break语句跳出一个循环。

#include
#include
int lookahead;
void expr(void);
void term(void);
void match(int c);
void error(void);

void expr(void){
term();
while(1) {
if (lookahead == '+') {
match('+'); term(); putchar('+');
} else if (lookahead == '-') {
match('-'); term(); putchar('-');
} else
break;
}
}


void term(void){
if(isdigit(lookahead))
{
putchar(lookahead); match(lookahead);
} else
error();
}

void match(int c) {
if (lookahead == c)
lookahead = getchar();
else
error();
}

void error(void) {
fprintf(stderr, "syntax error\n");
exit(1);
}

int main (int argc, char const* argv[])
{
lookahead = getchar();
expr();
putchar('\n');
return 0;
}

没有评论: