Swift 的 PetitParser
编程语言的语法传统上静态指定。由于不可避免地会出现歧义,它们难以组合和重用。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
,它是 Success
或 Failure
的实例。在上述两个示例中,我们都是成功的,可以使用 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()
解析从 0 到 9 的任何数字。CharacterParser.letter()
解析从 a 到 z 以及从 A 到 Z 的任何字母。CharacterParser.word()
解析任何字母或数字。
在CharacterParser
和StringParser
中可用许多其他解析器。
因此,我们不必使用字母和数字谓词,可以将我们的标识符解析器编写如下
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。