Was sind gute Ressourcen zum Schreiben eines Lexikons in C ++ (Bücher, Tutorials, Dokumente), was sind einige gute Techniken und Vorgehensweisen?
Ich habe im Internet gesucht und jeder sagt, dass er einen Lexer-Generator wie Lex benutzen soll. Das will ich nicht, ich will ein Lexer von Hand schreiben.
Antworten:
Beachten Sie, dass jede Zustandsmaschine einem regulären Ausdruck entspricht, der einem strukturierten Programm mit
if
undwhile
Anweisungen entspricht.Um zum Beispiel ganze Zahlen zu erkennen, könnte man die Zustandsmaschine haben:
oder der reguläre Ausdruck:
oder der strukturierte Code:
Persönlich schreibe ich immer Lexer mit letzterem, weil es meiner Meinung nach nicht weniger klar ist und es nichts schnelleres gibt.
quelle
*pc
, richtig? Wiewhile(isdigit(*pc)) { value += pc; pc++; }
. Anschließend}
wandeln Sie den Wert in eine Zahl um und weisen ihn einem Token zu.n = n * 10 + (*pc++ - '0');
. Bei Fließkomma- und E-Notation wird es etwas komplexer, aber nicht schlecht. Ich bin sicher, ich könnte ein wenig Code sparen, indem ich die Zeichen in einen Puffer packe und aufrufeatof
oder was auch immer. Schneller würde es nicht laufen.Lexer sind Finite-State-Maschinen. Daher können sie von jeder allgemeinen FSM-Bibliothek erstellt werden. Für meine eigene Ausbildung habe ich jedoch meine eigene mit Ausdrucksvorlagen geschrieben. Hier ist mein Lexer:
Es wird durch eine iteratorbasierte Back-Tracking-Bibliothek für endliche Zustandsmaschinen mit einer Länge von ca. 400 Zeilen unterstützt. Es ist jedoch leicht einzusehen, dass ich nur einfache boolesche Operationen wie
and
,or
undnot
und ein paar regex-artige Operatoren wie*
für null oder mehr erstellen musste ,eps
um "irgendetwas stimmen" und "stimmen"opt
zu bedeuten alles andere als nicht verbrauchen ". Die Bibliothek ist vollständig generisch und basiert auf Iteratoren. Das MakeEquality-Zeug ist ein einfacher Test für die Gleichheit zwischen*it
und dem übergebenen Wert, und MakeRange ist ein einfacher<= >=
Test.Schließlich plane ich, vom Backtracking zum Predictive überzugehen.
quelle
MakeEquality
Snippet teilen ? Insbesondere das von dieser Funktion zurückgegebene Objekt. Sieht sehr interessant aus.Zunächst einmal geht es hier um verschiedene Dinge:
Im Allgemeinen erwarten wir, dass ein Lexer alle drei Schritte auf einmal ausführt. Letzteres ist jedoch von Natur aus schwieriger und es gibt einige Probleme mit der Automatisierung (dazu später mehr).
Das erstaunlichste Lexer, das ich kenne, ist Boost.Spirit.Qi . Es verwendet Ausdrucksvorlagen, um Ihre Lexer-Ausdrücke zu generieren, und sobald es an seine Syntax gewöhnt ist, fühlt sich der Code wirklich ordentlich an. Es wird jedoch sehr langsam kompiliert (umfangreiche Vorlagen). Es ist daher am besten, die verschiedenen Teile in dedizierten Dateien zu isolieren, um zu vermeiden, dass sie erneut kompiliert werden, wenn sie nicht berührt wurden.
Es gibt einige Fallstricke bei der Leistung, und der Autor des Epochen-Compilers erklärt, wie er durch intensive Profilerstellung und Untersuchung der Funktionsweise von Qi in einem Artikel eine 1000-fache Geschwindigkeit erzielt hat .
Schließlich wird auch Code von externen Tools (Yacc, Bison, ...) generiert.
Aber ich versprach eine Zusammenfassung dessen, was mit der Automatisierung der Grammatiküberprüfung nicht stimmte.
Wenn Sie beispielsweise Clang auschecken, werden Sie feststellen, dass anstelle eines generierten Parsers und so etwas wie Boost.Spirit die Grammatik manuell mit einer generischen Descent Parsing-Technik validiert werden soll. Das scheint doch rückständig zu sein?
In der Tat gibt es einen sehr einfachen Grund: Fehlerbehebung .
Das typische Beispiel in C ++:
Beachten Sie den Fehler? Ein fehlendes Semikolon direkt nach der Deklaration von
Foo
.Es ist ein häufiger Fehler, und Clang erholt sich ordentlich, indem er erkennt, dass er einfach fehlt und
void
nicht nur ein Beispiel,Foo
sondern ein Teil der nächsten Deklaration ist. Dies vermeidet schwer zu diagnostizierende kryptische Fehlermeldungen.Die meisten automatisierten Tools haben keine (zumindest offensichtlichen) Möglichkeiten, diese wahrscheinlichen Fehler zu spezifizieren und zu beheben. Oft erfordert die Wiederherstellung ein wenig syntaktische Analyse, so dass dies alles andere als offensichtlich ist.
Daher ist die Verwendung eines automatisierten Tools mit einem Kompromiss verbunden: Sie erhalten Ihren Parser schnell, aber er ist weniger benutzerfreundlich.
quelle
Da Sie lernen möchten, wie Lexer funktionieren, möchten Sie vermutlich wissen, wie Lexer-Generatoren funktionieren.
Ein Lexer-Generator verwendet eine lexikalische Spezifikation, bei der es sich um eine Liste von Regeln (Token-Paare aus regulären Ausdrücken) handelt, und generiert einen Lexer. Dieser resultierende Lexer kann dann eine Eingabe- (Zeichen-) Zeichenfolge gemäß dieser Liste von Regeln in eine Token-Zeichenfolge umwandeln.
Die am häufigsten verwendete Methode besteht hauptsächlich darin, einen regulären Ausdruck über einen nicht deterministischen Automaten (NFA) und einige Details in einen deterministischen endlichen Automaten (DFA) umzuwandeln.
Eine ausführliche Anleitung zur Durchführung dieser Transformation finden Sie hier . Beachten Sie, dass ich es nicht selbst gelesen habe, aber es sieht ganz gut aus. Außerdem wird in den ersten Kapiteln so gut wie jedes Buch zum Thema Compilerkonstruktion diese Transformation enthalten.
Wenn Sie sich für Vorlesungsfolien von Lehrveranstaltungen zum Thema interessieren, gibt es zweifellos unendlich viele von ihnen aus Lehrveranstaltungen zur Compilerkonstruktion. An meiner Universität finden Sie solche Folien hier und hier .
Es gibt noch einige Dinge, die normalerweise nicht in Lexern verwendet oder in Texten behandelt werden, die aber dennoch sehr nützlich sind:
Erstens ist der Umgang mit Unicode nicht ganz einfach. Das Problem ist, dass die ASCII-Eingabe nur 8 Bit breit ist. Dies bedeutet, dass Sie problemlos eine Übergangstabelle für jeden Status im DFA erstellen können, da sie nur 256 Einträge enthält. Da Unicode jedoch 16 Bit breit ist (wenn Sie UTF-16 verwenden), sind für jeden Eintrag im DFA 64-KB-Tabellen erforderlich. Wenn Sie komplexe Grammatiken haben, kann dies sehr viel Platz in Anspruch nehmen. Das Befüllen dieser Tische nimmt ebenfalls viel Zeit in Anspruch.
Alternativ können Sie Intervallbäume generieren. Ein Bereichsbaum kann beispielsweise die Tupel ('a', 'z'), ('A', 'Z') enthalten. Dies ist viel speichereffizienter als die vollständige Tabelle. Wenn Sie nicht überlappende Intervalle beibehalten, können Sie für diesen Zweck einen beliebigen ausgeglichenen Binärbaum verwenden. Die Laufzeit ist linear in der Anzahl der Bits, die Sie für jedes Zeichen benötigen, also O (16) im Unicode-Fall. Im besten Fall wird es jedoch in der Regel einiges weniger sein.
Ein weiteres Problem ist, dass die Lexer, wie sie üblicherweise erzeugt werden, tatsächlich eine quadratische Leistung im ungünstigsten Fall haben. Obwohl dieses Verhalten im schlimmsten Fall nicht häufig vorkommt, kann es Sie beißen. Wenn Sie in das Problem laufen und es lösen wollen, ein Papier beschreibt , wie die lineare Zeit zu erreichen , kann gefunden werden hier .
Möglicherweise möchten Sie reguläre Ausdrücke so beschreiben können, wie sie normalerweise angezeigt werden. Das Parsen dieser Beschreibungen regulärer Ausdrücke in NFAs (oder möglicherweise zuerst in eine rekursive Zwischenstruktur) ist jedoch ein kleines Henne-Ei-Problem. Zum Parsen von Beschreibungen regulärer Ausdrücke ist der Shunting Yard-Algorithmus sehr gut geeignet. Wikipedia scheint eine umfangreiche Seite über den Algorithmus zu haben .
quelle