flex用作词法分析,而bison用作语法分析。词法分析把输入分解成一个个有意义的词块,称作token
;语法分析则确定这些词块彼此之间如何关联(使用语法树表达)。比如:
A = B + C;
flex将其分解成A
、=
、B
、+
、C
和;
;接着bison将其确定为一个表达式,并对其建模成表达式树,简化如下
+---+
| = |
+---+
/ \
+---+ +---+
| A | | + |
+---+ +---+
/ \
+---+ +---+
| B | | C |
+---+ +---+
词法分析通常是使用正则表达式在输入中找寻字符的模式(称为pattern
)。flex程序由一组带有指令的正则表达式组成,这些指令确定输入在匹配了正则表达式的模式后执行的动作(action
),通过flex程序对这些指令进行解析后,生成一个词法分析器,它可以读取输入,匹配输入与正则表达式并执行匹配后关联的动作。以Unix下统计字符的wc
程序为例,使用flex模拟一个同样功能的词法分析器。
/*flex-wc.l*/
%option noyywrap
%{
int chars = 0;
int words = 0;
int lines = 0;
%}
%%
[a-zA-z]+ { words++; chars += strlen(yytext); }
\n { lines++; chars++; }
. { chars++; }
%%
int main() {
yylex();
printf("%8d %8d %8d\n", lines, words, chars);
return 0;
}
flex程序由flex语法与C语法混合组成,包含3个部分:
%option ...
和%{ ... %}
包含选项和声明,其中声明是纯C语法。这里声明了一些全局变量来统计词、字和行。%% ... %%
包含模式和动作。模式(pattern
)和动作(action
)的格式为 { [action] }
,flex要求模式必须出现在首行且位于行首,动作由{}
包裹,可以分为多行。匹配模式后就执行动作,yytext
指示匹配的输入文本。%%
之后的部分是纯C语法,它被原封不动的拷贝到flex生成的C代码中,用于调用词法分析的入口(yylex()
是其中的一个入口,它用标准输入作为匹配模式的输入)及处理逻辑。运行过程
flex flex-wc.l
cc lex.yy.c -lfl
./a.out
flex用flex
程序来转换脚本文件生成lex.yy.c
的文件,然后编译并使用-lfl
指向的flex库文件进行链接。如果没有第三部分也是可以的,你可以将生成的文件作为一个没有入口函数的文件使用,入口函数定义在其他的C文件中(如果没有入口,flex将使用一个极小的默认入口函数),然后进行多文件编译。
当flex词法分析器返回时(比如yylex()
),返回值作为一个记号流,它的定义由两部分组成记号编号(token number
)和记号值(token's value
)。在与bison联合使用时,标记编号的0值意味着文件结束;而为了与ASCII码和内定值冲突,其他的标记编号要从258开始定义。
用下面即将介绍的计算器程序片段为例:
%{ enum { NUMBER = 258, ADD = 259, }; int yylval = 0; %} %% "+" { return ADD; } [0-9]+ { yylval = atoi(yytext); return NUMBER; } %% int main() { int token;
while((token = yylex()) != 0) { printf("%d", token); if (token == NUMBER) printf(" = %d", yylval); printf("\n"); } return 0;
}
如果编译后运行并输入18 + 12
,则程序输出
258 = 18
259
258 = 12
可以看出在动作里添加return
语句可以使yylex()
例程返回这个值,之后可以再次调用yylex()
继续从上一次停止的地方接着匹配输入。
语法分析的任务就是找出输入记号之间的关系。常见的表示这种关系的结构就是树,比如带有优先级规则的语法树1 * 2 + 3 * 4 + 5
表示为
+---+
| + |
+---+
/ \
+---+ +---+
| + | | 5 |
+---+ +---+
/ \
+---+ +---+
| * | | * |
+---+ +---+
/ \ / \
+-+ +-++-+ +-+
|1| |2||3| |4|
+-+ +-++-+ +-+
根据树的结构特性,每个bison分析法则都可以构建一棵子树,然后组合成一棵更大的树。
为了编写一个语法分析器,需要一定的方法来描述语法分析器所使用的把一系列记号转换为语法分析数的规则。最常用的就是上下文无关文法(简称CFG
),书写这种文法的标准格式就是BFN文法
。如果将1 * 2 + 3 * 4 + 5
表达式用文法表示:
::=
| +
|
每一行就是一条规则,用来说明如何组建语法树的分支。::=
记作“变成“、|
记作“或者“。规则带有递归性质,即这个树分支可以由相似的分支组成;规则还有依赖关系,从上图中可以看出
是依赖
,从而形成了优先级。
现在使用一个计算器程序作为例子,说明语法分析与词法分析怎么联动,并借此说明用在bison中的BFN文法的语法格式。
先定义语法分析
/* simplest version of calculator */
%{
#include
int yyerror(const char *, …);
extern int yylex();
extern int yyparse();
%}
/* declare tokens */
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL
%%
calclist: /* nothing /
| calclist exp EOL { printf("= %d\n> “, $2); }
| calclist EOL { printf(”> "); } / blank line or a comment */
;
exp: factor
| exp ADD exp { KaTeX parse error: Can't use function '$' in math mode at position 4: = $̲1 + $3; } |… = $1 - $3; }
| exp ABS factor { $$ = $1 | $3; }
;
factor: term
| factor MUL term { KaTeX parse error: Can't use function '$' in math mode at position 4: = $̲1 * $3; } |… = $1 / $3; }
;
term: NUMBER
| ABS term { KaTeX parse error: Can't use function '$' in math mode at position 4: = $̲2 >= 0? $2 :… = $2; }
;
%%
int main()
{
printf("> ");
yyparse();
return 0;
}
int yyerror(const char *s, …)
{
int ret;
va_list va;
va_start(va, s);
ret = vfprintf(stderr, s, va);
va_end(va);
return ret;
}
再定义词法分析
/* recognize tokens for the calculator and print them out */
%{
extern int yyerror(const char *, …);
%}
%%
“+” { return ADD; }
“-” { return SUB; }
“*” { return MUL; }
“/” { return DIV; }
“|” { return ABS; }
“(” { return OP; }
“)” { return CP; }
[-+]?[0-9]+ { yylval = atoi(yytext); return NUMBER; }
\n { return EOL; }
“//”.*
[ \t] { /* ignore white space */ }
. { yyerror(“Mystery character %c\n”, *yytext); }
%%
编译与链接
bison -d fb1-5.y
flex fb1-5.l
cc fb1-5.tab.c lex.yy.c -lfl
bison程序也分为三个部分,定义与声明部分、规则部分、C代码部分。
定义与声明包含C代码和bison自身的token
记号(也是flex
的记号),一般记号用大写表示,如果没有定义记号,却在规则中出现,那么会被当做变量处理。前面提到过bison与flex联合使用时,yylex()
返回的记号值从258开始(例子中的EOF就是预定义的token
,表示匹配输入的结束),yyparse()
会自动处理输入的匹配时的文本和记号值。bison在实现上可以用宏加枚举的方式实现,并在生成的头文件中导出;
规则与BFN文法本身有些区别,它的格式为
。语法的最左边是规则语法起始符号(start symbol
),整个输入必须被它匹配;右边的是对这个规则的说明(规则本身也存在递归性质,整个规则存在依赖关系),首条规则表达式说明可以为空,并与其他的规则表达式通过或的方式组合起来。每条规则表达式可以联动一个动作,如果整个规则通过这条表达式匹配,则执行这个动作;可以使用$
加数字的方式获取匹配时表达式中的语法符号的语义值,而$$
表示这个规则本身的语义值,比如exp SUB factor { $$ = $1 - $3; }
它表示规则的语义值由exp
、SUB
和factor
等语法符号组成的表达式决定,当匹配时$1
、$2
和$3
分别代表了它们的值,对他们进行运算就得到了这条规则的语义值,其结果又参与了其他依赖这条规则结果的运算,比如例子中的calclist exp EOL { printf("= %d\n> ", $2); }
。
C代码部分即是整个语法分析器的入口。它使用了预定义的函数,这些函数可以和flex
一起工作。
把语法、词法分析器构建成一个程序时,词法分析器使用语法分析器生成的头文件,它包含记号的定义和yylval
的定义,因为语法分析器会调用词法分析器。
上面的计算器的例子中文法没有写成
exp : exp ADD exp
| exp SUB exp
| exp MUL exp
| exp DIV exp
| ABS exp
| NUMBER
;
是因为文法的递归性存在二义性,并且不能表达优先级。一旦规则存在优先级,文法就应当以多条相互依赖的方式来编写规则。其根本原因是bison创建的语法分析器总是以同一种方式分析输入,而且这些分析器只相同的文法。比如:
exp : exp SUB exp
| exp ADD exp
| factor
;
这种分析加减法的文法存在二义性,它对于1 - 2 + 3
这样的表达式,既可以解析为(1 - 2) + 3
,也可以解析为1 - (2 + 3)
,两种分析方式带来不同的结果。这个时候bison会给出冲突报告,并全程解析输入时都选定其中一种方式。
flex使用了扩展的POSIX正则表达式。元语言使用了标准的ASCII字符,根据上下文一部分表示模式一部分表示自身的含义。下面没有列出的数字、字母、符号等,将匹配自身。
chars | comments | flex support |
---|---|---|
"String" |
匹配引号中扩起来的字符串,即使字符串包含运算符。需要转椅的特殊字符请使用此项。 | 支持 |
\Character |
转义字符。当位于字符串中使用的字符类运算符之前时,\ 字符表明运算符符号代表文字字符,而不是运算符。有效转义序列包括:\a 响铃、\b 退格、\f 换页、\n 换行、\r 返回、\t 制表符、\v 纵向制表符、\\ 反斜杠。 |
支持 |
\Digits |
转义一到三位的8进制或16进制,比如\123 或\0abc 。不能使用\0 或\x0 ,这表示分析结束。 |
不支持 |
. |
匹配除\n 换行符以为的任何字符。 |
支持 |
[List] |
字符类,被扩起来的范围 ([x-y] ) 或者被扩起来的列表 ([xyz] ) 中的任一字符匹配或不匹配。所有运算符符号在括号表达式中失去它们的特殊含义,除 - (表示范围)、^ (表示不包含)和 \ (转义),如果要匹配这几个特殊符号需要转义,比如[\^abc] 表示包含字符 ^abc 的字符类。示例:[^abc-f] 表示不以 a 、b 、c 、d 、e 或 f 开头的模式匹配。 |
支持 |
[List]{-}[List] |
匹配前一个字符类前去后一个字符类的结果 | 支持 |
[:Class:] |
如当前语言环境中的 LC_TYPE 类别中所定义的,与属于 [::] 定界符之间所指定的字符类的任何字符匹配。下面的字符类名称被所有的语言环境所支持:alnum 字母与数字、cntrl 不可打印的控制字符、lower 小写字母、space 空格字符、alpha 字母、digit 十进制数字、print 可以打印字符、upper 小写字母、blank 空白字符、graph 可打印的非空白字符、punct 标点符号、xdigit 十六进制数字。这个类可以作为[list] 中的元素出现,如 [^[:space:]] 表示非空格符 |
不支持 |
^Expression |
仅当 Expression 在行起始处且 ^ 运算符是表达式中的第一个字符时指示匹配。示例:^h 与行首为 h 输入匹配。 |
支持 |
Expression$ |
仅当 Expression 在行末尾处且 $ 运算符是表达式的最后一个字符时指示匹配。示例:h$ 与行尾为 h 输入匹配。 |
支持 |
{Number1,Number2} |
与它前面紧挨着的模式的 Nubmer1 到 Number2 的具体值匹配。允许使用表达式 {Number} 和 {Number,} ,它们精确匹配表达式前的模式的 Number 的具体值。示例:xyz{2,4} 与 xyzxyz 、xyzxyzxyz 或 xyzxyzxyzxyz 匹配。这有别于 + 、* 和 ? 运算符,因为这些运算符仅与紧挨着前面的表达式匹配。要仅与间隔表达式前的字符匹配,请使用分组运算符。例如,xy(z{2,4}) 与 xyzz 、xyzzz 或者 xyzzzz 匹配。 |
支持 |
Expression* |
与 * 运算符之前紧挨着的表达式的零个或更多具体值匹配。例如,a* 为任意数目(包括零个)连续的 a 字符。在复杂表达式中与零个具体值匹配的作用更明显。 示例:表达式 [A-Za-z][A-Za-z0-9]* 指示以字母字符开头的所有字母数字字符串,包括仅为一个字母字符的字符串。能使用该表达式识别使用计算机语言的标识。 |
支持 |
Expression+ |
与 + 运算符之前紧挨着的模式的一个或更多具体值匹配。 示例:a+ 与一个或者更多 a 的实例匹配。同样,[a-z]+ 与所有小写字母字符串匹配。 |
支持 |
Expression? |
与 ? 运算符前紧挨着的表达式的零个或一个具体值匹配。 示例:ab?c 与 ac 或 abc 匹配。 |
支持 |
Expression|Expression |
指示与 | 选择运算符之前或之后的表达式匹配。 示例:ab|cd 与 ab 或者 cd 匹配。 |
支持 |
(Expression) |
与圆括号中的表达式匹配。() 运算符用于分组,并使圆括号中的表达式被读入 yytext 数组。圆括号中的组可用于代替任何其他模式的任何单个字符。 示例:(ab|cd+)?(ef)* 与诸如以下的字符串匹配:abefef 、efefef 、cdef 或者 cddd ;但是与 abc 、abcd 或者 abcdef 不匹配。 |
支持 |
相同的输入可以被多种不同的模式匹配,于是出现了二义性。flex
使用以下两个原则进行解决:
观察以下片段:
"+" { return ADD; }
"=" { return ASSIGN; }
"+=" { return ASSIGNADD; }
"if" { return KEYWORDIF; }
"else" { return KEYWORDELSE; }
[a-zA-Z][a-zA-Z0-9] { return IDENTIFIER; }
对于前三中模式,字符串+=
被匹配为ASSIGNADD
,因为+=
比+
更长。对于后三种模式,关键字的匹配模式先于标识符的匹配,所以关键字会被正确的匹配。
由于历史原因,当
yylex()
在读取到EOF
时会调用yywrap()
,这是很陈旧的特性,现在不需要使用了,可以通过%option noyywrap
关闭。
yyin
的文件句柄中读取。为了说明使用方法,现在改写字符统计程序,使其可以从文件读取数据,代码片段如下:
... int main() { if(argc > 1) { if(!(yyin = fopen(argv[1], "r"))) { perror(argv[1]); return -1; } }
yylex(); printf("%8d %8d %8d\n", lines, words, chars); return 0;
}
int yyrestart(FILE *f)
函数读取指定文件流中的数据进行匹配分析。还是以字符统计程序为例,从shell
传入多个文件进行分析,代码片段如下:
... int main(int argc, char **argv) { int i;
if(argc < 2) { /* just read stdin */ yylex(); printf("%8d %8d %8d\n", lines, words, chars); return 0; } for(i = 1; i < argc; i++) { FILE *f = fopen(argv[i], "r"); if(!f) { perror(argv[1]); return -1; } yyrestart(f); yylex(); fclose(f); printf("%8d %8d %8d %s\n", lines, words, chars, argv[i]); totchars += chars; chars = 0; totwords += words; words = 0; totlines += lines; lines = 0; } if(argc > 1) printf("%8d %8d %8d total\n", totlines, totwords, totchars); return 0;
}
yyin
来读取数据YY_BUFFER_STATE
输入缓冲区YY_INPUT
大多数情况下,词法分析器从文件或标准输入(比如终端)中读取输入,他们存在预读机制的差异。如果读取文件,词法分析器可以大段大段的读取数据,提高了IO效率;如果读取终端,则只能一行一行的处理。
词法分析器使用YY_BUFFER_STATE
的数据结构来处理输入,它描述的一个字符缓冲区和一些状态变量,一般情况下都关联一个FILE*
文件流,但也可以关联一个段内存区域来处理内存中的字符串。词法分析器的默认行为大致如下:
...
YY_BUFFER_STATE bp;
extern FILE* yyin;
for(…) {
…
yyin = fopen(“file”, “r”);
if (!yyin)
yyin = stdin;
bp = yy_create_buffer(yyin, YY_BUF_SIZE);// 创建句柄,YY_BUF_SIZE通常为16KB
if (!bp) {
if (yyin != stdin) fclose(yyin);
return -ENOMEM;
}
yy_switch_to_buffer(bp);// 切换到新的buffer中
while(yylex());// 开始解析
yy_delete_buffer(bp);// 销毁句柄
if(yyin != stdin) fclose(yyin);
}
…
这在分析处理嵌套输入时非常有用,因为YY_BUFFER_STATE
记录了读取的状态,可以在多个缓存中来回的切换,而不是像上面介绍的yyrestart()
函数一样,只能读取达到文件结尾后才能读取下一个,否则会丢失位置(当然通过C库也能做到,但是非常的复杂且效率不高)。如果要处理内存中的数据,可以使用 YY_BUFFER_STATE yy_scan_string(const char *)
和 YY_BUFFER_STATE yy_scan_buffer(char *, size_t)
来创建关联内存数据的YY_BUFFER_STATE
缓冲句柄。当缓冲区数据为空时,解析器会调用宏YY_INPUT
来填充缓冲区,所以如果我们重新定义这个宏,YY_BUFFER_STATE
任何的输入设备了,比如:
%{
#define YY_INPUT(buf,result,max_size) \
{ \
ssize_t n = read(socket_fd, buf, max_size); \
result = (n == EOF) ? YY_NULL : (typeof max_size) n; \
}
%}
我们可以从一个套接字中读取一些数据来填充缓冲。
当词法分析器达到末尾时,它将匹配伪模式<
,这可以做一些清理工作,我们将在下面小节的例子中使用说明。
输出是比较简单的,在默认情况下,词法分析器执行一条规则:
#define ECHO fwrite(yytext, yylen, 1, yyout)
. { ECHO; }
所有未匹配的输入都原本的回写到yyout
中,它一般是标准输出。有时我们需要在未匹配的情况下报错,那么可以使用%option nodefault
关闭这个默认特性。
flex的强大特性 ———— 起始状态 (start state
),它允许指定在特定时刻哪些模式可以用来匹配当前的输入。用户可以在定义处使用%x
的方式来定义新的独占起始状态,而%s
可以定义一个非独占起始状态,
默认情况下解析本身会定义一个名叫INITIAL
的状态,并全程使用它;如果要使某种模式只在某种状态中使用,则可以在模式前加状态的方式进行制定,如
。
下面给出一个解析C头文件中#include <...>
预定处理的解析器。它读取头文件,如果遇见包含文件就保存当前解析状态(比如已解析的行数等),然后打开包含文件,进行新一轮的解析。
%option noyywrap warn nodefault
%x IFILE
%{
/* 用栈表示动作 */
struct bufstack {
struct bufstack *prev; /* previous entry */
YY_BUFFER_STATE bs; /* saved buffer */
int lineno; /* saved line number */
char *filename; /* name of this file */
FILE *f; /* current file */
} *curbs = 0;
char curfilename; / name of current input file */
static int newfile(char fn);
static int popfile(void);
%}
%%
^"#"[ \t]include[ \t]["<] { BEGIN IFILE; }
int c;
/预读掉空白字符/
while((c = input()) && c != ‘\n’) ;
/任然属于当前文件的行号,但是不会打印/
yylineno++;
/打开新的文件,保存当前的状态,并压栈/
if(!newfile(yytext))
yyterminate(); /
/开始默认状态的模式匹配/
BEGIN INITIAL;
}
fprintf(stderr, “%4d bad include line\n”, yylineno);
yyterminate();
}
^. { fprintf(yyout, “%4d %s”, yylineno, yytext); }
^\n { fprintf(yyout, “%4d %s”, yylineno++, yytext); }
\n { ECHO; yylineno++; }
. { ECHO; }
<
%%
int main(int argc, char **argv)
{
if(argc < 2) {
fprintf(stderr, “need filename\n”);
return 1;
}
if(newfile(argv[1]))
yylex();
return 0;
}
int newfile(char *fn)
{
FILE *f = fopen(fn, “r”);
struct bufstack *bs = malloc(sizeof(struct bufstack));
/* die if no file or no room */
if(!f) { perror(fn); return 0; }
if(!bs) { perror("malloc"); exit(1); }
/* remember state */
if(curbs)curbs->lineno = yylineno;
bs->prev = curbs;
/* set up current entry */
bs->bs = yy_create_buffer(f, YY_BUF_SIZE);
bs->f = f;
bs->filename = fn;
/*切换到新的描述符进行新一轮的输入解析*/
yy_switch_to_buffer(bs->bs);
curbs = bs;
yylineno = 1;
curfilename = fn;
return 1;
}
int popfile(void)
{
struct bufstack *bs = curbs;
struct bufstack *prevbs;
if(!bs) return 0;
/* get rid of current entry */
fclose(bs->f);
yy_delete_buffer(bs->bs);
/* switch back to previous */
prevbs = bs->prev;
free(bs);
if(!prevbs) return 0;
/*切换到原来的描述符继续输入解析*/
yy_switch_to_buffer(prevbs->bs);
curbs = prevbs;
yylineno = curbs->lineno;
curfilename = curbs->filename;
return 1;
}
我们可以在定义处,预定义一些匹配模式片段 ———— 命名模式,然后在规则中的模式处使用它们(使用{}
包裹),如
/* float exponent */
EXP ([Ee][-+]?[0-9]+)
%{
…
%}
%%
…
([0-9]*.[0-9]+|[0-9]+.){EXP}?[flFL]? { … }
[0-9]+{EXP}[flFL]? { … }
…
%%
…
keyword | comments |
---|---|
noyywrap |
yylex 等分析函数结束后不再调用yywrap() 函数 |
nodefault |
关闭一些默认行为,比如不能匹配的输入则回射到标准输出 |
warn |
开启所有警告 |
case-insensitive |
整个过程中匹配输入不关心大小写,但是yytext 还是原本输入匹配的内容 |
yylineno |
自动的在yylineno 变量中维护当前解析的行数总值,如果是解析的多个文件,可以在开打文件后重置它 |
function | comments |
---|---|
yylex() |
开始词法分析 |
yyparse() |
开始语法分析 |
yyrestart(FILE *) |
使用指定文件流进行输入 |
yytext |
匹配时的文本 |
yyin |
默认输入文件流 |
yyout |
默认输出文件流 |
yylineno |
维护全局解析的行数 |
yylval |
规则的语义值(可以使用union 实现),默认情况下根据操作不同而不同(比如如果将整型赋予它,它就是整形),我们也可以通过定义来实现特殊结构的语义值类型(其实这个变量属于语法分析器)。 |
yy_create_buffer(FILE *, int) |
创建一个与文件流关联缓存区 |
yy_scaning_buffer(char *, yy_size_t) |
创建一个与内存块关联的动态缓存区 |
yy_scaning_bytes(const char *, int) |
创建一个与内存块关联的静态缓存区(拷贝一份) |
yy_scaning_string(const char *) |
创建一个与字符串关联的静态缓存区(拷贝一份) |
yy_switch_to_buffer(YY_BUFFER_STATE) |
切换到指定的缓存区,使用新的缓存区解析输入 |
yy_delete_buffer(YY_BUFFER_STATE) |
销毁缓存区 |
yy_flush_buffer(YY_BUFFER_STATE) |
丢弃当前缓存区的内容 |
macro | comments |
---|---|
YY_INPUT |
当YY_BUFFER_STATE 中没有数据时调用此宏,比如在yy_flush_buffer() 调用后,如果词法分析器需要数据,则调用宏 |
ECHO |
回射匹配的内容到输出中,默认是到yyout 中 |
flex可以使用正则表达式识别词法,而bison可以识别语法。flex把输入流分解成若干片段(记号);而bison则分析这些片段的逻辑组合,并进行逻辑组合(计算 ———— 逻辑组合的语义值计算)。
bison根据给定的语法分析规则生成一个可以识别语法中“语句”的语法分析器,分析的输入在语法上可以是完全正确的,但是语义的正确性需要程序员自己保证,bison不能处理语义错误,比如把整型赋值给字符串。
语法分析基于规则来识别输入:
statement: NAME '=' expression { ... }
;
expression: NUMBER ‘+’ NUMBER { … }
| NUMBER ‘-’ NUMBER { … }
;
|
意味着一个语法符号可以有两种可能性 ———— 加或减 构成一个表达式。:
左边称为规则左部(简称LHS
),记着语法符号的非终结符;它的右边称为规则右部(简称RHS
),它可以是其他规则的左部或者是词法分析器返回的语法符号的终结符(记号)的组合。终结符和非终结符一定不能相同,且终结符不能作为左部出现。{}
是匹配时的动作,每个右部都可以有或没有动作。表示语法分析的通常是一个树结构,bison是不会主动的创建这棵树的。
规则可以指向它自身,这就形成了递归。这里又分为左递归(P : aP)和右递归(P : Pa)。
bison语法分析器通过查找能够匹配当前记号的规则来运作。
bison分析时创建一组状态
,这些状态反应了分析过的规则中可能的位置。当读取到记号时,如果无法结束一条规则的匹配,则将这个记号
压栈,然后切换到一个新的能反应刚才处理记号的状态,这种行为叫着移进(shift
)。
当压栈的记号
(也可以是右部
)可以组合成规则的右部
时,bison将所有的记号
弹出(组合成新右部
),然后将对应的左部
压栈,开始新一轮的移进,这种行为叫着归约(reduction
)。归约消减了一定数量的符号。每当归约时,bison会执行用户定义的规则动作(关联的代码)———— 执行这条右部各个部分语义值($$
和$
占位符表示)的计算,或执行函数调用等。
分析赋值语句表达式A = 3 + 4
、规则一Exp : NUM + NUM
和规则二Stmt : Name '=' Exp
,移进过程如下:
fred /* 查询规则二,发现相似,置状态一 */
fred =
fred = 3 /* 查询规则一,发现相似,置状态二 */
fred = 3 +
fred = 3 + 4 /* 根据规则一,发现可以归约,弹出`3`、`+`、`4`进行归约 */
fred = /* 将原左部压栈变为下次操作的右部,恢复状态一,根据规则一,发现可以归约,弹出`fred`、`=`、``进行归约 */
/*没有输入和规则可用,完成解析*/
bison有两种语法分析方法,一种是LALR(1)
—————— 自左向右向前查看记号;另一种是GLR
—————— 通用自左向右查看标记。大多语法解析器都使用LALR(1)
,它虽然没有GLR
强大,但是够简单速度够快。LALR(1)
的缺点是不能分析有歧义的语法,虽然这可以通过自定义优先级和结核性进行解决,但是它不能分析需要向前查看多个记号才能匹配的规则的语法。
抽象语法树是所有编译器中最重要的数据结构,bison是不负责创建整棵树,但在归约每条规则时,相当生成了一个树中的节点,我们可以在此借机创建操作这个节点 ———— 链接到其他节点丰富树,或者遍历已有的树。当然你可以可以使用其他的数据结构来存储归约的结果。
现在以计算器为例来说明怎么使用抽象语法树,先给出代码:
extern int yylineno; /* 词法分析器内部变量 */
void yyerror(char *s, ...);
/* 抽象语法树非叶枝节点 */
struct ast
{
int nodetype;
struct ast *l;
struct ast *r;
};
/抽象语法树叶枝节点/
struct numval
{
int nodetype; /* type K */
double number;
};
/* 构建节点(非叶枝节点就是一棵树) */
struct ast *newast(int nodetype, struct ast *l, struct ast *r);
struct ast *newnum(double d);
/* 计算节点的值,具有递归性 */
double eval(struct ast *);
/* 释放节点,具有递归性*/
void treefree(struct ast *);
%option noyywrap nodefault yylineno
%{
# include "fb3-1.h"
# include "fb3-1.tab.h"
%}
/* 一个regex片段,加 {} 可以重用 */
EXP ([Ee][-+]?[0-9]+)
%%
“+” |
“-” |
"" |
“/” |
“|” |
“(” |
“)” { return yytext[0]; /直接返回这个字符,bison默认识别小于258的记号/}
[0-9]+"."[0-9]{EXP}? |
“.”?[0-9]+{EXP}? { yylval.d = atof(yytext); return NUMBER; }
\n { return EOL; }
“//”.*
[ \t] { /* ignore white space */ }
. { yyerror(“Mystery character %c\n”, *yytext); }
%%
%{
# include
# include
# include "fb3-1.h"
%}
%union {
struct ast *a;
double d;
}
/* 定义标记,并将其赋值给语义值联合的某个字段 */
%token
%token EOL
/* 将规则赋值给的语义值联合的某个字段*/
%type exp term
/定义优先级和结合性/
%left ‘+’ ‘-’
%left ‘*’ ‘/’
%nonassoc ‘|’
%right NEG
/定义起始规则/
%start calclist
%%
exp: term
| exp ‘+’ exp { KaTeX parse error: Can't use function '$' in math mode at position 16: = newast('+', $̲1,$3); } | exp… = newast(’-’, $1,$3);}
| exp ‘*’ exp { KaTeX parse error: Can't use function '$' in math mode at position 16: = newast('*', $̲1,$3); } | exp… = newast(’/’, $1,$3); }
| ‘|’ exp { KaTeX parse error: Can't use function '$' in math mode at position 16: = newast('|', $̲2, NULL); } | … = newast(‘M’, $2, NULL); }
term : NUMBER { KaTeX parse error: Can't use function '$' in math mode at position 11: = newnum($̲1); } | '(' ex… = $2; }
;
calclist: /* nothing */
| calclist exp EOL {
/计算表达式的值,有递归性/
printf("= %4.4g\n", eval($2));
treefree($2);
printf("> ");
}
| calclist EOL { printf("> "); } /* blank line or a comment */
;
%%
#include
#include
#include
#include "fb3-1.h"
struct ast * newast(int nodetype, struct ast *l, struct ast *r)
{
struct ast *a = malloc(sizeof(struct ast));
if (!a) {
yyerror("out of space");
exit(0);
}
a->nodetype = nodetype;
a->l = l;
a->r = r;
return a;
}
struct ast * newnum(double d)
{
struct numval *a = malloc(sizeof(struct numval));
if (!a) {
yyerror("out of space");
exit(0);
}
a->nodetype = 'K';
a->number = d;
return (struct ast *)a;
}
double eval(struct ast *a)
{
double v;
switch (a->nodetype)
{
case 'K':
{ v = ((struct numval *)a)->number; }
break;
case '+':
{ v = eval(a->l) + eval(a->r); }
break;
case '-':
{ v = eval(a->l) - eval(a->r); }
break;
case '*':
{ v = eval(a->l) * eval(a->r); }
break;
case '/':
{ v = eval(a->l) / eval(a->r); }
break;
case '|':
{ v = eval(a->l);
if (v < 0) {
v = -v;
}
}
break;
case 'M':
{ v = -eval(a->l); }
break;
default:
printf("internal error: bad node %c\n", a->nodetype);
}
return v;
}
void treefree(struct ast a)
{
switch (a->nodetype)
{
/ 两个子节点 /
case ‘+’:
case ‘-’:
case '’:
case ‘/’:
{
treefree(a->r);
/* one subtree */
}
/* 单个子节点 */
case '|':
case 'M':
{
treefree(a->l);
/* no subtree */
}
/*没有子节点*/
case 'K':
{
free(a);
}
break;
default:
printf("internal error: free bad node %c\n", a->nodetype);
}
}
void yyerror(char *s, …)
{
va_list ap;
va_start(ap, s);
fprintf(stderr, "%d: error: ", yylineno);
vfprintf(stderr, s, ap);
fprintf(stderr, "\n");
}
int main()
{
printf("> ");
return yyparse();
}
语法规则中使用%union { ... }
来声明语法符号的语义值类型的集合,bison中每个符号 ———— 包括 记号
和 非终结符
都有一个不同类型的语义值,并使用yylval
变量在移进和归约中传递这些值。在规则动作中使用$$
和$
来指示参与归约的语法符号的语义值,以便进行操作。一般情况这些值使用整数就可以表示,但是在这个例子中,我们同时用了数值和指针作为非终结符或记号的结果。所以我们定义一个集合,并使用集合中指定的字段绑定指定的到记号和非终结符上,格式分别是token
、type
,这样就可以使操作记号或非终结符时使用指定的类型了。
比如 定义 %union { struct ast *a; double d; }
,然后绑定数字类的记号到d
字段,%token
,再绑定节点类的非终结符到a
字段,这里使用了列表,同时绑定多个,%type exp term
。
在之后的归约计算中,比如在exp: exp '+' exp { $$ = newast('+', $1, $3); }
中,$1
表示exp
,根据绑定它是非终结符,且是struct ast
类型、使用yylval.a
字段传递;$3
也表示exp
;$$
表示这个规则的非终结符,而没有使用的$2
表示'+'
这个符号。又比如在exp: NUMBER { $$ = newnum($1); }
中,$1
表示NUMBER
,根据绑定它是一个记号,且是double
类型、使用yylval.d
字段传递,由于记号属于词法分析器,所以在词法规则中使用"."?[0-9]+{EXP}? { yylval.d = atof(yytext); return NUMBER; }
来传递以匹配的词句,它是一个文本表示的浮点值。在这些计算中,如果赋值号两遍的值类型不匹配,bison会报错;如果没有默认的归约动作,则$$ = $1
作为默认的归约动作,当然归约可以没有语法符号参与,即整个归约都是空的。
如果语法有优先级和结合性,在语法分析的时候会产生歧义,最通常的处理方法就是进行文法的分组编写,依靠依赖性进行解决。但是这会产生代码的维护成本,容易出错,所以bison提供优先级和结合性的预定义描述,文法变得简短易于维护。bison使用%
来定义记号的结合性,其中left
表示左结合性、right
表示右结合性、nonassoc
表示没有结合性;并从自上而下的定义顺序来决定优先级的高低,先定义的优先级越低,反之越高。如果每个记号有两种含义且优先不同,则使用赋予绑定伪记号的方式提供不同的优先级,比如这里的'-'
符号,它既可以是减号,也可以是负号,先通过%right NEG
定义一个NEG
的伪记号,再通过'-' %prec NEG { ... }
将优先级冲突的记号赋予新记号。
以语句1 + 2 * 3
为例,如果没有定义优先级,它有两种解析结果(1 + 2) * 3
或1 + (2 * 3)
(具体使用哪一种,由解析器决定,但是肯定的是全程解析只会使用其中一种)。如果定义了优先级,在移进和归约中,解析器会查询优先级和结合性的表,从而确定真正的结果。移进和归约过程如下(我们省略从数值归约到表达式的过程):
1
1 + 2 /*从文法中发现可以归约,但是查表后发现有更高级别的操作符,暂停归约,预分析下一个记号*/
1 + 2 * /*发现新记号的优先级高于旧记号,继续移进*/
1 + 2 * 3 /*发现可以归约,且没有新记号可使用,则归约*/
1 + E1 /*还可以继续归约*/
E2 /*最终结果*/
这里的例子除了上面说的新特性以外,与前面计算器例子相比还有一个细微的区别。我们没有对每个词法返回的记号进行%token ...
的主动定义,因为我们提过bison识别的记号是从258开始的,所以在处理ASCII码做的记号时,不需要明确的定义,字节使用它的值作为记号就非常合适。