2.5 习题:扩展计算器

2.5.1 让计算器支持括号

如果要说普通计算器有什么不方便的地方,不能直接输入括号进行计算就是其中之一。因此为了让 mycalc支持括号,我做了一些修改。

因为使用的是yacc/lex,所以首先在lex中增加 (和 )两个记号。

(前略)

"+"   return ADD;

"-"   return SUB;

"*"   return MUL;

"/"   return DIV;

"("   return LP; ←新增

")"   return RP; ←新增

"\n"   return CR;

(后略)

LP、 RP分别是left paren、right paren的缩写。

然后将 primary_expression的语法规则替换为下面这样:

primary_expression

: DOUBLE_LITERAL

| LP expression RP

{

$$ = $2;

}

;

一看就能明白,意思是被 ( ) 包裹的 expression 还是一个 primary_expression。

不过仅这两处修改还不能让 mycalc支持括号。

使用递归下降分析法制作语法分析器的话, primary_expression的语法图需更改为图2-6那样。

figure_0075_0022

图2-6 在语法图中引入括号

这表示用括号将 expression 包裹的部分,整体将会作为 primary_expression来处理。

那么按这个思路重新编写parser.c如下所示。

1: static double

2: parse_primary_expression()

3: {

4: Token token;

5: double value;

6:

7: my_get_token(&token);

8: if (token.kind == NUMBER_TOKEN) {

9:  return token.value;

10: } else if (token.kind == LEFT_PAREN_TOKEN) {

11:  value = parse_expression();

12:  my_get_token(&token);

13:  if (token.kind != RIGHT_PAREN_TOKEN) {

14:   fprintf(stderr, "missing ')' error.\n");

15:   exit(1);

16:  }

17:  return value;

18: } else {

19:  unget_token(&token);

20:  return 0.0; / make compiler happy /

21: }

22: }

将语法图直接转换为代码应该不是很难,只要按图中的思路去做即可。如果进入的不是 DOUBLE_LITERAL 而是 (,则把括号中的部分作为一个 expression去解析就可以了。此外,如果 expression解析完毕后没有找到标记结束的右括号 ),则需要报错。

2.5.2 让计算器支持负数

其实目前做出来的计算器,还无法支持负数。因为在定义数值时用的正则表达式是 [1-9][0-9]或 [0-9].[0-9]*,根本没有把负数作为一种数值考虑进来。

那么,如果我们想修改计算器让其支持负数,要怎么办呢?

可能有人会想,只要在词法分析器中将 -5 这样的输入也作为 DOUBLE_LITERAL 来处理不就行了吗?按这种思路, 3-5 这样的输入会被解析成 3 和 -5两个记号(请参考2.1节中的补充知识)。

因此,如果不想将负数作为记号处理,就应该在语法分析器中想办法。

如果用yacc的话,我们可能首先会想到这样做:

primary_expression

: DOUBLE_LITERAL

| SUB DOUBLE_LITERAL ←DOUBLE_LITERAL之前带“-”的部分也解析为表达式

{

$$ = -$2;

}

(下面省略)

确实,用这种方法可以给定值的实数加上负号 -。

但是,用这种方法给 -(3 * 2)这样带括号的算式再加上负号是办不到的(有人可能觉得办不到也无所谓,请允许我吹毛求疵一下)。为了再支持这种括号的处理,还需要这样修改:

primary_expression

: DOUBLE_LITERAL

| SUB primary_expression

{

$$ = -$2;

}

(下面省略)

那么在递归下降分析法中,可以允许负号的语法图如图2-7所示(这个语法图还包含了对括号的支持)。

figure_0077_0023

图2-7 包含负号的语法图

将其转换为代码,如下所示。

1: static double

2: parse_primary_expression()

3: {

4: Token token;

5: double value = 0.0;

6: int minus_flag = 0;

7:

8: my_get_token(&token);

9: if (token.kind == SUB_OPERATOR_TOKEN) {

10:  minus_flag = 1;

11: } else {

12:  unget_token(&token);

13: }

14:

15: my_get_token(&token);

16: if (token.kind == NUMBER_TOKEN) {

17:  value = token.value;

18: } else if (token.kind == LEFT_PAREN_TOKEN) {

19:  value = parse_expression();

20:  my_get_token(&token);

21:  if (token.kind != RIGHT_PAREN_TOKEN) {

22:   fprintf(stderr, "missing ')' error.\n");

23:   exit(1);

24:  }

25: } else {

26:  unget_token(&token);

27: }

28: if (minus_flag) {

29:  value = -value;

30: }

31: return value;

32: }

注 释

[1]. 严格讲,包含代码中所有记号的叫作分析树或语法树,将一些无用记号剔除的才叫作抽象语法树,本书中并没有特意区分。

[2]. 也称为扫描器(lexer或scanner)。

[3]. 最近这样的例子还有Ruby 的 VM 的 YARV,即“ Yet Another Ruby VM”的缩写,还有YAML起初也是叫作“Yet Another Markup Language”。

[4]. Windows的计算器支持使用键盘输入,但是有很多初级用户并不知道这个功能,仍然用鼠标点击。此外一些迷你键盘去掉了数字键盘区域,在上面使用计算器很不方便。 ——译者注

[5]. 关于lex的正则表达式,请参考2.2.2节。

[6]. 因为后面还会有 [0-9]*这样的写法,所以严格讲,“匹配前面的字符”其实是“匹配前面的表达式”。

[8]. 正则表达式有很多不同的风格,lex所使用的风格支持使用双引号转义元字符,而常用的PCRE风格则只支持反斜线转义。 ——译者注

[9]. 按本书所采用的命名规范,文件内的 static变量需要附带前缀 st_。请参考3.2.1节。

[10]. 数值可以分为整数部分、小数点和小数部分。使用lex时,用一个正则表达式描述其特征,之后全部交给lex处理。而在这里我们就只能自力更生了。

[11]. 这里的例子仅限于BASIC的旧版本,在N88-BASIC 中 IFA 的写法也是不允许的。

[12]. 文章的标题是“Fortran story - the real scoop”, 是当时在NASA的Fred Webb向新闻组alt.folklore.computers的一篇投稿,有兴趣的朋友可以去搜索一下。

[13]. 即被称为“C语言圣经”的The C Programming Language一书,作者名缩写为K&R[2]。中文版为《C程序设计语言(第2版·新版)》,机械工业出版社于2004年出版。