目录
当服务器接收到一条 SQL 语句时,其处理过程为:
词法分析 -> 语法分析 -> 语义分析 -> 构造执行树 -> 生成执行计划 -> 计划的执行。
其中,词法语法解析的处理过程根编译原理上的东西基本类似;MySQL 并没有使用 lex 来实现词法分析,但是语法分析却用了 yacc。
与之对比的 SQLite 数据库,其词法分析器是手工写的,语法分析器由 Lemon 生成。
在此介绍其在 MySQL 中的使用。
简介
Lex-Yacc (词法扫描器-语法分析器) 在 Linux 上就是 flex-bison;使用 bison 时,采用的语法必须是上下文无关文法 (context-free grammar);可以通过 yum install flex flex-devel bison
进行安装。
首先,介绍下基本的概念。
BNF
也就是 Backus-Naur Form 巴科斯范式,由 John Backus 和 Peter Naur 首先引入的,用来描述计算机语言语法的符号集,现在,几乎每一位新编程语言书籍的作者都使用巴科斯范式来定义编程语言的语法规则。
其推导规则通过 ::=
定义,左侧为非终结符,右侧为一个表达式;表达式由一个符号序列,或用 '|'
分隔的多个符号序列构成,从未在左端出现的符号叫做终结符。
"..." : 术语符号,表示字符本身,用double_quote用来代表双引号 < > : 必选项 [ ] : 可选项,最多出现一次 { } : 可重复0至无数次的项 | : 左右两边任选一项,相当于OR ::= : 被定义为
如下是 Java 中的 For 语句实例:
FOR_STATEMENT ::= "for" "(" ( variable_declaration | ( expression ";" ) | ";" ) [ expression ] ";" [ expression ] ";" ")" statement
其中 RFC2234 定义了扩展的巴科斯范式 (ABNF)。
上下文无关文法
简单来说就是每个产生式的左边只有一个非终结符。首先,试着用汉语来稍微解释一下。
本来这个进球就是违例的,但你不肯承认也没办法
我有一本来自美国的花花公子杂志
拿我的笔记本来
如果汉语是上下文无关文法,那么我们任何时候看见 "本来"
两个字,都可以把它规约为一个词;可惜汉语不是上下文无关文法,所以能否归约为一个词,要看它的上下文是什么。如上的示例中,只有第一句可以规约为一个词。
Lex 词法分析
Flex 采用的是状态机,通过分析输入流 (字符流),只要发现一段字符能够匹配一个关键字 (正则表达式),就会采取对应的动作。
Flex 文件被 %%
分成了上中下三个部分:
-
第一部分中要写入 C/C++ 代码必须用
%{
和%}
括起来,将原封不动放到生成源码中。 -
第二部分是规则段,包括了模式 (正则表达式) 和动作,由空白分开,当匹配到模式时,就会执行后面的动作。
-
第三部分可以直接写入 C/C++ 代码。
yylex() 是扫描程序的入口,调用该函数启动或重新开始,该函数会初始化一些全局变量,然后开始扫描。如果定义的 flex 动作是将数值传递给调用程序 (return),那么对 yylex() 的下次调用就从它停止的地方继续扫描。
%{ #include <stdio.h> %} %% is | are { printf("%s: VERB ", yytext); } island printf("LAND "); [0-9]+ printf("NUMBER "); [ \t]+ /* ignore whitespace */; [a-zA-Z][a-zA-Z0-9]* printf("COMMON WORD "); .|\n { ECHO; } %% int main(void) { yylex(); }
特殊字符 '.'
表示匹配换行符以外的任意单个字符,'\n'
匹配换行符;ECHO
表示输出匹配的模式,默认行为为,也就是将输入原样输出:
#define ECHO fwrite(yytext, yyleng, 1, yyout)
默认将 stdin 作为输入,可以通过如下命令测试。
----- 首先按照规则生成C源码lex.yy.c $ flex example.l ----- 然后进行编译 $ cc lex.yy.c -o example -lfl ----- 直接执行测试,Ctrl-D退出 $ ./example is is: VERB are are: VERB island LAND 89 NUMBER foobar COMMON WORD ^*& ^*&
在处理时,flex 采用两个原则:A) 只匹配一次;B) 执行当前输入的最长可能匹配值。也就是对与 island 不会匹配 is 和 land 。当然,我们可以使用一个文件作为关键字列表,而非每次都需要编译。
解析时会通过 yyin 读取,如果需要在 yacc 或者其它文件中设置,那么可以通过如下方式修改。
extern FILE *yyin; yyin = fopen("filename","r");
通过 flex 处理文件后,会将匹配转化为指定的符号,然后供 yacc 处理。
常用功能
简单列举常用函数。
正则表达式
flex 常用正则表达式:
- 格式与 grep 相似;
<<EOF>>
标示文件结束;- 常用字符集,如
[:alpha:]
,[:digit:]
,[:alnum:]
,[:space:]
等; {name}
使用预定义的 name 。
简单示例,计算平均值。
%{ #include <stdio.h> #include <stdlib.h> %} dgt [0-9] // 通过name方式定义 %% {dgt}+ return atoi(yytext); %% void main() { int val, total = 0, n = 0; while ( (val = yylex()) > 0 ) { // 到文件结束时返回0 total += val; n++; } if (n > 0) printf(“ave = %d\n”, total/n); }
如上如果在编译时使用 -Wall
参数,会报 warning: `yyunput’ defined but not used
之类的异常,如下介绍可以通过如下选项关闭。
%option nounput %option noinput
多规则匹配
如果有多个值匹配,那么 flex 会按照如下的规则选取。
- 贪婪匹配,选择最大的匹配值;
- 多个规则匹配,选择第一个;
- 没有规则匹配则会选择默认规则。
例如,通过 "/*"(.|\n)*"*/"
规则匹配 C 语言中的注释,那么如下场景可能出错。
#include <stdio.h> /* definitions */ int main(int argc, char * argv[ ]) { if (argc <= 1) { printf("Error!\n"); /* no arguments */ } printf("%d args given\n", argc); return 0; }
贪婪匹配,会从 /* def
到 nts */
之间的内容都作为注释,此时就需要使用条件 (Condition) 规则。例如,以 <S>
开始的规则,只有在条件 S 时才会进行匹配,可以在 definition section 段通过如下方式定义条件。
%x S exclusive start conditions %s S inclusive start conditions
然后,通过 BEGIN(S)
进入条件,另外,flex 有个初始条件,可以通过 BEGIN(INITIAL)
返回;如果使用多个状态,那么实际上可以实现一个状态机,详见 lex tutorial.ppt 或者 本地文档 。
关于上述内容,也可以参考 Start conditions 中的介绍。
yyterminate()
可在一个动作中代替 return 使用,用于结束扫描并向扫描器的调用者返回 0;可以通过如下方式自定义。
#ifdef yyterminate # undef yyterminate #endif #define yyterminate() \ do { free (foobar); foobar = NULL; pos = 0; len = 0; \ return YY_NULL; } while (0)
配置选项
%option yylineno 提供当前的行信息,通常用于后续打印错误行信息 %option noyywrap 不生成yywrap()声明 %option noinput 会生成#define YY_NO_INPUT 1定义 %option nounput 会生成#define YY_NO_UNPUT 1定义
flex 会声明一个 int yywarp(void);
函数,但是不会自动定义,所以通常会在最后的 section 实现该函数。该函数的作用是将多个输入文件打包成一个输入,也就是当 yylex()
读取到一个文件结束 (EOF) 时,会调用 yywrap()
,如果返回 1 则表示后面没有其它输入文件了,此时 yylex()
函数结束;当然,yywrap()
也可以打开下一个输入文件,再向 yylex()
函数返回 0 ,告诉它后面还有别的输入文件。
如果只有一个文件,那么可以通过 %option noyywrap
不声明该函数,也就不需要再实现。
其它
值传递
在通过 flex 进行扫描时,会将值保存在 yylval 变量中,而 bison 则读取 yylval 中的值,该变量默认是 int 类型,如果要使用字符串类型,那么可以在 .l+.y 的头部第一句加入 #define YYSTYPE char*
即可。
// 在.l赋值的时候,要特别注意,需要拷贝字符串 yylval = strdup(yytext); return WORD; // 在.y取用的时候,直接强转就可以了 (char*)$1
关于更优雅的实现方式,当然是用 union 啦,仿照上面,很容易写出来的。
标准格式
%{ /* C语言定义,包括了头文件、宏定义、全局变量定义、函数声明等 */ }% %option noinput /* 常见的配置选项 */ WHITE_SPACE [\ \t\b] /* 正则表达式的定义,如下section时可以直接使用这里定义的宏 */ COMMENT #.* %% {WHITE_SPACE} | {COMMENT} {/* ignore */} /* 规则定义处理 */ %% /* C语言,函数实现等 */
YACC 语法分析
bison 读入一个 CFG 文法的文件,在程序内经过计算,输出一个 parser generator 的 c 文件;也就是说 Bison 适合上下文无关文法,采用 LALR Parser (LALR语法分析器)。
在实现时,bison 会创建一组状态,每个状态用来表示规则中的一个可能位置,同时还会维护一个堆栈,这个堆栈叫做分析器堆栈 (parser stack)。每次读入一个终结符 (token),它会将该终结符及其语意值一起压入堆栈,把一个 token 压入堆栈通常叫做移进 (shifting)。
当已经移进的后 n 个终结符可以与一个左侧的文法规则相匹配时,这个 n 各终结符会被根据那个规则结合起来,同时将这 n 个终结符出栈,左侧的符号如栈,这叫做归约 (reduction)。
如果可以将 bison+flex 混合使用,当语法分析需要输入标记 (token) 时,就会调用 yylex() ,然后匹配规则,如果找到则返回。
语法定义
同 flex 相似,仍然通过 %%
将文件分为三部分:
-
第一部分将原封不动放到生成源码中,如果要写入 C/C++ 代码,则必须用
%{
和%}
括起来。 -
第二部分是规则段,包括了模式 (正则表达式) 和动作,由空白分开,当匹配到模式时,就会执行后面的动作。每条规则都是由
':'
操作符左侧的一个名字、右侧的符号列表、动作代码、规则结束符(;)
组成。 -
第三部分可以直接写入 C/C++ 代码。
如下,是一个简单示例,分别是 frame.l 和 frame.y 文件。
%{ int yywrap(void); %} %% %% int yywrap(void) { return 1; }
%{ void yyerror(const char *s); %} %% program: ; %% void yyerror(const char *s) { } int main() { yyparse(); return 0; }
然后,通过如下命令进行测试。
----- 编译生成lex.yy.c文件 $ flex frame.l ----- 产生frame.tab.c和frame.tab.h文件 $ bison -d frame.y ----- 编译生成二进制文件 $ gcc frame.tab.c lex.yy.c
常用功能
yacc 中定义了很多的符号,详细的可以查看 Bison Symbols 中的介绍,如下简单介绍常见的符号定义:
%start foobar 修改默认的开始规则,例如从foobar规则开始解析,默认从第一条规则开始
字符串解析
通过如下方式可以设置使用内存字符串而非文件:
YY_BUFFER_STATE yy_scan_string(const char *str); YY_BUFFER_STATE yy_scan_bytes(const char *bytes, int len);
这里会返回一个 YY_BUFFER_STATE
类型,在使用完之后,需要通过 yy_delete_buffer()
删除,也就是在通过 yylex()
解析前会先复制一份数据,然后解析时会修改缓存。
如果不希望复制,那么可以使用如下函数。
YY_BUFFER_STATE yy_scan_buffer(char *base, yy_size_t size)
下面是一个简单的示例:
int main() { yy_scan_string("a test string"); yylex(); }
高级yylval
YACC 的 yylval 类型取决于 YYSTYPE 定义 (一般通过 typedef 定义),可以通过定义 YYSTYPE 为联合体,在 YACC 中,也可以使用 %union
语句,此时会自动定义该类型的变量。
%token TOKHEATER TOKHEAT TOKTARGET TOKTEMPERATURE %union { int number; char *string; } %token <number> STATE %token <number> NUMBER %token <string> WORD
定义了我们的联合体,它仅包含数字和字体串,然后使用一个扩展的 %token
语法,告诉 YACC 应该取联合体的哪一个部分。
%token TOKEN1 TOKEN2 TOKEN3 ... 用于定义终结符。 %left,%right,%nonassoc 类似于终结符,不过同时具有某种优先级和结核性,分别表示左结合、右结合、不结合 (也就是终结符不能连续出现, 例如<,此时不允许出现a<b<c这类句子)。 优先级与其定义的顺序相关,先定义的优先级低,最后定义的优先级最高,同时定义的优先级相同。 例如,如上程序关于计算器中优先级的定义。
杂项
变量
$$ $1 $2 ...
定义了默认的参数,示例如下:
exp: | exp '+' exp { $$ = $1 + $3; } exp[result]: | exp[left] '+' exp[right] { $result = $left + $right; }
示例程序
实现一个简单的计算器程序,能进行加、减、乘、除、幂运算,需要注意优先级。
calc.l
文件。
%{ #include "calc.tab.h" #include <stdlib.h> void yyerror(char *); %} %% [a-z] { yylval = *yytext - 'a'; return VARIABLE; } [0-9]+ { yylval = atoi(yytext); return INTEGER; } [-+()=/*\n] { return *yytext; } [ \t] ; /* skip whitespace */ . yyerror("Unknown character"); %% int yywrap(void) { return 1; }
calc.y
文件。
%{ #include <stdio.h> void yyerror(char *); int yylex(void); int sym[26]; %} %token INTEGER VARIABLE %left '+' '-' %left '*' '/' %% program: program statement '\n' | /* NULL */ ; statement: expression { printf("%d\n", $1); } | VARIABLE '=' expression { sym[$1] = $3; } ; expression: INTEGER | VARIABLE { $$ = sym[$1]; } | expression '+' expression { $$ = $1 + $3; } | expression '-' expression { $$ = $1 - $3; } | expression '*' expression { $$ = $1 * $3; } | expression '/' expression { $$ = $1 / $3; } | '(' expression ')' { $$ = $2; } ; %% void yyerror(char *s) { fprintf(stderr, "%s\n", s); } int main(void) { yy_scan_string("1+1\n"); yyparse(); }
all: bison -d calc.y flex -o calc.lex.c calc.l gcc calc.lex.c calc.tab.h calc.tab.c -o calc -lm clean: rm -f calc.lex.c calc.tab.c calc.tab.h calc test
源码解析
Linux 一般来说,词法和语法解析都是通过 Flex 与 Bison 完成的;而在 MySQL 中,词法分析使用自己的程序,而语法分析使用的是 Bison;Bison 会根据 MySQL 定义的语法规则,进行语法解析。
完成语法解析后,会将解析结果生成的数据结构保存在 struct LEX 中,该结构体在 sql/sql_lex.h 文件中定义。
struct LEX: public Query_tables_list { friend bool lex_start(THD *thd); SELECT_LEX_UNIT *unit; ///< Outer-most query expression /// @todo: select_lex can be replaced with unit->first-select() SELECT_LEX *select_lex; ///< First query block SELECT_LEX *all_selects_list; ///< List of all query blocks private: /* current SELECT_LEX in parsing */ SELECT_LEX *m_current_select; ... ... }
优化器会根据这里的数据,生成相应的执行计划,最后调用存储引擎执行。
执行过程
以下是语法解析模块掉用过程。
mysql_parse() |-mysql_reset_thd_for_next_command() |-lex_start() |-query_cache_send_result_to_client() # 首先查看cache |-parse_sql() # MYSQLparse的外包函数 |-MYSQLparse() # 实际的解析函数入口
如上,SQL 解析入口会调用 MYSQLparse ,而在 sql/sql_yacc.cc 中有如下的宏定义,也就说,在预编译阶段,会将 yyparse 替换为 MYSQLparse ,所以 实际调用的仍是 yyparse 函数。
#define yyparse MYSQLparse
记下来详细介绍其实现细节。
词法解析
MYSQL 的词法分析并没有使用 LEX,而是有自己的一套词法分析,代码详见 sql/sql_lex.cc 中的实现,其入口函数是 MYSQLlex() 。
int MYSQLlex(YYSTYPE *yylval, YYLTYPE *yylloc, THD *thd) { ... ... token= lex_one_token(yylval, thd); yylloc->cpp.start= lip->get_cpp_tok_start(); yylloc->raw.start= lip->get_tok_start(); switch(token) { case WITH: /* Parsing 'WITH' 'ROLLUP' or 'WITH' 'CUBE' requires 2 look ups, which makes the grammar LALR(2). Replace by a single 'WITH_ROLLUP' or 'WITH_CUBE' token, to transform the grammar into a LALR(1) grammar, which sql_yacc.yy can process. */ token= lex_one_token(yylval, thd); switch(token) { case CUBE_SYM: yylloc->cpp.end= lip->get_cpp_ptr(); yylloc->raw.end= lip->get_ptr(); lip->add_digest_token(WITH_CUBE_SYM, yylval); return WITH_CUBE_SYM; case ROLLUP_SYM: yylloc->cpp.end= lip->get_cpp_ptr(); yylloc->raw.end= lip->get_ptr(); lip->add_digest_token(WITH_ROLLUP_SYM, yylval); return WITH_ROLLUP_SYM; default: /* Save the token following 'WITH' */ lip->lookahead_yylval= lip->yylval; lip->yylval= NULL; lip->lookahead_token= token; yylloc->cpp.end= lip->get_cpp_ptr(); yylloc->raw.end= lip->get_ptr(); lip->add_digest_token(WITH, yylval); return WITH; } break; } ... ... }
语法分析
Bison 和词法分析的函数接口是 yylex(),在需要的时候掉用 yylex() 获取词法解析的数据,并完成自己的语法解析。
正常来说,Bison 的实际入口函数应该是 yyparse() ,而在 MySQL 中通过宏定义,将 yyparse() 替换为 MYSQLParse();如上所述,实际调用的仍然是 yyparse() 。
另外,我们可以根据 Bison 中的 Action 操作来查看 MySQL 解析结果的存储结构。
调试
在这里通过考察存储的 WHERE 数据结构来查看语法解析的结果。
(gdb) attach PID (gdb) set print pretty on # 设置显示样式 (gdb) b mysql_execute_command # 可以用来查看所有的SQL (gdb) p thd->lex->select_lex (gdb) p ((Item_cond*)thd->lex->select_lex->where)->list # 查看WHERE中的list (gdb) detach
参考
Flex/Bison
关于最原始的论文,可以参考 Lex - A Lexical Analyzer Generator ,以及 Yacc: Yet Another Compiler-Compiler 。
对于 Lex 和 Yacc 来说,比较经典的入门可以参考 Lex & Yacc Tutorial,其中包括了如何编写一个计算器,以及相关的调试等信息;也可以参考 本地文档,以及相关的 源码 。
关于总体介绍可以参考 Lex and YACC primer
关于调试方法可以参考 Understanding Your Parser