@[toc]
引言
词法分析是查询解析的第一步,主要工作是将输入的sql命令转化为带有属性的token,然后进入下一步的语法分析进行语法结构的匹配。本文首先介绍词法分析的基本原理,然后介绍海山数据库中使用的词法分析工具lex,最后对词法分析scan.l文件进行解析。
词法分析基本原理
词法分析的主要工作是识别出sql语句中的各个token,并确定其属性,如下图所示。
要理解该模块的工作原理,需要搞清楚以下几个问题:
1.如何来解析sql命令然后获得token;
2.属性有哪些,如何确定属性。
对于第一个问题,如何将sql语句转化为一组tokens。首先要定义一组模式,该模式表示token的可能表现形式,从sql语句中使用正则表达式进行匹配,提取出一个一个的token。在匹配过程中,有两个指针start_ptr和end_ptr,从初始位置开始扫描,使用状态转化图顺序解析,举一个例子,假设需要识别的模式有【<,>,=,<=,>=,<>】。
在初始时,可能的状态转化图如下图所示:
该图中,若读入的第一个符号是【<】,end_ptr指针往后读入一位,此时可能的状态有【<,<=,<>】,end_ptr指针再往后读入一位,若读到的是【=】,则返回【<=】,若读到的是>,则返回【<>】,若为其他字符,则end_ptr回退到前一位置,并返回【<】。
对于这个过程,可以简单总结一下,存在两个指针:start_ptr和end_ptr,两个指针用来表示当前读取的字符串,若当前的字符串可能与多种模式匹配到,则end_ptr继续向后读取,直到实现最大字符串的匹配或者进行回退消除歧义性。当确定一个token后,start_ptr更新当前位置,并开始下一阶段的匹配。
对于第二个问题,属性有哪些,如何确定其属性。在编译原理中,属性一般可分为以下几类:
属性 | 例子 |
---|---|
标识符 | 表名,列名等 |
关键字 | SELECT,UPDATE,DELETE,... |
运算符 | <,>,<=,>=... |
常量 | 整型,浮点型... |
界限符 | ;,()... |
token的属性在正则匹配时其实已经可以确定,不同的模式对应不同的属性。这样,在逻辑上实现了词法分析的过程。
lex工具
海山数据库中使用了lex工具进行词法分析,其开发流程如下图所示:
首先,用lex语言写一个lex.l文件,然后lex编译器会将lex.l文转化为c语言文件,最后lex.c编译器将其转化为lex.out文件。lex.out文件便可以对输入流提出词法单元序列。
一个lex.l文件的结构如下所示:
声名部分
%%
转化规则
%%
辅助函数
第一个部分声明部分包含变量和明示常量(明示常量是被声明的表示一个常量的标识符)。
第二个部分转化规则中具有以下形式:
模式 {动作}
其中,模式是一个正则表达式,可以使用在声明部分给出的正则定义,动作部分是代码片段。该定义即表示,当识别到指定模式后,则执行对应的代码片段。
第三个部分则是所需要的一些辅助函数。
下面是一个具体的例子:
%{
/*
LT,LE,EQ,NE,GT,GE,
IF,THEN,ELSE,
ID,NUMER,RELOP
*/
%}
DIGIT [0-9]
LETTER [a-zA-Z]
ID {LETTER}({LETTER}|{DIGIT})*
IF "if"
THEN "then"
ELSE "else"
LT "<"
GT ">"
LE "<="
GE ">="
EQ "="
%%
{IF} { return(IF); }
{THEN} { return(THEN);}
{ELSE} { return(ELSE); }
{LT} { yylval= LT;return(RELOP); }
{GT} { yylval= GT;return(RELOP); }
{LE} { yylval= LE;return(RELOP); }
{GE} { yylval= GE;return(RELOP); }
{ID} { yylval= int installID();return(ID); }
%%
int installID()
{
/*function implement*/
}
通过对以上代码的观察,可以发现lex的很多特点。
1.代码开头的%{%}中的内容会被直接插入到.c文件中;
2.当匹配到【IF,THEN,ELSE】等关键词时,直接返回;
3.当匹配到【LT,LE,GT,GE】等关键词时,返回【RELOP】属性,并将具体的值存放在yylval中,yylval是一个全局变量,可以被后续的组件语法分析器使用;
4.当匹配到可变长度标识符时,返回【ID】,调用installID辅助函数完成。
scan.l文件解析
海山数据库中的scan.l文件中定义了词法分析的主要规则。
scan.l文件的主要结构如下:
/*声明段*/
%top{...}
%{...%}
%option
%x xc
space [ \t\n\r\f]
horiz_space [ \t\f]
newline [\n\r]
non_newline [^\n\r]
comment ("--"{non_newline}*)
whitespace ({space}+|{comment})
...
%%
/*规则段*/
{whitespace} {
/* ignore */
}
%%
/*辅助函数段*/
%%
在声明段,具体的符号含义如下:
1.%top{...}:该部分中的内容会被原样复制到scan.c文件中,并且位于.c文件的最顶部,一般在这个部分加入一些文件描述注释,以及要include的头文件;
2.%{...%}:该部分内容同样会被原样复制到scan.c文件中,这里可以定义一些宏,在规则段使用的函数声明和结构体声明。如关键词的定义在此处声明:
#define PG_KEYWORD(kwname, value, category, collabel) value,
const uint16 ScanKeywordTokens[] = {
#include "parser/kwlist.h"
kwlist.h
文件中存放了海山数据库定义的关键词,关键词的定义如下所示:
PG_KEYWORD("abort", ABORT_P, UNRESERVED_KEYWORD, BARE_LABEL)
3.%option:该部分进行lex支持的参数设置,如%option prefix="core_yy"
,可以将原来的yylex
等函数变成core_yylex
,这样可以在一个程序上建立多个词法分析器,用来分析不同的输入流。
4.%x:该部分定义开始状态,表示规定一个特定的状态,在规则段只会在特定的状态下进行匹配;
5.space [ \t\n\r\f]:为要匹配的表达式命名,这样就可以在规则段使用这个命名来匹配执行动作;
在规则段,下面通过举几个具体的例子进行说明。
{real} {
SET_YYLLOC();
yylval->str = pstrdup(yytext);
return FCONST;
}
以上代码中,SET_YYLLOC();
设置当前词法的位置信息,保存到语法分析器的上下文中(词法分析器和语法分析器是交互工作的,具体执行流程将在语法分析中介绍),yylval->str = pstrdup(yytext);
表示将匹配到的数字存放到语法分析器的语义值中(yytext
是语法分器的全局变量,yytext
是lex提供的全局变量,存储当前匹配的词法单元的文本内容,pstrdup
是postgres中的一个函数,用于复制yytext的内容并返回一个新的值);FCONST
是返回的该token的属性。
{identifier} {
int kwnum;
char *ident;
SET_YYLLOC();
/* Is it a keyword? */
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.
*/
ident = downcase_truncate_identifier(yytext, yyleng, true);
yylval->str = ident;
return IDENT;
}
该段代码是对关键词和标识符进行识别,通过SET_YYLLOC
进行位置设置后,使用GetScanKeyword
在keywordlist
中查找是否存在该关键词,若是则返回该关键词在wordlist中的序号,若未找到则返回-1
。
当kwnum>=0
时即匹配到了关键词,通过GetScanKeyword
将值赋给yylval
,然后yyextra->keyword_tokens[kwnum]
获得属性后返回。
当kwnum<0
时即未匹配未关键词,通过downcase_truncate_identifier
函数将其转为小写或截断,然后将处理后的值返回给yyval
,然后返回属性未IDENT
,表示其为一个标识符。
在辅助函数段,可以看几个例子:
core_yyscan_t
scanner_init(const char *str,
core_yy_extra_type *yyext,
const ScanKeywordList *keywordlist,
const uint16 *keyword_tokens)
{
Size slen = strlen(str);
yyscan_t scanner;
if (yylex_init(&scanner) != 0)
elog(ERROR, "yylex_init() failed: %m");
core_yyset_extra(yyext, scanner);
yyext->keywordlist = keywordlist;
yyext->keyword_tokens = keyword_tokens;
yyext->backslash_quote = backslash_quote;
yyext->escape_string_warning = escape_string_warning;
yyext->standard_conforming_strings = standard_conforming_strings;
/*
* Make a scan buffer with special termination needed by flex.
*/
yyext->scanbuf = (char *) palloc(slen + 2);
yyext->scanbuflen = slen;
memcpy(yyext->scanbuf, str, slen);
yyext->scanbuf[slen] = yyext->scanbuf[slen + 1] = YY_END_OF_BUFFER_CHAR;
yy_scan_buffer(yyext->scanbuf, slen + 2, scanner);
/* initialize literal buffer to a reasonable but expansible size */
yyext->literalalloc = 1024;
yyext->literalbuf = (char *) palloc(yyext->literalalloc);
yyext->literallen = 0;
return scanner;
}
该函数为词法分析器的一个初始化,(该函数在parse.c
中的raw_parser
函数被调用,涉及到词法语法解析的执行流程,在后文中进行详细的介绍),core_yyset_extra
将用于将词法分析器的上下文信息yyext
绑定到词法分析器实例scanner
上,可以通过scanner
访问yyext
中的信息。然后设置词法分析器的上下文信息,如关键字列表。然后准备输入缓冲区,用于存储待解析的SQL字符串。最后初始化字面缓冲区,用于存储解析过程中遇到的字符串字面量。
总结
本文介绍了词法分析的基本原理,海山数据库使用的lex词法分析工具,并对scan.l文件进行了解析,在后文中将进一步解析词法分析,并梳理sql命令是如何经过词法和语法分析转化为一颗分析树。
参考文献
2.编译原理-机械工业出版社