Як налаштувати граматику, яка може вирішити двозначність


9

Я намагаюся створити граматику для розбору деяких формул, схожих на 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визначенням, і він вважає, що слід передати аргумент функції, що має сенс.
  • Якщо я перезначую граматику, щоб зарезервувати символ ampersand лише для функцій і додати в дужки 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 у новій граматиці вище, то все працює як слід, але я повернусь до прямої.


У вас є символи, які визначають, що відбувається після них (ваш STARTSYMBOL), і ви додаєте роздільники і круглі дужки, де потрібно, щоб це було зрозуміло; Я не бачу тут ніякої неоднозначності. Вам все одно доведеться розділити свій STARTSYMBOLсписок на окремі елементи, щоб їх можна було відрізняти.
jbndlr

Я незабаром опублікую відповідь, над нею працюю вже кілька днів.
iliar

Я дав відповідь. Незважаючи на те, що до закінчення терміну дії щедрості закінчується лише 2 години, ви все одно можете вручну присвоїти винагороду в наступний пільговий період, який становить 24 години. Якщо моя відповідь не є хорошою, будь ласка, скажіть мені скоро, і я це виправлю.
iliar

Відповіді:


3
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у коді вище. Я визначив це як правильні дужки, за якими не йде розділення аргументів або кінець рядка. Я зробив це за допомогою регулярного розширення виразів(?!...) яке відповідає лише тому, що за ним не дотримується, ...але символи не вживаються. На щастя, це навіть дозволяє узгодити кінець рядка всередині цього спеціального розширення регулярного виразу.

Редагувати:

Вищезазначений метод працює лише у тому випадку, якщо у вас немає аргументу, що закінчується на a), оскільки тоді регулярний вираз MIDTEXTRPAR не вловить це) і подумає, що це кінець функції, навіть якщо є більше аргументів для обробки. Також можуть бути неясності, такі як ... asdf),, ..., це може бути закінченням функції функції всередині аргументу або "подібним до тексту") всередині аргументу, і декларація функції продовжується.

Ця проблема пов'язана з тим, що те, що ви описуєте у своєму запитанні, не є контекстною граматикою ( https://en.wikipedia.org/wiki/Context-free_grammar ), для якої існують парсери, такі як жайворонок. Натомість це контекстно-залежна граматика ( https://en.wikipedia.org/wiki/Context-sensitive_grammar ).

Причина його граматики, залежно від контексту, полягає в тому, що вам потрібен аналізатор, щоб «пам’ятати», що він вкладений всередині функції, і скільки рівнів вкладеності є, і чи має ця пам’ять наявна у синтаксисі граматики.

EDIT2:

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


_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())

1
Дякую, це працює за призначенням! Присуджував суму, оскільки вам не потрібно уникати дужок ні в якому разі. Ви пройшли зайву милю і це показує! Досі є крайній випадок аргументу "текст", що закінчується дужками, але мені просто доведеться жити з цим. Ви також чітко пояснили неоднозначності, і мені просто потрібно перевірити це трохи більше, але я думаю, що для моїх цілей це буде дуже добре. Дякуємо, що надали більше інформації про граматику, залежну від контексту. Я дійсно ціную це!
Dima1982

@ Dima1982 Дуже дякую!
iliar

@ Dima1982 Погляньте на редагування, я зробив аналіз, який, можливо, може вирішити вашу проблему ціною експоненціальної складності часу. Крім того, я подумав про це, і якщо ваша проблема має практичну цінність, уникнення дужок може бути найпростішим рішенням. Або Зробити дужки функції щось інше, як, наприклад, обмеження кінця списку аргументів функцій, &наприклад.
iliar

1

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

  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

Я думаю, що це майже те саме, що власне рішення OP щодо заміни "(" і ")" LEFTPAR і RIGHTPAR.
iliar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.