【发布时间】:2020-06-06 21:49:46
【问题描述】:
我正在尝试使用来自 scala-parser-combinators 的 Scala 的 Packrat 解析器将布尔表达式解析为 Expr 树。
sealed trait Expr
case class Term(term: String) extends Expr
case class And(x: Expr, y: Expr) extends Expr
case class Or(x: Expr, y: Expr) extends Expr
aaa and bbb --> And(Term(aaa),Term(bbb))
aaa and bbb or ccc --> Or(And(Term(aaa),Term(bbb)),Term(ccc))
aaa and (bbb or ccc) --> And(Term(aaa),Or(Term(bbb),Term(ccc)))
这个语法似乎工作得很好:
object Parsing extends RegexParsers with PackratParsers {
override val skipWhitespace = false
val validIdentifiers = List("aaa", "bbb", "ccc", "ddd")
lazy val term: PackratParser[Term] = """\s*""".r ~> """\w+""".r flatMap { identifier =>
if (validIdentifiers.contains(identifier))
success(Term(identifier))
else
err(s"expected one of: $validIdentifiers")
}
lazy val and: PackratParser[And] =
expr ~ """\s+and\s+""".r ~ (term | parensExpr) ^^ { case e1 ~ _ ~ e2 => And(e1, e2) }
lazy val or: PackratParser[Or] =
expr ~ """\s+or\s+""".r ~ (term | parensExpr) ^^ { case e1 ~ _ ~ e2 => Or(e1, e2) }
lazy val parensExpr: PackratParser[Expr] = """\s*\(""".r ~> expr <~ """\s*\)""".r
lazy val expr: PackratParser[Expr] =
term ||| and ||| or ||| parensExpr
lazy val root: PackratParser[Expr] =
phrase(expr)
def parseExpr(input: String): ParseResult[Expr] =
parse(root, new PackratReader(new CharSequenceReader(input)))
}
但错误消息有时......很糟糕。
如果解析器在 and 的左侧发现无效标识符,它会正确地告诉我们。
println(parseExpr("invalidIdentifier and aaa"))
[1.18] error: expected one of: List(aaa, bbb, ccc, ddd)
invalidIdentifier and aaa
^
但是,如果它在and 的右侧 一侧发现无效标识符,它会给我们这个误导性的错误消息。
println(parseExpr("aaa and invalidIdentifier"))
[1.4] failure: end of input expected
aaa and invalidIdentifier
^
我很确定会发生这种情况,因为expr 将尝试所有 4 个选项:and/or/parensExpr 将失败,但 term 将通过 Term("aaa") 成功。
然后,root 的phrase 将启动并检查是否有任何输入要消耗,并失败,因为有:“和 invalidIdentifier”。
所以我想,我会将phrase 推低一层。换句话说,我改变了这个:
lazy val expr: PackratParser[Expr] =
term ||| and ||| or ||| parensExpr
lazy val root: PackratParser[Expr] =
phrase(expr)
进入这个:
lazy val expr: PackratParser[Expr] =
term ||| and ||| or ||| parensExpr
lazy val root: PackratParser[Expr] =
phrase(term) ||| phrase(and) ||| phrase(or) ||| phrase(parensExpr)
现在,所有 4 个选项都应该失败,但我们应该看到 and 的错误消息,因为 and 消耗的输入比其他 3 个选项多
我现在收到了更好的错误消息,但令我惊讶的是,一些以前有效的输入现在无效!
println(parseExpr("aaa or bbb"))
[1.4] failure: string matching regex '\s+and\s+' expected but ' ' found
aaa or bbb
^
println(parseExpr("aaa and bbb or ccc"))
[1.12] failure: end of input expected
aaa and bbb or ccc
^
我不明白为什么。
事实上,即使只是这样一个更简单、微不足道的改变:
// before
lazy val root: PackratParser[Expr] =
phrase(expr)
// after
lazy val root: PackratParser[Expr] =
phrase(term ||| and ||| or ||| parensExpr)
...打破以前有效的输入。
怎么会? root 的这两个定义不应该是等价的吗?这些解析器不是引用透明的吗?
更重要的是,我应该如何解决这个问题?
【问题讨论】:
-
不熟悉 Packrat,但有一个非常棒的解析库,它与 Scala 的语法相似:fastparse。如果重构不是那么大的开销,请看一下。我可能会为您省去一些麻烦,因为它有更好的文档。 PS:这个问题在那里可以解决。
-
@lprakashv 当我开始尝试实现这个时,我实际上正在使用 fastparse,但后来意识到 fastparse 不适合这些语法。我需要语法是左关联的(即
x and y and z应该被解析为(x and y) and z而不是x and (y and z)),并且fastparse 在左递归语法上永远递归,并溢出堆栈。另一方面,Packrat 解析器使用记忆来避免堆栈溢出,并且非常适合左递归语法。 -
这里有人和我在 fastparse->packrat 解析器上遇到了完全相同的问题:users.scala-lang.org/t/with-input-from-string/4737/20
-
我认为左递归语法可以通过一系列步骤来移除左递归,但我也不知道该怎么做:/
-
这是一个逐步删除左递归的算法:en.wikipedia.org/wiki/Left_recursion#Removing_left_recursion 注意:在您的情况下,它可能会导致树稍微丑陋(因为您提到了左关联性)
标签: scala parsing functional-programming parser-combinators