Postgres数据库词法分析和语法分析源码解析
Posted 丶Summer ~Z
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Postgres数据库词法分析和语法分析源码解析相关的知识,希望对你有一定的参考价值。
目 录
学习参考书籍、网站或博文:
- 参考书籍:《PostgreSQL数据库内核分析》
- Lex & Yacc 点击前往
- Postgresql源码学习之词法和语法分析 点击前往
Postgres数据库之词法分析和语法分析学习汇总
概述
postgreSQL数据库将用户输入的sql命令作为字符串传递给查询分析器,并对其进行词法分析和语法分析生成分析树。词法分析和语法分析是由Unix工具Yacc和Lex制作的。使用的是 Bison 和Flex。
词法分析和语法分析依赖的文件定义在src\\backend\\parser
下的scan.l
和gram.y
中。
词法分析器 scan.l
负责识别标识符,SQL 关键字等,对于发现的每个关键字或者标识符都会生成一个记号并且传递给分析器;语法分析器 gram.y
包含一套语法规则和触发规则时执行的动作;raw_parser
函数(在src/backend/parser/parser.c
下)主要通过调用采用Lex和Yacc配合预生成的base_yyparse
函数来实现词法分析和语法分析的工作。
重要源码文件及调用关系:
kwlist.h
:SQL关键字定义,注意:关键字名要小写,按照字符串值顺序定义kwlookup.h
:定义结构体ScanKeyword;kwlookup.c
:使用kwlist.h初始化关键字数组ScanKeywords,提供ScanKeywordLookup函数,该函数判断输入的字符串是否是关键字,若是则返回当前标识符指向关键字列表中对应单词的指针,采用二分法查找;scanup.c
:提供几个词法分析时常用的函数。scanstr函数处理转义字符,downcase_truncate_identifier函数将大写英文字符转换为小写字符,truncate_identifier函数截断超过最大标识符长度的标识符,scanner_isspace函数判断输入字符是否为空白字符。scan.l
:定义词法结构,编译生成scan.c;这里会忽略comment等无用信息。gram.y
:定义语法结构,编译生成gram.c;分析后生成语法分析树。gram.h
:定义关键字的数值编号。
辅助脚本:check_keywords.pl
:检查在gram.y 和 kwlist.h 中定义的关键字列表是否一致。
接口
接口名称 | 功能描述 |
---|---|
raw_parser | 调用gram.y生成的语法分析函数base_yyparse和scan.l生成的词法分析函数 |
base_yyparse | 实现语法分析,并返回语法分析树 |
base_yylex | 实现词法分析 |
ScanKeywordLookup | 采用二分法查找关键字 |
truncate_identifier | 截断超过最大标识符长度的标识符 |
Lex和Yacc
词法分析器
Flex:生成词法分析器的编译工具,生成词法分析器代码,将.l文件编译后生成.c和.h文件。
词法分析通常所做的就是在输入中寻找字符的模式(pattern)。它使用正则表达式匹配输入的字符串并且把它们转换成对应的标记,正则表达式就是一种对模式的简介明了的描述方式。匹配正则表达式的规则,然后执行对应的动作。其实就是提取编程语言占用的各种保留字、操作符、特殊符号等等语言的元素。
.l文件的结构
scan.l
文件包含定义段、规则段、C程序代码段三部分,每个部分之间用“%%”
分割。
- 定义段:
%top ...
这部分,括号中内容将被原样copy到生成的scan.c中,并且位于C文件的最顶部。主要包含此文件描述、注释,以及需要的C头文件。% ... %
这部分的代码会被原样copy到生成的C文件中。这里可以重定义一些Flex中的宏,如YYSTYPE, 以及一些在规则段使用的函数声明、结构体声明和定义等%option
此部分是Flex支持的一些参数,通过%option
来设置
%option reentrant
可重入词法分析器,传统词法分析器只能一次处理一个输入流,所以很多变量都定义的为静态变量,这样分析器才能记住上次分析的地方继而可以继续分析。但是不能同时处理多个输入流。为了解决这个问题引入了可重入词法分析器。通过参数reentrant来控制。%option prefix="core_yy"
通过加入前缀,可以将原来的yylex等函数 变成core_yylex.这样可以在一个程序中建立多个词法分析器。用来分析不同的输入流。%option bison-bridge
,bison桥模式,为什么会有这个模式呢,因为bison的发展和flex的发展沟通并不是很密切,导致了一个不好的情况,即在bison调用yylex的时候是yylex(YYSTYPE *yylvalp);
即必须传入一个yylval的指针,但是flex中定义的yylex函数为int yylex(yyscan_t scaninfo).
这样两者就不一样了。就无法互相协作的工作了。所以在flex中提拱了桥模式,如果按%option bison-bridge做了声明,那么在flex中yylex将被声明为int yylex(YYSTYPE* lvalp, yyscan_t scaninfo);
这样就兼容了bison.%option bison-locations
此模式同上面参数同时使用,如果做了此声明,yylex 将被声明为int yylex (YYSTYPE* lvalp, YYLTYPE* llocp, yyscan_t scaninfo);
加入了location参数。而在flex中yylex 中宏yylval 和 yylloc其实就是lvalp 和llocp的一个拷贝。
%x
声明起始状态(有两种模式:%x
声明独占模式%s
声明共享模式),起始状态代表进入一个特定的状态,在规则段只有定义了特定状态的规则才会匹配,这种规则通过<start stat>
来标识。例如 定义段定义了 %x xb 则在规则段只有开头的规则才会匹配,其他的的规则则不会被匹配。Lex
开始时的标准状态是状态零,也称为INITIAL
- 预定义正则表达式的代理,可以为一些要匹配的正则表达式命名,这样在规则段可以用这个代理名字,来代替这个表达式。例如
space [ \\t\\n\\r\\f]
,给[ \\t\\n\\r\\f]
命名为space, 后面在规则段即可使用space
来代替[ \\t\\n\\r\\f]
,同时代理也可以被嵌套,如whitespace (space+|comment)
这里定义了新的代理whitespace
, 它代理了(space+|comment)
其中space
就被嵌套代理了。
-
规则段 %%
规则名
该规则对应的执行的动作
例如:whitespace /* ignore */ xcstart /* Set location in case of syntax error in comment */ SET_YYLLOC(); /* 用于标记起始位置 */ yyextra->xcdepth = 0; /* 将当前深度设置为0 */ BEGIN(xc); /* 接下来要进入注释的状态 */ /* Put back any characters past slash-star; see above */ yyless(2); <xc> xcstart (yyextra->xcdepth)++; /* 如果再次遇到xcstart,那么当前深度+1*/ /* Put back any characters past slash-star; see above */ yyless(2); xcstop if (yyextra->xcdepth <= 0) /* 深度<=0,表示当前处于最外层的注释结束位置 */ BEGIN(INITIAL); /* 开始最初的0状态 */ else (yyextra->xcdepth)--; /* 否则说明当前处于多个注释的嵌套内层注释的结束位置,深度-1 */ xcinside /* ignore */ /* 当前处于注释内部,什么也不做 */ op_chars /* ignore */ \\*+ /* ignore */ <<EOF>> yyerror("unterminated /* comment"); /* <xc> */
yyless(), 此函数意思是保留参数个数的字符流,其他的返回给输入流。但是匹配的字符流长度,为匹配的总长度。例如:如果yyless(2),这个时候匹配的字符是5个字符,则保留前两个字符,后面三个字符返回给输入流。
词法解析器的规则段还会匹配特殊符号(如代表隐式类型转换的
::
、<=
、>=
、!=
等),操作符、部分类型以及identifier
等
typecast
SET_YYLLOC();
return TYPECAST;
dot_dot
SET_YYLLOC();
return DOT_DOT;
colon_equals
SET_YYLLOC();
return COLON_EQUALS;
equals_greater
SET_YYLLOC();
return EQUALS_GREATER;
less_equals
SET_YYLLOC();
return LESS_EQUALS;
......
identifier
int kwnum;
char *ident;
SET_YYLLOC();
/* 扫描是否是关键字 */
kwnum = ScanKeywordLookup(yytext,
yyextra->keywordlist);
if (kwnum >= 0)
yylval->keyword = GetScanKeyword(kwnum,
yyextra->keywordlist);
return yyextra->keyword_tokens[kwnum];
/*
* No. Convert the identifier to lower case, and truncate
* if necessary.
*/
/* 不是关键字则将其转换成小写,PG是默认小写 */
ident = downcase_truncate_identifier(yytext, yyleng, true);
yylval->str = ident;
return IDENT;
......
词法分析器调用yylex()函数来读取一小部分输入流匹配相应的规则然后返回一个记号流,每个记号实际上有两个组成部分,记号编号(token number)和记号值(token’s value)。
记号编号是一个较小的整数,数字随意,零值意味着文件的结束。Bison创建的语法分析器,会自动从258开始(目的是为了避免与文字字符记号产生冲突)
PostgreSQL的关键字列表和记号列表如下图:
- 代码段
代码段可以是任意C代码,它们会被复制到lex.yy.c的最末端
通常用于一些未定义接口的定义,例如
int yywrap()
return 1;
语法分析器
Bison:
语法分析器的编译工具,生成语法分析器代码,将.y
文件编译后生成.c
和.h
文件。
语法分析器的任务是找出输入记号之间的关系。一种常见的关系表达式就是语法分析树(parse tree)。例如,基于通常的算术规则,算术表达式 12+34+5 有如下图的表示。
这课树的每个分支都显示了记号之间或者记号与下面子树的关系,每一个bison
语法分析器在分析其输入时都会构造一棵语法分析树。在有些应用里,它会把整棵树作为一个数据结构创建在内存中以便于后续使用。在其他应用里,语法分析树只是隐式地包含在语法分析器进行的一系列操作中。
Bison
的规则基本是基于BNF
文法(巴斯克范式(BNF范式)),它是一种书写上下文无关文法的标准格式。每一行就是一条规则,用来说明如何创建语法分析树的分支。BNF
规定是推导规则(产生式)的集合,写为:
<符号> ::= <使用符号的表达式>
在BNF里,::=
被读作“是”或者“变成”,|
是“或者”,意味着同一个语法符号有两种可能性。规则左边的名称是语法符号(symbol
)。
这里的 <符号>
是非终结符,而表达式由一个符号序列,或用指示选择的竖杠'|'
分隔的多个符号序列构成,每个符号序列整体都是左端的符号的一种可能的替代。在输入中出现并且被词法分析器返回的符号是终结符或称记号,而规则左部的语法符号是非终结符。终结符和非终结符必须不同,从未在左端出现的符号叫做终结符。
.y文件的结构
BIison规范与flex规范一样,由三部分组成。定义段、规则段和代码段,其中各个段由“%%”
符号分隔
- 定义段:处理语法解析器的控制信息,建立分析器操作所需要的的执行环境;
定义段可分为两部分,第一部分是%…%,内部是C语言变量、类型和宏定义以及函数声明等;第二部分是对文法的终结符和非终结符做相关的声明。
# 以下仅对第二部分相关声明进行解释说明
/* 声明此语法分析器是纯语法分析器。这样可以实现可重入。同时需要
* %parse-param core_yyscan_t yyscanner 和%lex-param core_yyscan_t yyscanner
* 配合使用,即为了调用纯词法分析器flex,需要scanner实例,即需要传入这个参数.
* 通过定义%parse-param 即可给yyparse()函数传入参数。
* 定义%lex-param.即可把parse-param中定义的参数传递给yylex.
*/
%pure-parser
/* %expect N表示Bison解析器应该有N个shift/reduse冲突,如果与此处指定的N不匹配,多余的在使用Bison编译时会提示相应的错误 */
%expect 0
/*
* 命名函数名称,默认为yy,此处指定base_yy意味着默认的yyxx()会变成base_yyxx().
* eg:yyparse ---> base_yyparse,源码目录下/src/backend/parser/parser.c中
* 词法语法分析的入口函数raw_parser调用了base_yyparse函数进入语法分析阶段
*/
%name-prefix="base_yy"
/* 声明使用位置信息 */
%locations
/* %parse-param声明base_yyparse()函数的入参.
* 下面的说明该函数的参数类型为core_yyscan_t 参数名为yyscanner.
* 对应源码目录下/src/backend/parser/parser.c中
* 词法语法分析的入口函数raw_parser调用的base_yyparse函数
*/
%parse-param core_yyscan_t yyscanner
%lex-param core_yyscan_t yyscanner
/* 定义yylval类型,在flex中通过yylval的返回匹配的值。 */
%union
core_YYSTYPE core_yystype;
/* these fields must match core_YYSTYPE: */
int ival;
char *str;
const char *keyword;
char chr;
bool boolean;
JoinType jtype;
DropBehavior dbehavior;
OnCommitAction oncommit;
List *list;
Node *node;
Value *value;
......
/* 此语法是定义非终结符(在规则段,: 左边是非终结符,右边是终结符)和union中变量的绑定。
* 在Bison中,每个符号(终结符和非终结符)都有一个值与之对应。
* 默认是一个整数值,为了扩展,可以定义union类型。$$代表非终结符的值。
* 这里的意思就是把$$和union中某一个变量绑定。
*/
%type <union中的变量名> 非终结符
/* 此语法是定义终结符和union中变量的绑定。这样就可以在flex中直接通过yylval->(union中的变量名)返回匹配的值 */
%token <union 中的变量名> 终结符
/* 优先级: 从低到高,同一行并列代表优先级相同 */
/*
* %nonassoc symbol 用来定义非关联操作符。同%prec 联合使用可以定义某个表达式的优先级。
* %left 用来定义左结合操作符
* %right 用来定义右结合操作符
*/
%nonassoc SET /* see relation_expr_opt_alias */
%left UNION EXCEPT
%left INTERSECT
%left OR
%left AND
%right NOT
%nonassoc IS ISNULL NOTNULL /* IS sets precedence for IS NULL, etc */
%nonassoc '<' '>' '=' LESS_EQUALS GREATER_EQUALS NOT_EQUALS
%nonassoc BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA
%nonassoc ESCAPE /* ESCAPE must be just above LIKE/ILIKE/SIMILAR */
%left POSTFIXOP /* dummy for postfix Op rules */
......
%left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
/* kluge to keep xml_whitespace_option from causing shift/reduce conflicts */
%right PRESERVE STRIP_P
- 规则段:规则段实际上定义了文法的非终结符及产生式集合,以及当归约整个产生式时应执行的操作。
/*
* 规则对应的动作使用括起来,使用";"作为此规则的结束符
* stmtblock是非终结符号,冒号语法分隔符,
* stmtmulti代表终结符号,其值通过位置获取,因只有一个终结符,所以其值为$1.
* stmtblock的值,用$$表示。
*/
stmtblock: stmtmulti
pg_yyget_extra(yyscanner)->parsetree = $1; /*这里的结果最终返回给raw_parser函数 */
;
/*
* 下面的规则是stmtmulti对应的规则
* 这里说一下@N的含义:在语法分析器的语义动作中,可以通过@$来使用左部符号的位置信息,通过@N使用
* 右部符号的位置信息。
* $N的含义:$$代表冒号:左边记号,$N表示冒号右边第N个位置的记号
* 词法分析器必须把每个记号的位置信息放到yylloc中,语法分析器会在每次词法分析器返回记号时定义对应的yyloc
*/
stmtmulti: stmtmulti ';' stmt
if ($1 != NIL) //$1代表stmtmulti 对应的位置
/* update length of previous stmt */
updateRawStmtEnd(llast_node(RawStmt, $1), @2);
if ($3 != NULL) //$3代表stmt 对应的位置
$$ = lappend($1, makeRawStmt($3, @2 + 1));
else
$$ = $1;
| stmt
if ($1 != NULL)
$$ = list_make1(makeRawStmt($1, 0));//将stmt中的值$1,作为参数生成RawStmt并加入到列表中。
else
$$ = NIL;
;
......
stmt :
AlterEventTrigStmt
| AlterCollationStmt
| AlterDatabaseStmt
| AlterDatabaseSetStmt
| AlterDefaultPrivilegesStmt
| AlterDomainStmt
| AlterEnumStmt
| AlterExtensionStmt
| AlterExtensionContentsStmt
| AlterFdwStmt
| AlterForeignServerStmt
| AlterForeignTableStmt
| AlterFunctionStmt
| AlterGroupStmt
| AlterObjectDependsStmt
| AlterObjectSchemaStmt
| AlterOwnerStmt
| AlterOperatorStmt
| AlterTypeStmt
| AlterPolicyStmt
| AlterSeqStmt
| AlterSystemStmt
| AlterTableStmt
| AlterTblSpcStmt
......
- 代码段:一般指在规则段中用到或者在语法分析器的其他部分用到的函数。这一部分一般会被直接拷贝到yacc编译器产生的c源文件中。
yylex()
是词法分析程序,它返回记号。语法分析驱动程序yyparse()
将会调用yylex()
获取记号。如果不使用lex
生成这个函数,则必须在代码段用C语言写这个程序。记号由记号名和属性值构成,记号名一般作为yylex
的返回值(注意,记号名是由%token
等定义的终结符名,这些终结符名在yacc
内部会被宏定义成一些常数。),而属性值则由yacc
内部定义的变量yylval
来传递。yylval
的类型与属性值栈元素的类型相同,即,默认状态下,yylval
为int
类型,若使用#defineYYSTYPE double
将属性值栈元素定义为double
类型,则yylval
就是double
类型,若用%union
将属性值栈元素定义为联合类型,则yylval
也是联合类型。注意,当yylval
是联合类型时,对它的引用要注意。
移进/规约冲突
是指一个输入字符存在两种可能的语法分析器,并且其中一个分析器结束一条规则(选择归约),而另外一个并不结束(选择移进)。例如,
%%
e: 'X'
| e '+' e
;
对于输入字符串X+X+X
,有两种可能的语法分析器:(x+x)+x
或者x+(x+x)
。选择归约将是语法分析器使用第一条规则,而选择移进则使语法分析器使用第二条规则。除非用户使用操作符的优先级声明,否则bison
选择移进。参见bison的默认规则,158页的“优先级和结合性声明”。
当存在移进/归约冲突时,bison
会比较可能移进的记号和可能归约的规则的优先级。如果记号的优先级更高,那么就移进;如果规则的优先级更高,那么就归约。如果两者具有相同的优先级,bison
将检查结合性。如果它们是左结合,那么就归约,如果它们是右结合,那么就移进,如果它们没有结合性,那么bison
就报告错误。
规约/规约冲突
发生在同一个记号可以结束两条不同规则的时候。例如,
%%
proga: proga | progb ;
proga: 'x';
progb: 'x';
x可以是proga也可以是progb。大多数归约/归约冲突比这个的歧义要小,但它们通常意味着语法存在错误
案例分析
此案例是用于实现PostgreSQL
数据库支持DELETE
带LIMIT
语法
delete from test where id < 6 limit 3;
先从scan.l中开始分析:
- 在
yyparse
中通过调用yylex
来分析第一个单词这需要注意一下:在gram.y
中已经使用了%name-prefix="base_yy"
,所以这里的yyparse
和yylex
,实际上最后都是调用的base_yyparse
和base_yylex
,这里需要关心的是base_yylex
.因为在scan.l
中定义了%option prefix="core_yy"
,所以在scan.c
中生成的yylex
的函数名被重定义为来了core_yylex
.这里出现了不匹配,PG
,是通过自己写了base_yylex
函数,其中调用了core_yylex
.
那为什么要这么做呢,因为Flex
解析的时候默认只是向前多看一个token
,来做匹配的, 但是在SQL
中有些语句匹配是需要向前多看多于一个token
的,为了实现这点,PG采用在base_yylex
中读取token
,然后做替换的方式实现匹配多个token
.例如 如果是 WITH TIME
,那么这两个token 将被替换成WITH_LA
.
- 在scan.l的规则段
identifier
const ScanKeyword *keyword;
char *ident;
SET_YYLLOC();
/* 在关键字列表yyextra->keywords中查找匹配的字符串是否为关键字
* ScanKeywordLookup函数被声明在keywords.h中keywords.h被包含在scanner.h中
* scanner.h被包含在gramparse.h中,gramparse.h被scan.l引用
*/
/* 变量yyextra->keywords是我们传入的kwlist.h中的所有关键字. */
keyword = ScanKeywordLookup(yytext,yyextra->keywords,yyextra->num_keywords);
if (keyword != NULL)//如果找到 在kwlist.h中存在PG_KEYWORD("delete", DELETE_P, UNRESERVED_KEYWORD)
yylval->keyword = keyword->name;//token 对应的值这里为 “delete”
return keyword->value;//返回token, 这里为宏“DELETE_P”
/*
* No. Convert the identifier to lower case, and truncate
* if necessary.
*/
ident = downcase_truncate_identifier(yytext, yyleng, true);
yylval->str = ident;
return IDENT;
- 在yylex返回DELETE_P这个token.然后分析gram.y中这个token 对应的规则 由于flex 默认向前查看一个token, 根据第二部可知第二个token 为FROM.在规则段中找到如下规则,opt_with_clause 可以为空,因此可以匹配到的规则有如下两条;这里我们可以看出当opt_with_clause为空时会产生移进/规约冲突,后面我们会讲到如何来解决该冲突(下面我们以匹配上第二条规则进行讲解)
DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
using_clause where_or_current_clause returning_clause
DeleteStmt *n = makeNode(DeleteStmt);
n->relation = $4;
n->usingClause = $5;
n->whereClause = $6;
n->returningList = $7;
n->withClause = $1;
$$ = (Node *)n;
| DELETE_P FROM relation_expr_opt_alias where_or_current_clause limit_clause
int i = 0;
char *relname = NULL;
relname = psprintf("%s_table_%d", "DEL",i);
/* 构造子查询select ctid from t1 where id < 7 limit 3 */
ColumnRef *cr = makeNode(ColumnRef);
cr->fields = list_make1(makeString("ctid"));
cr->location = -1;
ResTarget *rt = makeNode(ResTarget);
rt->name = NULL;
rt->indirection = NIL;
rt->val = (Node *)cr;
rt->location = -1;
.......
- 继续调用yylex分析test,根据第二步返回token IDENT,对应上面第二条规则的relation_expr_opt_alias
规则的递归调用关系:
# relation_expr_opt_alias---> relation_expr---> qualified_name-->ColId-->IDENT
relation_expr_opt_alias: relation_expr %prec UMINUS
$$ = $1;
......
relation_expr:
qualified_name
/* inheritance query, implicitly */
$$ = $1;
$$->inh = true;
$$->alias = NULL;
......
qualified_name:
ColId
$$ = makeRangeVar(NULL, $1, @1);
//创建了一个RangeVar类型node,参考RangeVar结构体。并赋给了$$即qualified_name.
- 继续调用yylex分析,在上面规则DeleteStmt,需要匹配的第二条规则指示下一个需要匹配的项是where_or_current_clause 继续分析输入流
where id < 6
,where
将会和delete
的解析方式一样,最终返回WHERE
,id < 6
即对应下面的a_expr
,其中"id"
"<"
"6"
都会递归地调用a_expr
进行解析
#------------------------------"id"--------------------------
# where_or_current_clause ---> a_expr---> c_expr-->columnref
where_or_current_clause:
WHERE a_expr $$ = $2;
......
a_expr: c_expr $$ = $1;
......
c_expr: columnref $$ = $1;
......
columnref: ColId
$$ = makeColumnRef($1, NIL, @1, yyscanner);
// id 最终会构造ColumnRef类型的node,详细参考ColumnRef结构体
#------------------------------"<"--------------------------
# where_or_current_clause ---> a_expr
a_expr: ......
| a_expr '<' a_expr
$$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<", $1, $3, @2);
#------------------------------"6"--------------------------
# where_or_current_clause ---> a_expr---> c_expr-->AexprConst--->Iconst
where_or_current_clause:
WHERE a_expr $$ = $2;
......
a_expr: c_expr $$ = $1;
......
c_expr:
| AexprConst $$ = $1;
......
AexprConst: Iconst
$$ = makeIntConst($1, @1);
// "6"最终会构造出一个Const类型的node
- 继续调用yylex分析,在上面规则DeleteStmt,需要匹配的第二条规则指示下一个需要匹配的项是
limit_clause
#-------------------------------"limit"------------------------以上是关于Postgres数据库词法分析和语法分析源码解析的主要内容,如果未能解决你的问题,请参考以下文章