编译器是怎样工作的?用lex和yacc 写一个计算器(2)


幸运的是

-----------------------------------------

现在你可以看明白了上边的规则,问题又来了,我们怎么来处理这种语法的规则呢?

该不会要自己写一个人肉编译吧?该不会要自己写一个人肉编译吧?该不会要自己写一个人肉编译吧?


哈哈,不要担心,我们有现成的工具,它的名字叫另一种C编译器(yacc , yet another C compiler) ,

 Gnu 的实现叫一头野牛(bison),而不用重新发明轮子。

 yacc 可以直接读取我们上边所写的规则 ,执行我们所要作的,只是将匹配的规则规定出来一个动作就行。

比如:

如果我们遇到了expression + number  这个语法 ,我们就把两个表达式加起来就行了

遇到了number 语法,把这个数返回就行了

这两条规则,在yacc 里写出来是这样的:


expression:

number    {     $$  = $1 ;}

|  expression  ‘+’  number   {  $$ = $1 + $3 }

yacc 能接受的语法是:

   语法   {动作}


就是说,遇到了一条能接受的语法,我们执行什么样的动作。

对于每个能接受的语法,每个语法中的符号(比如number ,express ‘+’),

yacc 都会把它们添加到$1,$2,$3…中去,在action语句里,直接使用它,整条语句的结果,用$$表示


这样完成以后,我们的整个的语法文件如下所示:


ch3-01.y:

 

%token  NUMBER
%%
result:	expression	 { printf("%d\n", $1); }
;


expression:
expression '+' expression { $$ = $1 + $3; } 
|	NUMBER	 { $$ = $1; }
;



我们看一看,里边的各个变量的推导,除了NUMBER,其他所有的量,都可以由其他规则由NUMBER推导出来

那么我们怎么从输入里取出这个符号变量呢?我们怎么判断输入符合我们规定的语法呢?

【抓狂:心里有个声音总在呼喊:快人肉它~】

哈哈,你还是不用人肉了,因为我们有lex ,这个google前 CEO shmit(他同是还是一个世界资产排名100之内的亿万富翁)发明的工具,就是为我们分析词法用的。

在使用lex 之前,你得学会正则表达式,不过就本例来说,不会也不大碍事,因为可以稍微讲解一下

首先,我们要取出数字

数字由什么组成?数字有多个连续的0-9的数连续组成。

0-9之间任意一个数在正则表达式里,用[0-9]表示

多个连续的数?  在后边添加一个加号就行了:

[0-9]+

所以我们有了以下lex 规则,lex 规则是不是和yacc 长得差不多啊

[0-9]+ {

printf("N:%s\n", yytext);

yylval = atoi(yytext);

return NUMBER;

 }


在关联动作的代码里,printf语句是为了调试。

yylval 可以把我们要的数字取出来送给yacc ,这个共享变量有一个规定的名字叫yylval(yy left value)

最后的return 语句,表明这条语句识别出了哪个token,而这个token就是我们定义在yacc里的


\n return0/* logical EOF */

这句表明,我们遇到一个新行(即输入中的回车)时,

结束整个分析(就是说,我们这个计算器,只是单行的)

. { printf(".:%s\n", yytext);

return yytext[0]; }

这个语句起到的效果是,如果遇上了任意字符(.表示任意字符,\n除外),就把它送给yacc,由于上边我们已经把 number 和空格送给yacc吃掉了,所以,这条语句还要去除数字和空格。

整个文件就是这样:

 

%{
#include "ch3-01.h"
#include <stdio.h>
extern int yylval;
%}

%%
[0-9]+ {
printf("N:%s\n", yytext);
yylval = atoi(yytext);
return NUMBER;
 }

[ \t] {printf("\\t");} /* ignore white space */

\n return0; /* logical EOF */

. { printf(".:%s\n", yytext);
return yytext[0]; }
%%

int main(void)
{
yyparse();
}

int yyerror(char *errmsg)
{
printf("%s:%d \n", errmsg,yylineno);
}


整个程序的效果:

 

$ make 
yacc -d ch3-01.y
mv y.tab.c ch3-01.c
mv y.tab.h `echo ch3-01.c | sed 's/c$/h/' `
cc  -g    -c -o ch3-01.o ch3-01.c
flex -t ch3-01.l > ch3-01.l.c
cc  -g    -c -o ch3-01.l.o ch3-01.l.c
cc  -g   -ly -ll  ch3-01.o ch3-01.l.o   -o ch3-01
rm ch3-01.c ch3-01.l.c

$ ./ch3-01
1+2+3 +4 +5
N:1
.:+
N:2
.:+
N:3
 :\t
:\t
.:+
N:4
 :\t
.:+
N:5
15
$ ./ch3-01
1+2+
N:1
.:+
N:2
.:+
syntax error:1 


可以看到,我们的计算器,由yacc和lex 制作而成的计算器,已经成功地识别了输入,计算出了整个表达式的值。

而错误的语法,无法通过我们的语法检查,出现了语法错误


一般的计算器解析如下:

设定语法->给语法规定动作并转换成BNF(就是我们上边写的语法表达式了)->写出识别符号的lex程序

经过这3步就可以完成。


那么yacc 如何分析呢?

它将语法分析时的规则变成一组状态,每个状态都表示在分析过程中可能出现的一种位置。

当分析程序读取标记时,每次读取一个未完成规则的村记,就把它放到栈中并更新到能代表此符号已经移进的状态

,然后扫描这个状态栈,当它发现某个规则被匹配时,它就把这个规则相关的符号都弹出栈,

并用这个规则的左侧符号来替代右侧的所有相关符号


举个例子

读取1+2并分析的过程如下:

读取1 匹配number 规则,把$$=$1的规则应用,然后把1弹出栈,再放左值的1到栈中:1 , 更新状态:1已经移入,

读取+  不匹配规则,放入栈中:1 +,没有发现栈中可以更新的规则    :更新状态  +已经移入

读取2 匹配 number 规则,把2放入栈中  更新状态: 2已经移入

发现一条可以匹配 express : express + number 的规则,把1+2 弹出栈,把我们规定的结果:$1+$3(1+2=3)  即3 移入栈中。

读取完毕:发现最终要实现的规则匹配到栈中的3:应用规则(printf(‘%d’ , $1);然后把3移出,更新状态,3已经移出


woo , ČÕÖŁ,我们实现了刚才我们人肉计算器一样的计算过程。


所有的代码都可以在这里找到:

https://github.com/lbaby/javalearn/tree/master/lexYacc

----------------

练习:

添加 -的支持,让程序支持减法

如果不加 statement  :  expression;这条规则 ,最终程序会停在什么情况下?验证一下

如果将 expression '+' NUMBER换成  expression  ‘+’ expression,程序执行中间状态会不会不同,结果呢?验证一下

创建能解析如下语法的规则的程序,并测试它:

(+    (+  (+  1  2)  3  )   (+  4     5) ) 



------------

休息一下,广告之后更精彩。


后续的故事:没有变量和函数的日子不幸福。

原文地址:https://www.cnblogs.com/xinyuyuanm/p/3028946.html