Wie simuliert man Rückreferenzen, Lookaheads und Lookbehinds in Automaten mit endlichen Zuständen?

26

Ich habe ein einfaches Lexer und einen Parser für reguläre Ausdrücke erstellt, um einen regulären Ausdruck zu verwenden und dessen Analysebaum zu generieren. Das Erstellen eines nicht deterministischen Automaten mit endlichen Zuständen aus diesem Analysebaum ist für grundlegende reguläre Ausdrücke relativ einfach. Ich kann mich jedoch nicht mit der Simulation von Rückreferenzen, Lookaheads und Lookbehinds auseinandersetzen.

Nach dem, was ich in dem Buch über lila Drachen gelesen habe, habe ich verstanden, dass Sie ein nicht deterministisches Endliches erzeugen, wenn Sie ein Lookahead- simulieren möchten, bei dem der reguläre Ausdruck r übereinstimmt, und nur dann, wenn auf die Übereinstimmung eine Übereinstimmung mit dem regulären Ausdruck s folgt Zustandsautomat, in dem / durch ε ersetzt wird . Ist es möglich, einen deterministischen endlichen Automaten zu erzeugen, der dasselbe tut?r/srs/ε

Was ist mit der Simulation negativer Lookaheads und Lookbehinds? Ich würde es wirklich begrüßen, wenn Sie mich mit einer Ressource verlinken würden, die ausführlich beschreibt, wie das geht.

Aadit M Shah
quelle

Antworten:

21

Erstens können Rückreferenzen nicht durch endliche Automaten simuliert werden, da Sie damit nicht reguläre Sprachen beschreiben können. Zum Beispiel ([ab]^*)\1entspricht , was nicht einmal kontextfrei ist.{www{ein,b}}

Vorausschau und Rückschau sind in der Welt der endlichen Automaten nichts Besonderes, da wir hier nur ganze Eingaben abgleichen. Daher ist die spezielle Semantik "Nur prüfen, aber nicht konsumieren" bedeutungslos. Sie verketten und / oder kreuzen einfach prüfende und konsumierende Ausdrücke und verwenden die resultierenden Automaten. Die Idee ist, die Look-Ahead- oder Look-Behind-Ausdrücke zu überprüfen, während Sie die Eingabe "verbrauchen" und das Ergebnis in einem Zustand speichern.

Wenn Sie reguläre Ausdrücke implementieren, möchten Sie die Eingabe über einen Automaten ausführen und Start- und Endindizes von Übereinstimmungen zurückerhalten. Das ist eine ganz andere Aufgabe, es gibt also eigentlich keine Konstruktion für endliche Automaten. Sie erstellen Ihren Automaten so, als ob der Look-Ahead- oder Look-Behind-Ausdruck aufwendig wäre, und ändern Ihren Index, der gespeichert bzw. geändert wird. dementsprechend berichten.

Nehmen Sie zum Beispiel einen Blick hinter die Kulissen. Wir können die Regexp-Semantik imitieren, indem wir den überprüfenden Regexp gleichzeitig mit dem implizit verbrauchenden "Match-all" -Regexp ausführen. Nur aus Zuständen, in denen sich der Automat des Look-Behind-Ausdrucks in einem Endzustand befindet, kann der Automat des geschützten Ausdrucks eingegeben werden. Zum Beispiel kann die regexp /(?=c)[ab]+/(unter der Annahme ist die volle Alphabet) - beachten Sie, dass es zu dem regulären Ausdruck übersetzt { a , b , c } * c { a , b } + { ein , b , c }{ein,b,c} - könnte mit übereinstimmen{ein,b,c}c{ein,b}+{ein,b,c}

Bildbeschreibung hier eingeben
[ Quelle ]

und du müsstest

  • Speichern Sie den aktuellen Index als wenn Sie q 2 eingeben (anfangs oder ab q 2 ) undichq2q2
  • melde eine (maximale) Übereinstimmung von mit dem aktuellen Index ( - 1 ), wenn du q 2 drückst (verlässt) .ich-1q2

Beachten Sie, wie der linke Teil des Automaten der Parallelautomat der kanonischen Automaten für [abc]*bzw. c(iteriert) ist.

ichjichj

Beachten Sie, dass Nicht-Determinismus damit verbunden ist: Der Haupt- und der Vorausschau- / Hinterkopf-Automat können sich überlappen. Sie müssen daher alle Übergänge zwischen ihnen speichern, um die passenden Übergänge später oder den Backtrack zu melden.

Raphael
quelle
11

Die maßgebliche Referenz zu den pragmatischen Aspekten der Implementierung von Regex-Engines ist eine Reihe von drei Blog-Posts von Russ Cox . Wie dort beschrieben, werden Rückverweise mithilfe von Backtracking implementiert, da Ihre Sprache durch Rückverweise unregelmäßig wird .

Lookaheads und Lookbehinds passen wie viele Features von Regex-Pattern-Matching-Engines nicht ganz in das Paradigma, zu entscheiden, ob ein String Mitglied einer Sprache ist oder nicht. Bei regulären Ausdrücken suchen wir normalerweise nach Teilzeichenfolgen in einer größeren Zeichenfolge. Die "Übereinstimmungen" sind Teilzeichenfolgen, die Mitglieder der Sprache sind, und der Rückgabewert ist der Anfangs- und Endpunkt der Teilzeichenfolge in der größeren Zeichenfolge.

Der Punkt von Lookaheads und Lookbehinds besteht weniger darin, die Fähigkeit einzuführen, nicht reguläre Sprachen abzugleichen, sondern vielmehr die Position anzupassen, an der die Engine den Anfangs- und Endpunkt der übereinstimmenden Teilzeichenfolge meldet.

Ich verlasse mich auf die Beschreibung unter http://www.regular-expressions.info/lookaround.html . Die Regex-Engines, die diese Funktion unterstützen (Perl, TCL, Python, Ruby, ...), scheinen alle auf Backtracking zu basieren (dh sie unterstützen einen viel größeren Satz von Sprachen als nur die regulären Sprachen). Sie scheinen diese Funktion als eine relativ "einfache" Erweiterung des Backtracking zu implementieren, anstatt zu versuchen, echte endliche Automaten zu konstruieren, um die Aufgabe auszuführen.

Positiver Lookahead

Die Syntax für den positiven Lookahead ist (?=Regex) . So q(?=u)stimmt zum Beispiel qnur überein , wenn es gefolgt wird u, aber nicht mit dem übereinstimmt u. Ich stelle mir vor, sie implementieren dies mit einer Variation des Backtrackings. Erstellen Sie eine FSM für den Ausdruck vor dem positiven Lookahead. Wenn diese Übereinstimmungen gefunden wurden, merken Sie sich, wo sie geendet haben, und starten Sie eine neue FSM, die den Ausdruck im positiven Lookahead darstellt. Wenn dies zutrifft, haben Sie eine "Übereinstimmung", aber die Übereinstimmung "endet" kurz vor der Position, an der die positive Vorausschau-Übereinstimmung begonnen hat.

Das Einzige, was ohne Backtracking schwierig wäre, ist, dass Sie sich an den Punkt in der Eingabe erinnern müssen, an dem der Lookahead beginnt, und Ihr Eingabeband an diese Position zurückschieben, nachdem Sie mit dem Match fertig sind.

Negativer Lookahead

Die Syntax für den negativen Lookahead ist (?!Regex) . So q(?!u)passt zum Beispiel qnur, wenn es nicht gefolgt wird u. Dies kann entweder ein qgefolgt von einem anderen Zeichen oder ein Zeichen qganz am Ende der Zeichenfolge sein. Ich stelle mir vor, dass dies implementiert wird, indem eine NFA für den Lookahead-Ausdruck erstellt wird, die dann nur erfolgreich ist, wenn die NFA nicht mit der nachfolgenden Zeichenfolge übereinstimmt.

Wenn Sie dies tun möchten, ohne sich auf das Zurückverfolgen zu verlassen, können Sie die NFA des Lookahead-Ausdrucks negieren. Behandeln Sie sie dann genauso, wie Sie den positiven Lookahead behandeln.

Positiver Lookbehind

(?<=)(?=q)uuqqnnn

Sie könnten in der Lage sein , dies , indem der Schnittpunkt von „Zeichenfolge, die Enden mit ohne Rückzieher zu implementieren regex “ mit dem, was Teil der Regex , die vor dem Bediener kommt Lookbehind. Dies wird jedoch schwierig, da der Lookbehind- Regex möglicherweise weiter zurückschauen muss als der aktuelle Anfang der Eingabe.

Negativer Lookbehind

Die Syntax für negative Lookbehind ist (?<!Regex) . So zum Beispiel (?<!q)uStreichhölzer u, aber nur, wenn es nicht vorangestellt ist q. Also würde es mit dem uIn umbrellaund dem uIn übereinstimmen doubt, aber nicht mit dem uIn quick. Wiederum scheint dies durch Berechnen der Länge von Regex , Sichern dieser Anzahl von Zeichen, Testen der Übereinstimmung mit Regex zu geschehen , wobei nun jedoch die gesamte Übereinstimmung fehlschlägt, wenn der Lookbehind übereinstimmt.

Möglicherweise können Sie dies ohne Zurückverfolgung implementieren, indem Sie die Negation von Regex verwenden und dann dasselbe tun, wie Sie es für einen positiven Lookbehind tun würden.

Wandering Logic
quelle
5

Zumindest für Rückreferenzen ist dies nicht möglich. Beispielsweise (.*)\1repräsentiert der reguläre Ausdruck eine Sprache, die nicht regulär ist. Das bedeutet, dass es unmöglich ist, einen endlichen (deterministischen oder nicht deterministischen) Automaten zu erstellen, der diese Sprache erkennt. Wenn Sie dies formal beweisen möchten, können Sie das pumpfähige Lemma verwenden .

svick
quelle
4

Ich habe mich selbst damit befasst, und Sie sollten in der Lage sein, Lookahead mithilfe eines Alternating Finite Automaton zu implementieren . Wenn Sie auf Lookahead stoßen, führen Sie nicht deterministisch sowohl den Lookahead als auch den Rest des Ausdrucks aus und akzeptieren ihn nur, wenn beide Pfade ihn akzeptieren. Sie können einen AFA in einen NFA mit angemessener Vergrößerung (und damit in einen DFA) umwandeln, obwohl ich nicht überprüft habe, dass die offensichtliche Konstruktion mit Erfassungsgruppen gut funktioniert.

Lookbehind mit fester Breite sollte ohne Backtracking möglich sein. Sei n die Breite. Ausgehend von dem Punkt in Ihrer NFA, an dem der Lookbehind begann, haben Sie rückwärts schauende Zustände aufgeteilt, sodass jeder Pfad in den Lookbehind mit n Zeichen endete, deren Zustände nur in den Lookbehind gingen. Fügen Sie dann Lookahead am Anfang dieser Zustände hinzu (und kompilieren Sie den Subgraphen sofort von AFA nach NFA, falls gewünscht).

Rückreferenzen sind, wie andere bereits erwähnt haben, nicht regelmäßig, so dass sie nicht von einem endlichen Automaten implementiert werden können. Tatsächlich sind sie NP-vollständig. Bei der Implementierung, an der ich arbeite, ist ein schneller Ja / Nein-Abgleich von größter Bedeutung, daher habe ich mich dafür entschieden, überhaupt keine Rückverweise zu implementieren.

Thom Smith
quelle