词法解析(flex)和语法解析(bison)
自然语言中语言学习包含:单词和语法。
在编程语言中也包含单词和语法,分别有flex和bison来实现。
所以flex和bison的作用就是:用C语言来实现另外一种语言(SQL、python等)
flex :LEX 词法分析 (lexical analysis)
单词提取:提取编程语言占用的各种保留字、操作符等等语言的元素
lex把每个扫描出来的单词叫做token,token可以有很多类。对比自然语言中名词、形容词等,而汽车就是属于名词类型的一个具体token。在编程语言token类型时有限的:关键字。
lex工具会帮我们生成一个yylex函数,yacc通过调用这个函数来得知拿到的token是什么类型的,但是token的类型是在yacc中定义的。
lex的输入文件一般会被命名成 .l文件,通过执行命令 lex XX.l
我们得到输出的文件是lex.yy.c
bison :YACC 语法分析syntactic analysis
语法表达:如何使用LEX中的“单词”组合成语法
yacc会帮我们生成一个yyparse函数,这个函数会不断调用上面的yylex函数来得到token的类型。
yacc的输入文件一般会被命名成 .y文件,通过yacc -d XX.y
我们得到的输出文件是y.tab.h y.tab.c,前者包含了lex需要的token类型定义,需要被include进 .l文件中
lex和yacc的输入文件格式
Definition section
%%
Rules section
%%
C code section
.l和.y的文件格式都是分成三段,用%%来分割,含义如下:
- Definition Section
可以放C语言的各种各种include,define等声明语句,但是要用%{ %}括起来。
如果是.l文件,可以放预定义的正则表达式:minus “-” 还要放token的定义,方法是:代号 正则表达式。然后到了,Rules Section就可以通过{符号} 来引用正则表达式
如果是.y文件,可以放token的定义,如:%token INTEGER PLUS ,这里的定一个的每个token都可以在y.tab.h中看到
- Rules section
.l文件在这里放置的rules就是每个正则表达式要对应的动作,一般是返回一个token
.y文件在这里放置的rules就是满足一个语法描述时要执行的动作
不论是.l文件还是.y文件这里的动作都是用{}扩起来的,用C语言来描述,这些代码可以做你任何想要做的事情
- C code Section
main函数,yyerror函数等的定义
YACC声明部分
标识符 | 含义 |
---|---|
%start | 指定起始规则 |
%token | 定义文法中的终结符 |
%left | 左结合 |
%right | 右结合 |
%nonassoc | 不能结合 |
%union: | 声明标识出的符号值可能拥有的所有C类型 |
%type: | 声明一些有特定语义值类型的非终结符 |
实现步骤
-
在.y中定义token类型。token既会被lex使用到,也会被.y文件中的BNF使用到。
-
在.l文件(就是lex的输入文件)中写词汇分析代码。
定义方式是:正则表达式–>对应操作。如果和yacc一起来使用的话,对应的操作通常是返回一个token类型,这个token的类型要在yacc中提前定义好。
- 写BNF。定义语言的规约方式。
BNF
用于对编程语言、文档格式、指令集、或者通信协议等的语法定义
在YAGG中书写方式如下:
<symbol> : __expression1__ {operation}
| __expression2__ {operation}
operation 是 满足语法时要执行的C语言代码,这里的C语言代码可以使用一些变量,他们是:$$
1
!
[
]
(
h
t
t
p
s
:
/
/
g
.
y
u
q
u
e
.
c
o
m
/
g
r
/
l
a
t
e
x
?
2
1 ![](https://g.yuque.com/gr/latex?2%E7%AD%89%E7%AD%89%E3%80%82#card=math&code=2%E7%AD%89%E7%AD%89%E3%80%82)
1![](https://g.yuque.com/gr/latex?2代表规约的结果,就是表达式symbol
的值,$1代表的是前面 __expression1__
中出现的各个word
例子
expr2: expr3 { $$ == $1; }
| expr2 PLUS expr3 { $$ = plus($1, $3); }
| expr2 MINUS expr3 { $$ = minus($1, $3); }
;
-
expr2 expr3都是BNF中定义的non-terminal
- PLUS和MINUS都是.y中定义的token类
- plus和minus 是事先定义好的C语言函数
- expr2表示递归调用
- “|” 表示或的情况
移进(shift)和规约(reduce)
-
yacc 在内部维护着两个堆栈;一个分析栈和一个内容栈。分析栈中保存着终结符和非终结符,并且代表当前剖析状态。内容栈是一个YYSTYPE 元素的数组,对于分析栈中的每一个元素都保存着一个对应的值。例如,当yylex 返回一个INTEGER标记时,y acc 把这个标记移入分析栈。同时,相应的yylval 值将会被移入内容栈中。分析栈和内容栈的内容总是同步的,因此从栈中找到对应于一个标记的值是很容易实现的。
-
对
expr: expr1 '+' expr2 { $$ = $1 + $3; }
来说,在分析栈中我们其实用左式替代了右式。在本例中,我们弹出expr1 '+' expr2
然后压入expr
。 我们通过弹出三个成员,压入一个成员缩小的堆栈。在我们的C 代码中可以用通过相对地址访问内容栈中的值,“ $1”代表右式中的第一个成员,“ $2”代表第二个,后面的以此类推。“ $$ ”表示缩小后的堆栈的顶部。在上面的动作中,把对应两个表达式的值相加,弹出内容栈中的三个成员,然后把造得到的和压入堆栈中。这样,分析栈和内容栈中的内容依然是同步的。
移进:用一个寄存符号的先进后出栈,把输入符号一个一个地移进到分析栈里,这就是移进。上述的expr1
和expr2
。
规约:当栈顶形成某个产生式的表达式时,即把栈顶的这一部分替换成(归约为)该产生式的左部符号。上述中的expr
。
移进规约冲突
当移进和规约同事满足时,不知道应该执行移进还是规约。
expr:
expr - expr
| expr * expr
| - expr
对于输入:
- 1 * 2
解析完1后,可以继续移进 * ,或者根据规则 expr:-expr 规约为 -1。也就是说,解析方式有两种:
-1 * 2 ---> = (-1)*2
-1 * 2 ---> = -(1*2)
解决方法
定义优先级:先定义的优先级低,后声明的优先级高,同行的优先级相同
%left '+' '-'
%right '*' '/'
%nonassoc '<'
注意:语法解析中很多都是移进规约冲突导致失败,在修改该处代码时需要学习相关语法
编译
LEX编译
flex XXX.l (编译为lex.yy.c)
gcc lex.yy.c -lfl (默认编译为a.out)
./a.out
YACC编译
bison -d XXX.y (编译为XXX.tab.c和XXX.tab.h)
gcc XXX.tab.c (默认编译为a.out)
./a.out
示例
实现简单的计算器语法
calc.y代码如下:
%{
#include <stdio.h>
int yyerror(char *s);
#include "lex.yy.c"
%}
%token NUM
%token ADD SUB MUL DIV
%token LP RP
%token EOL
%%
start:
| start expr EOL { printf("= %d\n> ", $2); }
| start EOL { printf("> "); }
;
expr: term { $$ = $1; }
| expr ADD term { $$ = $1 + $3; }
| expr SUB term { $$ = $1 - $3; }
;
term: factor { $$ = $1; }
| term MUL factor { $$ = $1 * $3; }
| term DIV factor { $$ = $1 / $3; }
;
factor: NUM { $$ = $1; }
| LP expr RP { $$ = $2; }
;
%%
int main(int argc, char **argv)
{
printf("> ");
yyparse();
return 0;
}
int yyerror(char *s)
{
fprintf(stderr, "error: %s\n", s);
}
int yywrap()
{
return 1;
}
calc.l代码如下:
%{
# include "y.tab.h"
int yylval;
int lexerror(char *s);
%}
%%
"+" { return ADD;}
"-" { return SUB;}
"*" { return MUL;}
"/" { return DIV;}
"(" { return LP;}
")" { return RP;}
^[-+][0-9]+ { yylval = atoi(yytext); return NUM;}
[0-9]+ {yylval = atoi(yytext); return NUM;}
[\n] { return EOL;}
[ \t] {/* ignore white space */}
. { }
%%
int lexerror(char *s)
{
fprintf(stderr, "flex error: %s\n", s);
}
使用步骤:
- 生成lex.yy.c、y.tab.c、y.tab.h文件
yacc -d calc.y
lex calc.l
- 编译
gcc -o calc y.tab.c
- 执行
./calc
生成lex.yy.c、y.tab.c、y.tab.h文件中相关信息解释
Lex 变量
- yyin
FILE* 类型。 它指向 lexer 正在解析的当前文件。
- yyout
FILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。
- yytext
匹配模式的文本存储在这一变量中(char*)。
- yyleng
给出匹配模式的长度。
- yylineno
提供当前的行数信息。 (lexer不一定支持。)
Lex 函数
- yylex()
开始分析函数,由 Lex 自动生成。
- yywrap()
在文件(或输入)的末尾调用。 如果函数的返回值是1,就停止解析。 因此它可以用来解析多个文件。 代码可以写在第三段,这就能够解析多个文件。 方法是使用 yyin 文件指针(见上表)指向不同的文件,直到所有的文件都被解析。 最后,yywrap() 可以返回 1 来表示解析的结束。
- yyless(int n)
可以用来送回除了前n个字符外的所有读出标记。
- yymore()
Lexer 将下一个标记附加到当前标记后。