自今年八月底开馆,今天是第九次来这里,娃来自习写作业,我来这里或学习一会儿、或工作一会儿。

这是一个系列文章,旨在了解如何使用Flex/Lex和Yacc/Bison进行词法和语法解析。在前面,已经完成了使用Lex/flex做基础的词法解析。接着,开始这个系列的第二部分,使用flex和bison完成一个简单的计算器。
为了简化实现,将注意力放在简单flex和bison使用上,这里对计算器做了几个简化:
也就是该程序可以计算加法、减法、乘法,以及他们的任意组合,如: 3+4、 3+4*2、 3+4*2-3、 3+4*2-3*2
后续,还将考虑增加更急复杂的计算器,包括:
这里先从简单的开始。
这是在vim中写出的第一遍代码,包含了词法文件cal.l和语法文件cal.y,详细如下。这其中当然是有很多错误的,之所以,依旧写出来,是为了和正确代码对比,以此看看对哪些概念理解有偏差。如果你是找一个正确例子的话,可以跳过这一段。
%{
#inlcude <stdlib.h>
%}
/* 十进制整数 */
%token INTEGER
%union {
int a;
}
/* 操作符 + - * / */
%token OPERATOR
%%
program:
program expression \n { printf("%d",$2); } // 这里就是以回车结尾,也可以考虑使用 = 结尾
|
expression:
INTEGER
| expression '+' expression {$$ = $1 + $2}
| expression '-' expression {$$ = $1 - $2}
| expression '*' expression {$$ = $1 * $2}
dec_integer:
INTEGER
{
$$ = $1.a;
}
这里也有几个已知的问题,例如:运算符的优先级没有定义,也就说4+3*2可能会算成14。没错,如果眼尖的话,还发现有一些拼写错误。
接着是cal.l文件:
#inlcude <stdlib.h>
#include "y.tab.h"
%}
[:digit:]+ {
yylval.a = atoi(yytext)
RETURN INTEGER;
}
当然,这里面有很多的错误。一会儿来看后面正确的内容。
t.col < 2 or t.col < 11 and t.col > 4 是什么意思:-- 猜测一下,如下的 SELECT 查询是否能够返回记录:
CREATE TABLE t(col INT);
INSERT INTO t values (1);
WHERE t.col < 2 or t.col < 11 and t.col > 4
扯远了,再回到cal.y文件,优先级和结合律需要进行如下修改,以定义”*”优先级高于”+”、”-“:
%left '+' '-'
%left '*'
这里的代码先后,就定义了优先级;优先级越高,定义在越在后面;left表示,左结合。
'\n' 。%token <a> INTEGER
%type <a> expression
完成这样的定义后,在lex的文件cal.l中,就可以对yylval进行赋值,如:yylval.a = atoi(yytext); 这时,对于yacc文件中cal.y,如果引用这个TOKEN,就可以使用$1(或者是$2、$3)来引用lex解析后的返回值,如:expression: INTEGER { $$ = $1;}。
cat cal.y
...
%token O_ADD O_MINUS O_MULTIPLY O_EQ
%left O_ADD O_MINUS
%left O_MULTIPLY
%token <a> INTEGER
%type <a> expression
...
cat cal.l
...
"=" { return O_EQ;};
"+" { return O_ADD;};
"-" { return O_MINUS;};
"*" { return O_MULTIPLY;};
...
cal.tab.c:(.text+0x53b): undefined reference to `yyerror'
按照文档,可以定义一个最简单的yerror函数(参考:The Error Reporting Function yyerror),如下:
void
yyerror (char const *s)
{
fprintf (stderr, "something error: %s\n over", s);
}
补充一个main入口函数,修改cal.l、cal.y即可。
修正后的cal.l
%{
#include "cal.tab.h"
#include <stdio.h>
%}
%option noyywrap
%%
[0-9]+ {
yylval.a = atoi(yytext);
return INTEGER;
}
"=" { return O_EQ;};
"+" { return O_ADD;};
"-" { return O_MINUS;};
"*" { return O_MULTIPLY;};
%%
修正后的cal.y
%{
#include <stdlib.h>
#include <stdio.h>
int main(){
yyparse();
}
void
yyerror (char const *s)
{
fprintf (stderr, "something error: %s\n over", s);
}
%}
%union {
int a;
}
%token O_ADD O_MINUS O_MULTIPLY O_EQ
%token <a> INTEGER
%left O_ADD O_MINUS
%left O_MULTIPLY
%type <a> expression
%%
program:
|
program expression O_EQ { printf("result is : %d",$2); }
;
expression:
INTEGER { $$ = $1;}
| expression O_ADD expression {$$ = $1 + $3; }
| expression O_MINUS expression {$$ = $1 - $3; }
| expression O_MULTIPLY expression {$$ = $1 * $3;}
;
然后,就可以生成c文件并编译可执行文件了:
lex cal.l
bison -d cal.y
gcc cal.tab.c lex.yy.c -o a.out
./a.out
4+3*2=
也可以像这样:
lex cal.l && bison -d cal.y && gcc cal.tab.c lex.yy.c -o a.out && ./a.out
%{ // %{ ... %} 包含了完整的C语言代码
#include <stdlib.h> // 这里包含需要的头文件、入口函数main
#include <stdio.h> // 以及一个简答的yyerror函数
int main(){
yyparse();
}
void
yyerror (char const *s)
{
fprintf (stderr, "something error: %s\n over", s);
}
%}
%union { // 这是一个非常重要的联合体,用于定义
int a; // 各种不同类型的Token所返回的数据
} // 广泛的被yylex使用,并用于与.yy文件共享数据
// 也就是 YYSTYPE
%token O_ADD O_MINUS O_MULTIPLY O_EQ
%token <a> INTEGER // 这里表示INTEGER(这是一个被lex识别的token)
// INTEGER(被lex识别的token返回值为YYSTYPE.a
%left O_ADD O_MINUS // 这里定义 + -为左结合律
%left O_MULTIPLY // 这里定义 * 为左结合律,并且优先级高于 + -
%type <a> expression // 这里定义语法规则(grammer rule)expression
// 的返回值为 YYSTYPE.a
%%
program: // 这是start symbol,所有的program都需要符合该定义
|
program expression O_EQ { printf("result is : %d",$2); }
;
expression:
INTEGER { $$ = $1;}
| expression O_ADD expression {$$ = $1 + $3; }
| expression O_MINUS expression {$$ = $1 - $3; }
| expression O_MULTIPLY expression {$$ = $1 * $3;}
;
·

真的是浅读。
最近在和孩子一起读《伊利亚特》(版本),随意翻读了书中的一小段故事,恰好是关于阿基琉斯(Achilleus Ἀχιλλεύς,更多时候被翻译作阿喀琉斯)的,一下子就被吸引了。这一小段是关于,阿基琉斯在出生之后,因为他的妈妈忒提斯得知神的预言,她的孩子将死与特洛伊战争,所以,忒提斯就把阿基琉斯以女孩子的身份送入王宫,让他和宫女们一起玩耍长大。而奥德修斯为了找到预言中的阿基琉斯,他来到王宫,看到一众宫女,智慧或者说夹杂的奥德修斯于是心生一计,他将长矛盾牌和女孩子们喜欢的玩具放到一起,然后故意制造出被入侵的混乱假象,很多的宫女都在混乱中四处逃跑,而有一个红头发的孩子,并没有像其他人一样害怕和逃走,而是勇敢的拿起了长矛和盾牌要去战斗。奥德修斯明白,这就是他要找的阿基琉斯。
最终,阿基琉斯带着自己的宿命去参加了特洛伊战争,立下了赫赫战功,也与预言最终死在了这场战争中。
在看完上面这一小段之后,就深深被《伊利亚特》吸引了。
极力避免预言中的悲剧,却发现始终无法逃脱。这似乎是众多希腊故事的基础设定之一。在看完上面的段落后,我又继续开始从头阅读伊利亚特。
不和女神厄里斯(Eris)在忒提斯和珀琉斯的婚礼上,扔下了那颗“金苹果”之后,而最终所导致的一系列事件最终导致了特洛伊战争的发生。在婚礼上,厄里斯扔出的金苹果上刻着“献给最美丽的女神”,落在了赫拉、雅典娜、阿芙洛狄忒(也做阿佛洛狄忒,他的罗马名“维纳斯”更为人所熟知)的身边。为了分出谁是世间最美丽的女神,三位女神在赫尔墨斯的带领下,来到了,还自以为自己是一个普通牧民的帕里斯(Paris)身边,要求他来最终决定金苹果的归属。
最终,在阿芙洛狄忒许诺帮他赢取世间万人垂怜的海伦芳心之后,帕里斯将金苹果判给了阿芙洛狄忒。
那么,这个普通的牧民帕里斯是谁呢?他当然不是普通的牧民,他原本是特洛伊的王子,因为出生时被预言,他将会给特洛伊带来毁灭,所以,出生后,特洛伊的国王便忍痛命人将他送走,并命令将其杀死。不过,由于此人的仁慈,不忍心下手,最终自己收养了这个孩子,因为在平时劳作时,他们都将孩子放在篮子中,于是将孩子命名为帕里斯(Paris)。
在帕里斯成年后,遇到了带着金苹果的阿芙洛狄忒。他的命运终于迎来了改变。后面,他终于回到了特洛伊王宫与父母相认,并最终辗转见到了海伦,并在爱神的帮助下,赢得了海伦的芳心。也最终给特洛伊带来了毁灭。
如前所述,在希腊神话中有着大量的“悲剧”,这些悲剧似乎有一些模式:预言了悲剧的发生,然后做很多努力避免悲剧,最后发现都是徒劳。这一小段就有两个这种模式:
今天就简单的写到这里,真的是非常有意思的故事。故事本身引人入胜,又包含了古希腊人对于命运、战争、爱情、美丽的理解。
这是一个系列文章,旨在了解如何使用Flex/Lex和Yacc/Bison进行词法和语法解析。这个系列,分成了几个部分,包括
了解这个系列需要一定的编译原理知识作为背景知识,了解程序如何从字符串先解析成Token,而后使用语法解析器生成解析树,最后执行该解析树。
lex/flex可以按照“词法文件”的定义,将文本解析成单个的Token,然后通过执行“词法文件”中定义的Action来完成一些操作,一般,flex的输出会通过函数/变量将结果传递给yacc/bison进行进一步的语法解析。为了简化,本文将仅通过独立的“词法文件”完成一些操作,以了解flex的基础使用。
这里完成的程序是一个简单的“count”程序,输入是一个文件,程序输出文件中包含的字符数、词语数、以及行数。
1. 安装lex: yum/apt-get install flex
2. 编写如下词法文档:
%{ //
int characters = 0; // %{ ... }% 之间的部分是"Declarations"
int words = 0; // Declarations 部分声明的变量,是可以在全局使用的
int lines = 0; // 例如,在该示例的main程序中,就通过extern声明的方式
%} // 使用了这些变量
%% //
\n { // 从这里开始是Translations阶段
++lines; // 这里定了Token,以及遇到了Token之后
++characters; // 应该采取/执行什么,例如这里遇到了\n字符
} // 则,将lines/characters变量都加1
[ \t]+ characters += yyleng; //
[^ \t\n]+ { // 注释部分的文本需要删除,程序才能正常编译
++words; // 删除注释的vim命令:1,$s/\/\/.*$//g
characters += yyleng; //
} //
//
%%
直接使用如上代码的话,后面就会在gcc编译的时候遇到如下错误:
$ lex zzx.l
$ gcc lex.yy.c wc.c -o wc.out
/tmp/cc1SPYm2.o:In function yylex':
lex.yy.c:(.text+0x42f):undefined reference toyywrap'
/tmp/cc1SPYm2.o:In function input':
lex.yy.c:(.text+0xf73):undefined reference toyywrap'
collect2: ld returned 1 exit status
如果你也遇到了这个错误,不用担心,你并不孤单,在Stackoverflow上看到解决该失败的的答案一共有150点赞(up),就知道大家都一样了(参考@Stackoverflow)。因为默认的,lex生成的词法解析程序中,在最后是需要调用的yywrap函数的(关于yywrap),如果不打算提供该函数,则可以使用lex选项 %option noyywrap 禁用该调用。那么上面的代码就需要修改为:
$ cat zzx.l
%{
int characters = 0;
int words = 0;
int lines = 0;
%}
%option noyywrap
%%
\n {
++lines;
++characters;
}
[ \t]+ characters += yyleng;
[^ \t\n]+ {
++words;
characters += yyleng;
}
%%
词法文件需要使用工具flex将其编译生成一个c语言文件,然后再使用gcc将其编译成一个可执行文件。编译前,我们需要先编写一个简单的main函数. 再编写一个程序的入口函数(main),并调用yylex()就可以了。具体如下:
$ cat wc.c
#include <stdio.h>
int yylex(void);
int main(void)
{
extern int characters, words, lines;
yylex();
printf("%d characters, ", characters);
printf("%d words, ", words);
printf("%d lines\n", lines);
return 0;
}
这里需要注意:在程序中,我们通过调用yylex()完成了实际的词法解析过程,并获得执行结果。这是一个非常简单的示例,实际过程比这要更加复杂,在词法文件中,每一次rule解析完成后,再起action部分,通常都会有return语句结束本次yylex调用,所以会是一个反复调用yylex的过程。
$ lex zzx.l
$ gcc lex.yy.c wc.c -o wc.out
$ chmod +x wc.out
$ cat s.txt
this is a input file.
this is a input file.
$ ./wc.out < zzx.l
404 characters, 36 words, 18 lines
$ ./wc.out < s.txt
44 characters, 10 words, 2 lines
好了,至此,我们就完成一个词法解析的任务,因为这个任务不涉及任何语法(yyac)解析,所以比较适合初学者学习词法解析工具lex。
为了再略微增强该示例的,这里对上面的示例又做了一个小调整,新增一行“Definitions”,有时候为了增强可读性,会对一些expression定义一个名称,如下,将\n定义为NL:
%{ //
int characters = 0; // %{ ... }% 之间的部分是"Declarations"
int words = 0; // Declarations 部分声明的变量,是可以在全局使用的
int lines = 0; // 例如,在该示例的main程序中,就通过extern声明的方式
%} // 使用了这些变量
//
NL \n // 这里新增了一行,这是一行 Definitions
// 将\n用字母NL定义,所以下面的\n也就可以使用NL
// 试想,如果表达式很复杂用这种方式,可读性会增强很多
%% //
NL { // 从这里开始是Translations阶段
++lines; // 这里定了Token,以及遇到了Token之后
++characters; // 应该采取/执行什么,例如这里遇到了\n字符
} // 则,将lines/characters变量都加1
[ \t]+ characters += yyleng; //
[^ \t\n]+ { // 注释部分的文本需要删除,程序才能正常编译
++words; // 删除注释的vim命令:1,$s/\/\/.*$//g
characters += yyleng; //
} //
//
%%
自此,我们就了解一个词法解析文件的几个主要部分:Definitions、Declarations、rule(以及rule对应的Action)。
参考资料:
yylex()函数,该函数每次基于某个规则(rule)解析到一个新的Token的时候,则会执行对应的“Action”(也就是每个Token后面的代码)部分的代码。例如,上面的程序中会执行++lines或++words代码。yyleng、yytext、yyin等,完整的列表可以参考:Values Available To the User。另外,这些“变量”并不是真的变量,大部分都是一些“宏”,例如,yytext的真实定义可能是这样的:#define yytext (((struct yyguts_t*)yyscanner)->yytext_r)。了解这一点,有利于理解这些”变量”并不能在外部直接引用。
return,例如如果遇到一个整数,可能会看到类似这样的代码:return INTEGER; 这时候,yylex()遇到一个对应的字符就会返回INTEGER。INTEGER,语法为 %token INTEGERINTEGER的预定义:#define INTEGER 257INTEGER了cat cal.y
...
%token INTEGER
...
cat cal.tab.h //这是bison生成的文件
...
#define INTEGER 257
...
cat cal.l
...
[[:digit:]]+ { return INTEGER; }
...
return语句给yyparse()中调用yylex时,返回当前Token的类型。如果一个Token是一个[:digit:]+的时候,我们除了需要知道这个Token是一个整数之外,至少yyparse()还需要知道这个是一个什么整数,具体数值是多少,当然并不是所有的token都需要,一般identifier都是需要的。而,前面的yytext都是yylex本地的“变量”。这时候,通常会使用yylval。yylval是一个由yacc/bison定义的变量(默认是int类型),用于存储词法解析需要传递给yyparse的数据,在yacc/bison的语句处理的Action阶段,可以使用变量,以获得词法解析阶段的一些值。例如,一个Token是一个整数、字符串(并非keyword)的时候,我们会将对应的值存储在yylval中。所以,yylval通常会被定义为一个联合体(union类型),用于存储不同类型的值。关于这几个概念的更详细细致的解释可以参考最前面提到的“IBM的z/OS系统的文档中关于lex和yacc的介绍”(参考:Tutorial on using lex and yacc)。