詞法分析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:本文只代表本人的學習感悟,如有異議,歡迎指正。