Почему комбинатор парсера не отступает?

Учитывать

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» выражения соответствует первому входному символу, и синтаксический анализ завершается. Чтобы получить больше, нам нужно сделать сумму первой альтернативой. Впрочем, глупо

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

заканчивается переполнением стека. Поэтому авторы библиотеки подходят к ней с другой стороны

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

Здесь мы начинаем sum с терминала, что хорошо, но этот синтаксис более многословен, и, кроме того, он создает терминал, за которым следует список, что хорошо для оператора редукции, такого как sum, но его трудно сопоставить с рядом. бинарных операторов.

Что, если ваш язык определяет выражение, допускающее бинарный оператор? Вы хотите сопоставить каждое вхождение expr op expr и сопоставить его с соответствующим узлом дерева.

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

Как ты это делаешь? Короче говоря, мне нужен жадный парсер, который потребляет всю строку. Это то, что я подразумеваю под «жадным», а не жадным алгоритмом, который прыгает в первый вагон и заканчивается в тупике.


person Valentin Tihomirov    schedule 16.11.2015    source источник
comment
Я думаю, что это объясняет руководство по Artima. Это объясняет, почему мы должны предпочесть expr ::= term {"+" term} возврату expr ::= term + expr ∣ term и впадению в бесконечную рекурсию expr ::= expr + term ∣ term.   -  person Valentin Tihomirov    schedule 24.12.2015
comment
Вероятно, одна из причин заключается в том, что после сопоставления префикса они вызывают semantic action, который нельзя «откатить».   -  person Little Alien    schedule 02.11.2016


Ответы (2)


Как я нашел здесь, нам нужно заменить | альтернативный оператор секретным |||

//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.

Другой ответ говорит, что нам нужно нормализовать, то есть вместо

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

  def foobar = foo ~ obar

нам нужно использовать

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

Но первое решение более поразительно.

person Valentin Tihomirov    schedule 13.01.2016

Парсер делает возврат. Попробуйте, например, val expr: P[String] = P(("1" | "1" ~ "+" ~ "1").!) и expr.parse("1+1").

Проблема в твоей грамматике. expr анализирует 1, и по вашему определению это успешный анализ. Затем sum терпит неудачу, и теперь вы хотите обвинить в случившемся послушного expr?

Существует множество примеров того, как работать с бинарными операторами. Например, первый пример здесь: http://lihaoyi.github.io/fastparse/

person lastland    schedule 25.11.2015
comment
Я видел эти примеры. Вы не замечаете, что вопрос собственно в том, почему парсер не жадный. Входная строка соответствует грамматике. Его можно разобрать как 1 ~ сумма ~ 1 выражение. Однако это не так. Я просто хочу знать, почему я должен определять правила производства грамматики как expr = expr ~ rep(op ~ expr), а не просто expr op expr. Я предполагаю, что причина в природе парсера. Какие контекстно-свободные продукционные правила он может обрабатывать, зависит от синтаксического анализатора. Я хочу, чтобы специалист в этой области разъяснил. - person Valentin Tihomirov; 25.11.2015
comment
Это может стать жадным. Однако жадность не решит вашу проблему, как вы уже видели, поменяв местами 1 и sum. - person lastland; 25.11.2015
comment
Однако, если ваш вопрос заключается в том, может ли синтаксический анализатор анализировать грамматику, которую вы определили здесь, я думаю, что это, возможно, то, что вы ищете: richard.myweb.cs.uwindsor.ca/ПУБЛИКАЦИИ/PADL_08.pdf - person lastland; 25.11.2015
comment
Вы загадочно выражаете себя. Почему один из компиляторов должен интерпретировать неудачу при переключении позиций 1 и sum как жадный парсер? - person Valentin Tihomirov; 25.11.2015
comment
Предположим, парсер жадный. Что случится? expr выбирает sum вместо 1 => sum анализирует expr => expr является жадным, поэтому он снова выбирает sum вместо 1... Это приведет только к тому же бесконечному циклу, что и в P(sum | "1"), который обычно называют леворекурсивной грамматикой. Я хочу сказать, что то, что вы хотите, требует большего, чем просто жадный парсер. Существуют методы получения того, что вы хотите, но с совершенно другим алгоритмом и более высокой сложностью времени/пространства - для получения подробной информации проверьте ссылку, которую я предоставил ранее. - person lastland; 25.11.2015