Mysql源碼學習——詞法分析MYSQLlex

 

詞法分析MYSQLlex

 

       客戶端向服務器發送過來SQL語句後,服務器首先要進行詞法分析,而後進行語法分析,語義分析,構造執行樹,生成執行計劃。詞法分析是第一階段,雖然在理解Mysql實現上意義不是很大,但作為基礎還是學習下比較好。

 

詞法分析即將輸入的語句進行分詞(token),解析出每個token的意義。分詞的本質便是正則表達式的匹配過程,比較流行的分詞工具應該是lex,通過簡單的規則制定,來實現分詞。Lex一般和yacc結合使用。關於lex和yacc的基礎知識請參考Yacc 與Lex 快速入門- IBM。如果想深入學習的話,可以看下《LEX與YACC》。

 

然而Mysql並沒有使用lex來實現詞法分析,但是語法分析卻用瞭yacc,而yacc需要詞法分析函數yylex,故在sql_yacc.cc文件最前面我們可以看到如下的宏定義:

 

/* Substitute the variable and function names.  */

#define yyparse         MYSQLparse

#define yylex           MYSQLlex

 

  這裡的MYSQLlex也就是本文的重點,即MYSQL自己的詞法分析程序。源碼版本5.1.48。源碼太長,貼不上來,算啦..在sql_lex.cc裡面。

 

  我們第一次進入詞法分析,state默認值為MY_LEX_START,就是開始狀態瞭,其實state的宏的意義可以從名稱上猜個差不多,再比如MY_LEX_IDEN便是標識符。對START狀態的處理偽代碼如下:

 

case MY_LEX_START:

{

Skip空格

獲取第一個有效字符c

state = state_map[c];

Break;

}

 

  我困惑瞭,這尼瑪腫麼出來個state_map?找到瞭在函數開始出有個賦值的地方:

 

uchar *state_map= cs->state_map;

  cs?!不會是反恐精英吧!!快速監視下cs為my_charset_latin1,哥瞭然瞭,原來cs是latin字符集,character set的縮寫吧。那麼為神馬state_map可以直接決定狀態?找到其賦值的地方,在init_state_maps函數中,代碼如下所示:

 

/* 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;

 

  先來看這個for循環,256應該是256個字符瞭,每個字符的處理應該如下規則:如果是字母,則state = MY_LEX_IDENT;如果是數字,則state = MY_LEX_NUMBER_IDENT,如果是空格,則state = MY_LEX_SKIP,剩下的全為MY_LEX_CHAR。 

       for循環之後,又對一些特殊字符進行瞭處理,由於我們的語句“select @@version_comment limit 1”中有個特殊字符@,這個字符的state進行瞭特殊處理,為MY_LEX_USER_END。

對於my_isalpha等這幾個函數是如何進行判斷一個字符屬於什麼范疇的呢?跟進去看下,發現是宏定義:

#define    my_isalpha(s, c)  (((s)->ctype+1)[(uchar) (c)] & (_MY_U | _MY_L))

Wtf,腫麼又來瞭個ctype,c作為ctype的下標,_MY_U | _MY_L如下所示,

#define    _MY_U   01    /* Upper case */

#define    _MY_L   02    /* Lower case */

 

  ctype裡面到底存放瞭什麼?在ctype-latin1.c源文件裡面,我們找到瞭my_charset_latin1字符集的初始值:

 

CHARSET_INFO my_charset_latin1=

{

    8,0,0,                           /* number    */

    MY_CS_COMPILED | MY_CS_PRIMARY, /* state     */

    "latin1",                        /* cs name    */

    "latin1_swedish_ci",              /* name      */

    "",                                /* comment   */

    NULL,                         /* tailoring */

    ctype_latin1,

    to_lower_latin1,

    to_upper_latin1,

    sort_order_latin1,

    NULL,           /* contractions */

    NULL,           /* sort_order_big*/

    cs_to_uni,             /* tab_to_uni   */

    NULL,           /* tab_from_uni */

    my_unicase_default, /* caseinfo     */

    NULL,           /* state_map    */

    NULL,           /* ident_map    */

    1,                  /* strxfrm_multiply */

    1,                  /* caseup_multiply  */

    1,                  /* casedn_multiply  */

    1,                  /* mbminlen   */

    1,                  /* mbmaxlen  */

    0,                  /* min_sort_char */

    255,        /* max_sort_char */

    ' ',                /* pad char      */

    0,                  /* escape_with_backslash_is_dangerous */

    &my_charset_handler,

    &my_collation_8bit_simple_ci_handler

};

 

  可以看出ctype = ctype_latin1;而ctype_latin1值為:

 

static uchar ctype_latin1[] = {

    0,

   32, 32, 32, 32, 32, 32, 32, 32, 32, 40, 40, 40, 40, 40, 32, 32,

   32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,

   72, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,

  132,132,132,132,132,132,132,132,132,132, 16, 16, 16, 16, 16, 16,

   16,129,129,129,129,129,129,  1,  1,  1,  1,  1,  1,  1,  1,  1,

    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 16, 16, 16, 16, 16,

   16,130,130,130,130,130,130,  2,  2,  2,  2,  2,  2,  2,  2,  2,

    2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2, 16, 16, 16, 16, 32,

   16,  0, 16,  2, 16, 16, 16, 16, 16, 16,  1, 16,  1,  0,  1,  0,

    0, 16, 16, 16, 16, 16, 16, 16, 16, 16,  2, 16,  2,  0,  2,  1,

   72, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,

   16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,

    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,

    1,  1,  1,  1,  1,  1,  1, 16,  1,  1,  1,  1,  1,  1,  1,  2,

    2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,

    2,  2,  2,  2,  2,  2,  2, 16,  2,  2,  2,  2,  2,  2,  2,  2

};

 

  看到這裡哥再一次瞭然瞭,這些值都是經過預計算的,第一個0是無效的,這也是為什麼my_isalpha(s, c)定義裡面ctype要先+1的原因。通過_MY_U和_MY_L的定義,可以知道,這些值肯定是按照相應的ASCII碼的具體意義進行置位的。比如字符'A',其ASCII碼為65,其實大寫字母,故必然具有_MY_U,即第0位必然為1,找到ctype裡面第66個(略過第一個無意義的0)元素,為129 = 10000001,顯然第0位為1(右邊起),說明為大寫字母。寫代碼的人確實比較牛X,如此運用位,哥估計這輩子也想不到瞭,小小佩服下。State的問題點到為止瞭。

 

繼續進行詞法分析,第一個字母為s,其state = MY_LEX_IDENT(IDENTIFIER:標識符的意思),break出來,繼續循環,case進入MY_LEX_IDENT分支:

 

Case MY_LEX_IDENT:

{

由s開始讀,直到空格為止

If(讀入的單詞為關鍵字)

{

nextstate = MY_LEX_START;

Return tokval;        //關鍵字的唯一標識

}

Else

{

return IDENT_QUOTED 或者IDENT;表示為一般標識符

}

}

 

  這裡SELECT肯定為關鍵字,至於為什麼呢?下節的語法分析會講。

 

解析完SELECT後,需要解析@@version_comment,第一個字符為@,進入START分支,state = MY_LEX_USER_END;

 

進入MY_LEX_USER_END分支,如下:

 

case MY_LEX_USER_END:        // end '@' of user@hostname

      switch (state_map[lip->yyPeek()]) {

      case MY_LEX_STRING:

      case MY_LEX_USER_VARIABLE_DELIMITER:

      case MY_LEX_STRING_OR_DELIMITER:

    break;

      case MY_LEX_USER_END:

    lip->next_state=MY_LEX_SYSTEM_VAR;

    break;

      default:

    lip->next_state=MY_LEX_HOSTNAME;

    break;

 

  哥會心的笑瞭,兩個@符號就是系統變量吧~~,下面進入MY_LEX_SYSTEM_VAR分支

 

case MY_LEX_SYSTEM_VAR:

      yylval->lex_str.str=(char*) lip->get_ptr();

      yylval->lex_str.length=1;

      lip->yySkip();                                    // Skip '@'

      lip->next_state= (state_map[lip->yyPeek()] ==

            MY_LEX_USER_VARIABLE_DELIMITER ?

            MY_LEX_OPERATOR_OR_IDENT :

            MY_LEX_IDENT_OR_KEYWORD);

      return((int) '@');

 

  所作的操作是略過@,next_state設置為MY_LEX_IDENT_OR_KEYWORD,再之後便是解析MY_LEX_IDENT_OR_KEYWORD瞭,也就是version_comment瞭,此解析應該和SELECT解析路徑一致,但不是KEYWORD。剩下的留給有心的讀者瞭(想起瞭歌手經常說的一句話:大傢一起來,哈哈)。

 

Mysql的詞法解析的狀態還是比較多的,如果細究還是需要點時間的,但這不是Mysql的重點,我就淺嘗輒止瞭。下節會針對上面的SQL語句講解下語法分析。

 

PS: 一直想好好學習下Mysql,總是被這樣或那樣的事耽誤,當然都是自己的原因,希望這次能走的遠點…..

 

PS again:本文隻代表本人的學習感悟,如有異議,歡迎指正。

 

摘自 心中無碼

發佈留言