Как настроить грамматику, которая может справляться с двусмысленностью

Я пытаюсь создать грамматику для анализа некоторых формул, подобных Excel, которые я разработал, где специальный символ в начале строки означает другой источник. Например, $ может обозначать строку, поэтому "$This is text" будет рассматриваться как входная строка в программе, а & может обозначать функцию, поэтому &foo() можно рассматривать как вызов внутренней функции foo.

Проблема, с которой я сталкиваюсь, заключается в том, как правильно построить грамматику. Например, это упрощенная версия MWE:

grammar = r'''start: instruction

?instruction: simple
            | func

STARTSYMBOL: "!"|"#"|"$"|"&"|"~"
SINGLESTR: (LETTER+|DIGIT+|"_"|" ")*
simple: STARTSYMBOL [SINGLESTR] (WORDSEP SINGLESTR)*
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: STARTSYMBOL SINGLESTR "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''
parser = lark.Lark(grammar, parser='earley')

Таким образом, с этой грамматикой такие вещи, как: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2) и &foo(!w1,w2,w3,,!w4,w5,w6), анализируются должным образом. Но если я хочу добавить больше гибкости моему терминалу simple, мне нужно начать возиться с определением токена SINGLESTR, что неудобно.

Что я пробовал

Часть, которую я не могу пройти, заключается в том, что если я хочу иметь строку, содержащую круглые скобки (которые являются литералами func), то я не могу обработать их в моей текущей ситуации.

  • Если я добавлю круглые скобки в SINGLESTR, то получу Expected STARTSYMBOL, потому что он смешивается с определением func и считает, что должен быть передан аргумент функции, что имеет смысл.
  • Если я переопределю грамматику, зарезервировав символ амперсанда только для функций, и добавлю скобки в SINGLESTR, тогда я смогу разобрать строку со скобками, но каждая функция, которую я пытаюсь разобрать, даст Expected LPAR.

Мое намерение состоит в том, чтобы все, что начинается с $, анализировалось как токен SINGLESTR, а затем я мог бы анализировать такие вещи, как &foo($first arg (has) parentheses,,$second arg).

Мое решение на данный момент заключается в том, что я использую в своих строках слова-экраны, такие как LEFTPAR и RIGHTPAR, и я написал вспомогательные функции, чтобы преобразовать их в круглые скобки при обработке дерева. Итак, $This is a LEFTPARtestRIGHTPAR создает правильное дерево, и когда я его обрабатываю, оно преобразуется в This is a (test).

Чтобы сформулировать общий вопрос: могу ли я определить свою грамматику таким образом, чтобы некоторые символы, которые являются специальными для грамматики, рассматривались как обычные символы в некоторых ситуациях и как специальные символы в любом другом случае?


РЕДАКТИРОВАТЬ 1

Основываясь на комментарии от jbndlr, я пересмотрел свою грамматику, чтобы создать отдельные режимы на основе начального символа:

grammar = r'''start: instruction

?instruction: simple
            | func

SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|")")*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

Это подпадает (отчасти) под мой второй тестовый пример. Я могу анализировать все simple типов строк (токены TEXT, MD или DB, которые могут содержать круглые скобки) и пустые функции; например, &foo() или &foo(&bar()) анализируются правильно. В тот момент, когда я помещаю аргумент в функцию (независимо от типа), я получаю UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP. В качестве доказательства концепции, если я уберу круглые скобки из определения SINGLESTR в новой грамматике выше, тогда все будет работать как надо, но я вернусь к исходной точке.


person Dima1982    schedule 17.11.2019    source источник
comment
У вас есть символы, которые определяют, что следует за ними (ваш STARTSYMBOL), и вы добавляете разделители и круглые скобки там, где это необходимо для ясности; Я не вижу здесь двусмысленности. Вам все равно придется разделить список STARTSYMBOL на отдельные элементы, чтобы их можно было различить.   -  person jbndlr    schedule 27.11.2019
comment
Я собираюсь опубликовать ответ очень скоро, работаю над ним уже несколько дней.   -  person iliar    schedule 30.11.2019
comment
Я дал ответ. Хотя до истечения срока действия вознаграждения осталось всего 2 часа, вы все равно можете назначить вознаграждение вручную в течение следующего льготного периода в 24 часа. Если мой ответ не подходит, сообщите мне об этом в ближайшее время, и я это исправлю.   -  person iliar    schedule 30.11.2019


Ответы (2)


import lark
grammar = r'''start: instruction

?instruction: simple
            | func

MIDTEXTRPAR: /\)+(?!(\)|,,|$))/
SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|MIDTEXTRPAR)*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

parser = lark.Lark(grammar, parser='earley')
parser.parse("&foo($first arg (has) parentheses,,$second arg)")

Выход:

Tree(start, [Tree(func, [Token(FUNCNAME, 'foo'), Tree(simple, [Token(TEXT, '$first arg (has) parentheses')]), Token(ARGSEP, ',,'), Tree(simple, [Token(TEXT, '$second arg')])])])

Надеюсь, это то, что вы искали.

Это были сумасшедшие несколько дней. Я пробовал жаворонок и потерпел неудачу. Я также пробовал persimonious и pyparsing. У всех этих различных синтаксических анализаторов была одна и та же проблема с токеном «аргумент», использующим правильную скобку, которая была частью функции, и в конечном итоге терпела неудачу, потому что скобки функции не были закрыты.

Хитрость заключалась в том, чтобы выяснить, как определить правую скобку, которая не является особенной. См. регулярное выражение для MIDTEXTRPAR в приведенном выше коде. Я определил его как правую скобку, за которой не следует разделение аргументов или конец строки. Я сделал это, используя расширение регулярного выражения (?!...), которое соответствует, только если за ним не следует ..., но не использует символы. К счастью, он даже позволяет сопоставлять конец строки внутри этого специального расширения регулярного выражения.

РЕДАКТИРОВАТЬ:

Вышеупомянутый метод работает только в том случае, если у вас нет аргумента, оканчивающегося на ), потому что тогда регулярное выражение MIDTEXTRPAR не поймает это ) и будет думать, что это конец функции, даже если есть больше аргументов для обработки. Кроме того, могут быть двусмысленности, такие как ...asdf),,..., это может быть конец объявления функции внутри аргумента или «текстовый») внутри аргумента, и объявление функции продолжается.

Эта проблема связана с тем, что то, что вы описываете в своем вопросе, не является контекстно-свободной грамматикой (https://en.wikipedia.org/wiki/Context-free_grammar), для которых существуют парсеры, такие как lark. Вместо этого это контекстно-зависимая грамматика (https://en.wikipedia.org/wiki/Context-sensitive_grammar).

Причина того, что это контекстно-зависимая грамматика, заключается в том, что вам нужно, чтобы синтаксический анализатор «помнил», что он вложен внутри функции, и сколько существует уровней вложенности, и каким-то образом эта память доступна внутри синтаксиса грамматики.

РЕДАКТИРОВАТЬ2:

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


_funcPrefix = '&'
_debug = False

class ParseException(Exception):
    pass

def GetRecursive(c):
    if isinstance(c,ParserBase):
        return c.GetRecursive()
    else:
        return c

class ParserBase:
    def __str__(self):
        return type(self).__name__ + ": [" + ','.join(str(x) for x in self.contents) +"]"
    def GetRecursive(self):
        return (type(self).__name__,[GetRecursive(c) for c in self.contents])

class Simple(ParserBase):
    def __init__(self,s):
        self.contents = [s]

class MD(Simple):
    pass

class DB(ParserBase):
    def __init__(self,s):
        self.contents = s.split(',')

class Func(ParserBase):
    def __init__(self,s):
        if s[-1] != ')':
            raise ParseException("Can't find right parenthesis: '%s'" % s)
        lparInd = s.find('(')
        if lparInd < 0:
            raise ParseException("Can't find left parenthesis: '%s'" % s)
        self.contents = [s[:lparInd]]
        argsStr = s[(lparInd+1):-1]
        args = list(argsStr.split(',,'))
        i = 0
        while i<len(args):
            a = args[i]
            if a[0] != _funcPrefix:
                self.contents.append(Parse(a))
                i += 1
            else:
                j = i+1
                while j<=len(args):
                    nestedFunc = ',,'.join(args[i:j])
                    if _debug:
                        print(nestedFunc)
                    try:
                        self.contents.append(Parse(nestedFunc))
                        break
                    except ParseException as PE:
                        if _debug:
                            print(PE)
                        j += 1
                if j>len(args):
                    raise ParseException("Can't parse nested function: '%s'" % (',,'.join(args[i:])))
                i = j

def Parse(arg):
    if arg[0] not in _starterSymbols:
        raise ParseException("Bad prefix: " + arg[0])
    return _starterSymbols[arg[0]](arg[1:])

_starterSymbols = {_funcPrefix:Func,'$':Simple,'!':DB,'#':MD}

P = Parse("&foo($first arg (has)) parentheses,,&f($asdf,,&nested2($23423))),,&second(!arg,wer))")
print(P)

import pprint
pprint.pprint(P.GetRecursive())
person iliar    schedule 30.11.2019
comment
Спасибо, это работает, как задумано! Награжден наградой, так как вам не нужно каким-либо образом избегать круглых скобок. Вы сделали все возможное, и это видно! По-прежнему существует крайний случай «текстового» аргумента, заканчивающегося скобками, но мне просто придется с этим смириться. Вы также ясно объяснили двусмысленность, и мне просто нужно еще немного проверить это, но я думаю, что для моих целей это сработает очень хорошо. Спасибо за дополнительную информацию о контекстно-зависимой грамматике. Я очень ценю это! - person Dima1982; 01.12.2019
comment
@Dima1982 Dima1982 Взгляните на редактирование, я сделал парсер, который, возможно, может решить вашу проблему за счет экспоненциальной временной сложности. Кроме того, я подумал об этом, и если ваша проблема имеет практическое значение, экранирование круглых скобок может быть самым простым решением. Или Сделать скобки функции чем-то другим, например, разграничив конец списка аргументов функции с помощью &. - person iliar; 01.12.2019

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

  SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"\("|"\)")*

Аналогичное решение, используемое C, для включения двойных кавычек ("") как части строковой константы, где строковая константа заключена в двойные кавычки.

  example_string1='&f(!g\()'
  example_string2='&f(#g)'
  print(parser.parse(example_string1).pretty())
  print(parser.parse(example_string2).pretty())

Выход

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g
person Venkatesh Nandigama    schedule 28.11.2019
comment
Я думаю, что это почти то же самое, что и собственное решение OP по замене ( и ) на LEFTPAR и RIGHTPAR. - person iliar; 29.11.2019