引言
本文介绍查询解析的第二步:语法分析。语法分析是从词法分析器中获得带有属性的token后,根据tokens的属性匹配语法规则,最后获得一棵分析树。本文首先介绍词法分析基本原理,然后介绍海山数据库中使用的yacc工具,最后对语法分析gram.y进行解析。
基本原理
语法分析器识别模式的方法主要分为两种:自顶而下的分析方法和自底而上的分析方法。自顶而下的方法从分析树的根节点开始向叶节点构建,自底而上的方法反之,从叶节点开始构建。两种分析方法中,语法分析树总是按照从左到右的方式被扫描,每次通过词法分析器获得一个词法单元,如下图所示:
语法分析器依次从词法分析器中获得词法单元,类似于词法分析器中依次读入字符,然后从词法单元中匹配语法规则。
海山数据库中使用的yacc工具采用自底向上的方法,因此本文主要介绍该类方法。
在介绍该方法的具体实现之前,首先介绍文法的概念。
文法:
文法是描述词法、语法规则的工具。用一组规则严格定义句子的结构,一个例子如下:
E -> E + T | T
T -> T * F | F
F -> (E) | id
E
是开始符号,代表了所描述语言的最顶层结构。所有合法的句子都是从开始符号开始推导得到的。
这里的每一个表达式被称为产生式
,式中的|
为或,表示E
可以被E+T
或者T
替换。
产生式中的
E
、T
、F
是非终结符号
,相当于一个非叶节点,如T
可以被进一步展开为T * F | F
,而这里的id
,+
,*
,(
,)
是终结符
号,相当于一个叶节点,无法再被展开。
自底向上的文法分析过程可以被看做输入tokens规约为文法开始符号的过程。
假设由词法分析器获得的tokens属性为id*id
,自底向上的分析流程如下:
第一次规约将最左边的id
规约为F
,得到F*id
;
第二次规约将F
规约为T
,生成T*id
;
第三次规约将id
规约为F
,得到T*F
;
第四次规约将T*F
规约为T
;
第五次将T
规约为E
,到达文法开始符,规约结束。
在具体的实现中,采用了移入-规约的语法分析技术,使用一个栈来保存文法符号,使用输入缓存区来存放将要进行语法分析的其他符号,上面的例子具体的实现方式如下图所示:
step | 栈 | 输入 | 动作 |
---|---|---|---|
1 | $ | id*id | 移入 |
2 | $id | *id | 按照F ->id规约 |
3 | $F | *id | 按照T -> F规约 |
4 | $T | *id | 移入 |
5 | $T* | id | 移入 |
6 | $T*id | 按照F ->id规约 | |
7 | $T*F | 按照T -> T * F规约 | |
8 | $T | 按照E -> T规约 | |
9 | $E | 接受 |
当堆栈中最终获得文法开始符号E
的时候,表示规约成功,即这样我们就得到了一棵分析树。
但在以上流程中,出现了规约和移入冲突,如在step4时,此时有两种选择:一是按照产生式E->T
进行规约(但按照此方法最终规约会失败),二是按照上图的方法进行移入操作。
在实际的场景中,除了规约-移入冲突,还包括规约-规约冲突,面对这些冲突的情况,主要解决方法有以下几种:
1.定义结合性和优先级
定义+
和*
为左结合(左结合表示从左到右进行结合,如a+b+c被定义为(a+b)+c),即a+b先进行规约,再进行与c的规约),且*
的优先级高于+
,这样在分析在 id + id * id```时,优先移进 ```*
,而不是规约 +
。
2.设置默认规则
当出现规约-规约冲突时,选择第一个可用的规约式;当出现-移入冲突时,选择移入操作。
3.优化文法
避免在文法设计中出现歧义,可以将相同前缀规则的提取公因子,避免规约-规约冲突。
4.使用更强大的分析算法
如使用LR(1)和GLR分析方法,但复杂度更高。
yacc工具
yacc工具是海山数据库中使用的语法分析工具,是一种自底而上的分析方法,其开发流程如下图所示:
首先编写yacc.y文件,使用yacc编译器将其转化为y.tab.c,然后使用c编译器得到a.out文件,最后就可以将词法分析器得到的<token,[属性]>经过a.out得到输出。
yacc.l文件结构和lex.l文件类似,通过两个%%
分为3段:声明段、翻译规则和辅助代码,如下所示:
声明
%%
规则段
%%
辅助代码
在声明部分中,通常包含C声明,在翻译和过程中使用的临时变量,和对词法单元的声明。
在规则段,每个规则是由一个文法产生式和一个关联的语义动作组成,假设有如下所示的文法:
<产生式头>→<产生式体1>|<产生式体2>|<产生式体3>|...|<产生式体n>
在yacc中被写为:
<产生式头>: <产生式体1>{<语义动作1>}
|<产生式体2>{<语义动作2>}
|<产生式体3>{<语义动作3>}
...
|<产生式体n>{<语义动作n>}
辅助函数段中则是语义动作中所需要的一些函数。
以下是一个简单yacc.l例子:
%{
#include <stdio.h>
#include <stdlib.h>
// 声明词法分析器函数
int yylex();
void yyerror(const char *s);
%}
// 定义 tokens
%token NUMBER
// 定义运算符的优先级和结合性
%left '+' '-'
%left '*' '/'
%right UMINUS
%%
lines: lines expr '\n' { printf("Result: %d\n", $1); }
| lines '\n'
| /*empty*/
;
expr:
expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
| expr '*' expr { $$ = $1 * $3; }
| expr '/' expr { $$ = $1 / $3; }
| '(' expr ')' { $$ = $2; }
| '-' expr %pred UMINUS {$$ = -$2;}
| NUMBER
;
%%
// 词法分析器
int yylex() {...}
以一个具体的实例来进行说明,假设输入字符串为2*3
,经过词法分析器后得到<2,NUMBER>,<*>,<3,NUMBER>
,
其规约过程如下:
step | 栈 | 输入 | 动作 |
---|---|---|---|
1 | $ | NUMBER*NUMBER | 移入 |
2 | $NUMBER | *NUMBER | 按照expr->NUMBER规约 |
3 | $expr | *NUMBER | 移入 |
4 | $expr* | NUMBER | 移入 |
6 | $expr*NUMBER | 按照expr->NUMBER规约 | |
7 | $expr*expr | 按照expr->expr*expr规约 | |
8 | $expr | 接受 |
因此,2*3
最后被规约到expr '*' expr { $$ = $1 * $3; }
中,最后执行该语法规则中的语义动作,
在语义动作中,符号$$
表达了和相应产生式头的非终结符号关联的属性值,$i
表示和相应产生体中第i
个文法符号关联的属性值。
gram.y解析
gram.y文件被分为3段:定义段,规则段和代码段,下面依次来看。
定义段
定义段可分为代码段和声明部分,代码段由%{
和%}
包括在内,编译时会被直接插入到c文件中,部分代码如下:
%{
#include "storage/lmgr.h"
#include "utils/date.h"
#include "utils/datetime.h"
#include "utils/numeric.h"
#include "utils/xml.h"
/*
* Location tracking support --- simpler than bison's default, since we only
* want to track the start position not the end position of each nonterminal.
*/
#define YYLLOC_DEFAULT(Current, Rhs, N) \
do { \
if ((N) > 0) \
(Current) = (Rhs)[1]; \
else \
(Current) = (-1); \
} while (0)
%}
声明部分代码及解析如下所示:
%pure-parser //纯解析器,不依赖于全局变量或静态变量,而是通过参数传递和返回值来管理状态
%expect 0 //希望冲突数量为0
%name-prefix="base_yy" //代表生成的函数和变量前缀从yy变成base_yy
%locations
%parse-param {core_yyscan_t yyscanner}
%lex-param {core_yyscan_t yyscanner}
声明段中的union
来声明所有表示符号的可能c类型,将标识符和C语言中定义的类型对应起来。
%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;
....
}
%type
表示非终结符语义值的类型,如下所示,stmt
表示为node
类型,add_drop
表明为ival
类型,alter_identity_column_option_list
表明为list
类型。
%type <node> stmt toplevel_stmt schema_stmt routine_body_stmt
%type <ival> add_drop opt_asc_desc opt_nulls_order
%type <list> alter_identity_column_option_list
%token
表示终结符,终结符一般大写,yacc中出现的所有终结符须在这边定义,这一部分其实就是词法分析器返回的属性值。部分示例如下:
%token <str> IDENT UIDENT FCONST SCONST USCONST BCONST XCONST Op
%token <ival> ICONST PARAM
%token TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER
%token LESS_EQUALS GREATER_EQUALS NOT_EQUALS
再往下定义的是结合性和优先级,前面在基本原理中介绍过,这部分用来解决规约和移入过程中产生的冲突。%left
表示左结合性,%right
表示右结合性,%nonassoc
表示不可结合性。优先级的顺序由定义的先后顺序决定,在同一行中表示优先级相同。如在下面的代码中,UNION
和EXCEPT
为左结合,优先级相同,ESCAPE
为不可结合,在示例的代码中具有最高优先级。
/* Precedence: lowest to highest */
%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 */
规则段
规则段中定义了所有sql的语法结构和匹配到某一结构后进行的语义动作,使用规约的方法处理各种复杂的sql命令,最后返回一棵分析树。
文法开始符为parse_toplevel
,在该产生式中定义了几种语法结构,如下所示:
parse_toplevel:
stmtmulti
{
pg_yyget_extra(yyscanner)->parsetree = $1;
(void) yynerrs; /* suppress compiler warning */
}
| MODE_TYPE_NAME Typename
{
pg_yyget_extra(yyscanner)->parsetree = list_make1($2);
}
进入到stmtmulti
的产生式,有两种可能情况,主要用于处理有;
的情况。
stmtmulti: stmtmulti ';' toplevel_stmt
{
if ($1 != NIL)
{
/* update length of previous stmt */
updateRawStmtEnd(llast_node(RawStmt, $1), @2);
}
if ($3 != NULL)
$$ = lappend($1, makeRawStmt($3, @2 + 1));
else
$$ = $1;
}
| toplevel_stmt
{
if ($1 != NULL)
$$ = list_make1(makeRawStmt($1, 0));
else
$$ = NIL;
}
;
toplevel_stmt
产生式中分为以下两种情况:
toplevel_stmt:
stmt
| TransactionStmtLegacy
;
stmt
中展开了各种sql命名的语法,如SelectStmt
和CreateStmt
等,如下所示:
stmt:
AlterEventTrigStmt
| AlterCollationStmt
...
| RuleStmt
| SecLabelStmt
| CreateStmt
| SelectStmt
...
再通过多层产生式进入到simple_select
,其产生式如下所示:
simple_select:
SELECT opt_all_clause opt_target_list
into_clause from_clause where_clause
group_clause having_clause window_clause
{
SelectStmt *n = makeNode(SelectStmt);
n->targetList = $3;
n->intoClause = $4;
n->fromClause = $5;
n->whereClause = $6;
n->groupClause = ($7)->list;
n->groupDistinct = ($7)->distinct;
n->havingClause = $8;
n->windowClause = $9;
$$ = (Node *) n;
}
| ...
当匹配到该模式时,生成一个node节点,将select语句下的各个从句放到node节点下的各个字段,然后将node返回给上一层。规约的方式实现对各类复杂场景如子查询的处理,最终得到一棵分析树。
gram.y中涉及了非常多的文法,此处不具体展开。
总结
本文介绍了语法分析的基本原理,语法分析工具yacc,并对部分gram.y文件进行了解析,后文将介绍语义分析。
参考文献
1、编译原理-机械工业出版社 2.blog.csdn.net/m0_60340015…