轉自:項目總結之詞法分析器
無論是詞法分析,還是語法分析,給我的第一感覺就是邏輯要嚴謹。由於項目有自己一套完整的語言和語法,設計好其對應的詞法分析器和語法分析器顯得尤為重要。
我們采用flex進行詞法分析。flex是一個用來生成掃描器(scanners)的工具,其中掃描器就是可以識別文本中詞法模式的程序。具體流程為:flex讀取給定的輸入文件,或標准輸入(當沒有給定文件名時)讀取信息來生成一個掃描器。信息以正則表達式和C代碼組成,這種形式稱為規則(rule)。flex生成C源代碼文件lex.yy.c,其中定義了一個函數yylex()。這個文件通過編譯,並用-lfl 鏈接生成可執行文件。當可執行文件被執行時,它分析輸入中可能存在的符合正則表達的內容。當找到任何一個與正則表達式相匹配內容時,相應的C 代碼將被執行。
flex輸入文件由三段組成:定義(definitions),規則(rules),用戶代碼(user code)
一、定義段(definitions)
定義段包含了簡單名稱的聲明(這些聲明可以簡化掃描器的說明)和開始條件。在本項目中,定義段中還包含了選項options。現將介紹一些比較常用的options。
flex 提供一個機制用來在掃描器的說明中,而不是在flex 命令中控制選項。在掃描器的說明文件(flex 的輸入文件)的第一段中使用%option 指令就可以實現。你可以用單個%option 指令指定多個選項,也可以使用多個%option指令。
%option 7bit,%option 8bit——指示flex生成一個7bit或8bit的掃描器與-7,-8 選項等價。
%option backup——生成一個備份信息到lex.backup,與-b選項等價。
%option caseful,%option case-sensitive——區分大小寫,與-i相反。
%option case-insensitive,%option caseless——忽略大小寫,與-i選項等價。
%option debug——讓生成的掃描器運行在debug模式,與-d選項等價。
%option default,%option nodefault——%default與-s選項相反,后者與其等價。-s選項作用:使不匹配的輸入回顯到輸出設備的rule失去作用。在此種情況下,如果掃描器不能匹配到任何規則rule的輸入,它會終止並返回錯誤。在查找掃描器的規則漏洞時,-s和%option nodefault都非常有用。
%option interactive——指示flex生成一個交互式的掃描器。交互式掃描器就是向前查看下一個匹配的token是什么。結果就是總向前多看了一個字符,即使是在掃描器已經看夠了文本已經排除了token 的歧義。但向前查看給了掃描器強大的交互能力。與-I等價。
%option warn——與-w選項相反。%option nowarn與-w選項等價。
%option array——與%array等價。
%option pointer——與%point等價。
以下為%option中定義,但在命令行里沒有的特性。
%option always-interactive——指示flex 生成的掃描器總是把它的輸入認為是"interactive"。
%option main——指示flex 為掃描器提供一個缺省的main()函數,它只是簡單的調用了yylex()。這個選項暗示noyywrap。
%option never-interactive——flex 生成的掃描器從不認為輸入是交互的(不會調用isatty())。這和總是interactive 正好相反。
%option yylineno——flex 生成的掃描器用全局變量yylineno 維護着輸入文件的當前行編號。option lex-compat隱含有這個選項。
%option yywrap——如果沒有設置(就如%option noyywrap),當掃描器遇到end-of-file 時,不會調用yywrap(),但簡單的假定沒有更多的文件可以掃描(直到用戶把yyin 指向新的文件並再一次調用yylex())。
flex 通過掃描rule 中的action 來判斷你是否使用了REJECT 或是yymore 屬性。你可用%option reject 表示要使用這個特性而用%option noyymore 表示不使用這個特性。
三個選項使用了字符串值,從'='開始:%option outfile="ABC"等同於-oABC ;%option prefix="XYZ" 等同於-PXYZ;最后,%option yyclass="foo" 只有當生成C++掃描器(-+選項)時才有效。
有些選項可以限制一個例程不出現在生成的掃描器中。下面這些例程如果不被設置(如%option nounput)將不會出現在生成的掃描器中。
input unput yy_push_state yy_pop_sate yy_top_state yy_scan_buffer yy_scan_bytes yy_scan_string
可重入c掃描器(Reentrant C Scanners)
flex能夠生成一個可重入的掃描器。通過定義%option reentrant(與-R選項等價)來實現可重入。所生成的掃描器在一個或多個控制線程中不僅可移植,而且安全性好。可重入掃描器通常應用於多線程應用程序。任何一個線程都可以在不考慮與其他線程同步的情況下創建並執行一個可重入的flex掃描器。
默認情況下,flex生成一個不可重入的掃描器。本項目為了實現多線程,因而在定義段指定%option reentrant。
性能考慮(performance consideration)
flex的設計目標就是生成一個高性能的掃描器。它已經對處理大量rule 做了優化。除了用-C 選項進行表格壓縮之外,還有一些option/action 會影響到掃描器的速度。從最大影響到最弱,有這一些:
REJECT %option yylineno arbitrary trailing context
pattern sets that require backing up %array %option interactive %option always-interactive
'^'beginning-of-line operator yymore()
頭三個的開銷最大,后兩個的開銷最小。注意unput()有可能被用例程實現而造成更多操作,而yyless()是一個開銷相當低的宏;所以如果只是回放一些你多掃了的文本,可以用yyless()。
本項目中也用到了名字定義和開始條件。其中名字定義包括數字、字符、空白符,多行注釋,單行注釋,引號間的字符串,整數、浮點數、實數,標示符,變量,日期。
數字—digit [0-9],字符—character [a-zA-Z],空白符—space [ \t\r](在制表符前面留有空格表示空格符)
多行注釋(以/#開頭,中間可以為任意非#非\n字符,也可以為一串#后面緊跟非/非\n字符,最后結尾為1個或多個#后跟/)
comstart \/\#
comstop \#+\/
cominside ([^#\n]*|#+[^#/\n])
單行注釋 line_comment ^#[^\n]*
引號間的字符串(以雙引號開頭以雙引號結尾。內容為非轉義字符和雙引號,當遇到轉義字符時,進行特殊處理;當遇到雙引號時,停止匹配)
dquotes \"
stringstart {dquotes}
stringstop {dquotes}
stringinside [^\\\"]+
注意:在多行注釋和引號間的字符串的匹配中,采用了排斥條件(開始條件分為排斥和共享條件)
排斥條件的定義為 %xc(針對多行注釋)
%xs(針對引號間的字符串)
整數 integer {digit}+
浮點數 decimal (({digit}+\.{digit}*)|({digit}*\.{digit}+))
decimalfail {digit}+\.\.
實數 real ({integer}|{decimal})[eE][+-]?{digit}+
realfail1 ({integer}|decimal)[eE]
realfail2 ({integer}|decimal)[eE][+-]
標示符 identstart [a-zA-Z\200-\377_]
identcont [a-zA-Z\200-\377_0-9\$]
identifier {identstart}{identcont}*
變量($后跟一個或多個字符) variable \${character}+
日期 date {digit}+\-{digit}+(\-{digit}+)?
datefail1 {digit}+\-{digit}+\-
datefail2 {digit}+\-
二、規則段(rules)
規則段包含模式(pattern)和動作(action),其中模式不能有縮進,而且動作必須在同一行上跟在動作后面。
在規則段可以使用開始條件(start conditions)。flex 提供了一種按條件激活規則rule 的機制。所有模式以"<sc>"為前綴的rule 只有在掃描器是在一個名為"sc"的啟動條件時才會被激活。使用BEGIN action 可以激活一個開始條件。直到下一個BEGIN action 被執行,在給出開始條件的rule將被激活並且其他給出其他開始條件的rule 並不會被激活。如果使用的是排他的開始條件,那么只有以開始條件修飾的rule 才會被激活。跟在同一個排他開始條件后的rule 說明在掃描器中,這些rule 是獨立於flex 輸入中的其他rule。
本項目中涉及到排斥條件的有多行注釋、引號間的字符串。
其中MOVELOC,SAVETOKEN為定義段中定義的宏
#define MOVELOC {yylloc->first_column = yylloc->last_column;\
yylloc->last_column = yylloc->first_column + yyleng;}
#define RESETLOC {yylloc->first_column = yylloc->last_column = 1;\
yylloc->first_line++;\
yylloc->last_line++;}
#define SAVETOKEN yylval->str = new std::string(yytext, yyleng)
多行注釋(語句輸出省略)
{comstart} { MOVELOC;
BEGIN(xc);
}
<xc>{cominside} { MOVELOC; }
<xc>\n { RESETLOC; }
<xc>{comstop} { MOVELOC;
BEGIN(INITIAL);
}
<xc><<EOF>> { BEGIN(INITIAL);
std::cerr << "unterminated /# comment" << endl;
yyterminate();
}
引號間的字符串(輸出語句省略)
{stringstart} { MOVELOC;
BEGIN(xs);
SAVETOKEN;
}
<xs>{stringstop} { MOVELOC;
BEGIN(INITIAL);
*(yylval->str) += yytext;
return QUOTES_STRING;
}
<xs>\n { RESETLOC;
*(yylval->str) += yytext;
}
<xs>\\n { MOVELOC;
*(yylval->str) += "\n";
}
<xs>\\t { MOVELOC;
*(yylval->str) += "\t";
}
<xs>\\r { MOVLOC;
*(yylval->str) += "\r";
}
<xs>\\b { MOVELOC;
*(yylval->str) += "\b";
}
<xs>\\f { MOVELOC;
*(yylval->str) += "\f";
}
<xs>\\. { MOVELOC;
*(yylval->str) += yytext[1];
}
<xs>\\\n { RESETLOC;
*(yylval->str) += "\n";
}
<xs>{stringinside} { MOVELOC;
*(yylval->str) += yytext;
}
<xs><<EOF>> { BEGIN(INITIAL);
std::cerr << "unterminated \"" << endl;
delete yylval->str;
yyterminate();
}
三、用戶代碼段
用戶代碼段只會簡單的拷貝到lex.yy.c中。這個和掃描器一起,調用掃描器或者被掃描器調用。如果被省略,則第二個%%可以省略。
使用了%option reentrant后
1所有的函數都會帶一個額外的參數yyscanner。
2所有的全局變量都被它們的宏等價替換。
這些變量包括yytext
,yyleng
, yylineno
, yyin
, yyout
,yyextra
, yylval
, and yylloc,你可以在action部分安全地使用這些宏(如同使用普通變量一樣),但不能夠在外部直接使用。以yytext為例,在一個可重入的掃描器中,yytext以及其他類似變量都不是全局變量,因而不能通過action外部或是其他函數來直接訪問yytext,而應該使用yyget_text訪問器函數來實現對yytext的訪問。
3在使用yylex之前調用yylex_init,在使用之后調用yylex_destroy。
init以及destroy函數
int yylex_init ( yyscan_t * ptr_yy_globals ) ;
int yylex_init_extra ( YY_EXTRA_TYPE user_defined, yyscan_t * ptr_yy_globals ) ;
int yylex ( yyscan_t yyscanner ) ;
int yylex_destroy ( yyscan_t yyscanner ) ;
函數yylex_init必須在調用任意其他函數之前調用,其參數是一個未初始化的指針地址,並由該函數初始化,這樣會覆蓋以前的內容。ptr_yy_global中存儲的值會傳遞給yylex和yylex_destroy。flex不會保存傳遞給yylex_init的變量,因而傳遞一個局部指針的地址值給yylex_init是很安全的,只要其在調用掃描器到調用yylex_destroy期間一直存在就行。
yylex的可重入版本帶一個參數,該參數即為yylex_init通過變量返回的值。
yylex_destroy函數用來釋放掃描器使用過的資源。當要重復使用時,就不必destroy。
4獲取函數(get或set)提供了訪問普通flex變量的途徑。
5用戶自定義數據可以再yyextra中存儲。
在一個可重入的掃描器中,使用全局變量讓程序的不同部分通信或是保持狀態是不明智的。然而,你需要在action中使用額外的數據或是調用額外的函數。同樣,你需要傳遞信息給你的掃描器。在一個不可重入的掃描器中,實現這的唯一方式就是使用全局變量。flex允許你存儲任意的、額外的數據到掃描器中。定義如下:
#define YY_EXTRA_TYPE void*
YY_EXTRA_TYPE yyget_extra ( yyscan_t scanner );
void yyset_extra ( YY_EXTRA_TYPE arbitrary_data , yyscan_t scanner);
項目中最后的代碼如下,其中scanner_init初始化yylex,yy_scan_buffer函數(作用是建立輸入緩存)從yyext->scanbuf指定的開始位置掃描slen+2個字節,最后兩個字節必須是YY_END_OF_BUFFER_CHAR。
- yyscan_t
- scanner_init(const char *str, inl_yylex_extra *yyext)
- {
- int slen = strlen(str);
- yyscan_t scanner;
- if(yylex_init(&scanner) != 0)
- {
- std::cerr << "yylex_init() failed" << std::endl;
- exit(1);
- }
- inl_yyset_extra(yyext, scanner);
- yyext->scanbuf = (char *)malloc(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);
- return scanner;
- }
- void scanner_finish(yyscan_t yyscanner)
- {
- free((*((inl_yylex_extra**)(yyscanner)))->scanbuf);
- inl_yylex_destroy(yyscanner);
- }