經過 LPeg 介紹解析表達式語法(Parsing Expression Grammars)

經過 LPeg 介紹解析表達式語法(Parsing Expression Grammars)

說明: 本文是對 An introduction to Parsing Expression Grammars with LPeg 的翻譯, 此文很是好, 從最簡單的模式(Pattern)講起, 逐步擴展, 最終完成一個計算器表達式的解析器, 很是適合初學者按部就班地理解學習 LPeghtml

目錄

什麼是 PEG

PEG 或者說 解析表達式語法, 是用字符串匹配來描述語言(或模式)的方式. 跟正則表達式不一樣, PEG 能夠解析一門完整的語言, 包括遞歸結構. 從分類而言, PEG 跟上下文無關文法--經過像YaccBison這樣的工具來實現--很類似.node

注意: 語法是語言的規格. 在實際中, 咱們用語法把某些語言的輸入字符串轉換成內存中咱們能夠操做的對象.git

跟上下文無關語法(CFG)相比, PEG 的實現方式很是不一樣. 不一樣於CFG中被歸約爲狀態機, PEG 採用順序解析. 這意味着你編寫的解析規則的順序很重要. 隨後將會提供一個例子. 它們可能會比 CFG 更慢一些, 可是實際中它們至關快. 從概念上來講 PEG 跟一種一般被稱爲遞歸降低的手寫模式很類似.github

PEG 更容易寫, 一般你不須要寫一個單獨的掃描器(譯者注:此處指詞法掃描): 你的語法直接做用於輸入的文本, 而不須要標識化的步驟(譯者注:此處指把輸入文本經過詞法掃描器分解成 token 序列)。正則表達式

注意: 若是你對上面這些都沒什麼感受也別擔憂, 經過這個指南你會明白它們是如何工做的.算法

什麼是 LPeg

關於 PEG 我首先介紹的是 LPeg, PEG 算法的一個 Lua 實現. LPeg 語法直接在Lua代碼中指定. 這和大多數其餘採用編譯器-編譯器模式(compiler compiler pattern)的工具都不一樣.express

在編譯器-編譯器模式中, 你用一種定製的領域特定語言來書寫語法, 而後把它編譯爲你的目標語言. 有了 LPeg 你只需編寫 Lua 代碼便可. 每一個解析單元(Parsing Unit)或模式對象都是語言中的一類對象(first class). 它能夠被 Lua 的內置操做符和控制語句組合起來. 這使得它成爲一種表達語法的很是強大的方式.編程

MoonScript’s grammar 是一個規模更大用來解析一種完整的編程語言moonscriptLPeg 語法的例子.數組

安裝 LPeg

你能夠經過 luarocks.org 來安裝 LPeg:編程語言

luarocks install lpeg

一旦安裝完成, 你能夠經過 require 來加載模塊:

local lpeg = require("lpeg")

一些簡單的語法

LPeg 提供一系列的單和雙字母命名的函數, 把 Lua 字面量轉換成模式對象. 模式對象能被組合起來製造出更復雜的模式, 或對一個字符串調用檢查匹配, 模式對象的操做符被重載用來提供不一樣的組合方式.

爲了簡潔起見, 咱們假定 lpeg 被導入到全部的例子中:

local lpeg = require("lpeg")

我會嘗試解釋用在這個指南中的例子裏每同樣東西, 可是爲了對全部內置函數都能有一個全面的瞭解,我建議閱讀 LPeg 官方使用手冊

字符串等價

咱們能作出的最簡單的例子就是檢查一個字符串跟另外一個字符串相等:

lpeg.P("hello"):match("world") --> 不匹配, 返回 nil
lpeg.P("hello"):match("hello") --> 一個匹配, 返回 6
lpeg.P("hello"):match("helloworld") --> 一個匹配, 返回 6

默認狀況下,成功匹配時,LPeg 將返回字符消耗數(譯者注: 也就是成功匹配子串以後的下一個字符的位置). 若是你只是想看看是否匹配這就足夠好了,但若是你試圖解析出字符串的結構來,你必須用一些 LPeg 的捕獲(capturing)函數.

注意: 值得注意的是, 即便沒有獲得字符串的末尾, 匹配仍然會成功. 你能夠用 -1 來避免這種狀況. 我會在下面描述.

模式組合

乘法和加法運算符是用於組合模式的最經常使用的兩個被重載的運算符.

  • 乘法能夠被想成跟 and 同樣的,左邊的操做數必須匹配,同時右邊的操做數必須匹配.

  • 加法能夠被想成跟 or 同樣的,要麼是左操做數匹配,要麼右操做數必須匹配。

這兩個運算符都被要求保持順序. 左邊操做數一直要在右邊操做數以前被檢查. 這裏有一些例子:

local hello = lpeg.P("hello")
local world = lpeg.P("world")

-- 譯者注:若是是在 Lua 的命令行交互模式下執行, 記得去掉 local, 不然會報錯
local patt1 = hello * world
local patt2 = hello + world


-- hello followed by world
patt1:match("helloworld") --> matches
patt1:match("worldhello") --> doesn't match

-- either hello or world
patt2:match("hello") --> matches
patt2:match("world") --> matches

注意: 正常的Lua 運算符的處理規則應用到這些操做符上, 所以當須要的時候你將不得不使用括號.

解析數字

有了這個基礎, 咱們如今能夠寫一個語法來作些什麼. 讓咱們寫一個從任意字符串中提取全部整數的的語法。

該算法將工做以下:

  • 對於每一個字符...
    • 若是它是十進制數字字符, 開始捕獲...
      • 消耗掉每一個字符, 若是它是一個十進制數字字符(譯者注:這裏的消耗意指比較指針後移一位)
    • 不然忽略, 跳到下一個字符

LPeg 中寫一個解析器我喜歡的途徑是首先寫出最具體的模式。而後使用這些模式做爲積木來組裝出最終結果。幸運的是,每個模式都是一個 Lua 中使用 LPeg 的一類對象,因此很容易單獨測試每一個部件.

首先, 咱們寫一個解析一個整數的模式:

local integer = lpeg.R("09")^1

這個模式將會匹配 09 之間的任一個數字字符 1 次或者屢次. LPeg 中的全部模式都是貪婪的.

咱們但願做爲返回值的數字值不是匹配結束的字符偏移值. 咱們可以當即用一個 / 運算符來應用一個捕獲變換函數:

local integer = lpeg.R("09")^1 / tonumber

print(integer:match("2923")) --> The number 2923

譯者注: 這裏爲清楚顯示 / 的做用, 補充下面的對比, 若是不加 / tonumber, 那麼返回的就是匹配子串位置後移一位的位置:

> integer = lpeg.R("09")^1 / tonumber
> print(integer:match("2923"))
2923
> integer = lpeg.R("09")^1
> print(integer:match("2923"))
5
>

它的工做機制是, 經過把模式匹配的結果「2923」作爲一個字符串來捕獲,並將其傳遞給Lua函數 tonumbermatch 的返回值是一個從字符串解析獲得的標準數字值。

注意: 若是在調用 match 時一個捕獲被使用, 那麼缺省的返回值會被替換成捕獲到的值.

如今咱們寫一個解析器, 用來匹配一個整數或者一些其餘字符:

local integer_or_char = integer + lpeg.P(1)

注意: 當使用 LPeg 的操做符重載時, 它會經過把全部的 Lua 字面量傳遞給 P 而自動地把它們轉換爲模式. 在上述的例子中咱們能夠只寫 1 來取代 lpeg.P(1)

(譯者注: 也就是形如: local integer_or_char = integer + 1)

在這裏順序是很重要的:

完成咱們的語法只須要重複咱們已有的部件, 而且用 Ct 把捕獲到的結果存儲到一個表中:

local extract_ints = lpeg.Ct(integer_or_char^0)

這裏是完整的語法個一些運行例子:

local integer = lpeg.R("09")^1 / tonumber
local integer_or_char = integer + lpeg.P(1)
local extract_ints = lpeg.Ct(integer_or_char^0)

-- Testing it out:

extract_ints:match("hello!") --> {}
extract_ints:match("hello 123") --> {123}
extract_ints:match("5 5 5 yeah 7 7 7 ") --> {5,5,5,7,7,7}

一個計算器語法解析器

接下來咱們準備構建一個計算器表達式解析器. 我重點強調解析器是由於咱們不會去求值表達式而是去構建一個被解析表達式的語法樹.

若是你曾打算創建一種編程語言,你幾乎老是要解析出一個語法樹. 針對這個計算器的語法樹例子是一個很好的練習.

在寫下任意代碼以前咱們應該定義能被解析的語言. 它應該可以解析整數, 加法, 減法, 乘法和除法. 它應該清楚運算符優先級。它應該容許操做符和數字之間的任意空格.

這裏是一些輸入例子(由換行符分割):

1*2
1 + 2
5 + 5/2
1*2 + 3
1 * 2 - 3 + 3 + 2

接着咱們設計語法樹的格式: 咱們如何把這些解析表達式映射爲 Lua 友好的表示?

對於普通的整數, 咱們能夠直接把它們映射爲 Lua 中的整數. 對於任意二元表達式(加法,除法等), 咱們將會使用 Lisp 風格的 S-表達式(S-Expression) 數組, 數組中的第一個項目是被當作字符串的運算符, 數組中的第 2, 第 3 個項目是運算符左邊的操做數和右邊的操做數.

光用嘴說很麻煩, 用例子很容易領悟:

parse("5") --> 5
parse("1*2") --> {"*", 1, 2}
parse("9+8") --> {"+", 9, 8}
parse("3*2+8") --> {"+", {"*", 3, 2}, 8}

上面的 parse 函數將會成爲咱們建立的語法.

以規範的方式進行,咱們就能夠開始編寫解析器。像之前同樣,咱們開始儘量具體:

local lpeg = require("lpeg")

-- 譯者注:處理空格,包括製表符, 回車符, 換行符
local white = lpeg.S(" \t\r\n") ^ 0

local integer = white * lpeg.R("09") ^ 1 / tonumber
local muldiv = white * lpeg.C(lpeg.S("/*"))
local addsub = white * lpeg.C(lpeg.S("+-"))

爲了容許任意空格, 我製造了一個空格模式對象 white, 並把它加到全部其餘模式對象的前面. 經過這種方式, 咱們能夠自由地使用模式對象, 而沒必要去考慮空格是否已經被處理.

咱們重複利用了上面的整數模式 integer, 匹配運算符的模式是直線前進. 基於它們的優先級我已經把運算符分紅兩組不一樣的模式.

在嘗試編寫整個語法以前,讓咱們專一於讓單個組件工做起來。我選擇編寫整數或乘法/除法的解析程序.

注意: 編程語言的創造者一向把乘法優先級稱爲因子(factor), 把加法優先級稱爲項(term)。咱們將在這裏使用這一術語.

local factor = integer * muldiv * integer + integer

factor:parse("5") --> 5
factor:parse("2*1") --> 2 "*" 1

咱們上面工做的乘法運算,但有一個問題。 該模式的捕獲(在本例中的返回值)是錯誤的。它按: 運算符 的順序返回多個值。咱們須要的是一個第一個項目爲運算符的表。

爲了修復這個問題, 咱們將會建立一個變換函數, 節點構造器(node constructor)以下:

local function node(p)
  return p / function(left, op, right)
    return { op, left, right }
  end
end

local factor = node(integer * muldiv * integer) + integer

factor:match("5") --> 5
factor:match("2*1") --> {"*", 2, 1}

看起來很好, 如今咱們能夠利用節點構造器來構建剩下的語法了.

由於咱們正在構建一個遞歸語法, 咱們將會使用 lpeg.P 的語法形式. 讓咱們以這種語法形式重寫上述代碼:

local calculator = lpeg.P({
  "exp",
  exp = lpeg.V("factor") + integer,
  factor = node(integer * muldiv * integer)
})

這裏有一些新東西. 第一個是語法表裏的 "exp", 它是咱們語法的根模式. 這意味着被命名爲 exp 的模式將會第一個被執行.

lpeg.V 在咱們的語法中被用來引用非終結符(non-terminal). 這就是咱們如何作的遞歸,經過對未被聲明過的模式的引用. 這種特殊的語法不是遞歸的,但它仍然演示了 v 如何被使用。

PEG 中咱們不能使用任何一種會致使解析器進入無限循環的遞歸. 爲了達到咱們想要的優先級,咱們須要聰明地構造咱們的模式。

因爲factor、乘法和除法的優先級最高,因此它應該是模式層次結構中最深的。

讓咱們從新設計咱們的 factor 解析器來處理有重複乘法/除法的狀況:

local calculator = lpeg.P({
  "exp",
  exp = lpeg.V("factor") + integer,
  factor = node(integer * muldiv * (lpeg.V("factor") + integer))
})

譯者注: 這裏能夠根據這段代碼寫出對應的 BNF :

<calculatar> ::= <exp>
<exp> ::= <factor> | <integer>
<factor> ::= <integer>  <muldiv>  { <factor> | <integer>}
<integer> ::= Number
<muldiv> ::= '*' | '/'

使用右遞歸容許任意數量的乘法鏈。咱們能夠把同一優先級的運算符鏈起來而沒有任何問題。它能夠被解析爲:

calculator:match("5*3*2") --> {"*", {"*"}}

咱們工做的方式是下降優先級直到咱們到達語法的頂層. 接着是解析 term

local calculator = lpeg.P({
  "exp",
  exp = lpeg.V("term") + lpeg.V("factor") + integer,
  term = node((lpeg.V("factor") + integer) * addsub * lpeg.V("exp")),
  factor = node(integer * muldiv * (lpeg.V("factor") + integer))
})

term 模式只是對可能發生的狀況進行考慮。左側能夠是一個高優先級的 factor,或一個整數 integer. 右側能夠是相同優先級的 term, 或高優先級的 factor, 或整數 integer(請注意,這些都根據優先級順序列出)

咱們可以複用 exp 模式做爲 term 模式的左邊, 由於它恰好符合咱們想要的全部東西。

最後是使用了節點構造器的語法:

local lpeg = require("lpeg")
local white = lpeg.S(" \t\r\n") ^ 0

local integer = white * lpeg.R("09") ^ 1 / tonumber
local muldiv = white * lpeg.C(lpeg.S("/*"))
local addsub = white * lpeg.C(lpeg.S("+-"))

local function node(p)
  return p / function(left, op, right)
    return { op, left, right }
  end
end

local calculator = lpeg.P({
  "input",
  input = lpeg.V("exp") * -1,
  exp = lpeg.V("term") + lpeg.V("factor") + integer,
  term = node((lpeg.V("factor") + integer) * addsub * lpeg.V("exp")),
  factor = node(integer * muldiv * (lpeg.V("factor") + integer))
})

注意: 我增長了一個(line)模式, 檢查確保解析器到達了輸入的末尾.

結束

這就是這篇指南. 但願它對於你在本身的項目中開始使用 LPeg 已經足夠了. 用 LPeg 寫的語法 是對 Lua模式或正則表達式的一種很好的替代, 由於它們更容易閱讀,調試和測試. 此外,他們足夠強大到到可以實現這樣的解析器, 它能夠用於完整的編程語言!

在將來我但願寫更多的包括了我在實施 moonscript 中用到的更先進的技術的指導文檔。

相關文章
相關標籤/搜索