нежадное сопоставление в Scala RegexParsers

Предположим, я пишу элементарный парсер SQL на Scala. У меня есть следующее:

class Arith extends RegexParsers {
    def selectstatement: Parser[Any] = selectclause ~ fromclause
    def selectclause: Parser[Any] = "(?i)SELECT".r ~ tokens
    def fromclause: Parser[Any] = "(?i)FROM".r ~ tokens
    def tokens: Parser[Any] = rep(token) //how to make this non-greedy?
    def token: Parser[Any] = "(\\s*)\\w+(\\s*)".r
}

При попытке сопоставить selectstatement с SELECT foo FROM bar, как мне предотвратить поглощение selectclause всей фразы из-за rep(token) в ~ tokens?

Другими словами, как указать нежадное сопоставление в Scala?

Чтобы уточнить, я полностью осознаю, что могу использовать стандартный нежадный синтаксис (*?) или (+?) внутри самого шаблона String, но мне было интересно, есть ли способ указать его на более высоком уровне внутри токенов def. Например, если бы я определил токен следующим образом:

def token: Parser[Any] = stringliteral | numericliteral | columnname

Тогда как я могу указать нежадное сопоставление для rep (токена) внутри токенов def?


person Magnus    schedule 18.10.2011    source источник
comment
Похоже, здесь мы имеем дело с с функцией PEG: жадно сопоставляется, но затем будет возвращаться и пытаться найти более короткие совпадения, если они не пройдены, и CFG пробует все возможности, операторы PEG *, + и ? всегда ведут себя жадно, потребляя как можно больше входных данных и никогда не откатываясь: выражение a* всегда будет потреблять как можно больше a как последовательно доступны во входной строке, что всегда приводит к сбою (a* a).   -  person Little Alien    schedule 02.11.2016


Ответы (1)


Не легко, потому что успешное совпадение не повторяется. Рассмотрим, например:

object X extends RegexParsers {
  def p = ("a" | "aa" | "aaa" | "aaaa") ~ "ab"
}

scala> X.parseAll(X.p, "aaaab")
res1: X.ParseResult[X.~[String,String]] = 
[1.2] failure: `ab' expected but `a' found

aaaab
 ^

Первое совпадение было успешным, в парсере внутри скобок, поэтому он перешел к следующему. Тот провалился, так что p провалился. Если бы p было частью альтернативных совпадений, альтернатива была бы опробована, поэтому хитрость заключается в том, чтобы создать что-то, что может справиться с такими вещами.

Допустим, у нас есть это:

def nonGreedy[T](rep: => Parser[T], terminal: => Parser[T]) = Parser { in =>
  def recurse(in: Input, elems: List[T]): ParseResult[List[T] ~ T] =
    terminal(in) match {
      case Success(x, rest) => Success(new ~(elems.reverse, x), rest)
      case _ => 
        rep(in) match {
          case Success(x, rest) => recurse(rest, x :: elems)
          case ns: NoSuccess    => ns
        }
    }

  recurse(in, Nil)
}  

Затем вы можете использовать его следующим образом:

def p = nonGreedy("a", "ab")

Между прочим, я всегда находил, что наблюдение за тем, как определяются другие вещи, полезно при попытке придумать такие вещи, как nonGreedy выше. В частности, посмотрите, как определяется rep1 и как он был изменен, чтобы избежать повторной оценки его параметра повторения - то же самое, вероятно, было бы полезно для nonGreedy.

Вот полное решение с небольшими изменениями, чтобы не потреблять «терминал».

trait NonGreedy extends Parsers {
    def nonGreedy[T, U](rep: => Parser[T], terminal: => Parser[U]) = Parser { in =>
      def recurse(in: Input, elems: List[T]): ParseResult[List[T]] =
        terminal(in) match {
          case _: Success[_] => Success(elems.reverse, in)
          case _ => 
            rep(in) match {
              case Success(x, rest) => recurse(rest, x :: elems)
              case ns: NoSuccess    => ns
            }
        }

      recurse(in, Nil)
    }  
}

class Arith extends RegexParsers with NonGreedy {
    // Just to avoid recompiling the pattern each time
    val select: Parser[String] = "(?i)SELECT".r
    val from: Parser[String] = "(?i)FROM".r
    val token: Parser[String] = "(\\s*)\\w+(\\s*)".r
    val eof: Parser[String] = """\z""".r

    def selectstatement: Parser[Any] = selectclause(from) ~ fromclause(eof)
    def selectclause(terminal: Parser[Any]): Parser[Any] = 
      select ~ tokens(terminal)
    def fromclause(terminal: Parser[Any]): Parser[Any] = 
      from ~ tokens(terminal)
    def tokens(terminal: Parser[Any]): Parser[Any] = 
      nonGreedy(token, terminal)
}
person Daniel C. Sobral    schedule 18.10.2011
comment
Я заметил, что изменение на def p = (a ||| aa ||| aaa) ~ ab действительно выполняет синтаксический анализ в вашем примере, но я не понимаю, что такое ||| оператор действительно делает или имеет ли это какое-либо отношение к исходному вопросу - person Magnus; 19.10.2011
comment
@Magnus ||| просто выберет самое большое совпадение, которое окажется правильным. Добавьте один ||| "aaaa", и вы увидите, что он потерпит неудачу. - person Daniel C. Sobral; 20.10.2011
comment
Спасибо за это определение нежадного решения. Я не понимаю, как это применить... nonGreedy принимает два аргумента, но rep (токен), который я пытаюсь сделать нежадным, принимает один аргумент. Какими должны быть аргументы в моем конкретном случае? - person Magnus; 21.10.2011
comment
Вау, это работает! Впечатляющее решение... совершенно не интуитивное для меня. Спасибо еще раз - person Magnus; 24.10.2011
comment
Четыре года спустя это все еще полезный ответ. - person WestCoastProjects; 30.10.2015
comment
Я пытаюсь использовать это, но не знаю, что предназначено для терминала. Реклама в вашем ответе будет оценена по достоинству. Это точка с запятой (завершение оператора sql)? - person WestCoastProjects; 11.11.2015
comment
@javadba Терминал - это то, что завершает работу этого синтаксического анализатора. Если вы посмотрите на последний пример, выбор завершается после того, как найдено from, и from завершается после завершения ввода. - person Daniel C. Sobral; 13.11.2015
comment
Не будет ли генератор парсеров более выгодным в таких случаях? Я вижу, что комбинаторы парсеров часто заканчиваются переполнением стека, тогда как генераторы парсеров сообщат вам о левой рекурсии во время компиляции. - person Valentin Tihomirov; 10.01.2016
comment
@ValentinTihomirov Вы имеете в виду что-то вроде YACC? Я думаю, что они неуклюжи, и я бы взял хороший анализатор DSL в любой день. Тем не менее, я давно не пользовался встроенным парсером Scala; в настоящее время есть гораздо лучшие альтернативы. - person Daniel C. Sobral; 12.01.2016
comment
Почему вы говорите, что синтаксические анализаторы DSL неуклюжи, и вместо этого вы бы использовали их? Мне кажется противоречивое утверждение. О каких альтернативах вы говорите? FastParse является заменой, но авторы умалчивают, что он выигрывает только по скорости. Говорят, что JavaCC производит довольно приличный синтаксический анализатор. Единственная проблема в том, что это Java, что означает нежелательный дополнительный шаг/инструмент компиляции. - person Valentin Tihomirov; 12.01.2016
comment
@ValentinTihomirov YACC — это язык (и инструмент), который скомпилирован в код C, анализирующий описываемую им грамматику. Scala Parsers — это DSL в Scala, используемый для описания грамматик для синтаксического анализа. Первый требует дополнительного шага компиляции, и исходный код, который он создает, не может быть изменен, поскольку он, конечно же, сгенерирован. По этой причине я предпочитаю второй, DSL, напрямую закодированный на целевом языке. YACC, ANTLR = плохо. Парсеры Scala, Parboiled2 = хорошо. И Parboiled2 — это то, что я использовал в последнее время. - person Daniel C. Sobral; 21.01.2016