swift-petitparser 1.2.1

swift-petitparser 1.2.1

Philipp Arndt 维护。



  • Philipp Arndt

Swift 的 PetitParser

GitHub License

编程语言的语法传统上静态指定。由于不可避免地会出现歧义,它们难以组合和重用。PetitParser 结合了无扫描解析、解析器组合器、解析表达式语法和 packrat 解析器的想法,以将语法和解析器建模为可以动态重新配置的对象。

此库基于 Lukas Renggli 的 java-petitparser。它已主要作为编码 kata 调整为 Swift。

安装

Swift 包

https://github.com/philipparndt/swift-petitparser.git 用作 Swift 包

CocoaPod

target 'MyApp' do
  pod 'swift-petitparser', '~> 1.0'
end

教程

编写简单的语法

使用 PetitParser 编写语法饼图非常简单,就像编写 Swift 代码一样。例如,要编写一个可以解析以字母开头,后跟零个或多个字母或数字的识别符的语法,可以定义如下所示:

我们使用以下导入和类型别名来表示所有示例:

import swift_petitparser
typealias CP = CharacterParser
typealias SP = StringParser
class Example {
  init() {
    let id = CP.letter().seq(CP.letter().or(CP.digit()).star())
    ...
  }
}

或者使用运算符重载

class Example {
  init() {
    let id = CP.letter() + (CP.letter() | CP.digit())<*>
    ...
  }
}

如果您在调试器中查看对象 id,会发现上面的代码构建了一个解析器对象的树

  • SequenceParser:此解析器接受一串解析器。
    • CharacterParser:此解析器接受单个字母。
    • PossessiveRepeatingParser:此解析器接受零个或多个另一个解析器。
      • ChoiceParser:此解析器接受单个单词字符。
        • CharacterParser:此解析器接受单个字母。
        • CharacterParser:此解析器接受单个数字。

解析一些输入

要实际解析一个 String,我们可以使用方法 Parser#parse(String)

let id1 = id.parse("yeah")
let id2 = id.parse("f12")

方法 String 返回 Result,它是 SuccessFailure 的实例。在上述两个示例中,我们都是成功的,可以使用 Success#get() 来检索解析结果

print(id1.get()!)  // ["y", ["e", "a", "h"]]
print(id2.get()!)  // ["f", ["1", "2"]]

虽然以字符作为返回值的嵌套数组看起来很奇怪,但这通常是输入到解析树中的默认分解。稍后我们将看到如何定制它。

如果我们尝试解析无效的内容,我们将得到一个 Failure 实例作为答案,并且我们可以使用 Failure#getMessage() 来检索一个描述性的错误消息

let text = "123"
let id3 = id.parse(text)
print(id3.message!)                         // "letter expected"
print(id3.position.utf16Offset(in: text))   // 0

尝试通过调用 Failure#get() 来检索解析结果将返回一个空的可选值。可以使用 Result#isSuccess()Result#isFailure() 来判断解析是否成功。

如果您只对某个字符串是否匹配感兴趣,可以使用辅助方法 Parser#accept(String)

print(id.accept("foo"))  // true
print(id.accept("123"))  // false

不同类型的解析器

PetitParser 提供了一个大型的预构建解析器集,您可以组合它们来消费和转换任意复杂的语言。端解析器是最简单的。我们之前已经见过一些

  • CharacterParser.of("a") 解析字符 a
  • StringParser.of("abc") 解析字符串 abc
  • CharacterParser.any() 解析任何字符。
  • CharacterParser.digit() 解析从 09 的任何数字。
  • CharacterParser.letter() 解析从 az 以及从 AZ 的任何字母。
  • CharacterParser.word() 解析任何字母或数字。

CharacterParserStringParser中可用许多其他解析器。

因此,我们不必使用字母和数字谓词,可以将我们的标识符解析器编写如下

let id = CP.letter().seq(CP.word().star())

或者甚至

let id = CP.letter() + CP.word()<*>

下一组解析器用于组合其他解析器。

  • p1.seq(p2)解析紧跟在p1后面的p2(序列)。
  • p1.or(p2)如果解析失败,则解析p1,失败后解析p2(有序选择)。
  • p.star()解析p零次或多次。
  • p.plus()解析p一次或多次。
  • p.optional()尝试解析p
  • p.and()解析p,但不消费其输入。
  • p.not()解析p并在p失败时成功,但不消费其输入。
  • p.end()解析p并在输入末尾成功。

要给解析器添加操作或转换,可以使用以下方法

  • p.map { somthing_with_$0 }基于函数执行转换。
  • p.pick(n)返回p返回的列表中的第n个元素。
  • p.flatten()p的结果创建一个字符串。
  • p.token()p的结果创建一个令牌。
  • p.trim()修剪p前后空白。

要返回解析标识符的字符串,我们可以按如下方式修改我们的解析器

let id_b = CP.letter().seq(CP.word().star()).flatten()
print(id_b.parse("yeah").get()!) // yeah

或者

let id_b = (CP.letter() +  CP.word()<*>).flatten()
print(id_b.parse("yeah").get()!) // yeah

要方便地在给定的输入字符串中找到所有匹配项,您可以使用Parser#matchesSkipping(String)

let id = CP.letter().seq(CP.word().star()).flatten()
let matches: [String] = id.matchesSkipping("foo 123 bar4")
print(matches)  // ["foo", "bar4"]

这些是构建解析器的基本元素。在Parser类中还有一些更详细说明和测试的工厂方法。如果您想,可以浏览它们的文档和测试。

编写更复杂的语法

现在我们能够编写一个更复杂的语法来评估简单的算术表达式。在文件中,我们从数字(实际上是一个整数)的语法开始

let number = CP.digit().plus().flatten().trim()
    .map { (d: String) -> Int in Int(d)! }

// let number = CP.digit()<+>.flatten().trim()
//            .map { (d: String) -> Int in Int(d)! }
            
print(number.parse("123").get()!) // 123

然后,按照优先级定义加法和乘法的生成式。请注意,我们预先实例化生成式,因为它们会相互递归。稍后,我们可以通过设置它们的引用来解决这种递归。

let term = SettableParser.undefined()
let prod = SettableParser.undefined()
let prim = SettableParser.undefined()

term.set(prod.seq(CP.of("+").trim()).seq(term)
    .map { (nums: [Any]) -> Int in (nums[0] as! Int) + (nums[2] as! Int) }
    .or(prod))

prod.set(prim.seq(CP.of("*").trim()).seq(prod)
    .map { (nums: [Any]) -> Int in (nums[0] as! Int) * (nums[2] as! Int) }
    .or(prim))

prim.set((CP.of("(").trim().seq(term).seq(CP.of(")").trim()))
    .map { (nums: [Any]) -> Int in nums[1] as! Int }
    .or(NumbersParser.int()))

为确保我们的解析器消耗所有输入,我们将它用end()解析器包裹在起始生成式中

let start = term.end()

那就行了,现在我们可以测试我们的解析器和评估器

print(start.parse("1 + 2 * 3").get()!)  // 7
print(start.parse("(1 + 2) * 3").get()!)  // 9

作为练习,我们可以扩展解析器以接受负数和浮点数,而不仅仅是整数。此外,支持减法和除法也会很有用。所有这些功能都可以使用几行PetitParser代码添加。

使用表达式构建器

编写这样的表达式解析器很常见,但也可能相当棘手。为了简化任务,PetitParser附带了一个构建器,可以帮助您轻松定义此类语法。它支持定义运算符优先级;以及前缀、后缀、左结合和右结合运算符。

以下代码创建了一个空的表达式构建器

let builder = ExpressionBuilder()

然后我们按降序定义运算符组。最高优先级的是文字数字本身。这次我们接受浮点数,而不仅仅是整数。在同一个组中,我们添加了对括号的支持

builder.group()
    .primitive(NumbersParser.double())
    .wrapper(CP.of("(").trim(), CP.of(")").trim(), { (nums: [Any]) -> Any in nums[1] })

接下来是普通的算术运算符。注意,操作块会按照出现在解析输入中的顺序接收项和解析过的运算符。

// negation is a prefix operator
builder.group()
.prefix(CP.of("-").trim(), { (nums: [Any]) -> Double in
    -(nums[1] as! Double)
})

// power is right-associative
builder.group()
.right(CP.of("^").trim(), { (nums: [Any]) -> Double in
    pow((nums[0] as! Double), (nums[2] as! Double))
})

// multiplication and addition are left-associative
builder.group()
.left(CP.of("*").trim(), { (nums: [Any]) -> Double in
    (nums[0] as! Double) * (nums[2] as! Double)
})
.left(CP.of("/").trim(), { (nums: [Any]) -> Double in
    (nums[0] as! Double) / (nums[2] as! Double)
})

builder.group()
.left(CP.of("+").trim(), { (nums: [Any]) -> Double in
    (nums[0] as! Double) + (nums[2] as! Double)
})
.left(CP.of("-").trim(), { (nums: [Any]) -> Double in
    (nums[0] as! Double) - (nums[2] as! Double)
})

最后,我们可以构建解析器

let parser = builder.build().end()

执行以上代码后,我们得到一个高效的解析器,可以正确评估如下表达式

parser.parse("-8").get()!    // -8
parser.parse("1+2*3").get()! // 7
parser.parse("1*2+3").get()! // 5
parser.parse("8/4/2").get()! // 1
parser.parse("2^2^3").get()! // 256

您可以在此作为测试用例找到此示例: ExamplesTest.java

杂项

历史

原始的PetitParser由Lukas Renggli在小型机上实现

并修改为 Swift,由Philipp Arndt完成

这些实现在API和功能上非常相似。如果可能的话,它们会采用目标语言的最佳实践。

实现

许可协议

MIT许可证,请参阅 LICENSE