Ассоциативность операторов с использованием парсеров Scala

Итак, я пытался написать калькулятор с помощью синтаксического анализатора Scala, и это было забавно, за исключением того, что я обнаружил, что ассоциативность операторов обратная, и что когда я пытаюсь сделать свою грамматику леворекурсивной, даже при том, что она совершенно недвусмысленна, я получаю переполнение стека.

Чтобы уточнить, если у меня есть правило вроде: def subtract: Parser[Int] = num ~ "-" ~ add { x => x._1._1 - x._2 }, тогда оценка 7 - 4 - 3 получается 6 вместо 0.

То, как я на самом деле реализовал это, заключается в том, что я составляю двоичное дерево, в котором операторы являются нелистовыми узлами, а листовые узлы являются числами. То, как я оцениваю дерево, - это левый дочерний элемент (оператор) и правый дочерний элемент. При построении дерева для 7 - 4 - 5 я хотел бы, чтобы оно выглядело так:

-
-   5
7   4   NULL   NULL

где - корень, его дети - и 5, а второй - дети 7 и 4.

Однако единственное дерево, которое я могу легко построить, это

-
7   -
NULL   NULL   4   5

что отличается, а не то, что я хочу.

По сути, простая скобка 7 - (4 - 5), тогда как я хочу (7 - 4) - 5.

Как я могу взломать это? Я чувствую, что должен быть в состоянии написать калькулятор с правильным приоритетом оператора независимо от этого. Должен ли я сначала токенизировать все, а затем перевернуть свои токены? Могу ли я просто перевернуть свое дерево, взяв всех левых детей правых детей и сделав их правыми детьми родителя правого ребенка и сделав родителя левым ребенком бывшего правого ребенка? В первом приближении это кажется хорошим, но я действительно не задумывался об этом слишком глубоко. Я чувствую, что должен быть какой-то случай, который я упускаю.

У меня сложилось впечатление, что я могу сделать парсер LL только с парсерами scala. Если вы знаете другой способ, подскажите!


person nnythm    schedule 03.06.2012    source источник
comment
Пожалуйста, уточните, что вы подразумеваете под ассоциативностью операторов, которая является обратной.   -  person Daniel C. Sobral    schedule 03.06.2012
comment
Кстати, проверьте scala-dist для дальнейших примеров - я просто редактирую свой ответ по этой ссылке.   -  person Daniel C. Sobral    schedule 06.06.2012


Ответы (2)


Стандартная реализация комбинаторов парсеров в Scala (черта Parsers) не поддерживает леворекурсивные грамматики. Однако вы можете использовать PackratParsers если вам нужна левая рекурсия. Тем не менее, если ваша грамматика представляет собой простой анализатор арифметических выражений, вам определенно не нужна левая рекурсия.

Изменить

Есть способы использовать правую рекурсию и при этом сохранить левую ассоциативность, и если вы заинтересованы в этом, просто найдите арифметические выражения и парсеры рекурсивного спуска. И, конечно же, как я уже сказал, вы можете использовать PackratParsers, которые разрешают левую рекурсию.

Но самый простой способ справиться с ассоциативностью без использования PackratParsers — это не использовать рекурсию. Просто используйте один из операторов повторения, чтобы получить List, а затем foldLeft или foldRight по мере необходимости. Простой пример:

trait Tree
case class Node(op: String, left: Tree, right: Tree) extends Tree
case class Leaf(value: Int) extends Tree

import scala.util.parsing.combinator.RegexParsers

object P extends RegexParsers {
  def expr = term ~ (("+" | "-") ~ term).* ^^ mkTree
  def term = "\\d+".r ^^ (_.toInt)
  def mkTree(input: Int ~ List[String ~ Int]): Tree = input match {
    case first ~ rest => ((Leaf(first): Tree) /: rest)(combine)
  }
  def combine(acc: Tree, next: String ~ Int) = next match {
    case op ~ y => Node(op, acc, Leaf(y))
  }
}

Вы можете найти другие, более полные примеры на репозиторий scala-dist.

person Daniel C. Sobral    schedule 03.06.2012
comment
Как я могу сделать это без левой рекурсии? Кроме того, у меня сложилось впечатление, что библиотеки синтаксического анализа Scala по умолчанию оценивают слева направо и являются леворекурсивными, следовательно, являются LL, если не LL (k). - person nnythm; 04.06.2012
comment
@nnythm: На самом деле, ты прав. Библиотеки синтаксического анализа Scala по умолчанию являются синтаксическими анализаторами с рекурсивным спуском и, следовательно, LL (k), хотя я не знаю, что такое k для комбинаторов синтаксических анализаторов Scala. Грамматики LL(k) не могут обрабатывать левую рекурсию. Это парсеры LR, которые могут обрабатывать левую рекурсию, а комбинаторы парсеров Scala не являются парсерами LR. - person Ken Bloom; 04.06.2012
comment
правильно, я имел в виду, что они генерируют крайний левый вывод, а не то, что они рекурсивны слева. - person nnythm; 04.06.2012
comment
Вот синтаксический анализатор patrax import scala.util.parsing.combinator._ object SO JavaTokenParsers with PackratParsers { lazy val left: Parser[String] = left ~ ("+" ~> ident) ^^ {case a1 ~ a2 => s"Sum($a1,$a2)"} | ident ; println(parseAll(left, "a+b+c+d"))}. Почему происходит переполнение стека, несмотря на lazy val? - person Valentin Tihomirov; 06.01.2016

Я интерпретирую ваш вопрос следующим образом:

Если вы пишете такие правила, как def expression = number ~ "-" ~ expression, а затем оцениваете каждый узел синтаксического дерева, то вы обнаружите, что в 3 - 5 - 4 сначала вычисляется 5 - 4, что дает в результате 1, а затем вычисляется 3 - 1, что дает в результате 2.

С другой стороны, если вы пишете такие правила, как def expression = expression ~ "-" ~ number, эти правила являются леворекурсивными и переполняют стек.

Есть три решения этой проблемы:

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

  2. Определите правило как def expression = repsep(number, "-"), а затем при оценке вычислений перебирайте проанализированные числа (которые будут отображаться в плоском списке) в любом направлении, обеспечивающем вам необходимую ассоциативность. Вы не можете использовать это, если появится более одного вида оператора, так как оператор будет отброшен.

  3. Определите правило как def expression = number ~ ( "-" ~ number) *. У вас будет начальный номер плюс набор пар оператор-номер в плоском списке для обработки в любом нужном вам направлении (хотя слева направо, вероятно, здесь проще).

  4. Используйте PackratParsers, как предложил Даниэль Собрал. Это, вероятно, ваш лучший и самый простой выбор.

person Ken Bloom    schedule 04.06.2012
comment
Я строю дерево перед выполнением какой-либо оценки. Могу ли я просто преобразовать правоассоциативное дерево в левоассоциативное дерево? Я не смог найти никакой литературы об этом в Интернете, хотя в моей голове это, кажется, работает нормально. PackratParsers также дает мне переполнение стека в моей левой рекурсии, поэтому я думаю, что я собираюсь преобразовать дерево, если оно правильное. - person nnythm; 04.06.2012
comment
@nnythm Вероятно, вы объявляете парсеры packrat не как lazy val, а как def. Вы используете def с традиционными комбинаторами парсеров, lazy val с парсерами packrat. На самом деле, def просто позволяет без проблем использовать прямые ссылки и рекурсии, что lazy val тоже делает с небольшой ценой производительности. В грамматике без прямых ссылок или рекурсии вы можете объявить все как val. - person Daniel C. Sobral; 04.06.2012
comment
@Daniel: Честно говоря, я не знаю, невозможно ли создать комбинаторы LL (k) parserc или они просто не реализованы. Если вы знаете, что это невозможно, не стесняйтесь удалить (в настоящее время), но имейте в виду, что я внес некоторые другие изменения, которые также разъяснили ваш ответ, поэтому не удаляйте их, если они также неверны. - person Ken Bloom; 04.06.2012
comment
@Daniel: После небольшого исследования Википедии я думаю, что везде, где мы до сих пор упоминали парсеры LL, мы на самом деле имели в виду парсеры LR? - person Ken Bloom; 04.06.2012
comment
@KenBloom Похоже на то. Я собираюсь отредактировать свой ответ соответственно. - person Daniel C. Sobral; 04.06.2012