【问题标题】:Why doesn't parser combinator backtrack?为什么解析器组合器不回溯?
【发布时间】:2015-11-16 22:32:25
【问题描述】:

考虑

import util.parsing.combinator._
object TreeParser extends JavaTokenParsers {

    lazy val expr: Parser[String] = decimalNumber | sum
                                                  //> expr: => TreeParser.Parser[String]
    lazy val sum: Parser[String] = expr ~ "+" ~ expr ^^ {case a ~ plus ~ b => s"($a)+($b)"}
                                                  //> sum: => TreeParser.Parser[String]
    println(parseAll(expr, "1 + 1"))                       //> TreeParser.ParseResult[String] = [1.3] failure: string matching regex 
                                              //| `\z' expected but `+' found
}

与 fastparse 相同的故事

import fastparse.all._
val expr: P[Any] = P("1" | sum)
val sum: P[Any] = expr ~ "+" ~ expr
val top = expr ~ End
println(top.parse("1+1")) // Failure(End:1:2 ..."+1")

解析器很高兴发现采用第一个字面量是一个坏主意,但不要试图退回到求和产生式。为什么?

我知道解析器采用第一个可以成功吃掉输入字符串的一部分并退出的分支。这里,表达式的“1”匹配第一个输入字符,解析完成。为了获取更多,我们需要将 sum 设为第一个选择。然而,愚蠢的

lazy val expr: Parser[String] = sum | “1”

endы up with stack overflow。因此,图书馆作者从另一个方面接近它

val sum: P[Any] = P( num ~ ("+".! ~/ num).rep )
val top: P[Any]   = P( sum ~ End )

在这里,我们以终端开始求和,这很好,但这种语法更冗长,此外,它会产生一个终端,然后是一个列表,这对归约运算符很有用,如 sum,但很难映射到一系列二元运算符。

如果您的语言定义了允许二元运算符的表达式怎么办?您想匹配expr op expr 的每一个出现并将其映射到相应的树节点

expr ~ "op" ~ expr ^^ {case a ~ _ ~ b => BinOp(a,b)"} 

你是怎么做到的?简而言之,我想要一个贪婪的解析器,它会消耗整个字符串。这就是我所说的“贪婪”而不是贪婪的算法,它跳进第一辆马车并最终陷入死胡同。

【问题讨论】:

标签: scala regex-greedy parser-combinators fastparse


【解决方案1】:

因为我有found here,我们需要用秘密|||替换|替代运算符

//lazy val expr: Parser[String] = decimalNumber | sum
lazy val backtrackGreedy: Parser[String] =  decimalNumber ||| sum

lazy val sum: Parser[String] = decimalNumber ~ "+" ~ backtrackGreedy ^^ {case a ~ plus ~ b => s"($a)+($b)"}

println(parseAll(backtrackGreedy, "1 + 1")) // [1.6] parsed: (1)+(1)

替代的顺序与此运算符无关。为了阻止堆栈溢出,我们需要消除左递归,sum = expr + expr => sum = number + expr

Another answer 表示我们需要归一化,即代替

  def foo = "foo" | "fo"
  def obar = "obar"

  def foobar = foo ~ obar

我们需要使用

def workingFooBar = ("foo" ~ obar) | ("fo" ~ obar)

但第一个解决方案更引人注目。

【讨论】:

    【解决方案2】:

    解析器会回溯。以val expr: P[String] = P(("1" | "1" ~ "+" ~ "1").!)expr.parse("1+1") 为例。

    问题出在你的语法上。 expr 解析 1 并且根据您的定义它是成功的解析。然后sum 失败了,现在你想责怪尽职尽责的expr 发生了什么?

    有很多关于如何处理二元运算符的例子。比如这里的第一个例子:http://lihaoyi.github.io/fastparse/

    【讨论】:

    • 我见过这些例子。您没有注意到问题实际上是为什么解析器不贪婪。输入字符串对应于语法。可以解析为 1 ~ sum ~ 1 表达式。但是,事实并非如此。我只是想知道为什么要将语法产生规则定义为expr = expr ~ rep(op ~ expr) 而不是简单的expr op expr。我想解析器的性质是原因。这取决于解析器可以处理哪些上下文无关的生产规则。我希望该领域的专家澄清一下。
    • 它会变得贪婪。但是,通过切换1sum 的位置,变得贪婪并不能解决您的问题。
    • 但是,如果您的问题是,如果解析器可以解析您在此处定义的语法,我想这也许就是您要寻找的:richard.myweb.cs.uwindsor.ca/PUBLICATIONS/PADL_08.pdf
    • 你神秘地表达自己。为什么一个解释编译器在切换1sum 的位置时会失败?
    • 假设解析器是贪婪的。会发生什么? expr 选择 sum 而不是 1 => sum 解析 expr => expr 是贪婪的,所以它再次选择 sum 而不是 1... 这只会导致同样的无穷无尽循环如P(sum | "1"),通常称为左递归语法。我的观点是,您想要的不仅仅是贪婪的解析器。有一些技术可以得到你想要的,但算法完全不同,时间/空间复杂度更高——有关详细信息,请查看我之前提供的链接。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多