So richten Sie eine Grammatik ein, die mit Mehrdeutigkeiten umgehen kann


9

Ich versuche, eine Grammatik zu erstellen, um einige von mir entwickelte Excel-ähnliche Formeln zu analysieren, wobei ein Sonderzeichen am Anfang einer Zeichenfolge eine andere Quelle kennzeichnet. $Kann beispielsweise eine Zeichenfolge kennzeichnen, sodass " $This is text" als Zeichenfolgeneingabe im Programm behandelt wird und &eine Funktion kennzeichnen kann, sodass &foo()dies als Aufruf der internen Funktion behandelt werden kann foo.

Das Problem, mit dem ich konfrontiert bin, ist, wie man die Grammatik richtig konstruiert. Beispiel: Dies ist eine vereinfachte Version als 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')

Also, mit dieser Grammatik, Dinge wie: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2)und &foo(!w1,w2,w3,,!w4,w5,w6)sind alle wie erwartet analysiert. Aber wenn ich meinem simpleTerminal mehr Flexibilität hinzufügen möchte , muss ich anfangen, mit der SINGLESTRToken-Definition herumzuspielen, was nicht bequem ist.

Was habe ich versucht?

Der Teil, an dem ich nicht vorbeikommen kann, ist, dass ich funcin meiner aktuellen Situation nicht damit umgehen kann , wenn ich eine Zeichenfolge mit Klammern (die Literale sind ) haben möchte .

  • Wenn ich die Klammern hinzufüge SINGLESTR, bekomme ich Expected STARTSYMBOL, weil es mit der funcDefinition verwechselt wird und es denkt, dass ein Funktionsargument übergeben werden sollte, was Sinn macht.
  • Wenn ich die Grammatik neu definiere, um das kaufmännische Und-Symbol nur für Funktionen zu reservieren und die Klammern hinzuzufügen SINGLESTR, kann ich eine Zeichenfolge mit Klammern analysieren, aber jede Funktion, die ich zu analysieren versuche, gibt Expected LPAR.

Meine Absicht ist, dass alles, was mit a beginnt $, als SINGLESTRToken analysiert wird und ich dann Dinge wie analysieren kann &foo($first arg (has) parentheses,,$second arg).

Meine Lösung besteht derzeit darin, dass ich in meinen Zeichenfolgen "Escape" -Wörter wie LEFTPAR und RIGHTPAR verwende und Hilfsfunktionen geschrieben habe, um diese bei der Verarbeitung des Baums in Klammern zu setzen. So $This is a LEFTPARtestRIGHTPARproduziert die richtigen Baum , und wenn ich es verarbeiten, dann wird diese übersetzt This is a (test).

Um eine allgemeine Frage zu formulieren: Kann ich meine Grammatik so definieren, dass einige Zeichen, die für die Grammatik spezifisch sind, in bestimmten Situationen als normale Zeichen und in anderen Fällen als speziell behandelt werden?


BEARBEITEN 1

Basierend auf einem Kommentar von habe jbndlrich meine Grammatik überarbeitet, um individuelle Modi basierend auf dem Startsymbol zu erstellen:

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
'''

Dies fällt (etwas) unter meinen zweiten Testfall. Ich kann alle simpleArten von Zeichenfolgen (TEXT-, MD- oder DB-Token, die Klammern enthalten können) und Funktionen analysieren , die leer sind. zum Beispiel &foo()oder &foo(&bar())richtig analysieren. In dem Moment, in dem ich ein Argument in eine Funktion einfüge (egal welcher Typ), erhalte ich eine UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP. Wenn ich als Proof of Concept die Klammern aus der Definition von SINGLESTR in der neuen Grammatik oben entferne, funktioniert alles so, wie es sollte, aber ich bin wieder auf dem ersten Platz.


Sie haben Zeichen, die identifizieren, was nach ihnen kommt (Ihre STARTSYMBOL), und Sie fügen Trennzeichen und Klammern hinzu, wenn dies erforderlich ist, um klar zu sein. Ich sehe hier keine Mehrdeutigkeit. Sie STARTSYMBOLmüssten Ihre Liste immer noch in einzelne Elemente aufteilen , um unterscheidbar zu sein.
jbndlr

Ich werde sehr bald eine Antwort veröffentlichen und arbeite seit einigen Tagen daran.
Iliar

Ich gab eine Antwort. Obwohl es nur 2 Stunden dauert, bis das Kopfgeld abläuft, können Sie das Kopfgeld in der folgenden Nachfrist von 24 Stunden manuell vergeben. Wenn meine Antwort nicht gut ist, sag es mir bitte bald und ich werde es reparieren.
Iliar

Antworten:


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)")

Ausgabe:

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

Ich hoffe, es ist das, wonach Sie gesucht haben.

Das waren ein paar verrückte Tage. Ich versuchte es mit Lerche und scheiterte. Ich habe es auch versucht persimoniousund pyparsing. Alle diese verschiedenen Parser hatten alle das gleiche Problem mit dem 'Argument'-Token, das die richtige Klammer verbrauchte, die Teil der Funktion war, und scheiterten schließlich, weil die Klammern der Funktion nicht geschlossen wurden.

Der Trick bestand darin, herauszufinden, wie Sie eine richtige Klammer definieren, die "nicht besonders" ist. Siehe den regulären Ausdruck für MIDTEXTRPARim obigen Code. Ich habe es als eine rechte Klammer definiert, auf die keine Argumenttrennung oder kein Ende der Zeichenfolge folgt. Ich habe das getan, indem ich die reguläre Ausdruckserweiterung verwendet habe, (?!...)die nur übereinstimmt, wenn sie nicht gefolgt wird, ...aber keine Zeichen verbraucht. Glücklicherweise erlaubt es sogar das passende Ende der Zeichenfolge innerhalb dieser speziellen Erweiterung für reguläre Ausdrücke.

BEARBEITEN:

Die oben erwähnte Methode funktioniert nur, wenn Sie kein Argument haben, das mit a) endet, da der reguläre Ausdruck MIDTEXTRPAR dies dann nicht abfängt und denkt, dass dies das Ende der Funktion ist, obwohl mehr Argumente zu verarbeiten sind. Es kann auch Unklarheiten wie ... asdf) ,, ... geben, es kann ein Ende einer Funktionsdeklaration innerhalb eines Arguments oder ein 'textähnliches') innerhalb eines Arguments sein und die Funktionsdeklaration geht weiter.

Dieses Problem hängt mit der Tatsache zusammen, dass das, was Sie in Ihrer Frage beschreiben, keine kontextfreie Grammatik ( https://en.wikipedia.org/wiki/Context-free_grammar ) ist, für die Parser wie Lerche existieren. Stattdessen handelt es sich um eine kontextsensitive Grammatik ( https://en.wikipedia.org/wiki/Context-sensitive_grammar ).

Der Grund dafür, dass es sich um eine kontextsensitive Grammatik handelt, besteht darin, dass der Parser sich daran erinnern muss, dass er in einer Funktion verschachtelt ist und wie viele Verschachtelungsebenen vorhanden sind, und dass dieser Speicher in irgendeiner Weise in der Syntax der Grammatik verfügbar ist.

EDIT2:

Schauen Sie sich auch den folgenden Parser an, der kontextsensitiv ist und das Problem zu lösen scheint, jedoch eine exponentielle zeitliche Komplexität in der Anzahl der verschachtelten Funktionen aufweist, da er versucht, alle möglichen Funktionsbarrieren zu analysieren, bis eine funktionierende gefunden wird. Ich glaube, es muss eine exponentielle Komplexität haben, da es nicht kontextfrei ist.


_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
Danke, das funktioniert wie vorgesehen! Verliehen mit dem Kopfgeld, da Sie den Klammern in keiner Weise entkommen müssen. Sie gingen die Extrameile und es zeigt! Es gibt immer noch den Randfall eines 'Text'-Arguments, das mit einer Klammer endet, aber ich muss nur damit leben. Sie haben auch die Unklarheiten klar erklärt, und ich muss das nur ein bisschen mehr testen, aber ich denke, dass dies für meine Zwecke sehr gut funktionieren wird. Vielen Dank, dass Sie weitere Informationen zur kontextsensitiven Grammatik bereitgestellt haben. Ich weiß das wirklich zu schätzen!
Dima1982

@ Dima1982 Vielen Dank!
iliar

@ Dima1982 Schauen Sie sich die Bearbeitung an, ich habe einen Parser erstellt, der Ihr Problem möglicherweise auf Kosten einer exponentiellen Zeitkomplexität lösen kann. Außerdem habe ich darüber nachgedacht, und wenn Ihr Problem von praktischem Wert ist, ist es möglicherweise die einfachste Lösung, den Klammern zu entkommen. Oder machen Sie die Funktionsklammer zu etwas anderem, zum Beispiel zum Abgrenzen des Endes einer Funktionsargumentliste &.
iliar

1

Das Problem ist, dass Funktionsargumente in Klammern stehen, wobei eines der Argumente Klammern enthalten kann.
Eine der möglichen Lösungen besteht darin, die Rücktaste \ vor (oder) zu verwenden, wenn sie Teil von String ist

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

Ähnliche Lösung, die von C verwendet wird, um doppelte Anführungszeichen (") als Teil der Zeichenfolgenkonstante einzuschließen, wobei die Zeichenfolgenkonstante in doppelte Anführungszeichen eingeschlossen ist.

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

Ausgabe ist

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g

Ich denke, es ist so ziemlich das Gleiche wie OPs eigene Lösung, "(" und ")" durch LEFTPAR und RIGHTPAR zu ersetzen.
Iliar
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.