Elixir中如何使用 Leex 和 Yecc - 譯

Elixir中如何使用 leex 和 yecc - 1【譯】

翻譯: horsley 2019-5-31

前言

最近在學習 Elixir,想把之前用 ruby 寫的一個 DSL 遷移過來,所以看到了這篇文章,隨手翻譯,以茲備忘。 對於一些專業詞彙,保留英文更有利於理解,所以不做翻譯,加入本身的理解,記錄以下:html

  • grammar - 文法:應該比詞法、語法概念更大,是二者的綜合
  • parser - 語法分析器:好比 1 + 2 就是一個語法
  • lexer - 詞法分析器:1 + 2 會分解成 num, :+ , num 三個 token
  • token - (詞法分析後獲得的)關鍵詞
  • ast - 抽象語法樹
  • definition - .xrl 文件中:定義匹配詞的正則
  • rule - .xrl 中定義詞到 token 的轉換規則

原文地址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。單元測試

咱們看下 lexer 是如何構建的

%% 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 文件由三部分構成,這三部分都是必須的。學習

第一部分是 Definitions。

這一部分咱們會定義一些模板, lexer 以此來掃描輸入數據,進行適配識別。每一行的格式都是左邊匹配右邊的正則表達式,如 Whitespace = [\s\t]。注意,這些都是 Erlang 語言的正則表達,它是傳統正則表達式的子集,所以遇到複雜的表達式,你須要更有創造力些,本身想一想辦法。 細節能夠參考 leex 文檔。

要引用一條 definition,將其放到{}花括號裏面就好了,Definitions 部分或者 Rules 部分均可以這樣引用。

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 語法解析器

在咱們開始學習 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 文件由四部分構成:

  • Nonterminals
  • Terminals
  • Rootsymbol
  • Erlang code.

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 是很是有幫助的。我建議你也去看看。

相關文章
相關標籤/搜索