海山数据库(He3DB)查询解析模块分析(二):语法分析gram.y解析

41 阅读3分钟

引言

本文介绍查询解析的第二步:语法分析。语法分析是从词法分析器中获得带有属性的token后,根据tokens的属性匹配语法规则,最后获得一棵分析树。本文首先介绍词法分析基本原理,然后介绍海山数据库中使用的yacc工具,最后对语法分析gram.y进行解析。

基本原理

语法分析器识别模式的方法主要分为两种:自顶而下的分析方法和自底而上的分析方法。自顶而下的方法从分析树的根节点开始向叶节点构建,自底而上的方法反之,从叶节点开始构建。两种分析方法中,语法分析树总是按照从左到右的方式被扫描,每次通过词法分析器获得一个词法单元,如下图所示:

在这里插入图片描述

语法分析器依次从词法分析器中获得词法单元,类似于词法分析器中依次读入字符,然后从词法单元中匹配语法规则。

海山数据库中使用的yacc工具采用自底向上的方法,因此本文主要介绍该类方法。

在介绍该方法的具体实现之前,首先介绍文法的概念。

文法

文法是描述词法、语法规则的工具。用一组规则严格定义句子的结构,一个例子如下:

E -> E + T | T
T -> T * F | F
F -> (E) | id

E是开始符号,代表了所描述语言的最顶层结构。所有合法的句子都是从开始符号开始推导得到的。

这里的每一个表达式被称为产生式,式中的|为或,表示E可以被E+T或者T替换。

产生式中的 ETF非终结符号,相当于一个非叶节点,如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表示不可结合性。优先级的顺序由定义的先后顺序决定,在同一行中表示优先级相同。如在下面的代码中,UNIONEXCEPT为左结合,优先级相同,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命名的语法,如SelectStmtCreateStmt等,如下所示:

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…