あいまいさを処理できる文法のセットアップ方法


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定義と混同されており、関数の引数を渡す必要があると考えられているためです。
  • アンパサンド記号を関数のみに予約するように文法を再定義し、括弧をに追加すると、括弧で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
'''

これは、私の2番目のテストケースに(やや)該当します。すべての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')])])])

それがあなたが探していたものであることを願っています。

それらは狂った数日でした。ひばりを試しましたが失敗しました。私も試してみましたpersimoniouspyparsing。これらのさまざまなパーサーはすべて、関数の一部である右括弧を消費する「引数」トークンに同じ問題があり、関数の括弧が閉じられていなかったために最終的に失敗しました。

トリックは、「特別ではない」右括弧をどのように定義するかを理解することでした。MIDTEXTRPAR上記のコードの正規表現を参照してください。私はそれを引数の分離や文字列の終わりが続かない右括弧として定義しました。正規表現の拡張子(?!...)を使用してこれを行いました...。これは、後に続かないが文字を消費しない場合のみ一致します。幸いにも、この特別な正規表現拡張内で文字列の終わりを照合することもできます。

編集:

上記の方法は、)で終わる引数がない場合にのみ機能します。これは、MIDTEXTRPAR正規表現がそれをキャッチできず、処理する引数がまだある場合でも、これが関数の終わりであると考えるためです。また、... asdf),, ...などのあいまいさが存在する可能性があります。これは、引数内の関数宣言の終わり、または引数内の「テキストのような」)であり、関数宣言が続きます。

この問題は、質問で説明する内容が、larkなどのパーサーが存在するコンテキストフリーの文法(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

問題は、関数の引数が括弧で囲まれていて、引数の1つに括弧が含まれている可能性があることです。
考えられる解決策の1つは、文字列の一部である場合、バックスペース\ before(または)を使用することです

  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

「(」と「)」をLEFTPARとRIGHTPARで置き換えるというOP独自のソリューションとほとんど同じだと思います。
iliar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.