MYSQL词法分析

最近稍微研究了一下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_streamsql_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_stateresult_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_map0的字符;

      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语句分析完毕。



你可能感兴趣的:(MYSQL数据库)