【问题标题】:Scala Packrat Parsers, error messages and referential transparencyScala Packrat 解析器、错误消息和引用透明度
【发布时间】: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") 成功。

然后,rootphrase 将启动并检查是否有任何输入要消耗,并失败,因为有:“和 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


【解决方案1】:

您发布的确切代码出现以下错误,我相信这些是预期的错误消息吗?:

@ println(Parsing.parseExpr("invalidIdentifier and aaa"))
[1.18] error: expected one of: List(aaa, bbb, ccc, ddd)

invalidIdentifier and aaa
                 ^


@ println(Parsing.parseExpr("aaa and invalidIdentifier"))
[1.26] error: expected one of: List(aaa, bbb, ccc, ddd)

aaa and invalidIdentifier
                         ^

我将 ammonite repl 与 ivy 导入的 Scala Parser Combinators 库一起使用:

import $ivy.`org.scala-lang.modules::scala-parser-combinators:1.1.2`

可能是库版本问题?

【讨论】:

  • 这很奇怪,我也在菊石上尝试过,但我得到了failure: end of input expected 错误...这是我菊石壳的完整日志:gist.github.com/dcastro/18b586421b0d12fbeda0c69e56df6912
  • 只是在这里猜测,但是,尝试在有效标识符列表和输入字符串中将“aaa”更改为“aaaaa”。也许如果匹配时间更长,那么||| 将给予它更高的优先级。
  • 您使用的是哪个版本的解析器组合器库?
  • 和你的一样,我什至复制粘贴了你的 ivy 命令。你能复制我日志中的命令吗?
  • 嘿,我实际上用... extends App ... 尝试过这个,这对我来说很糟糕!我的代码实际上是这样的:object Parsing extends RegexParsers with PackratParsers {...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-24
  • 2011-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-18
  • 2011-07-30
相关资源
最近更新 更多