2007年12月7日星期五

lex/yacc系列:yacc进阶(四)

为计算器添加的另一个功能是数学函数,例如平方根、指数和对数。

蛮干的方法是把每个函数名作为单独的标记,然后为每个函数添加单独的规则:

%token SQRT LOG EXP
...
%%
expression: ...
 ¦ SQRT '(' expression ')' { $$ = sqrt($3); }
 ¦ LOG '(' expression ')'  { $$ = log($3); }
 ¦ EXP '(' expression ')'  { $$ = exp($3); }

在扫描程序中,则需要为输入“sqrt”返回标记SQRT,等等:

sqrt return SQRT;
log return LOG;
exp return EXP;

[A-Za-z][A-Za-z0-9]* { ...

特殊模式应放在一般模式的前面,以确保正确的匹配。

这种方式可以工作,但是存在问题。一个问题是必须在语法分析程序和词法分析程序中为每个函数硬性编码,这样的工作单调乏味,很难添加很多的函数。另一个问题是函数名应该是保留字,也就是说,sqrt不能作为变量名使用。后者是否成为问题取决于你的意图。

我们将函数名放置在符号表中,而不是作为特殊模式放入词法分析程序中。为符号表条目增加一个新的字段funcptr,如果该条目是一个函数名,它的值就是指向C函数的指针。

struct symtab {
 char *name;
 double (*funcptr)();
 double value;
} symtab[NSYMS];

必须在分析程序开始之前将函数名放入符号表,因此需要编写自己的main()函数,它调用新例程addfunc(),将每个函数名添加到符号表,然后调用yyparse()addfunc()的代码实现是,得到对应于名字的符号表条目并设置funcptr字段。

main()
{
 addfunc("sqrt", sqrt);
 addfunc("exp", exp);
 addfunc("log", log);

 yyparse();
}

void
addfunc(char *name, double (*func)())
{
 struct symtab *sp = symlook(name);
 sp->funcptr = func;
}

定义代表函数名的标记FUNC,词法分析程序看到函数名时返回FUNC,看到变量名时则返回NAME,两者的值都是符号表指针。

在语法分析程序中,用一个通用的函数规则替代每个函数的单独的规则:

%token <symp> NAME FUNC
%%
expression: ...
 ¦ FUNC '(' expression ')' { $$ = ($1->funcptr)($3); }

当语法分析程序看到函数引用时,它可以通过该函数在符号表中的条目找到真正的内部函数引用。

在词法分析程序中,不使用显式匹配函数名的模式,而是修改名字的动作代码,如果符号表条目表明是一个函数名就返回FUNC

[A-Za-z][A-Za-z0-9]* {
  struct *symtab *sp = symlook(yytext);

  yylval.symp = sp;
  if (sp->funcptr)
   return FUNC;
  else
   return NAME;
 }

上面的实现可以达到目的,但我们将再次进行修改。这最后的修改从技术角度而言很小,但从编程语言的层次而言是个很大的改变。将函数名和变量名区分开毫无理由:语法分析程序可以通过语法辨别函数和变量!

因此恢复词法分析程序,对于任何名字总是返回NAME。然后修改语法分析程序,在处理函数时仍使用NAME

%token NAME
%%
expression: ...
 ¦ NAME '(' expression ')' { ... }

下面是完整的源代码。头文件:

#define NSYMS 20

struct symtab {
 char *name;
 double (*funcptr)();
 double value;
} symtab[NSYMS];

struct symtab *symlook();

语法分析程序:

%{
#include "calc5.h"
#include <string.h>
#include <math.h>

int nsym = 0;
%}

%union {
 double dval;
 struct symtab *symp;
}

%token <symp> NAME
%token <dval> NUMBER
%left '+' '-'
%left '*' '/'
%nonassoc UMINUS

%type <dval> expression

%%
statement_list : statement '\n'
 ¦ statement_list statement '\n'
 ;

statement : NAME '=' expression { $1->value = $3; }
 ¦ expression { printf("= %g\n", $1); }
 ;

expression : expression '+' expression { $$ = $1 + $3; }
 ¦ expression '-' expression { $$ = $1 - $3; }
 ¦ expression '*' expression { $$ = $1 * $3; }
 ¦ expression '/' expression
    { if ($3 == 0.0)
       yyerror("divide by zero");
      else
       $$ = $1 / $3;
    }
 ¦ '-' expression %prec UMINUS { $$ = -$2; }
 ¦ '(' expression ')' { $$ = $2; }
 ¦ NUMBER
 ¦ NAME { $$ = $1->value; }
 ¦ NAME '(' expression ')'
    { if ($1->funcptr)
       $$ = ($1->funcptr)($3);
      else {
       printf("%s not a function\n", $1->name);
       $$ = 0.0;
      }
    }
 ;

%%
struct symtab *
symlook(char *s)
{
 int i;

 for (i = 0; i < nsym; ++i) {
  if (!strcmp(symtab[i].name, s))
   return &symtab[i];
 }

 if (nsym < NSYMS) {
  symtab[nsym].name = strdup(s);
  ++nsym;
  return &symtab[nsym-1];
 }

 yyerror("Too many symbols");
 exit(1);
}

void
addfunc(char *name, double (*func)())
{
 struct symtab *sp = symlook(name);
 sp->funcptr = func;
}

main()
{
 addfunc("sqrt", sqrt);
 addfunc("exp", exp);
 addfunc("log", log);

 yyparse();
}

在语法分析程序中,添加了错误检测,以确保用户调用的函数是一个真正的函数。

词法分析程序:

%{
#include "y.tab.h"
#include "calc5.h"
#include <math.h>
%}

%%
([0-9]+|([0-9]*\.[0-9]+)([eE][-+]?[0-9]+)?) {
  yylval.dval = atof(yytext);
  return NUMBER;
 }

[ \t] ;

[A-Za-z][A-Za-z0-9]* {
  yylval.symp = symlook(yytext);
  return NAME;
 }

"$"   { return 0; }

\n    ¦
.     { return yytext[0]; }

%%

除了函数名和变量名可以重合外,计算器如所期望的进行操作。

% calc5
sqrt(3)
= 1.73205
foo(3)
foo not a function
= 0
sqrt = 5
sqrt(sqrt)
= 2.23607

至于是否允许用户在同一程序中对函数和变量使用同样的名字,现在还有争论。如果允许,可能会造成程序难以理解;如果不允许,又可能迫使用户发明新的名字,以避免与保留字冲突。两者都可能走向极端。COBOL语言有300个保留字,没有人能够完全记住它们,因此程序员采用了一些奇怪的约定,例如每个变量名以数字开头,以确保不会与保留字冲突。另一方面,PL/I语言则根本没有保留字,因此如下的令人糊涂的代码是允许的:

IF IF = THEN THEN ELSE = THEN; ELSE ELSE = IF;

最后是编译该程序的Makefile:

LEX = lex
YACC = yacc

CC = cc -DYYDEBUG=1

calc5: y.tab.o lex.yy.o
  $(CC) -o calc5 y.tab.o lex.yy.o -ly -ll -lm

lex.yy.o: lex.yy.c "y.tab.h

lex.yy.o y.tab.o: calc5.h

y.tab.c "y.tab.h: calc5.y
  $(YACC) -d calc5.y

lex.yy.c: calc5.l
  $(LEX) calc5.l

有关make的更多信息,请参考其他资料。

没有评论: