经过 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 中用到的更先进的技术的指导文档。

相关文章
相关标签/搜索