第一部分 简单了解Flex和Bison
1、Flex 和 Bison 简介
Flex和Bison(前身分别为Lex和Yacc),是Linux下两个用来生成程序的工具,它们生成的程序分别叫做词法分析器和语法分析器。词法分析把输入分割成一个个有意义的词块,称为记号(token);语法分析则确定这些记号是如何彼此关联的。
举个例子:Flex文件定义pattern(什么是黄豆,什么是绿豆……),输入文件(一袋豆子)通过Flex处理(词法分析),将输入划分为一段段的token(将输入的豆子一个个摘出来),从而执行不同的action(黄豆就磨豆浆,绿豆就做绿豆糕……)。
也就是说,Flex生成的token可以喂给Bison进行处理,当然也可以选择直接自己处理,但是使用Bison可以更方便地处理复杂的逻辑,编写简单,调试方便。
2、从一个demo开始——简易计算器
该demo包含以下文件:calc.l、calc.y、Makefile。
calc.l:由Flex工具进行编译后产生词法分析器,主要用于识别运算符和数字,并将记号和值传给语法分析器进行解析。
// calc.l
%{
#include "calc.tab.h"
%}
%option noyywrap
EXP ([Ee][+-]?[0-9]+)
%%
"+" |
"-" |
"*" |
"/" |
"|" |
"(" |
")" { return yytext[0]; }
[0-9]+"."[0-9]*{EXP}? |
"."?[0-9]+{EXP}? { yylval.num = atof(yytext); return NUMBER; }
\n { return EOL; }
[ \t] { /* ignore whitespace characters */ }
. { yyerror("invalid input!"); }
%%
calc.y:由Bison工具进行编译后产生语法分析器,主要用于处理词法分析器传来的数据,对其进行运算处理,并输出相关信息。
// calc.y
%{
#include <stdio.h>
void yyerror(char *s);
%}
%union {
char operator;
double num;
}
%token <num> NUMBER
%token <operator> EOL
%type <num> exp factor term
%%
calclist: /* empty */
| calclist exp EOL { printf("= %4.4g\n> ", $2); }
| calclist EOL { printf("> "); }
;
exp: factor
| exp '+' factor { $$ = $1 + $3; }
| exp '-' factor { $$ = $1 - $3; }
;
factor: term
| factor '*' term { $$ = $1 * $3; }
| factor '/' term { $$ = $1 / $3; }
;
term: NUMBER { $$ = $1; }
| '|' term { $$ = $2 < 0 ? -$2 : $2; }
| '(' exp ')' { $$ = $2; }
| '-' term { $$ = -$2; }
;
%%
void yyerror(char *s)
{
fprintf(stderr, "%s\n", s);
}
int main()
{
printf("This is a simple calculator.\n");
printf("> ");
yyparse();
return 0;
}
Makefile:编译.l(Flex)文件,生成.lex.yy.c。编译.y(Bison)文件,生成calc.tab.c和calc.tab.h。并将它们链接为同一个可执行文件。
//Makefile
all: clean calc
calc: calc.l calc.y
bison -d calc.y
flex calc.l
cc -o $@ calc.tab.c lex.yy.c
clean:
rm -rf calc lex.yy.c calc.tab.c calc.tab.h
运行cacl可执行文件,执行结果如下:
$ make
rm -rf calc lex.yy.c calc.tab.c calc.tab.h
bison -d calc.y
flex calc.l
cc -o calc calc.tab.c lex.yy.c
$ls
calc calc.l calc.tab.c calc.tab.h calc.y lex.yy.c Makefile
$ ./calc
This is a simple calculator.
> 1+2+3
= 6
> 1-2-3
= -4
> (1+2)*3
= 9
> 1/2+3
= 3.5
> 99999999
= 1e+08
第二部分 Flex规范参考(包含书中第1、2、5章内容)
1、结构规范
Flex程序由三部分构成:定义部分、规则部分和用户子例程。由"%%"进行分割。
...定义部分...
%%
...规则部分...
%%
...用户子例程...
定义部分:包含选项、文字块、定义、开始条件和转换。
规则部分:包含模式行和C代码。
用户子例程:内容将被flex原样拷贝到C文件。
2、正则表达式
详见《flex & bison》P137-139,或参考:https://regexlearn.com/zh-cn/cheatsheet
3、REJECT
退回已经匹配模式的文本,然后继续寻找它的下一个最佳匹配。
// demo: 识别pink中的pin、ink及pink
pink {npink++; REJECT;}
ink {nink++; REJECT;}
pin {npin++; REJECT;}
4、上下文相关性
左上下文相关:特殊的行首模式字符^
、起始状态以及显式代码(flag标识符)
右上下文相关:特殊的行尾模式字符$
、斜线操作符/
以及字符回推函数yyless()
5、起始状态
用于限制特定规则的作用范围
%x xxx
:把xxx标记为一个“独占”起始状态,当该状态激活时,只有这个状态中的模式才可以进行匹配
%s xxx
:把xxx标记为一个“包含”起始状态,它允许未标记为任何状态的模式也可以进行匹配
BEGIN xxx
:用于切换起始状态
INITIAL
:表示初始状态
YY_START
:可以获得当前的起始状态值
6、二义性判断基本规则
(1)词法分析器匹配输入时匹配尽可能多的字符串
(2)如果两个模式都可以匹配的话,匹配在程序中更早出现的模式
7、输入管理
(1)标准输入
yyin
:输入,可以是文件(FILE *)或端口(stdin)
yyrestart(fp)
:把词法分析器的输入切换到文件fp,YY_NEW_FILE
等同于yyrestart(yyin)
(2)输入缓冲区
YY_BUFFER_STATE bp; // 类型YY_BUFFER_STATE是指向flex输入缓冲区的指针
FILE *fp;
fp = fopen(..., "r");
bp = yy_create_buffer(fp, YY_BUF_SIZE); // 从fp创建新的缓冲区
yy_switch_to_buffer(bp); // 使用缓冲区
...
yy_flush_bufer(bp); // 放弃缓冲区中的内容
...
void yy_delete_buffer(bp); // 释放缓冲区
// 当前缓冲区的宏为YY_CURRENT_BUFFER
(3)从字符串输入
bp = yy_scan_bytes(char *bytes, len); // 分析字节流拷贝
bp = yy_scan_string("string"); // 分析以空字节符结尾的字符串拷贝
bp = yy_scan_buffer(char *base, yy_size_t size); // 分析长度为(size-2)的字节流
yy_scan_buffer速度最快,它不会对字符串进行拷贝操作,直接分析文本,但是要求文本的最后两个字节为"\0",它们不会被分析。bp的使用方法同(2)。
(4)文件嵌套
void yypush_buffer_state(bp); // 切换到bp,把旧的缓冲区压入堆栈
void yypop_buffer_state(); // 删除当前缓冲区,继续使用上一个缓冲区
(5)input():该函数可获取后续单个字符
// demo:
"/*" {int c1 = 0, c2 = input();
for(;;) {
if (c2 = EOF) break;
if (c1 == '*' && c2 == '/') break;
c1 = c2;
c2 = input();
}
}
(6)YY_INPUT(buf, result, max_size)
:用于读取输入到当前缓冲区的宏
8、输出管理
默认规则:所有没有被匹配的输入都拷贝到yyout
在一个模式所关联的C代码中,宏ECHO
用来写出记号到当前的输出文件yyout。
等价于语句:fprintf(yyout, "%s", yytext);
9、交互模式和批处理模式
批处理模式:分析记号时总是向前查看(稍快)
交互模式:分析记号时仅仅在需要的时候才向前查看,一般在输入源是终端时使用
%option batch
和%option interactive
可以用来强制使用批处理模式或者交互模式。
10、行号和yylineno
%option yylineno
:flex将定义一个名为yylineno的整形变量来保存当前行号,它会在每次遇到\n字符时自动地更新行号。但flex本身并不会初始化yylineno,所以需要每次开始读取文件时把它设置为1。如果希望跟踪每个文件的行号时,就需要自己来保存和恢复每个文件的当前行号。
11、prefix和outfile
%option prefix = "foo"
:让flex使用前缀"foo"而不是"yy",如"yyin"将重定义为"fooin"
%option outfile = "foolex.c"
:生成的flex源文件将会是foolex.c
也可以通过命令行选项来实现:$ flex --outfie = foolex.c --prefix = foo foo.l
12、可重入词法分析器
较为复杂,详见《flex & bison》P134-P137
13、其他函数、宏
(1)unput(c)
:该宏可以返回字符c给输入流,允许多次调用来推回多个字符。
(2)yyinput() yyunput()
:在C++的词法分析器中,宏input和unput被变更为yyinput和yyunput,以避免和C++库函数名产生冲突。
(3)yyleng
:相当于strlen(yytext)。
(4)yyless()
:退回记号的前n个字符。
(5)yymore()
:把下一个记号也添加到当前记号中。
(6)yylex()
:开始匹配下一个记号。
14、其它option
(1)%option nodefault
:当输入无法被给定的规则完全匹配时,词法分析器将会报告错误
(2)%option noyywrap
:要求不使用yywrap,一般都需要添加
(3)%option case-insensitive
:flex生成一个大小写无关的词法分析器
如果需要关闭一个选项,只需要在该选项前加no。
第三部分 Bison规范参考(包含书中第1、3、6章内容)
1、结构规范
Bison程序由三部分构成:定义部分、规则部分和用户子例程。由"%%"进行分割。
...定义部分...
%%
...规则部分...
%%
...用户子例程...
定义部分:包含文字块。
规则部分:包含语法规则和语义动作的C代码。
用户子例程:通常包含语义动作中需要调用的C代码。
2、符号
(1)符号:包括记号(又称终结符)和非终结符。
(2)定义符号类型有两个方法:声明符号类型和显示符号类型。
声明符号类型:使用%union
声明列出所有可能的类型。之后必须声明符号的类型。
显式符号类型:使用$<xxx>n
或者$<xxx>$
来显式说明符号的类型。
(3)记号(token):记号可以是通过%token
(或者%left
、%right
、nonassoc
)定义的符号(建议为大写),或者是直接用引号引起的文字字符。当bison需要新的记号时,它调用yylex()
从输入中返回下一个记号。记号拥有记号编号和记号值属性,记号值总是保存在变量yylval
中。
// bison.y
%union {
enum optype opval;
double dval;
char * sval;
}
%token <dval> REAL
%token <sval> STRING
%nonassoc <opval> RELOP
// flex.l
%{
#include "parser.tab.h"
%}
...
[0-9]+\.[0-9]* {yylval.dval = atof(yytext); return REAL; }
\"[^"]*\" {yylval.sval= strdup(yytext); return STRING; }
"==" {yylval.opval= OPEQUAL; return RELOP; }
(4)非终结符:规则左部定义的符号,可以使用%type
来声明。一般为小写。
3、文字块
存在于行%{和%}或者%code中。它通常包括规则部分代码所需要使用的变量和函数的声明,以及#include行。可以使用%code
来将代码放置到所生成程序中的特定位置。
// 选项place称为限定符,可以为top、provides和requires,对应文件的顶部、在YYSTYPE与YYLTYPE的定义之前和定义之后。
%code [place] {
...代码...
}
声明包括%union、%start、%token、%type、%left、%right和%nonassoc。
4、规则和递归规则
(1)每个规则由一个非终结符开始,然后是冒号和可能为空的符号、文字记号和动作的列表,用|
隔开。规则用;
结束。
(2)递归规则可以用于分析不定长的项目列表。且任何递归规则必须至少有一条非递归的分支,才可以终止递归。
exprlist: expr
| exprlist ',' expr // 左递归,"expr ',' exprlist" 为右递归。左递归一般比右递归更有效率。
;
(3)可以通过定义YYINITDEPTH
来控制bison堆栈的长度,它表明堆栈的初始大小。也可以定义YYMAXDEPTH
来设置堆栈长度的最大值。
5、动作和嵌入动作
(1)动作:当bison匹配语法中一条规则时执行的C代码。
其中$$
表示左部符号的值,$n
表示右部第N个符号的值。
其中@$
表示左部符号的位置,$n
表示右部第N个符号的位置。每个位置信息实际上是一个结构。
对于没有动作的规则,bison使用默认动作:{ $$ = $1; }
继承属性:
declaration: class type namelist ;
class: GLOBAL { $$ = 1; }
| LOCAL { $$ = 2; } ;
type: REAL { $$ = 1; }
| INTEGER { $$ = 2; } ;
namelist: NAME { mksymbol($0, $-1, $1); } // $0指向type的值,$-1指向class的值。
| namelist NAME { mksymbol($0, $-1, %2); } ;
(2)嵌入动作:指嵌入在规则中间的语义动作。以下两种规则是等价的。
// demo1
thing : A { printf("seen an A"); } B ;
// demo2
thing : A fakename B;
fakename: /* 空 */ { printf("seen an A"); } ;
嵌入动作也会被转化为规则中的符号:
thing : A { $$ = 17; } B C { printf("%d", $2); } ; // 将会打印"17"
6、%start
默认情况下,bison首先开始分析的规则,是第一个出现的规则。可以用%start somename
来以规则somename作为起始规则。
7、%parse-param
通常调用yyparse()
不需要任何参数。但如果需要从周边程序导入一些信息,可以使用全局变量,也可以使用%parse-param
为其定义添加参数。
%parse-param {char * modulename}
%parse-param {int intensity}
// 允许调用yyparse("mymodule", 42),然后在动作代码中使用modulename和intensity。
8、分析方法
bison有两种分析方法:LALR和GLR。LALR为自左向右向前查看一个记号;GLR为通用的自左向右。
大部分情况下默认使用LALR,但它在如下情况会产生问题:
// demo1: 存在问题,因为需要向前查看两个符号。
phrase: cart_animal AND CART
| work_animal AND PLOW
cart_animal: HORSE | GOAT
work_animal: HORSE | OX
// demo2: 不存在问题,因为仅需要向前查看一个符号。
phrase: cart_animal CART
| work_animal PLOW
9、移进和归约
(1)移进(shift):当语法分析器读取记号时,每当它读到的记号无法结束一条规则时,它将把这个记号压入一个内部堆栈,然后切换到一个新状态,这个状态能够反映出刚刚读取完的记号。
(2)归约(reduce):当语法分析器发现压入的所有语法符号已经可以组成规则的右部时,它将把右部符号全部从堆栈中弹出,然后把左部语法符号压入堆栈。
10、二义性和冲突
(1)移进/归约冲突:无法确定应该首先移进记号,还是首先归约一条规则。除非有优先级声明,否则bison默认选择移进。
e: 'X'
| e '+' e
;
// 对于输入字符串"X+X+X",可能为"(X+X)+X"(选择归约)或者"X+(X+X)"(选择移进)
(2)归约/归约冲突:发生在同一个记号可以结束两条不同规则的时候。
prog: proga | progb ;
proga: 'X' ;
progb: 'X' ;
(3)%expect N
会预设有N个移进/归约冲突,%expect-rr N
会预设有N个归约/归约冲突,在不同时会报告一个编译期错误。
(4)冲突修复原则:发生冲突时,首先考虑修正语法来解决冲突,其次可以给规则赋予优先级。
11、结合性和优先级
(1)结合性:声明包括%left
、%right
和%nonassoc
,分别声明一个操作符左结合、右结合和无结合性。对于"A-B-C",如果’-‘是左结合,则等于"(A-B)-C";如果’-'是右结合,则等于"A-(B-C)"。
(2)优先级:%left
、%right
和%nonassoc
的出现顺序决定了由低到高的优先级顺序(越早出现,优先级越低)。
(3)每个记号都可以通过优先级声明来获得相应的优先级和结合性。每条规则也可以有各自的优先级和结合性,它可以通过%prec
子句来声明,如果没有的话,该规则的优先级由最右记号决定。
(4)使用优先级和结合性解决移进/归约冲突原则:
- 如果记号的优先级更高,则移进;如果规则的优先级更高,则归约。
- 如果记号和规则的优先级相同,则检查结合性。如果是左结合,则归约;如果是右结合,则移进。如果没有结合性,则报告冲突错误。
12、错误记号和错误恢复
当bison无法分析接收到的输入时,它会尝试基于下面的步骤来从错误中恢复:
- 它会调用
yyerror("syntax error")
报告错误给用户。 - 它抛弃任何部分分析的规则,直到它回到一个能够移进特殊符号error的状态。
- 它重新开始分析,首先会移进一个
error
。 - 如果在成功移进三个符号前另一错误发生,则不会报告该错误,直到回到步骤2。
13、prefix
可以使用%name-prefix "pdq"
来变更语法分析器所使用的名字前缀。
也可以使用命令行:bison -d -p pdq -b pref mygram.y
,将生成pref.tab.c和pref.tab.h,该语法分析器的入口函数是pdqparse。
14、日志文件
使用--report=all
生成日志文件,命名一般为y.output
或name.output
。
15、其他函数、宏
(1)YYABORT
:一条特殊语句,使得语法分析器的分析例程yyparse()立即返回一个非零值,表明存在错误。这在检测到一个非常严重的错误以至于无法继续分析时有用。
(2)YYACCEPT
:一条特殊语句,使得语法分析器的分析例程yyparse()立即返回一个零值,表明成功。这在语法分析器无法判定输入结束而语法分析器可以的时候有用。
(3)YYDEBUG
:定义该宏为1,来包含debug功能。
(4)yyerror()
:当语法分析器检测到语法错误时,它调用该函数来报告错误给用户。
(5)yyclearin
:这个宏可以放弃一个被预先读到的记号。它在交互式语法分析器的错误恢复中有用,可以帮助语法分析器在错误发生后进入一个已知状态。
(6)yyerrok
:告知语法分析器错误恢复已经结束。
(7)YYRECOVERING()
:检测YYRECOVERING()的值可以帮助动作例程却ing是否需要报告发现的错误。
(8)yyparse()
:语法分析器的入口函数就是yyparse()。当被调用时,语法分析器试图分析输入流。分析成功返回0,否则返回非0值。