使用yacc的方法
使用yacc的方法
被遗忘的强大的工具UNIX系统的功能的之所以强大,不是在于它本身有多好的内核,
而是在于它为我们提供了很多能完成小功能的命令,而这些命令的组
合使用使得它更加的强大。其实在这里它为我们体现了这样的一个观
点:
要完成一个项目,或者是个大型的程序。应该先从小做起,然后
不断的发展壮大。
在本文中,主要为您介绍一写UNIX 系统下面的3 个命令的,似乎
它不常用。可能make用得多一些,但是他们功能确实强大。
yacc 语法分析程序生成器,可以从语言的语法描述生成语法
分析程序。
make 通过指定和控制程序对复杂的程序进行编译的程序。
lex 类似yacc的程序,主要用语生成词法分析程序。
本文主要通过一个经典的hoc(high-order calculator)程序出
发,结合笔者自身在学习过程中的一些体会为您介绍它。本文只涉及
基本原理,我们假设您懂得C语言,或者身边正好有C语言方面的书。
下面我们进入正题:
1 hoc计算器
hoc 是一个功能强大的四则运算计算程序,当然在这里我不方便
去揣摩如果让您去开发一个类似的程序您会采用什么样的思路。下面
就来看看开发一个四则运算的小程序经典思路。
首先需要向您阐述一下巴科斯-诺尔范式(Backus-Naur Form),(正则
文法)作为一个四则运算,我们可以简单抽象成下面这样一个表达形
式
语句->表达式
表达式->token
表达式->表达式+表达式
表达式->表达式-表达式
表达式->表达式*表达式
表达式->表达式/表达式
表达式->(表达式)
或者我们用另外一个依赖关系来表达这样一个事实。
语句:表达式
表达式:token
表达式+表达式
表达式-表达式
表达式*表达式
表达式/表达式
(表达式)
其实看到这里您也许会觉得这样是否会非常得像我们平时写的
makefile文件呢?我无法回答makefile的语法和这有什么直接的联
系。但是makefile文件确实体现这样一个依赖关系的推导式的思想。
对于该疑问我们暂且放下。回归正题,其实按照严格的语法,上面的
形式语言描述是不完整的,在该文法中我们没有指定操作操作符号的
优先关系和运算符的结合性。
2 yacc程序
yacc 是一个语法分析生成器,它是叫一种语言的规范化的描述转换
成一个语法分析程序。作为yacc主要分4个阶段。
1) 描述语法(如上面所写的那样),yacc 可以帮我们检查我们描
述的语法的错误以及二义性。
2) 语法对应的C程序。
3) 词法分析程序。(词法的块一般标记为token)。
4) 控制流程,调用yacc生成的语法分析程序。
yacc 一般是将语法和语义操作封装为一个语法分析函数,名字是
(yyparse)。如果没有任何问题,yacc将为我们创建一个C程序文件,
我们可以使用任何的C编译器去编译该文件。值得我们注意的是语法
分析程序的入口必须命名为yylex。
因为每次yyprase在进行语法分析的时候都会去调用一个名为yylex
的函数。不过这一切都不是固定的。如果您有兴趣的话您可以在yacc
创建了C程序文件后,手工去修改一下函数的名字也可以的。
下面再为您介绍一下yacc的输入文件格式:
%{
C语句
%}
Yacc定义:词法标记(token),语法变量等
%%
语法规则动作
%%
其它的C代码
这就是一个yacc的输入原文件的格式,在经过yacc处理后会把输出
一个名字为y.tab.c的文件。该文件的格式一般是
%{和%}之间的C语句
第二个%%后的C语句
我真得很欣赏yacc 程序的设计者,它直接为我们生成C 文件,而不
是已经被编译过的目标文件。在这一点上,我觉得真是做得太好了。
其实在这一点上它体现了UNIX 的处理一般方法。其实该方法非常的
灵活。当我们有了新的想法我们甚至可以移植代码,正是因为这个原
因,我们得以将代码移植到WINDOWS平台上去。在本文的最后将为您
介绍如何将y.tab.c在WINDOWS平台下面的Visual Studio IDE环境
编译。不过yacc 确实强大,但是我们要想掌握它的话也是非常的不
容易,但是当您一旦会用了yacc 后您回发觉您掌握它而花费的努力
是值得的。它提供了一个可以随语言定义变化而随意快速生成C原代
码的一个途径。
下面我们首先来看看一个 yacc的输入文件
%{
/* 该部分在本文中未涉及到*/
%}
%token NUMBER
%left '+' '-'
%left '*' '/'
%%
list:
|list 'q' {exit(0);};
|list '/n'
|list expr '/n' {printf("/t%ld/n", $2);}
;
expr: NUMBER {$$ = $1;}
|expr '+' expr {$$ = $1 + $3;}
|expr '-' expr {$$ = $1 - $3;}
|expr '*' expr {$$ = $1 * $3;}
|expr '/' expr {$$ = $1 / $3;}
|'(' expr ')' {$$ = $2;}
;
%%
#include <stdio.h>
#include <ctype.h>
int lineno;
void main(int argc, char *argv[])
{
yyparse();
}
int yylex()
{
int c;
while((c = getchar()) == ' ' || c == '/t')
;
if(c == EOF)
return 0;
if(isdigit(c))
{
ungetc(c, stdin);
scanf("%d", &yylval);
return NUMBER;
}
if(c == '/n')
lineno++;
return c;
}
yyerror(char *s)
{
fprintf(stderr, "%s", s);
fprintf(stderr, " near line %d/n", lineno);
}
我分别用3种颜色区别了yacc输入文件的3个部分,这部分程序
包括了许多的信息,在这里不一一解释,也不详细描述分析器如何来
工作。更多的信息请参考yacc手册。希望读者能更多的自己去思考。
首先来解释一下第一部分。提供选择的规则用|分割,当输入的语
法规则被识别出后,该动作将被执。对应的C代码动作在最后被体现
出来,如下所示:
它的基本结构是
| 动作标志 {C代码}
值得注意的地方是C 代码最后一定要写上;yacc 无法为我们检查
这一问题,只是这个问题将在最后的C编译器检查出来,我曾经就遇
见过一次,又是初学。都不知道该从什么地方去检查问题。$n($1,$2)
分别表示子成分的返回值,$$是整个表达试的返回值。一般来说$$
就是$1,除非用户将它设置为其它的值。那么在上面中,我们可以这
样去理解。一个换行符号(/n)可以被识别为一个list。换句话说就
是一个语句的结束点。这有点像我们的C 语法中必须要用符号(;)来
结束一句一样的道理。
如果您学过编译原理,我们也可以从推导式的概念上来理解这一
个问题。List 可以推导出(;)(expr;)这样2 种可能性。expr 可以同
理去这样理解。这样把问题推导下去。下图是一个语法分析过程的推
导树。
/n
list
2
expt
expr
+ *
toke
n
3 4
toke
n
toke
n
好了,我们继续再看看蓝色部分的代码,在当中使用了%left 来
指定结合的方式。这样意味着(a-b)-c不会被解释成a-(b-c),您可以
试着将%left改为%right看看有什么效果。
关于C 代码的部分,我不想做太多的详细解释了。您只需要记着
yyprase,yylex,yyerror 就行了。有yyprase 是yacc 程序将会为我
们按照我们的描述文法创建的解析器代码,yylex是词法分析的部分
(将句子打断为token 串的功能)yyerror 是yacc 程序统一的错误
出口点。
3 编译
在看懂上面的代码后,OK,下面我们继续看看如何使用yacc生成
语法分析程序,您完全可以使用您个人最喜欢的文本编辑器去编辑这
样一个文件,您只需要保证您编辑的文件的内容和上面的一样。我们
假设您保存的文件的名字是guoguo.y
在shell命令行中执行命令:
yacc guoguo.y
看看屏幕有没有什么错误的提示?如果有的话,请仔细检查一下
guoguo.y文件里面有没有不符合yacc输入文件格式的地方。如果没
有错误,那就恭喜您了,实在就太棒了。我们可以继续进行下面的实
验。
下面一步,我们查看一下当前目录下面是不是有个y.tab.c 的文
件。这简直太好了。yacc 已经为我们创建了一个可以按照我们指定
的文法进行语法分析的一个c原程序。这一切结果我们都要感谢yacc
的作者steve Johnon 先生。呵呵,还是回归正题吧。我们拿到这个
c程序可以直接编译连接它。试着在shell命令中执行命令:
cc –o guoguo y.tab.c。
编译它,如果一切顺利的话,我们将得到一个名字为guoguo的执
行程序。如果今天您运气差了点的话,可能会在cc 编译的时候会报
告一些错误出来,无法创建guoguo 执行程序。不过您不要紧张,让
我们一起来和您分析一下可能的错误。下面我把我在学习过程中曾经
遇到过的问题列举出来。
问题1:
cc -o guoguo y.tab.c
"hoc.y", line 10: error: Syntax error before or at: }
"hoc.y", line 21: warning: statement not reached
"hoc.y", line 43: error: Syntax error before or at: <EOF>
*** Error code 1 (bu21)
这样的错误,CC编译器已经值出在文件hoc.y的10行出了错误。
这样的错误一般来说是规则的对应动作C 函数没有;号结束符号.对
于稍有一些经验的程序员来说这样的问题其实很好定位的。但是为什
么我要在这里说这个问题呢?如果您观察够仔细的话,您也许会注意
到我们编译的是y.tab.c文件,我们用cc编译器是在编译y.tab.c 文
件呀?它怎么能知道是hoc.y 文件的第10 行出了问题了呢?oh my
god!我相信上帝不会和我们开这个玩笑的。这一点问题您必须要了解
清楚,其实它非常有意思的。仔细看看y.tab.c 文件后您知道,在
y.tab.c文件中加上了#line 的C预处理命令。这个#line 的预处理
命令是告诉C编译器如何定位y.tab.c的行号处理的。我在这里扩展
一下,如果您经常都在写UNIX下面的ec程序,通常我们常常可以在
日志中通过__FILE__宏来得到行号,要知道ec 程序都是被预先处理
成.c的文件然后再又C编译器去编译它。因为ec程序和c程序之间
一些EXEC SELECT,EXEC UPDATA……等代码的处理将首先被ec 预编
译器解释成一些C 代码。这样一来ec 程序的行数和C 程序的行数有
有差别了。但是__FILE__只能处理C程序的那个行号。那么我们是否
就无法知道在ec程序中的行号了呢?其实不然。在ec的预编译器中
都加了#line指令的处理。这就是为什么我们能直接能在日志文件中
看到EC 原程序的行数。而不是被预编译为C程序的行数了。如这样:
xxx.ec->L->38 了。至于#line 具体的工作是怎么完成的,我在这里
不再描述,读者可以自行去研究。
好了,这个就是我在编译y.tab.c 的程序的时候遇见了的一个问
题。当解决了这个问题后。就得到C 编译器为我们生成的执行程序
guoguo。执行一下看看
1+2 Enter
3
2*3 Enter
0
2.0+1.2 Enter
Syntax error near line 2
上面的Enter 部分是表示我在键盘敲入了Enter 键。千万不要以
为是我敲入了Enter字母哦!从上面的结果来看,好象有逻辑问题。
在加法运算的时候guoguo 程序能正常工作。但是剩法的时候缺没办
法正常的工作。另外一个问题,我们目前的代码无法完成浮点类型的
计算。这个结果有2个问题值得我们去思考。有兴趣的读者可以试去
解决它。
1. 为什么2*3的结果是0
2. 我们怎么完成浮点数的运算。
由于本文仅仅是入门级的对yacc介绍,所以例子也是个非常的简
单的例子,其实用用yacc 还可以完成更加复杂的语法分析的程序,
有兴趣的朋友建议可以在网上去搜索一下hoc6的yacc的原程序。它
对我们学习yacc将会有非常高的知道价值。yacc所创建的分析器程
序,是采用LALR(1)设计的,在《UNIX 程序员手册》中有对中有对
yacc 非常详细的描述。另外它还分析了其它类似的分析器产生器的
原理。也许您还可以设计出来采用递归下降法的代码生成器。
4 y.tab.c代码的移植问题
有时候我们可能需要我们的语法分析程序y.tab.c 能够运行的
WINDWOS平台上。其实不难,只需要将y.tab.c的程序复制到WINDOWS
上去。只是需要在编译的时候需要注意2点问题。
1. 屏蔽y.tab.c 中的#include <unistd.h>语句,在cl.exe 中无
这个文件。这个文件主要是好象描述一些POSIX 标准的头文件
的。在LINUX下面它主要描述0x80系统调用的函数原形。也许
在这一点TMD 有点搞笑,我不知道Visual Studio 环境中为什
么没这个文件。不过你还可以试着使用著名的DOS下面的djgpp
(GUN for windows (32bit)gcc 编译器)编译器去编译它,编
译出来的程序完成可以在32位环境下使用,但是它好象编译出
来的程序不符合PE 执行文件格式,NT 系统下面有些程序好象
无法正常运行!它可以不修改#include <unistd.h>一句。
2. 如果您是在命令行中编译y.tab.c编译的时候cl.exe中一定要
加上/D "__NO_GETTXT__" 参数。因为WINDOWS 平台上没有
_gettxt函数。如果您是在IDE需要在您在project->settings
中的C/C++标签中的Preprocessor definitions 中加上宏
__NO_GETTXT__。
5 结束言
首先,语言开发工作很有用,它可以使我们集中精力去完成语法
的规则的定义。例如我们的设计我们自己系统的脚本文件。然后yacc
又提供了给我们一个非常广阔的空间,可以让我们自由的去发挥。
其次,把工作当做语言来开发,而不仅仅是“写一个程序”,这个
思考方式是具有一定的意义的。它给我们指导了一个思想。一个程序
将组织为语言处理器要求句法规则,换句话说就是用户接口。并构造
实现它。这一点也许和我们传统的编程概念不太一样。其实它体现了
“语言”并不局限于我们传统的编程语言-它还包括了很多其它的东
东。
最后,UNIX下面的大量的小工具;单独或者是组合使用;帮助我
们完成了很多的机械的工作。这也许就正是显示它存在的价值和意义
吧。
欢迎大家能来邮件和我一起讨论本文中的一些相关的问题。至于
本文中提到的make 和lex 命令由于时间关系,我会在以后再为大家
描述。文中不正确的地方尽请大家能指正。
我将在下一期的文中,为大家整理一篇自己在曾经网络编程上遇
见的一些问题《TCP服务端编程一些问题》文章。
天用唯勤 阳凌
yl.tienon@gmail.com
2006-07-23