Lex & Yacc 学习笔记
1. Lex & Yacc 简介
最近在学习 《跟我一起写 Makefile》的时候,里面提到 Yacc,一时好奇,网上查了些相关博客了解了下。
这里涉及到两个概念,词法解析器和语法解析器。通俗的说明下个人理解,不一定完全准确,大概是这么个意思。语言一般由最基本的单词和能够表达一定语义的语句构成,编程语言也是类似。词法解析器的作用就是提取编程语言中的单词或者叫 token,而 Lex 的作用则是根据一定的词法规则生成词法解析器。语法解析器的作用则是根据构成语句的词汇组合判断其是否符合语法要求以及表达的语义,而Yacc 的作用则是根据一定的语法规则生成语法解析器。
1.1 Lex - 词法解析器生成器
Lex 是生成词法解析器的程序。Lex 通常和 Yacc(语法解析器生成器)一起使用,GNU/Linux 版本叫做 Flex(fast lexical analyzer)。Lex 的输入一般是以 .l
后缀的文件,该文件定义了词汇(token)提取规则,Lex 输出是 C 语言书写的词法解析器源码。下面以一个示例程序说明下这个生成过程:
demo.l
内容如下:
%{
#include <stdio.h>
%}
%%
"+" printf("TOKADD\n");
"\n" printf("TOKCR\n");
([1-9][0-9]*)|0 printf("TOKNUM\n");
[ \t] printf("TOKTAB\n");
%%
Makefile
内容如下:
OBJ := demo
FLEXO := demo
FLEXI := $(FLEXO).l
FLEXC := $(FLEXO).c
CFLAGS := -lfl
LEX := flex
$(OBJ) : $(FLEXC)
$(CC) -o $(OBJ) $(FLEXC) $(CFLAGS)
$(FLEXC) : $(FLEXI)
$(LEX) -t $(FLEXI) > $(FLEXC)
test:
echo "12 + 34" | ./$(OBJ)
clean:
rm -f ./$(OBJ) ./$(FLEXC)
Linux 环境(与 Unix 环境命令及参数有所区别)生成和运行词法解析器步骤如下:
- step 0:环境中需要安装 flex 和 flex-devel
[root@localhost Lex-Yacc]# yum install flex flex-devel -y
- step 1:生成词法解析器 C 语言源码
[root@localhost Lex-Yacc]# flex -t demo.l > demo.c
[root@localhost Lex-Yacc]# ll
total 48K
-rw-r--r-- 1 root root 76 Jun 25 18:59 demo.l
-rw-r--r-- 1 root root 44K Jun 25 19:03 demo.c
- step 2: 编译生成词法解析器
[root@localhost Lex-Yacc]# gcc demo.c -o demo -lfl
# 注意:如果没有安装 flex-devel,会报如下错误:
/usr/bin/ld: cannot find -lfl
collect2: error: ld returned 1 exit status
以上两步可以直接执行 make
。
- step 3: 运行词法解析器并测试
[root@localhost demo4]# make test
echo "12 + 34" | ./demo
TOKNUM
TOKTAB
TOKADD
TOKTAB
TOKN
1.2 Yacc - 语法解析器生成器
Yacc (Yet Another Compiler Compiler)是生成语法解析器的程序,该语法解析器一般为编译器的一部分,主要功能是识别程序源码的句法意义,GNU/Linux 版本叫做 bison。Yacc 的输入一般是 .y
后缀的文件,该文件基于 BNF 定义了语法规则以及语法规约,Yacc 的输出是语法解析器,需要和 Lex 一起使用才能生成。下面同样以一个示例说明下生成过程,该示例实现两个数字相加(未进行错误处理):
demo.l
内容如下:
%{
#include <stdio.h>
#include "demo.tab.h"
extern int yylval;
%}
%%
"+" return TOKADD;
"\n" return TOKCR;
([1-9][0-9]*)|0 {
int temp;
sscanf(yytext, "%d", &yylval);
return TOKNUM;
}
[ \t] ;
%%
demo.y
内容如下:
%{
#include <stdio.h>
int yylex(void);
int yywrap(void);
void yyerror(char const *);
%}
%token TOKNUM TOKADD TOKCR
%%
line
: expression TOKCR
{
printf("%d\n", $1);
}
expression
: TOKNUM TOKADD TOKNUM
{
$$ = $1 + $3;
}
;
%%
void yyerror(char const *msg)
{
fprintf(stderr, "ERROR: %s\n", msg);
}
int yywrap(void)
{
return 1;
}
int main(void)
{
return yyparse();
}
Makefile
内容如下:
OBJ := demo
FLEXO := demo
FLEXI := $(FLEXO).l
FLEXC := $(FLEXO).c
BISONO := demo
BISONI := $(BISONO).y
BISONC := $(BISONO).tab.c
BISONH := $(BISONO).tab.h
LEX := flex
YACC := bison
$(OBJ) : $(FLEXC) $(BISONC)
$(CC) -o $(OBJ) $(FLEXC) $(BISONC)
$(FLEXC) : $(FLEXI)
$(LEX) -t $(FLEXI) > $(FLEXC)
$(BISONC) $(BISONH) : $(BISONI)
$(YACC) -d $(BISONI)
test:
echo "12 + 34" | ./$(OBJ)
clean:
rm -f ./$(OBJ) ./$(FLEXC) ./$(BISONC) ./$(BISONH)
Linux 环境(与 Unix 环境命令及参数有所区别)生成和运行语法解析器步骤如下:
- step 0:环境中需要安装 bison
[root@localhost Lex-Yacc]# yum install bison -y
- step 1:生成语法解析器和词法解析器 C 语言源码
[root@localhost Lex-Yacc]# flex -t demo.l > demo.c
[root@localhost Lex-Yacc]# bison -d demo.y
[root@localhost Lex-Yacc]# ll
total 108K
-rw-r--r-- 1 root root 44K Jun 25 22:24 demo.c
-rw-r--r-- 1 root root 230 Jun 25 22:08 demo.l
-rw-r--r-- 1 root root 46K Jun 25 22:23 demo.tab.c
-rw-r--r-- 1 root root 2.2K Jun 25 22:23 demo.tab.h
-rw-r--r-- 1 root root 422 Jun 25 22:09 demo.y
-rw-r--r-- 1 root root 455 Jun 25 22:10 Makefile
- step 2: 编译生成语法解析器
[root@localhost Lex-Yacc]# gcc demo.c demo.tab.c -o demo
以上两步可以直接执行 make
。
- step 3: 运行语法解析器并测试
[root@localhost Lex-Yacc]# make test
echo "12 + 34" | ./demo
46
2. Lex & Yacc 原理
2.1 Lex 和 Yacc 的关系
下面图片来自《Lex & Yacc 入门》
2.2 Lex & Yacc 输入文件格式
Defition section
%%
Rules section
%%
C code section
Lex & Yacc 输入文件格式都是分成三段,每段间通过 %%
分割,每段的含义:
- Defition section
这块可以放C语言语句,包括 #include,#define 等声明语句以及 yylex
的声明,但是要用%{ %}括起来。
如果是 .l
文件,可以放预定义的正则表达式的定义,如 number ([1-9][0-9]*)|0
,定义后,Rules section 就可以通过{number}
来引用正则表达式。
如果是 .y
文件,可以放 token 的定义,如:%token TOKADD TOKNUM
,这里的定义的每个 token 都可以在 *.tab.h
中看到。
- Rules section
.l
文件在这里放置的rules就是每个正则表达式要对应的动作,一般是返回一个token
.y
文件在这里放置的rules就是满足一个语法描述时要执行的动作。
不论是 .l
文件还是 .y
文件这里的动作都是用 {}
扩起来的,用C语言来描述。
- C code section
main
函数以及 yyerror
、yywrap
等函数定义。
2.3 Lex & Yacc 内部工作原理
在 Yacc 文件中,main
函数调用了 yyparse
,此函数由 Yacc 自动生成,在 *.tab.c
文件中。yyparse
从 yylex
中读输入流。你可以自己编码实现,或者让 Lex 完成(示例中的 yylex
由 Lex 生成,只需在 Defition section 中进行声明)。
Lex 生成的 yylex
从 yyin
指向的输入流读取字符,默认是 stdin
,输出为 yout
,默认为 stdout
。
可以在 yywrap
中修改 yyin
,此函数在每一个输入文件被解析完毕时被调用,它允许你打开其它的文件继续解析,如果是这样,yywarp
的返回值为 0,如果想结束解析文件,返回 1。
每次调用 yylex
函数用一个整数作为返回值,表示一种符号类型,告诉 Yacc 当前读取到的符号类型,此符号是否有值是可选的,yylval
即存放了其值。
默认yylval
的类型是整型(int),但是可以通过重定义 YYSTYPE 以对其进行重写。分词器需要取得yylval
,为此必须将其定义为一个外部变量。原始YACC不会帮你做这些,因此你得将下面的内容添加到你的分词器中,就在 #include <*.tab.h>
下即可:
extern YYSTYPE yylval;
Bison 会自动做这些工作(使用 -d
选项生成 *.tab.h
文件)。
3. Lex & Yacc 进阶
前两节是个人对 Lex & Yacc 入门知识的一点整理,对我来说已经足够。如果想要进一步深入了解,可以参考下面的资料:
英文版:《lex and yacc tutorial》
中文版:《Lex 和 Yacc 简明教程》
其他资料可以查看参考文献 [1] 中列出的资料。