最近稍微研究了一下MYSQL的词法和语法分析的代码,顺便在博客上做一些笔记方便以后翻看。
MYSQL的词法分析并没有使用常见的词法分析工具LEX来生成词法分析文件,而是有自己的一套词法分析,代码参见sql/sql_lex.cc。词法分析的主要入口函数是MYSQLlex,该函数的代码如下:
int MYSQLlex(void *arg, void *yythd)
{
THD *thd= (THD *)yythd;
Lex_input_stream *lip= & thd->m_parser_state->m_lip;
YYSTYPE *yylval=(YYSTYPE*) arg;
int token;
if (lip->lookahead_token >= 0)
{
/*
The next token was already parsed in advance,
return it.
*/
token= lip->lookahead_token;
lip->lookahead_token= -1;
*yylval= *(lip->lookahead_yylval);
lip->lookahead_yylval= NULL;
return token;
}
token= lex_one_token(arg, yythd);
switch(token) {
case WITH:
/*
Parsing 'WITH' 'ROLLUP' or 'WITH' 'CUBE' requires 2 look ups,
which makes the grammar LALR(2).
Replace by a single 'WITH_ROLLUP' or 'WITH_CUBE' token,
to transform the grammar into a LALR(1) grammar,
which sql_yacc.yy can process.
*/
token= lex_one_token(arg, yythd);
switch(token) {
case CUBE_SYM:
return WITH_CUBE_SYM;
case ROLLUP_SYM:
return WITH_ROLLUP_SYM;
default:
/*
Save the token following 'WITH'
*/
lip->lookahead_yylval= lip->yylval;
lip->yylval= NULL;
lip->lookahead_token= token;
return WITH;
}
break;
default:
break;
}
return token;
}
由上面的代码可以看出该函数是通过一个名为lex_one_token的函数得到分析的结果,并将这个结果赋给变量token。在看lex_one_token的源代码之前,要来先了解一下其中的几个重要的数据类型以及变量,分别是:
Lex_input_stream *lip= & thd->m_parser_state->m_lip;
uchar *state_map= cs->state_map;
变量lip中保存了所有读取的词法的信息。类Lex_input_stream在sql_lex.h中定义。state_map中保存了词法分析状态机中的各种状态,其具体的定义如下:
static my_bool init_state_maps(CHARSET_INFO *cs)
{
uint i;
uchar *state_map;
uchar *ident_map;
if (!(cs->state_map= (uchar*) my_once_alloc(256, MYF(MY_WME))))
return 1;
if (!(cs->ident_map= (uchar*) my_once_alloc(256, MYF(MY_WME))))
return 1;
state_map= cs->state_map;
ident_map= cs->ident_map;
/* Fill state_map with states to get a faster parser */
for (i=0; i < 256 ; i++)
{
if (my_isalpha(cs,i))
state_map[i]=(uchar) MY_LEX_IDENT;
else if (my_isdigit(cs,i))
state_map[i]=(uchar) MY_LEX_NUMBER_IDENT;
#if defined(USE_MB) && defined(USE_MB_IDENT)
else if (my_mbcharlen(cs, i)>1)
state_map[i]=(uchar) MY_LEX_IDENT;
#endif
else if (my_isspace(cs,i))
state_map[i]=(uchar) MY_LEX_SKIP;
else
state_map[i]=(uchar) MY_LEX_CHAR;
}
state_map[(uchar)'_']=state_map[(uchar)'$']=(uchar) MY_LEX_IDENT;
state_map[(uchar)'\'']=(uchar) MY_LEX_STRING;
state_map[(uchar)'.']=(uchar) MY_LEX_REAL_OR_POINT;
state_map[(uchar)'>']=state_map[(uchar)'=']=state_map[(uchar)'!']= (uchar) MY_LEX_CMP_OP;
state_map[(uchar)'<']= (uchar) MY_LEX_LONG_CMP_OP;
state_map[(uchar)'&']=state_map[(uchar)'|']=(uchar) MY_LEX_BOOL;
state_map[(uchar)'#']=(uchar) MY_LEX_COMMENT;
state_map[(uchar)';']=(uchar) MY_LEX_SEMICOLON;
state_map[(uchar)':']=(uchar) MY_LEX_SET_VAR;
state_map[0]=(uchar) MY_LEX_EOL;
state_map[(uchar)'\\']= (uchar) MY_LEX_ESCAPE;
state_map[(uchar)'/']= (uchar) MY_LEX_LONG_COMMENT;
state_map[(uchar)'*']= (uchar) MY_LEX_END_LONG_COMMENT;
state_map[(uchar)'@']= (uchar) MY_LEX_USER_END;
state_map[(uchar) '`']= (uchar) MY_LEX_USER_VARIABLE_DELIMITER;
state_map[(uchar)'"']= (uchar) MY_LEX_STRING_OR_DELIMITER;
/*
Create a second map to make it faster to find identifiers
*/
for (i=0; i < 256 ; i++)
{
ident_map[i]= (uchar) (state_map[i] == MY_LEX_IDENT ||
state_map[i] == MY_LEX_NUMBER_IDENT);
}
/* Special handling of hex and binary strings */
state_map[(uchar)'x']= state_map[(uchar)'X']= (uchar) MY_LEX_IDENT_OR_HEX;
state_map[(uchar)'b']= state_map[(uchar)'B']= (uchar) MY_LEX_IDENT_OR_BIN;
state_map[(uchar)'n']= state_map[(uchar)'N']= (uchar) MY_LEX_IDENT_OR_NCHAR;
return 0;
}
由上面的代码可以看出在state_map初始化中已经讲所有ASCII码表中的字符所属的状态都填满了。接着在lex_one_token中会对当前词法分析的状态进行判定来决定到底返回什么类型的token。在这个判定中有一个for循环,循环体中根据每次state变量的取值来决定经过哪一个分支,另外lip中也会保存下一个状态的信息,具体程序如下:
state=lip->next_state;
lip->next_state=MY_LEX_OPERATOR_OR_IDENT;
下面以分析一条sql语句来说明在进入switch/case之后的流程,待分析的sql语句为:
insert into tablename values (2,‘Y’,2.3);
第一步,state的为初始状态,会进入MY_LEX_OPERTOR_OR_IDENT或是MY_LEX_START分支。在刨除sql语句之前的一系列的空格之后开始读到语句中的第一个字母i,并通过state_map数组返回state的状态应该为MY_LEX_IDENT。代码片段如下:
case MY_LEX_OPERATOR_OR_IDENT: // Next is operator or keyword
case MY_LEX_START: // Start of token
// Skip starting whitespace
while(state_map[c= lip->yyPeek()] == MY_LEX_SKIP)
{
if (c == '\n')
lip->yylineno++;
lip->yySkip();
}
/* Start of real token */
lip->restart_token();
c= lip->yyGet();
state= (enum my_lex_states) state_map[c];
break;
第二步,state此时的值为MY_LEX_IDENT,状态机应该进如MY_LEX_IDENT分支。这时会将整个insert单词读完直到遇到空格,然后对读到的单词进行判定是否为关键字(判定函数为find_keyword),如果是关键字则会返回hash表中该单词所对应的关键字,由于insert是关键字那么返回该token,并讲下一个状态设置为MY_LEX_START。至此insert这个token被识别并被传到MYSQLlex中,被后续的语法分析所用。代码片段如下:
{
for (result_state= c; ident_map[c= lip->yyGet()]; result_state|= c) ;
/* If there were non-ASCII characters, mark that we must convert */
result_state= result_state & 0x80 ? IDENT_QUOTED : IDENT;
}
length= lip->yyLength();
start= lip->get_ptr();
if (lip->ignore_space)
{
/*
If we find a space then this can't be an identifier. We notice this
below by checking start != lex->ptr.
*/
for (; state_map[c] == MY_LEX_SKIP ; c= lip->yyGet()) ;
}
if (start == lip->get_ptr() && c == '.' && ident_map[lip->yyPeek()])
lip->next_state=MY_LEX_IDENT_SEP;
else
{ // '(' must follow directly if function
lip->yyUnget();
if ((tokval = find_keyword(lip, length, c == '(')))
{
lip->next_state= MY_LEX_START; // Allow signed numbers
return(tokval); // Was keyword
}
第三步,重新进入lex_one_token函数,由于into同样是关键字,因此对于into的分析和insert是一样的,在此就不再赘述。
第四步,进入lex_one_token函数,在经过初始处理之后,状态机会进入MY_LEX_IDENT分支。在读完tablename之后发现不是关键字,这事由get_token函数将读到的单词保存到yylval中(yylval->lex_str)并返回result_state,result_state的取值是根据单词是否存在非ASCII码字母来决定为IDENT或是IDENT_QUOTED(这两个都是yacc中的一种token类型)。代码片段如下:
yylval->lex_str=get_token(lip, 0, length);
/*
Note: "SELECT _bla AS 'alias'"
_bla should be considered as a IDENT if charset haven't been found.
So we don't use MYF(MY_WME) with get_charset_by_csname to avoid
producing an error.
*/
if (yylval->lex_str.str[0] == '_')
{
CHARSET_INFO *cs= get_charset_by_csname(yylval->lex_str.str + 1,
MY_CS_PRIMARY, MYF(0));
if (cs)
{
yylval->charset= cs;
lip->m_underscore_cs= cs;
lip->body_utf8_append(lip->m_cpp_text_start,
lip->get_cpp_tok_start() + length);
return(UNDERSCORE_CHARSET);
}
}
lip->body_utf8_append(lip->m_cpp_text_start);
lip->body_utf8_append_literal(thd, &yylval->lex_str, cs,
lip->m_cpp_text_end);
return(result_state); // IDENT or IDENT_QUOTED
第五步,values为关键字,分析同第二步;
第六步,读取‘(’之后,在state_map中对应的状态为MY_LEX_CHAR,直接返回;
第七步,读到2之后,状态为MY_LEX_NUMBER_IDENT。由于上一个读取的字符不是0,于是将数字全部读完直到碰到ident_map为0的字符;
while (my_isdigit(cs, (c = lip->yyGet()))) ;
if (!ident_map[c])
{ // Can't be identifier
state=MY_LEX_INT_OR_REAL;
break;
}
由于还没有返回token,判定循环继续进入MY_LEX_INT_OR_REAL分支。由于下一个字符为‘,’因此可以判定刚刚读到的是一个整型的数字,然后将该数字返回;代码片段如下:
case MY_LEX_INT_OR_REAL: // Complete int or incomplete real
if (c != '.')
{ // Found complete integer number.
yylval->lex_str=get_token(lip, 0, lip->yyLength());
return int_token(yylval->lex_str.str, (uint) yylval->lex_str.length);
}
第八步,读到“,”之后,分析同第六步,不同的是多了一步对’,‘的处理;
if (c == ',')
{
/*
Warning:
This is a work around, to make the "remember_name" rule in
sql/sql_yacc.yy work properly.
The problem is that, when parsing "select expr1, expr2",
the code generated by bison executes the *pre* action
remember_name (see select_item) *before* actually parsing the
first token of expr2.
*/
lip->restart_token();
}
第九步,读到“’”之后,进入MY_LEX_USR_VARIABLE_DELIMITER分支。函数会寻找下一个“‘”(引号配对),如果可以找到另一个配对的引号那么将引号之间的单词保存到yylval。代码片段如下:
case MY_LEX_USER_VARIABLE_DELIMITER: // Found quote char
{
uint double_quotes= 0;
char quote_char= c; // Used char
while ((c=lip->yyGet()))
{
int var_length;
if ((var_length= my_mbcharlen(cs, c)) == 1)
{
if (c == quote_char)
{
if (lip->yyPeek() != quote_char)
break;
c=lip->yyGet();
double_quotes++;
continue;
}
}
#ifdef USE_MB
else if (use_mb(cs))
{
if ((var_length= my_ismbchar(cs, lip->get_ptr() - 1,
lip->get_end_of_query())))
lip->skip_binary(var_length-1);
}
#endif
}
if (double_quotes)
yylval->lex_str=get_quoted_token(lip, 1,
lip->yyLength() - double_quotes -1,
quote_char);
else
yylval->lex_str=get_token(lip, 1, lip->yyLength() -1);
if (c == quote_char)
lip->yySkip(); // Skip end `
lip->next_state= MY_LEX_START;
lip->body_utf8_append(lip->m_cpp_text_start);
lip->body_utf8_append_literal(thd, &yylval->lex_str, cs,
lip->m_cpp_text_end);
return(IDENT_QUOTED);
}
第十步,同上一个“,”;
第十一步,基本同第七步,不同是的这次判定为浮点型,在此不多赘述;
第十二步,读到’)‘之后也进入MY_LEX_CHAR中,同样返回’)‘
第十三步,读到’;‘之后,将状态设置为MY_LEX_CHAR,在进入MY_LEX_CHAR分支(返回';')
第十四步,读到分号之后的换行符,判定是否为文件的结尾,如果不是进入MY_LEX_CHAR分支;如果是,返回END_OF_INPUT。代码片段如下:
case MY_LEX_EOL:
if (lip->eof())
{
lip->yyUnget(); // Reject the last '\0'
lip->set_echo(FALSE);
lip->yySkip();
lip->set_echo(TRUE);
/* Unbalanced comments with a missing '*' '/' are a syntax error */
if (lip->in_comment != NO_COMMENT)
return (ABORT_SYM);
lip->next_state=MY_LEX_END; // Mark for next loop
return(END_OF_INPUT);
}
state=MY_LEX_CHAR;
break;
至此,一条insert语句分析完毕。