最近在學習 Elixir,想把之前用 ruby 寫的一個 DSL 遷移過來,所以看到了這篇文章,隨手翻譯,以茲備忘。 對於一些專業詞彙,保留英文更有利於理解,所以不做翻譯,加入本身的理解,記錄以下:html
原文地址git
若是你須要解析些什麼東西的話,會發現 Leex 和 Yecc 是難以置信的強大。不幸的是你須要下點功夫去理解它。若是你像我同樣是個 elixir 開發者的話,你會發現他們描述 tokens 和 grammars 的 DSL沒有那麼一目瞭然。我花了幾天的功夫來研究這些工具,由於我並不會常常編寫解析器,因此把個人一些心得記錄下來,以備後用。這是兩篇文章的第一篇。本文中,咱們會使用 leex 和 yecc 來解析一個簡單 grammar ,而且解釋他們是如何一塊兒協同工做的。下一篇文章,我會探討如何使用他們來建立一個更爲複雜的 grammar 的解析器(parser),以及我學到的一些單元測試技巧等等。憑良心說,對於這些工具我不是什麼專家,我只是第一次使用並努力掌控他們的傢伙,並最終試圖理解他們的工做機理。github
若是你喜歡跟着代碼來,能夠訪問這裏: 示例代碼正則表達式
Mix 對這些處理的很好。你須要作的僅僅是在 project 的根目錄下建立 src 目錄。而後將 .xrl 和 .yrl 文件放進去,mix 就會注意他們,依次識別出他們是 leex 和 yecc 文件,而後將他們編譯成 .erl 文件,而後再編譯這些 erl 文件,全部這些都是自動的,很是簡單。ruby
leex 是一個 lexer(詞法分析器)。它讀取輸入數據,根據你定義的 rule 識別 tokens,將這些 tokens 轉換成你想要的東西,以列表的形式保存。列表中能夠保存任意元素,在本文中,咱們隨後須要使用 yecc,所以咱們最好將這些 tokens 轉換成 yecc 喜歡的樣式,下面是咱們要乾的第一件事。app
我寫了一個簡單 lexer。這個 lexer 的惟一功能就是識別整數和浮點數,而且忽略空白字符和逗號。在咱們研究代碼前,先展現下他們的實際功能:函數
iex(1)> :number_lexer.string('12 42 23.24 23') {:ok, [{:int, 1, 12}, {:int, 1, 42}, {:float, 1, 23.24}, {:int, 1, 23}], 1} iex(2)>
這個 lexer 讀入一個 char list,返回一個 tuple {:ok,list_of_tokens,line}。list_of_tokens 中的每個 token 又是以下格式 {type,line_number,value}。這種格式是 yecc 將要用到的。工具
咱們從 lexer 獲得輸出,傳入 parser,而後 parser 運用語法規則產生 AST。單元測試
%% src/number_lexer.xrl Definitions. Whitespace = [\s\t] Terminator = \n|\r\n|\r Comma = , Digit = [0-9] NonZeroDigit = [1-9] NegativeSign = [\-] Sign = [\+\-] FractionalPart = \.{Digit}+ IntegerPart = {NegativeSign}?0|{NegativeSign}?{NonZeroDigit}{Digit}* IntValue = {IntegerPart} FloatValue = {IntegerPart}{FractionalPart}|{IntegerPart}{ExponentPart}|{IntegerPart}{FractionalPart}{ExponentPart} Rules. {Comma} : skip_token. {Whitespace} : skip_token. {Terminator} : skip_token. {IntValue} : {token, {int, TokenLine, list_to_integer(TokenChars)}}. {FloatValue} : {token, {float, TokenLine, list_to_float(TokenChars)}}. Erlang code.
.xrl 文件由三部分構成,這三部分都是必須的。學習
這一部分咱們會定義一些模板, lexer 以此來掃描輸入數據,進行適配識別。每一行的格式都是左邊匹配右邊的正則表達式,如 Whitespace = [\s\t]。注意,這些都是 Erlang 語言的正則表達,它是傳統正則表達式的子集,所以遇到複雜的表達式,你須要更有創造力些,本身想一想辦法。 細節能夠參考 leex 文檔。
要引用一條 definition,將其放到{}花括號裏面就好了,Definitions 部分或者 Rules 部分均可以這樣引用。
用來真正識別 token,並告訴 leex 如何處理。rules 格式必須以下:
<pattern> : <result>.
冒號先後必須留空格,末尾的點必須。
在實際的 Erlang 代碼中,咱們還要作些靈活處理,用來生成咱們須要的東西。好比這行代碼用來建立浮點數 token:
{FloatValue} : {token, {float, TokenLine, list_to_float(TokenChars)}}.
代碼含義是,當你匹配到上面的 FloatValue 模板時,生成一個 token,格式爲 {<atom>, <line#>, <value>}。這裏的 atom 在 Elixir 語言中就是 :float,line# 行號由 leex 生成,value 由 Erlang 語言的內建函數 list_to_float 生成。
對於相似 Whitespace 和 Comma 這樣的模板,咱們經過設置爲 skip_token 將匹配結果直接丟棄。
在咱們開始學習 parser 前,先讓咱們設計一下值得解析的東西。咱們但願咱們的微型語言具有表達 lists 的能力,lists 用來存放 ints 和 floats,用逗號分隔,列表可嵌套,好比 [1,2,3,[2.1,2,2]]。首先咱們須要修改 lexer,須要識別帶方括號的 tokens。在 Definitions 部分添加以下一行:
Bracket = [\[\]]
這一句匹配任意方括號,而後咱們將 bracket(轉換成 atom,這很是重要,由於 parser 只能識別 atom)傳遞給 parser:
{Bracket} : {token, {list_to_atom(TokenChars), TokenLine, TokenChars} }.
如今咱們有一點解析的工做要作了,讓咱們將關注點移到 yecc。第一次咱們的微型語言終於有點像個 grammar 了:
Document :: Value(list) Value :: Int Float List List :: Value(list)
咱們定義了 Document,其包含一個或多個 Values,而每個 value 能夠是 Int,Float 或者一個包含更多 Values的 List。
如今讓咱們看看 .yrl 文件的結構,學習下如何定義 parser。
yrl 文件由四部分構成:
terminals 是 grammar 的最小單元結構。terminals 是沒法再進一步展開的。他們就是 leex 生成的 tokens,但這裏咱們必須再次把他們羅列出來:
Terminals int float '[' ']'.
nonterminals 是經過 rules 構建的更爲複雜的結構。很明顯 document,value,list 都是這種結構。這裏咱們還要添加一些輔助的 nonterminals,用來更好地實現 grammar。
Rootsymbol 表明 AST 的最頂層。這會是 parser 應用的第一條 rule,而後順藤摸瓜,一步一步導入到更底層的 rules。
下面是一條 Rootsymbol 語句,咱們擁有了全部的 rules,每一條 rule 的格式以下:
<nonterminal> -> <pattern> : <result
其基本含義就是,「運用 pattern 進行匹配,若是成功了,返回解析對應的 nonterminal」。聽起來有點繞吧,讓咱們看看基於咱們的微型 grammar,它是如何運做的:
%% src/number_parser.yrl Nonterminals document values value list list_items. Terminals int float '[' ']'. Rootsymbol document. document -> values : '$1'. values -> value : ['$1']. values -> value values : ['$1'] ++ '$2'. value -> int : {int, unwrap('$1')}. value -> float : {float, unwrap('$1')}. value -> list : '$1'. list -> '[' list_items ']' : '$2'. list_items -> value : ['$1']. list_items -> value list_items: ['$1'] ++ '$2'. Erlang code. unwrap({_,_,V}) -> V.
咱們設定 document 做爲 Rootsymbol,所以它是 grammar 的最頂層,而後咱們設定 int,float,方括號做爲 terminals,這些東西對應 lexer 中產生的 tokens。而後咱們設定 rules,可能語法會比較怪。咱們看下 value 的 rules。
value -> int : {int, unwrap('$1')}. value -> float : {float, unwrap('$1')}. value -> list : '$1'.
第一行是說若是你有個 int token,而後咱們就能構造一個 value。代碼中冒號後面的代碼就是構造器,咱們進一步分解。'$1'表示模板匹配中的第一個元素。這種場景下,pattern(int) 中只有一個元素,就是它自己所表明的值。這個 int 是 lexer 產生的 token,咱們回想下,lexer 產生的 tokens 是這種格式 {type_atom, line_num, value}。此時咱們知道它是一個 int,咱們不須要行號,咱們關心的只是它的值。咱們編寫了一個 helper 方法 unwrap,恰好放到文件的最底部,也就是 Erlang code 部分。這種狀況下,它會返回一個包含整數表現形式的 char_list,而後咱們會返回一個包含 int atom 的 tuple,還包含 char list。這裏咱們將 char list 轉換成 integer,而後簡單的將其返回。這就是一種設計方式,就看你怎麼處理了。
接下來看 float 的 rule,它的工做方式同 int是同樣的,代碼自己一目瞭然。
最後一個是 list 的 rule,此時,你會注意到咱們沒有對 list 的值作 unwrap,咱們只是直接返回值,由於它不是由 lexer 建立的,它是由咱們的其餘規則建立的。 具體以下:
list -> '[' list_items ']' : '$2'. list_items -> value : ['$1']. list_items -> value list_items: ['$1'] ++ '$2'
這個東西看上去有點複雜。第一條 rule 就是說:所謂 list 就是一對方括號裏面放上 list_items,其值由第二部分的 pattern 提供,就是 list_items。這條規則用來強制保證方括號的存在。
下一條 rule 用來描述一個 list_items 只包含一個元素的狀況。它會接受一個單一值,在列表中返回 ([$1]),其後第二條 rule 遞歸地獲取值,放入 list 中,而後追加到已存在的 list_items中。這種方式在 yecc 中是很是通用的捕獲列表的方式,咱們編寫一樣的模板,讓 document 可以容納列表值,代碼以下:
values -> value : ['$1']. values -> value values : ['$1'] ++ '$2'.
讓咱們將 lexer 跟 parser 放到一塊兒:
iex(1)> {:ok, tokens, _} = :number_lexer.string('1,[2,3]') {:ok, [{:int, 1, 1}, {:"[", 1, '['}, {:int, 1, 2}, {:int, 1, 3}, {:"]", 1, ']'}], 1} iex(2)> :number_parser.parse(tokens) {:ok, [{:int, 1}, [int: 2, int: 3]]}
正常工做了!下一篇文章,我會討論一些小技巧,用於表示更復雜的 grammar。 在本文寫做期間,我發現 Knut Nesheim’s simple calculator app calx 和這篇文章 on the relops blog 是很是有幫助的。我建議你也去看看。