《Flex 与 Bison》学习笔记

第一部分 简单了解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%rightnonassoc)定义的符号(建议为大写),或者是直接用引号引起的文字字符。当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)使用优先级和结合性解决移进/归约冲突原则:

  1. 如果记号的优先级更高,则移进;如果规则的优先级更高,则归约。
  2. 如果记号和规则的优先级相同,则检查结合性。如果是左结合,则归约;如果是右结合,则移进。如果没有结合性,则报告冲突错误。

12、错误记号和错误恢复

当bison无法分析接收到的输入时,它会尝试基于下面的步骤来从错误中恢复:

  1. 它会调用yyerror("syntax error")报告错误给用户。
  2. 它抛弃任何部分分析的规则,直到它回到一个能够移进特殊符号error的状态。
  3. 它重新开始分析,首先会移进一个error
  4. 如果在成功移进三个符号前另一错误发生,则不会报告该错误,直到回到步骤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.outputname.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值。

附:学习资料

自己动手写编译器
flex与bison中文版
The Lex & Yacc Page
Lex & Yacc

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值