Erlang詞法分析器、語法分析器(lexer-leex,yac-yecc)

一、簡介

一門編程語言的編譯器或者解釋器通常功能分解爲兩步:
1、讀取源碼文件然後分析它的結構
2、處理這些結構,例如生成目標程序
lexer和yacc就是能完成第一步以便生成程序段的工具。而第一步的任務又能分爲兩個子任務:
1、分割源碼文件內容爲很多tokens(lexer)
2、分析出程序的分級結構(yacc)

二、lexer(詞法分析工具)

lexer的源就是一個正則表達式表,其正則規則符合目標程序的代碼片段。正則表達式表讀取輸入流,並將其根據正則規則轉換爲分割的字符串,然後輸出爲輸出流,再翻譯爲一個程序。當 Lex 接收到文件或文本形式的輸入時,它試圖將文本與常規表達式進行匹配。 它一次讀入一個輸入字符,直到找到一個匹配的模式。 如果能夠找到一個匹配的模式,Lex 就執行相關的動作(可能包括返回一個標記)。 另一方面,如果沒有可以匹配的常規表達式,將會停止進一步的處理,Lex 將顯示一個錯誤消息。
  Erlang的lexer工具就是leex模塊。

三、yacc(語法分析工具)

是一個用來生成編譯器的編譯器(編譯器代碼生成器)。輸入詞法分析器的流,輸出目標語言的代碼。
Erlang的yacc工具就是yecc。

四、作用

有了這兩個工具,我們可以自定義自己的編程語言,自定義這個語言的語法規則,最終生成相同功能的erlang代碼。例如領域特定語言(DSL),我們可以定義個簡單類似makefile的mymakefile語法規則,然後生成erlang代碼來真正執行功能。

五、初識

這次研究leex和yecc也是在研究erlang protocolbuf時候,發現通用的做法就是書寫一個協議字段描述文件,然後來生成erlang文件,例如一條TestRequest協議,定義其協議號10000,編解碼文件爲test_proto.erl,那麼描述關係就是10000,TestRequest,test_proto,然後我們代碼中傳包解包時,都在協議頭帶上一個16位的協議號,通過協議號路由到真正的編解碼文件也就是test_proto.erl。而看了很多實現,都是用riak官方某個rebar插件,來將這種關係描述文件生成erlang文件。

這個功能很簡單,不過想是不是能用leex、yecc完成,最近正好也在研究,於是就產生了試一試的想法。

六、定義leex的.xrl文件

leex需要.xrl文件來描述自定義源文件的正則匹配規則,例如上面的協議描述關係,我們可以用[0-9]+來匹配出開始的協議號,用[,][a-zA-Z]([0-9a-zA-Z]*_?)*[,]匹配出協議名。

leex文件需要三個部分:Definitions Rules Erlangcode,Definitions表示正則匹配的變量,Rules表示匹配的規則和匹配後的輸出,Erlangcode則是作爲輔助的erlang函數。

我們可以按照第五步來編寫我們的.xrl文件(假設叫test_lexer.xrl):

Definitions.


TypeID = [0-9]+
MsgName = [,][a-zA-Z]([0-9a-zA-Z]*_?)*[,]
MsgModule = [a-zA-Z]([0-9a-zA-Z]*_?)*(\n)?
NoneLine = [\n]

Rules.

{TypeID} : {token, {msg_number, TokenLine, list_to_integer(TokenChars)}}.
{MsgName} : {token, {msg_name, TokenLine, drop_tokens(TokenChars)}}.
{MsgModule} : {token, {msg_module, TokenLine, drop_tokens(TokenChars)}}.
{NoneLine} : skip_token.

Erlang code.

drop_tokens(TokenChars) ->
    [Chars] = string:tokens(TokenChars, ","),
    [Chars1] = string:tokens(Chars, "\s"),
    [Chars2] = string:tokens(Chars1, "."),
    [Chars3] = string:tokens(Chars2, "\n"),
    Chars3.
    上面的代碼還是容易看懂的。

    使用方法則是先編寫一個協議描述的文件test_pb_desc.csv,隨便輸入個關係:
    
10000,fdsfds_232,fdsf_REWr
10001,fwefsd,terterr
    然後通過leex編譯出我們的詞法分析器:
   
 leex:file("test_lexer.xrl").
    不出意外,當前目錄就會多一個test_lexer.erl,然後來分析我們的描述文件:
   
 {ok, Lines} = file:read_file("test_pb_desc.csv"),
 {_, Tokens, _} = test_lexer:string(binary_to_list(Lines)),
    這裏得到的Tokens就是我們的詞法分析結果,可以輸入到下一步的語法分析器裏生成erlang代碼。

七、定義yecc的.yrl文件

yecc需要.yrl文件來描述詞法分析的分析方法以及分析的產出。

yecc文件需要四個部分:Nonterminals Terminals Rootsymbol Erlangcode,Nonterminals表示每次輸入的流,Terminals表示輸入流裏面的tokens關鍵字,Rootsymbol表示輸入的根(源),Erlangcode照例是輔助的erlang函數,
我們可以按照第六步的生成來編寫yrl文件(例如叫test_parser.yrl):

Nonterminals
combines combine.

Terminals msg_number msg_name msg_module.

Rootsymbol combines.

combines -> combine : '$1'.
combines -> combine combines : '$1' ++ '$2'.
combine -> msg_number msg_name msg_module :
    [{{save('$1'), save('$2')}, {save('$2'), save('$1')}, {save('$1'), save('$3')}}].

Erlang code.


save({msg_number, _, Value}) -> Value;
save({msg_name, _, Value}) -> list_to_atom(Value);
save({msg_module, _, Value}) -> list_to_atom(Value).
這裏的combines語法有點像[H | Rest]語法,第一個combines -> combine : '$1'表示整個輸入的詞法分析流列表只剩一個元素的做法,combines -> combine combines : ... 表示匹配出單個元素combine 以及剩下的combines,而combine又尋找到單個規則combine -> msg_number msg_name ....,這樣遞歸地處理輸入源。

使用方法是:
yecc:file("test_parser.yrl").
然後編譯生成的test_parser.erl文件,再調用test_parser:parse(Tokens).就生成erlang的目標代碼了,這裏我只生成了一個列表。

八、完整代碼

我將完整代碼用來編寫了一個rebar3插件,用戶定義協議描述文件xxx,然後裏面的內容是協議描述關係:數字,協議名,協議編解碼文件 這樣的內容,插件就能輸出成一個erlang文件。

完整代碼參照我的git repo:rebar3_pb_msgdesc 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章