SQLite源码分析2 Lemon语法分析器#
简介#
如何解析sql语句?可以手写硬来,但非常困难。
对于一门语言,可以借助比较成熟的编译器相关工具来解析。
重点关注语法定义即可。
lemon语法分析器 (The Lemon LALR(1) Parser Generator )
https://sqlite.org/src/doc/trunk/doc/lemon.html
lemon是一个LALR(1)语法解析器。由Richard Hipp在1980年代写的。
2001年起作为sqlite的一部分来维护。
较安全和健壮,比YACC/BISON那一套有所改进。sqlite使用lemon作为语法分析器。需要先学习lemon的用法。
原理#
简单说就是用一系列算法把一串符合某语法的字符进行解析,匹配某一个语法分支,最后转换成C代码。
有两个输入:
语法规则
parser模板文件
程序员一般只要编写语法规则即可,lemon有一个默认的通用模板lempar.c。
根据选项可生成三个文件:
实现parser的C代码。
包含每个token及其编号的头文件。
描述parser自动机状态的文件。
lemon不会生成一个完整的可执行程序,而是生成一系列实现parser的函数。
语法#
以一个简单的计算器语法为例,实现一个C++计算器。
calc1.y文件
%token_type {int}
%left PLUS MINUS.
%left DIVIDE TIMES.
%include {
#include <iostream>
#include "calc1.h"
}
%syntax_error {
std::cout << "Syntax error!" << std::endl;
}
program ::= expr(A). { std::cout << "Result=" << A << std::endl; }
expr(A) ::= expr(B) MINUS expr(C). { A = B - C; }
expr(A) ::= expr(B) PLUS expr(C). { A = B + C; }
expr(A) ::= expr(B) TIMES expr(C). { A = B * C; }
expr(A) ::= expr(B) DIVIDE expr(C). {
if(C != 0){
A = B / C;
}else{
std::cout << "divide by zero" << std::endl;
}
} /* end of DIVIDE */
expr(A) ::= INTEGER(B). { A = B; }
%include#
%include {
#include <iostream>
#include "xxx.h"
}
错误处理#
%syntax_error
%parse_failure
可以填入自定义的报错处理。比如打印错误信息。
比如运行时输入一条无法解析的句子,就会执行%syntax_error里的代码。
Terminal和Nonterminal(终结符和非终结符)#
https://en.wikipedia.org/wiki/Terminal_and_nonterminal_symbols
终结符可以看成语法里的一个最小单位,不能再被分解。比如PLUS等加减乘除四个运算符。lemon里需要大写字母开头,一般做成全大写字母。
非终结符可以被转换成其他句子。expr是非终结符。
优先级#
一般用%left(left-associative)来定义符号的优先级。越先定义的优先级越小。
比如:
%left PLUS MINUS.
%left DIVIDE TIMES.
这里加减优先级相同,最小。乘除优先级增加。
%destructor#
析构。当一个非终结符(non-terminal)处于某个节点时(可以理解为无用了),会走析构。
一般用来释放内存。
核心规则#
program ::= expr(A). { std::cout << "Result=" << A << std::endl; }
expr(A) ::= expr(B) MINUS expr(C). { A = B - C; }
expr(A) ::= expr(B) PLUS expr(C). { A = B + C; }
expr(A) ::= expr(B) TIMES expr(C). { A = B * C; }
expr(A) ::= expr(B) DIVIDE expr(C). {
if(C != 0){
A = B / C;
}else{
std::cout << "divide by zero" << std::endl;
}
} /* end of DIVIDE */
expr(A) ::= INTEGER(B). { A = B; }
按最后一行的定义,expr可以为一个数字。大括号中可填实际的C/C++代码。
按2-5行的定义,expr也可以为加减乘除的组合。
第一行定义最终结果,结果是一个expr。
语法分析器就是按这些定义来对表达式进行分析,得出一个唯一的语义。
比如 2 + 3 * 6
数字匹配为一个expr,得到expr(2) PLUS expr(3) TIMES expr(6)
。
按优先级先匹配乘法,按括号中的代码计算3x6,计算得到expr(2) PLUS expr(18)
。
匹配加法得到expr(20)
。
根据第一行定义,已经得到最终结果20。
试验#
ubuntu下可apt install lemon
安装lemon
输入lemon calc1.y
可得到calc1.c,calc1.h,calc1.out三个文件。
calc1.c里包含了大量生成的parser代码。不用关心生成的代码。
在calc1.c最后添加一个main。
int main()
{
void* pParser = ParseAlloc (malloc);
/* First input:
15 / 5
*/
Parse (pParser, INTEGER, 15);
Parse (pParser, DIVIDE, 0);
Parse (pParser, INTEGER, 5);
Parse (pParser, 0, 0);
/* Second input:
50 + 125
*/
Parse (pParser, INTEGER, 50);
Parse (pParser, PLUS, 0);
Parse (pParser, INTEGER, 125);
Parse (pParser, 0, 0);
/* Third input:
50 * 125 + 125
*/
Parse (pParser, INTEGER, 50);
Parse (pParser, TIMES, 0);
Parse (pParser, INTEGER, 125);
Parse (pParser, PLUS, 0);
Parse (pParser, INTEGER, 125);
Parse (pParser, 0, 0);
ParseFree(pParser, free );
}
其中ParseAlloc
,Parse
,ParseFree
是lemon特定的接口,按文档使用即可。
g++ calc1.c -o calc1
编译可得calc1。运行可得到三组结果。
可以修改这个main试一下各种组合,包括不合法的语法。
总结一下#
token_type 指定为token为C的类型int。也可以指定为C结构体。
比如如果想支持小数,可改为double。把token一个个喂给Parse()。第二个参数为规则里定义的token,生成的数值在calc.h里。
到此已经可以做一个可交互的程序了。用户输入表达式,我们依次找出每一个token,调用Parse即可。
.y里还可以用%code插入任意代码。比如main函数。这样lemon生成代码后直接编译即可。
这里并没有一个正规的tokenizer,是我们手动解出的token。
常用的tokenizer有flex等。
sqlite用的是自研的代码(tokenize.c)。
总结#
学习/复习了语法解析相关知识。
学习了lemon语法解析器的使用。
为学习sqlite的sql解析打下基础。
参考#
https://sqlite.org/src/doc/trunk/doc/lemon.html
http://souptonuts.sourceforge.net/readme_lemon_tutorial.html
https://en.wikipedia.org/wiki/LALR_parser