2 Lexikalische Analyse - Westfälische Wilhelms-Universität Münster
2 Lexikalische Analyse - Westfälische Wilhelms-Universität Münster
2 Lexikalische Analyse - Westfälische Wilhelms-Universität Münster
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
<strong>Westfälische</strong> <strong>Wilhelms</strong>-<strong>Universität</strong> <strong>Münster</strong><br />
Ausarbeitung<br />
<strong>Lexikalische</strong> <strong>Analyse</strong>, LEX<br />
im Rahmen des Seminars „Übersetzung von künstlichen Sprachen“<br />
Themensteller: Prof. Dr. Herbert Kuchen<br />
Betreuer: Tim Majchrzak<br />
Institut für Wirtschaftsinformatik<br />
Praktische Informatik in der Wirtschaft<br />
Karin Pietzonka
Inhaltsverzeichnis<br />
1 Motivation .................................................................................................................. 1<br />
2 <strong>Lexikalische</strong> <strong>Analyse</strong> ................................................................................................. 2<br />
2.1 Einordnung der lexikalischen <strong>Analyse</strong> in den Compilerbau .............................. 2<br />
2.1.1 Aufgaben der lexikalischen <strong>Analyse</strong> .......................................................... 3<br />
2.1.2 Trennung der lexikalischen und syntaktischen <strong>Analyse</strong> ............................. 4<br />
2.2 Das Verfahren der lexikalischen <strong>Analyse</strong> .......................................................... 5<br />
2.2.1 Einstiegsbeispiel ......................................................................................... 5<br />
2.2.2 Token .......................................................................................................... 6<br />
2.2.3 Reguläre Ausdrücke .................................................................................... 7<br />
2.2.4 Endliche Automaten ................................................................................... 8<br />
2.2.5 Pattern-Matching / Tokenerkennung ........................................................ 10<br />
2.2.6 Eingabepuffer ............................................................................................ 12<br />
2.3 Probleme .......................................................................................................... 13<br />
2.4 Aufwand und Optimierungsmöglichkeiten ...................................................... 14<br />
3 Generierung eines Scanners ..................................................................................... 15<br />
3.1 Anforderungen an einen Scanner ..................................................................... 15<br />
3.2 Manuelle Generierung ...................................................................................... 16<br />
3.3 Der Scannergenerator lex ................................................................................. 17<br />
3.3.1 Einführung in lex und Darstellung seiner Vorteile ................................... 17<br />
3.3.2 Arbeitsweise eines lex-Generators ............................................................ 18<br />
3.3.3 Struktur eines lex-Programmes ................................................................. 19<br />
3.4 Weitere Scannergeneratoren ............................................................................ 20<br />
4 Zusammenfassung und Ausblick ............................................................................. 21<br />
5 Literaturverzeichnis ................................................................................................. 23<br />
II
Kapitel 1: Motivation<br />
1 Motivation<br />
Der Compilerbau gilt als eines der ältesten Gebiete in der praktischen Informatik.<br />
Bereits in den 40er Jahren gab es die ersten Computer, welche Eingaben verarbeiten<br />
mussten. Mit der schnellen Weiterentwicklung, hin zu neuen Programmiersprachen,<br />
entwickelte sich auch der Compilerbau [ALSU08, S. 16 f]. Ein Compiler soll ein<br />
Quellprogramm in ein Zielprogramm, meist Maschinensprache für die Verarbeitung<br />
durch den jeweiligen Rechner, umwandeln. Moderne Compiler werden dazu heutzutage<br />
in verschiedene Phasen gegliedert.<br />
Die lexikalische <strong>Analyse</strong> ist, als erster Abschnitt einer der wichtigsten [Br04]. Ihre<br />
Aufgabe ist, das Quellprogramm einzulesen und für die weitere Verarbeitung<br />
vorzubereiten. Ohne den Einleseprozess innerhalb der lexikalischen <strong>Analyse</strong> würde der<br />
Compiler das Quellprogramm nicht erhalten und ein Kompilierungsvorgang wäre somit<br />
erst gar nicht möglich. Außerdem muss der eingelesene Text strukturiert werden, um<br />
den weiteren Umwandlungsprozess erheblich zu erleichtern. Die strukturierten und für<br />
den Rechner leichter zu verarbeitenden Ergebnisse aus der lexikalischen <strong>Analyse</strong><br />
können dann an den Parser und somit an den weiteren Umwandlungsprozess durch den<br />
Compiler weitergegeben werden. Aus diesem Grund soll in dieser Arbeit der Einlese-<br />
und Verarbeitungsprozess der lexikalischen <strong>Analyse</strong> genauer betrachtet und analysiert<br />
werden.<br />
Zu diesem Zweck wird die lexikalische <strong>Analyse</strong> mit ihren spezifischen Aufgaben<br />
zunächst noch einmal genauer in den Prozess des Kompilierens eingegliedert. In<br />
Kapitel 2 werden die grundlegenden Begriffe und Bestandteile der lexikalischen<br />
<strong>Analyse</strong> erläutert, um dann den Prozess der Mustererkennung und Vereinfachung zu<br />
klären. Anschließend soll in Kapitel 3 das Verfahren automatisiert und durch<br />
Implementierung von Generatoren für Nutzer erleichtert werden. Dazu wird der<br />
Scannergenerator lex und seine Arbeitsweise vorgestellt. Abschließend erfolgen eine<br />
Zusammenfassung der Arbeit sowie ein kurzer Ausblick auf künftige Entwicklungen.<br />
1
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
2 <strong>Lexikalische</strong> <strong>Analyse</strong><br />
2.1 Einordnung der lexikalischen <strong>Analyse</strong> in den Compilerbau<br />
Ein Compiler besteht aus verschiedenen Phasen, welche jeweils für bestimmte<br />
Aufgaben zuständig sind [He03, S. 4]. Die Aufgaben und Techniken des Compilerbaus<br />
sind im Allgemeinen dort anzutreffen, wo Zeichenfolgen sequentiell verarbeitet werden<br />
[Br04].<br />
Quellprogramm<br />
<strong>Lexikalische</strong><br />
<strong>Analyse</strong><br />
Syntaktische<br />
<strong>Analyse</strong><br />
Semantische<br />
<strong>Analyse</strong><br />
Zwischencode<br />
erzeugung<br />
Programmoptimierung<br />
Codegenerierung<br />
Zielprogramm<br />
Frontend<br />
(<strong>Analyse</strong><br />
phase)<br />
Backend<br />
(Synthese<br />
phase)<br />
Abbildung 1: Phasen des Compilers<br />
Quelle: Vgl. [He03, S. 4].<br />
Abbildung 1 zeigt, dass die Phasen des Compilers in zwei große Bereiche gegliedert<br />
werden, welche wiederum in je drei Teilaufgaben unterteilt werden [WH09, S. 57]. Der<br />
2
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
erste Bereich ist die <strong>Analyse</strong>phase (Frontend), welche den eingelesenen Quelltext<br />
analysiert, strukturiert und auf Fehler überprüft. Sie wird unterteilt in die lexikalische,<br />
die syntaktische und die semantische <strong>Analyse</strong>. Den zweiten Bereich stellt die<br />
Synthesephase (Backend) dar, deren Aufgabe die Erzeugung eines Zielprogramms ist.<br />
Die drei Aufgabenbereiche sind die Zwischencodeerzeugung, die<br />
Programmoptimierung und die Codegenerierung.<br />
Die lexikalische <strong>Analyse</strong> bildet somit die erste Phase eines Compilers, welche das<br />
unbearbeitete Quellprogramm einliest.<br />
2.1.1 Aufgaben der lexikalischen <strong>Analyse</strong><br />
Die lexikalische <strong>Analyse</strong> lässt sich, wie Abbildung 2 zeigt, in zwei aufeinanderfolgende<br />
Aufgabenbereiche unterteilen.<br />
Scannen<br />
<strong>Lexikalische</strong> <strong>Analyse</strong><br />
<strong>Lexikalische</strong><br />
<strong>Analyse</strong> i.e.S.<br />
Abbildung 2: Aufgabenbereiche der lexikalischen <strong>Analyse</strong><br />
Im ersten Schritt liest bzw. scannt die lexikalische <strong>Analyse</strong> die Eingabezeichen des<br />
Quellprogramms mit Hilfe eines Lexers ein. Durch den Prozess des Scannens bzw.<br />
Abtastens wird der Lexer auch häufig als Scanner bezeichnet. Damit der Parser in der<br />
nächsten Phase keine für das Ergebnis unnötigen Zeichen verarbeiten muss, entfernt der<br />
Lexer Kommentare und Leerraum, wie z.B. Leerzeichen, Tabulatoren, Zeilenwechsel<br />
und andere Trennzeichen. Außerdem kann der Lexer Fehlermeldungen zuordnen.<br />
<strong>Lexikalische</strong> Fehler wie beispielsweise eine zu große Zahl, die nicht dem Wertebereich<br />
entspricht, oder ein Kommentar, der nicht ordnungsgemäß mit „*/“ abgeschlossen wird,<br />
werden als Fehler gespeichert. Der Compiler übernimmt hierzu die Position des Tokens<br />
und verknüpft den Fehler mit der entsprechenden Zeilennummer. In einigen Compilern<br />
besteht die Möglichkeit, dass im Zuge der lexikalischen <strong>Analyse</strong> eine Kopie des<br />
Quellprogramms angelegt wird, um Fehlermeldungen direkt an der richtigen Stelle<br />
einzufügen. Dies dient in den nächsten Phasen des Compilers einer einfacheren<br />
Auffindung.<br />
3
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Die Hauptaufgabe des Lexers ist die lexikalische <strong>Analyse</strong> im engeren Sinne. Die<br />
eingelesenen Zeichen werden nach bestimmten Textmustern abgesucht und die<br />
erkannten Muster, auch Lexeme genannt, werden gruppiert. Für die identifizierten<br />
Gruppen von gleichen bzw. ähnlichen Textteilen werden sogenannte Token definiert.<br />
Die Ausgabe der lexikalischen <strong>Analyse</strong> ist somit eine Folge von Token, welche an den<br />
Parser zur syntaktischen <strong>Analyse</strong> gesendet wird [Wi02].<br />
Der Lexer kann durch die Möglichkeit der Erkennung verschiedenster Textzeichen und<br />
das Ersetzen von Textmustern, in einem anderen Kontext der Textverarbeitung, auch<br />
die Funktionen eines Präprozessors übernehmen, falls ein solcher fehlt. Dieser<br />
verarbeitet und entfernt, ähnlich wie der Lexer, beispielsweise Kommentare und<br />
Zeilenumbrüche.<br />
2.1.2 Trennung der lexikalischen und syntaktischen <strong>Analyse</strong><br />
Generell könnte die lexikalische <strong>Analyse</strong> zusammen mit der syntaktischen <strong>Analyse</strong><br />
beschrieben werden, da diese beiden Phasen eng zusammenarbeiten. Eine Trennung,<br />
wie sie hier vorgenommen wurde, ist allerdings aus verschiedenen Gründen sinnvoll<br />
[Br04].<br />
Zunächst dient eine Trennung der Vereinfachung des Entwurfs einer neuen Sprache, die<br />
der Compiler verarbeiten soll. Müsste der Parser in der Syntaxanalyse eine Grammatik<br />
einer Sprache verarbeiten, welche direkt auf den Eingabezeichen basiert, insbesondere<br />
Zeichen, wie z. B. Leerraum oder Kommata, wäre der Parser erheblich komplexer.<br />
Durch eine Trennung wird der Entwurf klarer und ergibt einen sauberen<br />
Gesamtentwurf.<br />
Ein weiterer Grund, der für eine Zerlegung der beiden Phasen spricht, ist die<br />
Effizienzverbesserung des Compilers. Getrennte Aufgabenbereiche können spezifischer<br />
und somit effizienter implementiert werden. Außerdem können so Techniken zur<br />
Beschleunigung der lexikalischen <strong>Analyse</strong> ermöglicht werden, ohne die syntaktische<br />
<strong>Analyse</strong> unnötig zu belasten.<br />
Weiterhin ermöglicht eine Trennung der Phasen eine bessere Portabilität. Es können<br />
Besonderheiten der Eingabegeräte berücksichtigt und ausschließlich auf den Scanner<br />
4
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
der lexikalischen <strong>Analyse</strong> beschränkt werden. Für diese Besonderheiten muss lediglich<br />
ein separater Compileraufruf in Kauf genommen werden.<br />
Eine häufige Implementierung der beiden Phasen beruht auf dem Aufruf des Lexers der<br />
lexikalischen <strong>Analyse</strong> durch den Parser der Syntaxanalyse (scan on demand) [WH09,<br />
S.59 f].<br />
Quellprogramm<br />
Token<br />
Lexer Parser<br />
getNextToken<br />
Symboltabelle<br />
Abbildung 3: Interaktion Lexer – Parser<br />
…<br />
Quelle: Vgl. [WH09, S. 59 f].<br />
Wie Abbildung 3 zeigt, liest der Lexer zunächst Zeichen aus der Eingabe und gibt,<br />
nachdem er ein Token identifiziert hat, dieses an den Parser zur syntaktischen <strong>Analyse</strong><br />
weiter. Mit dem Aufruf getNextToken veranlasst der Parser den Lexer die nächsten<br />
Zeichen der Eingabe zu lesen. Gefundene Muster bzw. Lexeme und andere<br />
Informationen, wie der Typ und die Stelle an der das Lexem das erste Mal aufgetaucht<br />
ist, fügt der Lexer in die Symboltabelle ein. Diese können dann vom Parser entnommen<br />
werden.<br />
2.2 Das Verfahren der lexikalischen <strong>Analyse</strong><br />
2.2.1 Einstiegsbeispiel<br />
Das Ziel der lexikalischen <strong>Analyse</strong> ist Muster bzw. Lexeme, d. h. vordefinierte<br />
Zeichenfolgen, innerhalb der Eingabe zu erkennen und diese in Token zu übertragen.<br />
Der Lexer erkennt ein Lexem als eine Instanz der festgelegten Tokenklasse. Hierbei<br />
kann ein Token nur eine oder auch mehrere Ausprägungen, d. h. Muster, besitzen. Das<br />
folgende Beispiel soll einen Einstieg in die Umwandlung von Mustern in Token geben.<br />
Es können beliebige Tokenklassen, beispielsweise Satzbausteine, definiert werden. Die<br />
lexikalische <strong>Analyse</strong> sucht in den Eingabezeichen nach den Mustern, die in einer<br />
5
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Tokenklasse definiert wurden und ersetzt diese durch den Tokennamen. Nach dem<br />
Prozess der lexikalischen <strong>Analyse</strong> würde der nachstehende deutsche Satz beispielsweise<br />
wie folgt umgewandelt:<br />
Ein Mann steigt in sein blaues Auto .<br />
artikel nomen verb präposition fürwort adjektiv nomen punkt<br />
Diese Folge von Token übergibt der Lexer dann zur weiteren Verarbeitung an den<br />
Parser.<br />
2.2.2 Token<br />
Token sind syntaktische Elementarbausteine [WH09, S. 58], die in verschiedene<br />
Klassen eingeteilt werden können. Sie bestehen aus einem Namen sowie einem<br />
optionalen Attributwert. Der Name ist ein selbstbestimmtes Symbol, das vom Parser<br />
weiterverarbeitet wird. Der Attributwert kann, wie z. B. bei Operatoren, die bereits<br />
durch ihren Namen, z. B. mult_op, eindeutig definiert sind, leer sein oder, wie bei<br />
Bezeichnern (ID), einen Zeiger enthalten, der auf einen entsprechenden Eintrag in der<br />
Symboltabelle verweist. Diese Einträge sind wichtig, da ansonsten der Compiler in<br />
späteren Phasen beispielsweise nicht entscheiden kann, ob es sich bei dem Token op um<br />
ein „+“ oder ein „-“ handelt.<br />
Tokenklassen können beliebig definiert werden und folgen keiner eindeutigen<br />
Vorschrift. Ein Großteil der Programmiersprachen jedoch umfasst die in Tabelle 1<br />
gezeigten Klassen [ALSU08, S. 137].<br />
Token Beispiel Beschreibung<br />
id abcd, Dipl.-Ing., E150d Buchstabe, auf den Buchstaben/Ziffern folgen<br />
num 3, 26.587, 0 alle Zahlen<br />
comp , =, !=, = alle Vergleichssymbole<br />
op +, -, /, * die Grundoperatoren<br />
saz (, ), ;, ,, … andere Satzzeichen (Klammern, Semikolon etc.)<br />
if if das Wort if<br />
else else das Wort else<br />
literal „ Ausgabe“ alles, was in Anführungszeichen steht<br />
Tabelle 1: Typische Tokenklassen<br />
Neben diesen Klassen existiert außerdem für jedes Schlüsselwort, z. B. begin oder<br />
end ein eigenes Token. Hierbei sind das Muster und der Tokenname gleich.<br />
6
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
2.2.3 Reguläre Ausdrücke<br />
Reguläre Ausdrücke bilden die Basis für die Beschreibung der zu erkennenden Muster<br />
im Eingabestrom des automatischen Scanners der lexikalischen <strong>Analyse</strong> und sind daher<br />
ein wesentlicher Bestandteil des Compilerbaus. Das folgende Beispiel zeigt den<br />
generellen Aufbau regulärer Ausdrücke:<br />
( `+` + `-`) ? (0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 ) + (, (0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 +<br />
8 + 9 ) + )?<br />
Mit Hilfe dieses regulären Ausdrucks soll die Darstellung einer Kommazahl<br />
aufgegriffen werden. Zunächst ist zu erkennen, dass zusammengehörige Abschnitte mit<br />
einer Klammer gebunden werden. Die verschiedenen Ausdruckteile werden ohne<br />
Zeichen aneinander gehängt. Somit bildet der gesamte Ausdruck die Konkatenation der<br />
einzelnen Stücke [VW06, S. 64]. Das große Pluszeichen ermöglicht eine Auswahl<br />
zwischen den Komponenten und definiert somit die vereinigte Menge der<br />
Komponenten. Ein Synonym für das „+“ bietet der senkrecht Strich „|“. Um<br />
festzulegen, wie oft ein Teil des regulären Ausdrucks wiederholt wird, d. h. wie viele<br />
Instanzen gebildet werden, gibt es die Zeichen „?“, „*“ und „ + “. Dabei steht das<br />
Fragezeichen, wie z. B. bei (+ | -) ?, für keine oder eine einzige Wiederholung. In einer<br />
Kommazahl sollen natürlich nicht beliebig viele Minuszeichen auftauchen, sondern<br />
höchstens eins zu Beginn. Eine weitere Darstellungsmöglichkeit für dieses Problem<br />
wäre die Schreibweise (+ | - | ε). Das Fragezeichen fällt in diesem Fall weg, da für<br />
„keine Wiederholung“ das leere Wort ε eingesetzt wurde. Das Pluszeichen steht für eine<br />
oder mehrere Instanzen des regulären Ausdrucks. Es soll nun also im mittleren Teil des<br />
Beispiels auf jeden Fall mindestens eine Ziffer abgebildet werden. Die dritte<br />
Möglichkeit, der Stern, bildet beliebig viele Wiederholungen ab, d. h. also auch keine<br />
ist möglich.<br />
Formal wird ein regulärer Ausdruck wie folgt definiert [VW06, S. 65]:<br />
Sei ∑ ein Alphabet, d. h. eine nicht leere, endliche Menge von Zeichen bzw.<br />
Zeichenreihen, die total geordnet ist.<br />
1. Ø ist ein regulärer Ausdruck und bezeichnet die leere Menge.<br />
2. ε ist ein regulärer Ausdruck und bezeichnet das leere Wort, d. h. die Menge {ε}.<br />
3. a ist ein regulärer Ausdruck und bezeichnet die Menge {a}.<br />
7
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Per Induktion sind nun auch nachfolgende Ausdrücke definiert [Sc08, S.28 f]:<br />
Seien a und b reguläre Ausdrücke, die die Mengen A und B beschreiben, dann<br />
a) ist (a + b) bzw. (a | b) ein regulärer Ausdruck und bezeichnet die Menge A B<br />
(Vereinigung).<br />
b) ist (ab) ein regulärer Ausdruck und bezeichnet die Menge AB (Konkatenation)<br />
c) ist (a*) ein regulärer Ausdruck und bezeichnet die Menge A* (Kleen’sche<br />
Hülle).<br />
d) Außerdem können weitere Klammern um die Ausdrücke gesetzt werden, ohne<br />
dass sich die zu den Ausdrücken gehörige Sprache ändert. Eine<br />
Klammereinsparung durch definierte Prioritäten der einzelnen Zeichen ist<br />
jedoch für eine bessere Lesbarkeit von Vorteil.<br />
Reguläre Sprachen [HJMJ02, S. 98] sind die Sprachen, die sich mit Hilfe regulärer<br />
Ausdrücke beschreiben lassen. Um regulären Ausdrücken beispielsweise<br />
verständlichere Namen zu geben, lassen sich reguläre Definitionen bilden [ALSU08,<br />
S. 149]. Sie werden in der Form<br />
Definition d → regulärer Ausdruck a (wobei a ∊ ∑),<br />
also beispielsweise<br />
ziffer → (0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 )<br />
gebildet. Die Definition d ist ein neues Symbol, welches nicht im Alphabet ∑ enthalten<br />
ist. Reguläre Definitionen sind hilfreich, um Rekursionen zu vermeiden und um die<br />
Lesbarkeit der Ausdrücke zu verbessern.<br />
2.2.4 Endliche Automaten<br />
Aus dem im vorigen Kapitel gebildeten regulären Ausdrücken können nun<br />
Übergangsdiagramme erstellt werden. Ein Übergangsdiagramm ist die grafische<br />
Darstellung eines endlichen Automaten [VW06, Kap. 2], der aus Zuständen und<br />
Übergängen zwischen diesen gebildet wird [HMJ02, S. 54]. Für den regulären<br />
Ausdruck der Kommazahl aus dem Beispiel würde das Übergangsdiagramm wie folgt<br />
aussehen:<br />
8
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Abbildung 4: Endlicher Automat für Kommazahl<br />
Zustände werden durch Kreise definiert, wobei der Anfangszustand mit einem Pfeil<br />
ohne Quelle gekennzeichnet wird. Ein möglicher Endzustand wird durch einen<br />
Doppelkreis abgebildet [ALSU08, S. 158]. Wie dieses einfache Beispiel zeigt, sind<br />
durchaus mehrere Endzustände erlaubt. Die Übergänge bzw. Kanten zwischen den<br />
Zuständen werden durch Pfeile symbolisiert. Als Beschriftung tragen sie das<br />
eingelesene Zeichen der Eingabe. Anstatt mehrere Pfeile mit unterschiedlichen<br />
Beschriftungen zwischen zwei Zuständen zu ziehen, werden alle Zeichen, getrennt<br />
durch ein Komma, auf einer Kante angeordnet.<br />
Formal wird ein endlicher Automat durch ein Tupel [Sc08, S. 19] definiert.<br />
EA = (Q, ∑, δ, qo,F)<br />
Q: endliche Menge von Zuständen hier: q0, q1, q2, q3 und q4<br />
∑: endliche Menge von Eingabesymbolen hier: +. -, ∊, ziffer und ,<br />
δ: Q × ∑ → Q Übergangsfunktion Menge von Regeln, durch die die<br />
Übergänge abgebildet werden<br />
qo ∊ Q: Startzustand<br />
F Q: Endzustand hier: q2 und q4<br />
Eine eingegebene Zeichenreihe bzw. ein regulärer Ausdruck wird von einem endlichen<br />
Automaten akzeptiert, wenn der Automat einen Endzustand erreicht. Die Menge aller<br />
akzeptierten Worte heißt dann reguläre bzw. akzeptierte Sprache.<br />
Übergangsdiagramme sind nur eine Darstellungsform für endliche Automaten. Neben<br />
dieser und der in der Definition gesehenen Mengen- bzw. Tupelschreibweise, gibt es<br />
noch die Darstellung in Übergangstabellen [ALSU08, S. 179]. In der Tabelle<br />
repräsentieren die Zeilen die Zustände und die Spalten die eingegebenen Zeichen. In der<br />
Tabellenmitte wird derjenige Zustand eingetragen, der vom Zustand durch Eingabe des<br />
9
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Zeichens erreicht wird. Diese Darstellungsform ist übersichtlich und gesuchte<br />
Übergänge lassen sich leicht finden. Allerdings verbraucht sie viel unnötigen Platz, da<br />
in der Regel die meisten Tabellenfelder ungenutzt bleiben.<br />
Bisher wurde nur der deterministische endliche Automat (EA) betrachtet. Ein<br />
nichtdeterministischer endlicher Automat (NEA) unterscheidet sich von einem EA<br />
dadurch, dass, wie in Abbildung 5 gezeigt, aus einem Zustand mehrere Kanten mit der<br />
gleichen Beschriftung in verschiedene Zustände laufen können [VW06, S. 27].<br />
Abbildung 5: Nichtdeterministischer endlicher Automat<br />
Jeder NEA lässt sich mit dem Verfahren der Potenzmengenkonstruktion in einen EA<br />
umwandeln. Die Idee dieses Verfahrens ist, dass der zu konstruierende EA die Zustände<br />
verwendet, in denen sich der NEA befinden könnte. Die auf diese Weise erstellte<br />
Zustandsmenge des EA ist somit ein Teil der Potenzmenge aus den NEA-Zuständen.<br />
Es ist leicht ersichtlich, dass ein EA auf Grund seiner Kürze und Eindeutigkeit schneller<br />
ist. Allerdings wird in den meisten Fällen der erste Entwurf ein NEA sein, der<br />
umgewandelt werden muss. Die Kosten für die Umwandlung lohnen sich jedoch im Fall<br />
der lexikalischen <strong>Analyse</strong>, da der erstellte Automat durch die Eingabestücke mehrfach<br />
genutzt wird [ALSU08, S. 197 ff].<br />
2.2.5 Pattern-Matching / Tokenerkennung<br />
Die Hauptaufgabe der lexikalischen <strong>Analyse</strong> ist die Mustererkennung im eingelesenen<br />
Quellprogramm und die Zuweisung von Token. Dieses Vorgehen kann in folgende<br />
Teile gegliedert werden.<br />
1. Ausblenden bedeutungsloser Zeichen, u. a. Stopworteliminierung<br />
2. Worterkennung<br />
3. Kodieren der Symbole<br />
10
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Zunächst werden nach dem Einlesen die für eine einfachere <strong>Analyse</strong> bedeutungslosen<br />
Zeichen, wie z. B. Leerzeichen und Wörter, die keinen Sinn für die <strong>Analyse</strong> machen,<br />
herausgefiltert und eliminiert [Kl99]. In einigen Fällen wird für diese Aufgabe ein<br />
separater Screener genutzt.<br />
Voraussetzung für die eigentliche <strong>Analyse</strong>phase sind vordefinierte reguläre Ausdrücke<br />
und ihnen zugeordnete Token, damit die erkannten Lexeme hiermit verglichen werden<br />
können.<br />
In der Phase der Worterkennung sollen nun Muster bzw. Lexeme im Eingabetext<br />
gefunden werden (Pattern-Matching). Um eine Eingabe zu prüfen, kann diese von<br />
einem durch reguläre Ausdrücke definierten Übergangsdiagramm gelesen werden.<br />
Erreicht die Eingabe einen Endzustand, d. h. wird sie akzeptiert, ist ein Lexem gefunden<br />
[ALSU08, S. 204]. In einem Automaten, wie in Abbildung 6 gezeigt, kann das Lexem<br />
„begin“ überprüft werden.<br />
Abbildung 6: EA für das Lexem "begin"<br />
Stimmt die Eingabe mit dem Lexem „begin“ überein, erreicht der Automat den<br />
Endzustand und „begin“ wurde erkannt. Die Ausgabe würde nun return (BEGIN);<br />
lauten. Damit wäre auch direkt das Token BEGIN zugewiesen. Würde das eingegebene<br />
Wort beispielsweise „begi“ lauten, würde der Automat nicht den Zustand q5 erreichen,<br />
sondern einen Bezeichner identifizieren und als Token ID ausgeben.<br />
Während der Eingabe kann es zu Problemen der Mehrdeutigkeit kommen. Es passen<br />
mehrere Muster und es muss entschieden werden, welches genommen wird.<br />
Beispielsweise kann die Zeichenfolge „< =“ auftauchen, die entweder als zwei einzelne<br />
Zeichen oder als ein Symbol interpretiert werden kann. Die lexikalische <strong>Analyse</strong> bietet<br />
für dieses Problem zwei Lösungsansätze. Im ersten Ansatz werden die eingegeben<br />
Zeichen so lang gelesen, bis das nächste Zeichen zu keinem Lexem mehr passt. Es wird<br />
also das längste, mögliche Muster (longest match) ausgewählt [LMB92, S. 34].<br />
11
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Einen zweiten Lösungsansatz bietet die Symboltabelle. Üblicherweise werden, wie in<br />
Abbildung 7, bis Index 9 die sogenannten reservierten Schlüsselwörter verwaltet<br />
[ALSU08, S. 160]. Diese Wörter sind, wie z. B. BEGIN, Wörter, die in der<br />
Programmiersprache eine bestimmte Rolle spielen und daher geschützt werden müssen.<br />
Index<br />
1<br />
2<br />
3<br />
4<br />
.<br />
.<br />
.<br />
9<br />
10<br />
11<br />
12<br />
const<br />
var<br />
begin<br />
end<br />
.<br />
.<br />
.<br />
odd<br />
abcd<br />
Dipl.-Ing.<br />
E150d<br />
Schlüsselwörter<br />
Bezeichner<br />
Abbildung 7: Symboltabelle mit reservierten Schlüsselwörtern<br />
Taucht nun das Problem der Mehrdeutigkeit auf, wird das Wort erkannt, welches als<br />
erstes in der Symboltabelle steht. Findet die lexikalische <strong>Analyse</strong> z. Β. das Muster<br />
„beginnen“, wird sie auf Grund des zweiten Lösungsansatzes das Lexem BEGIN sowie<br />
einen Bezeichner „nen“ identifizieren. Als Ergebnis werden die beiden Token BEGIN<br />
und ID ausgegeben.<br />
2.2.6 Eingabepuffer<br />
Das Einlesen der Eingabezeichen wird dadurch erschwert, dass oft über das nächste<br />
Lexem hinausgeblickt werden muss (Lookahead). Um das Muster zu identifizieren,<br />
müssen noch ein oder mehrere zusätzliche Zeichen gelesen werden. Der Eingabepuffer<br />
soll diese Problem mindern und den Einlesevorgang beschleunigen, indem nicht jedes<br />
Zeichen einzeln aufgerufen werden muss.<br />
Aufgebaut ist solch ein Eingabepuffer meist durch zwei separate Puffer der Größe N,<br />
wie ihn Abbildung 8 zeigt [ALSU08, S. 141]. Die Größe entspricht hierbei<br />
12
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
üblicherweise einem Festplattenblock. Jeder der Puffer kann somit bis zu N Zeichen<br />
einlesen. Sollte die Eingabe weniger Zeichen haben, wird das Ende durch eof<br />
gekennzeichnet. Normalerweise genügen N Pufferplätze, da Lexeme in modernen<br />
Sprachen kurz sind [ALSU08, S. 142] und ein Lookahead von ein bis zwei Zeichen<br />
genügt. Sollte ein Muster dennoch länger sein, wird die Zeichenkette in mehrere Zeilen<br />
unterteilt. Die Zeilen werden dann als Konkatenation behandelt und mit einem „+“<br />
verbunden.<br />
Umf = 2 * Pi * Rad eof<br />
lexemeBegin<br />
forward<br />
Abbildung 8: Eingabepuffer<br />
Quelle: Vgl. [ALSU08, S. 141]<br />
Für den Eingabepuffer gibt es zwei Zeiger, welche die Mustersuche kontrollieren. Der<br />
Zeiger lexemeBegin markiert den Anfang des aktuellen Lexems, während forward<br />
jeweils ein Zeichen vorrückt, bis er eine Musterübereinstimmung gefunden hat. Erreicht<br />
forward das Ende des einen Puffers, wird der andere gefüllt und die Suche wird dort<br />
fortgesetzt.<br />
2.3 Probleme<br />
Einige Probleme, wie z. B. Behandlung von Mehrdeutigkeiten, Zeichensetzungsfehler<br />
im Programmtext oder Beschleunigung des Einleseprozesses, kann die die lexikalische<br />
<strong>Analyse</strong> lösen.<br />
Ein großes Problem, auf das die lexikalische <strong>Analyse</strong> jedoch stößt, betrifft die<br />
Rechtschreibung [ALSU08, S. 139]. Wenn der Nutzer einen Quelltext mit falsch<br />
geschriebenem Text eingibt oder bei der Definition der regulären Ausdrücke einen<br />
unbeabsichtigten Fehler macht, kann dieser nicht erkannt werden. Beispielsweise würde<br />
das Wort „begni“ im <strong>Analyse</strong>prozess als Bezeichner identifiziert und bekäme anstatt<br />
BEGIN das Token ID zugewiesen. Der Lexer würde gegebenenfalls sogar abstürzen, da<br />
er kein passendes Präfix eines Musters findet.<br />
13
Kapitel 2: <strong>Lexikalische</strong> <strong>Analyse</strong><br />
Da in der Praxis die meisten Fehler nur ein Zeichen betreffen, ist es empfehlenswert zu<br />
schauen, ob sich die Eingabe durch einzelne Umformungen in ein gültiges Lexem<br />
umwandeln lässt. Dies geschieht mit Hilfe von Recovery-Aktionen, z. B. dem Panic<br />
Mode Recovery [Kl99]. Es werden nacheinander die eingegebenen Zeichen gelöscht,<br />
bis ein bekanntes, einwandfreies Token gefunden wird. Weitere Techniken zur<br />
Fehlerbehebung sind das Löschen eines überflüssigen Zeichens, das Einfügen eines<br />
fehlenden oder das Ersetzen eines Zeichens durch ein anderes. Außerdem können zwei<br />
benachbarte Zeichen vertauscht werden, damit Rechtschreibfehler wie in „begni“<br />
behoben werden [ALSU08, S. 140]. Die Gefahr bei diesen Techniken besteht darin,<br />
dass erneut das Problem der Mehrdeutigkeit entsteht. So könnte in diesem Fall sowohl<br />
BEGIN als auch BEGNII ein vordefiniertes, einwandfreies Token sein.<br />
2.4 Aufwand und Optimierungsmöglichkeiten<br />
Für eine effiziente Umsetzung der lexikalischen <strong>Analyse</strong> ist eine Aufwandsbetrachtung<br />
unumgänglich. Bisher gibt es kaum Studien, die den Aufwand bzw. die Kosten der<br />
lexikalischen <strong>Analyse</strong> genau bestimmen. Da jedoch während dieser Phase jedes<br />
Eingabezeichen einzeln geprüft werden muss, ist sie sehr teuer und es wird geschätzt,<br />
dass ca. 50% der gesamten Ressourcen der Kompilierung für die lexikalische <strong>Analyse</strong><br />
verwendet werden [Kl99].<br />
Der genaue Aufwand hängt jedoch von unterschiedlichen Faktoren ab. Jede<br />
Programmiersprache handhabt beispielsweise den Einsatz von Leerzeichen für die<br />
Lesbarkeit oder Verwendung von Schlüsselwörtern anders. So braucht ein Scanner, der<br />
in Fortran verfasst wurde, aus diesen Gründen wesentlich mehr Lookahead als ein in<br />
Pascal programmierter. Ein weiterer Aspekt, der bei der Aufwandsschätzung betrachtet<br />
werden sollte, ist die Länge der Eingabe. Je länger der Eingabestrom, desto länger läuft<br />
natürlich auch die gesamte <strong>Analyse</strong>. Hierbei kommt es nicht auf die Anzahl oder<br />
Komplexität der zu erkennenden Regeln an. Diese spielen lediglich für die Größe des<br />
lex-Programmes eine Rolle. Eine Ausnahme jedoch sind die Regeln, welche einen<br />
Lookahead voraussetzen. Durch diesen ergibt sich offensichtlich ein doppelter<br />
Aufwand, da hier im Eingabestrom im Voraus gelesen werden muss, um ein aktuelles<br />
Lexem zu bestimmen und dann erneut, um das nächste Muster zu finden.<br />
14
Kapitel 3: Generierung eines Scanners<br />
Es muss also bei der Generierung eines Lexers für die lexikalische <strong>Analyse</strong> besonderer<br />
Wert auf die Operationen, die einzelne Zeichen betreffen, gelegt werden, damit vor<br />
allem der zeitaufwändige Einleseprozess beschleunigt wird.<br />
Im Folgenden sollen kurz drei mögliche Optimierungsansätze vorgestellt werden,<br />
welche das Pattern Matching auf Basis regulärer Ausdrücke verbessern können<br />
[ALSU08, S. 209]. Der erste Algorithmus hilft beim Aufbau der Automaten aus den<br />
regulären Ausdrücken direkt einen EA zu erstellen, ohne den Umweg über einen NEA.<br />
Dadurch werden zusätzliche Kosten vermieden. Außerdem kann ein auf diese Weise<br />
erstellter Automat auch gegebenenfalls weniger Zustände aufweisen. Ein zweiter<br />
Optimierungsansatz beschäftigt sich mit der Minimierung der Anzahl der Zustände.<br />
Durch ein Zusammenfassen identischer Zustände wird die Mustererkennung effizienter<br />
und läuft in der Zeit O(n log n) ab, wobei n die Anzahl der Zustände markiert. Im<br />
letzten Algorithmus soll die Mustererkennung über Übergangstabellen erfolgen. Dazu<br />
müssen diese kompakter aufgebaut werden, als die Platz verbrauchende, ursprüngliche<br />
Schreibweise.<br />
3 Generierung eines Scanners<br />
3.1 Anforderungen an einen Scanner<br />
Bei der Entwicklung und Implementierung eines Scanners müssen zunächst einige<br />
Fragen bezüglich der Anforderungen geklärt werden. Die Implementierung des<br />
Scanners entscheidet später, in welcher Form die Eingabedaten für den Compiler<br />
vorliegen müssen, damit sie verarbeitet werden können.<br />
In der Designphase muss sich der Nutzer auch über einfache Fragestellungen, z. B.<br />
„Was ist ein Wort für meinen Compiler?“, Gedanken machen. Ein Wort ist zunächst<br />
einmal eine Zeichenkette beliebiger Länge. Jedoch muss festgelegt werden, ob „BeGin“<br />
das gleiche wie „BEGIN“ oder „begin“ ist. Soll das Programm Groß- und<br />
Kleinschreibung unterscheiden und wenn ja auf welche Weise? Geklärt werden muss<br />
auch, ob das Wort „Beginnen“ eine ID darstellt oder ob es das Schlüsselwort BEGIN<br />
mit einem folgenenden Bezeichner NEN ist.<br />
15
Kapitel 3: Generierung eines Scanners<br />
Eine andere Fragestellung betrifft die Darstellung der Zahlen. Sind diese grundsätzlich<br />
erlaubt? Was passiert mit Wörtern, die sowohl aus Zahlen, als auch aus Buchstaben<br />
bestehen (z. B. Farbstoff E150d)? Außerdem müssen Wörter mit Bindestrichen und<br />
weitere Zeichensetzungen berücksichtigt werden.<br />
Diese Probleme der Sprachbeschreibung sind nicht schwer zu lösen bzw. es existieren<br />
bereits Lösungen, wie z. B. die Methode der reservierten Schlüsselwörter. Dennoch<br />
sollte sich der Nutzer vor Beginn der Implementierung diese Probleme bewusst machen.<br />
Ansonsten kann es zur Laufzeit des Scanners Probleme geben, wenn der Scanner ein<br />
eingelesenes Zeichen einer Tokenklasse nicht erkennen kann und den gesamten<br />
Vorgang der lexikalischen <strong>Analyse</strong> unterbricht.<br />
Eine weitere Anforderung an den Scanner muss eine effiziente Durchführung der<br />
lexikalischen <strong>Analyse</strong> sein. Effizienz ist in allen Bereichen der Programmierung nahezu<br />
unumgänglich und sollte auch hier nicht vernachlässigt werden. Eine effiziente<br />
lexikalische <strong>Analyse</strong> dient dem kompletten Prozess des Compilers. Je schneller der<br />
Scanner ein klares und eindeutiges Ergebnis ohne Fehler für die Weiterverarbeitung<br />
liefert, desto schneller kann auch von der Syntaxanalyse und den folgenden Phasen ein<br />
Ergebnis erwartet werden. Besonders im parallelen Verarbeitungsprozess des scan on<br />
demand ist eine zügige Bearbeitung erforderlich, um einen effizienten Ablauf zu<br />
gewährleisten.<br />
3.2 Manuelle Generierung<br />
Die erste intuitive Erstellung eines Scanners kann per Hand erfolgen. Dazu wird im<br />
ersten Schritt ein Übergangsdiagramm erstellt, welches die Struktur der Symbole des<br />
Quellprogramms beschreibt.<br />
Im nächsten Schritt wird zu diesem Diagramm ein Programm erstellt, das die Symbole /<br />
Token erkennt. Dazu müssen die Zustände nummeriert und jeweils durch ein Stück<br />
Code wiedergegeben werden. Eine Variable state nimmt während des Durchlaufs die<br />
Nummer des gerade aktuellen Zustands auf.<br />
Um nun den erstellten Code des Übergangsdiagramms in einen Scanner zu überführen,<br />
gibt es drei verschiedene Möglichkeiten [ALSU08, S. 164] die eingelesenen Zeichen<br />
einem Token zuzuordnen. Die erste Möglichkeit besteht darin, die einzelnen<br />
16
Kapitel 3: Generierung eines Scanners<br />
Übergangsdiagramme nacheinander auszuprobieren, bis das Muster mit dem Diagramm<br />
eines Token übereinstimmt. Da dieses Verfahren allerdings äußerst lang dauern würde,<br />
sieht die zweite Option ein paralleles Testen der Übergangsdiagramme vor. Es wird<br />
immer das nächste Zeichen der Eingabe an alle Diagramme übermittelt und so das<br />
längste, passende Token ermittelt. Die effizienteste und damit am meisten genutzte<br />
Variante zielt auf ein einziges, zusammengefasstes Übergangsdiagramm ab. Genau wie<br />
bei der zweiten Möglichkeit wird die Eingabe solang gelesen, bis es keinen nächsten<br />
Zustand gibt und das längste, passende Muster ermittelt wurde.<br />
Die Implementierung eines Scanners per Hand kann, wenn sie fehlerfrei erfolgt, einen<br />
effizienten Scanner für ein kleines Problem, beispielsweise einen einfachen<br />
Taschenrechner, ergeben. Allerdings können zum einen leichte Änderungen hin zu<br />
einem anderen Problem äußerst umfangreich und langwierig sein. Zum anderen ist eine<br />
Fehlerfreiheit bei manueller Implementierung in den meisten Fällen nicht gegeben und<br />
eine Identifizierung und Auffindung auftretender Probleme ist äußerst schwierig.<br />
3.3 Der Scannergenerator lex<br />
3.3.1 Einführung in lex und Darstellung seiner Vorteile<br />
In den 70er Jahren wurde der Scannergenerator lex als Ergänzung zum Parser-Generator<br />
yacc entwickelt. Lex ist ein Unix-Standardwerkzeug [VW06, S.79] und ist selbst<br />
vergleichbar mit einem Compiler, der automatisch einen Scanner erzeugt und somit lex-<br />
Programme in ein C-Programm für die lexikalische <strong>Analyse</strong> umwandelt. Im Gegensatz<br />
zur manuellen Implementierung ermöglicht dieser Generator eine kürzere<br />
Entwicklungszeit, da die Eingabezeichen nicht mühsam per Hand gesucht und extrahiert<br />
werden müssen. Er ist somit besonders für komplizierte und komplexe Probleme<br />
geeignet. Außerdem ist durch den immer gleichen Aufbau eine bessere Lesbarkeit<br />
gegeben und auch Änderungen lassen sich leichter durchführen. Nutzer von lex<br />
benötigen keine umfangreichen Programmier- und Pattern-Matching-Kenntnisse,<br />
sondern es genügen Grundkenntnisse der theoretischen Informatik, insbesondere der<br />
regulären Ausdrücke [LS06].<br />
17
Kapitel 3: Generierung eines Scanners<br />
3.3.2 Arbeitsweise eines lex-Generators<br />
Als Eingabe akzeptiert der Scannergenerator lex ein sogenanntes lex-Programm. Ein<br />
lex-Programm ist eine Tabelle mit regulären Ausdrücken, die den zu erstellenden<br />
Scanner bzw. Lexer beschreibt [ALSU08, S. 171]. Außerdem ist in der Tabelle der<br />
dazugehörige Programmteil enthalten, welcher die zu treffenden Aktionen repräsentiert<br />
und üblicherweise in C verfasst ist. Möglich ist aber auch ein Programmteil in der<br />
Programmiersprache Ratfor aus dem Jahr 1976, zu der automatische Übersetzungen in<br />
Fotran angeboten werden [He03, S. 25]. Das Programm besteht aus drei Teilen, welche<br />
in Kapitel 3.3.3 näher beschrieben sind.<br />
Wie in Abbildung 9 [Be02] gezeigt, wird aus dem lex-Programm, hier lex.l genannt,<br />
mit Hilfe des Lex-Compilers die Funktion yylex() konstruiert und in die Datei<br />
lex.yy.c überführt. Im Hintergrund dieses Compilers werden, ähnlich wie bei der<br />
manuellen Generierung, aus dem eingegebenen lex-Programm ein Übergangsdiagramm<br />
bzw. der dazugehörige Code erzeugt. Die Datei lex.yy.c kann dann dieses<br />
Übergangsdiagramm simulieren.<br />
lex-Programm<br />
lex.l<br />
Lex-<br />
Compiler<br />
lex.yy.c<br />
lex.yy.c C-Compiler a.out<br />
Eingabezeichen a.out<br />
Folge von Token<br />
Abbildung 9: Lexer-Erstellung mit Lex<br />
Im nächsten Schritt wird die Datei lex.yy.c mit Hilfe des C-Compilers zu a.out<br />
kompiliert. Bei Verwendung der C-Funktion a.out wird eine Integerzahl<br />
zurückgegeben. Diese Zahl stellt dann einen Code für ein mögliches Token dar.<br />
Zusammenfassend müssen also für die Erstellung eines Lexers mit lex ein lex-<br />
Programm, welches insbesondere aus der Definition von Symbolen mittels regulärer<br />
Ausdrücke besteht, und ein Eingabestrom vorhanden sein. Der erstellte Scanner kann<br />
nun Lexeme erkennen und Folgen von Token ausgeben. Gleichzeitig kann er die im C-<br />
Teil des lex-Programmes verknüpften Aktionen ausführen.<br />
18
Kapitel 3: Generierung eines Scanners<br />
Der Lexer kann während seines Aufrufs Attributwerte, wie z. B. Zeiger auf die<br />
Symboltabelle oder weiteren Code, in der globalen Variable yylval ablegen. Während<br />
der Syntaxanalyse kann der Parser diese Informationen aus der Variablen beziehen.<br />
3.3.3 Struktur eines lex-Programmes<br />
Ein lex-Programm ist, wie in Abbildung 10 gezeigt, aus einem Deklarationsteil, den<br />
Übersetzungsregeln und den Hilfsfunktionen [LS06] aufgebaut.<br />
%{<br />
Deklaration<br />
%}<br />
%%<br />
Übersetzungsregeln<br />
%%<br />
Hilfsfunktionen<br />
Abbildung 10: lex-Spezifikation<br />
Der Deklarations- bzw. Definitionsteil ist optional und enthält C-Code [Vö96]. Dieser<br />
besteht aus Optionen, Deklarationen von Variablen, manifesten Konstanten, welche<br />
z. B. mit Hilfe von #define erzeugt wurden, und regulären Definitionen. Da dieser<br />
Teil zwischen den Zeichen %{ … %} steht, wird er, laut lex-Definition, unverändert in<br />
den erzeugten Scanner übernommen.<br />
Der Hauptteil des lex-Programmes besteht aus den Übersetzungsregeln. Dies ist eine<br />
Tabelle mit Mustern und Aktionen in der Form Muster{Aktion}. Jedes Muster stellt<br />
dabei einen regulären Ausdruck dar, der reguläre Definitionen des<br />
Deklarationsabschnitts nutzt. Es kann eine Aktion aufrufen, die durch eine einzelne oder<br />
eine Folge von C-Anweisung deklariert ist. Anstelle einer C-Anweisung kann auch das<br />
Zeichen „|“ stehen, welches besagt, dass für dieses Muster die gleiche Aktion wie für<br />
das folgende Muster ausgeführt wird. Sollte ein gelesenes Zeichen nicht in ein Muster<br />
passen, wird es wie %{ … %} unverändert in die Ausgabe kopiert.<br />
Der dritte Teil, welcher die Hilfsfunktionen enthält, ist wieder optional. Üblicherweise<br />
beinhaltet er lokale Funktionen, die durch die Übersetzungsregeln genutzt und in<br />
19
Kapitel 3: Generierung eines Scanners<br />
Aktionen eingesetzt werden. Auch dieser Teil kann Textstücke enthalten, die<br />
unverändert übernommen werden.<br />
Zur Veranschaulichung des Programmaufbaus soll das Beispiel eines Taschenrechners<br />
dienen. Als Eingabezeichen gibt es die Zahlen von 0 bis 9 sowie die Zeichen +, -, / und<br />
*. Aus einem einfachen C-Programm [HE03, S. 8] dieser Art ergibt sich für die<br />
lexikalische <strong>Analyse</strong> mit Hilfe von lex folgender Code:<br />
%{<br />
#include <br />
#include „global.h“<br />
int tokenwert = NICHTS; /*Programmglobale Variable, der ggfs.<br />
Zahlenwert zugewisen wird*/<br />
int zeilennr = 1; /*Programmglobale Variable, enthaelt<br />
immer Nr der aktuellen Eingabezeile*/<br />
%}<br />
%%<br />
[ \t]+ /*Leer- und Tabzeichen ueberlesen*/<br />
\n {return (ZEILENENDE);}<br />
[0-9]+{tokenwert = strtol(yytext,NULL,10); return(ZAHL)}<br />
/*strtol wandelt den String aus<br />
yytext in eine Zahl um und weist sie<br />
tokenwert zu'/<br />
"+" {return (PLUS);}<br />
"-" {return (MINUS);}<br />
"*" {return (MULT);}<br />
"/" {return (DIV);}<br />
%%<br />
Dieses Beispiel enthält einen Deklarationsteil mit C-Definitionen und im Anschluss die<br />
Übersetzungsregeln, die definieren, welches Token (ZEILENENDE, ZAHL, PLUS,<br />
MINUS, MULT, DIV) ausgegeben wird.<br />
3.4 Weitere Scannergeneratoren<br />
Neben dem Ur-Scannergenerator lex gibt es noch zahlreiche weitere<br />
Scannergeneratoren.<br />
Der am weitesten verbreitete und frei erhältliche Generator ist Flex [Pa95]. Er ist der<br />
Nachfolger von lex und ermöglicht nun auch eine Nutzung unter Windows. Die<br />
Implementierung ist schneller und effizienter geworden. Genau wie bei lex werden<br />
20
Kapitel 4: Zusammenfassung und Ausblick<br />
reguläre Ausdrücke verarbeitet und das Ergebnis ist ein tabellengesteuerter Automat in<br />
C. Außerdem bietet Flex wieder eine Schnittstelle zum Parser-Generator von yacc.<br />
Ähnlich zu Flex ist der Scannergenerator Rex des GMD – Forschungszentrum<br />
Informationstechnik GmbH Karlsruhe. Auch er bietet programmierbare<br />
Zustandsübergänge und eine Schnittstelle zu yacc. Die Implementierung ist ebenso<br />
tabellengesteuert in C und schneller als bei lex. Es gibt jedoch auch eine<br />
Implementierungsmöglichkeit mit Modula-2.<br />
Der Scannergenerator GLA der University of Colorado hingegen beruht auf einer etwas<br />
anderen Implementierung. GLA arbeitet mit Komponenten der Entwicklungsumgebung<br />
Eli zusammen und liefert als Ergebnis einen direkt programmierten Automaten in C.<br />
Weitere Scannergeneratoren sind u. a. SableCC, der ein Java-Ergebnis liefert, ALEX,<br />
COCO und der Scanner-, Parser- und Compilergenerator VCC [WH09, S. 121].<br />
4 Zusammenfassung und Ausblick<br />
Als erste Phase bei der Übersetzung von Quellprogrammen durch einen Compiler ist die<br />
lexikalische <strong>Analyse</strong> einer der wichtigsten Abschnitte. Ihre erste Aufgabe ist das<br />
Einlesen des Quelltextes. Anschließend werden mit Hilfe eines Lexers Muster erkannt<br />
und diese durch Token ersetzt. Die lexikalische <strong>Analyse</strong> trägt somit zu einer<br />
nennenswerten Erleichterung der Kompilierung bei. Um den gesamten Prozess dieser<br />
Phase für einen Nutzer so leicht wie möglich zu machen, werden die zu erkennenden<br />
Muster mit Hilfe regulärer Ausdrücke definiert. Da keine umfangreichen<br />
Programmierkenntnisse erforderlich sind, sind auch Scannergeneratoren wie lex sehr<br />
attraktiv. Sie erleichtern den Vorgang der lexikalischen <strong>Analyse</strong> erheblich.<br />
An der Vielzahl der bereits existierenden Generatoren kann man erkennen, dass die<br />
Entwicklung noch kein Ende hat. Auch wenn der Compilerbau mit fast 70 Jahren eines<br />
der ältesten Gebiete der praktischen Informatik ist, kann es ständig Neuerungen und<br />
Verbesserungen geben. Beispielsweise können Generatoren auf unterschiedliche<br />
Programmiersprachen erweitert werden, um noch mehr Einsatzmöglichkeiten zu bieten.<br />
Es werden daher beispielsweise so genannte Migrationscompiler entwickelt. Sie helfen,<br />
große Projekte, welche in wenig bekannten Programmiersprachen verfasst sind, auf<br />
moderne Programmiersprachen zu migrieren. So wurde z. B. das Toyota-<br />
21
Kapitel 4: Zusammenfassung und Ausblick<br />
Händlersystem, welches auf ROSI-SQL beruhte, mit Hilfe eines solchen Compilers auf<br />
C++ umgestellt, um die hohen Investitionskosten in dieses Projekt nicht zu verschenken<br />
[BJ94]. Außerdem ist die Effizienzverbesserung ein wichtiger Faktor. Zukünftig müssen<br />
alle Geräte noch schneller und noch kleiner werden. So sind Techniken, wie die<br />
Übergangstabellenverkleinerung, wichtige Ansatzpunkte. Die Methoden werden immer<br />
ausgefeilter, um eine möglichst hohe Effizienz zu bieten. Gleichzeitig sollen aber auch<br />
die Kosten reduziert werden.<br />
Dadurch, dass es aktuell und auch in der Zukunft auf anderen Gebieten der Forschung,<br />
wie z. B. der Größe und Kapazität von Speichermedien und Verbesserung der<br />
Prozessoren, ständig neue Erkenntnisse gibt bzw. geben wird, kann der Compilerbau<br />
ebenso fortschreiten und somit neue Techniken, auch für die lexikalische <strong>Analyse</strong>,<br />
bieten.<br />
22
Kapitel 5: Literaturverzeichnis<br />
5 Literaturverzeichnis<br />
[ALSU07] A. V. Aho, M. S. Lam, R. Sethi, J. D. Ulman: Compilers, Pearson, 2007.<br />
[ALSU08] Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman: Compiler:<br />
Prinzipien, Techniken und Werkzeuge, 2. Auflage, Pearson Studium, 2008.<br />
[Be02] Peter Becker: <strong>Lexikalische</strong> <strong>Analyse</strong> und Parsing, http://www2.inf.fh-rheinsieg.de/~pbecke2m/textalgorithmen/lexanalyse.pdf,<br />
2002.<br />
[Br04] Jan Bredereke: Übersetzergenerierung mit lex & yacc,<br />
http://www.informatik.uni-bremen.de/agbs/lehre/ss04/uegen/, 2004.<br />
[BJ94] Peter Brückner, Wolfgang Jarosch: R2C–Migrationscompiler – Migration von<br />
ROSI-SQL nach C++, http://www.bj-ig.de/compilerbau.html, 1994.<br />
[He03] Helmut Herold: lex & yacc: Die Profitools zur lexikalischen und syntaktischen<br />
Textanalyse, 3. Auflage, Addison-Wesley Verlag, 2003.<br />
[HMJ02] John E. Hopcroft, Rajeev Motwani, Jeffrey D. Ullman: Einführung in die<br />
Automatentheorie, Formale Sprachen und Komplexität, 2. Auflage, Pearson<br />
Studium, 2002.<br />
[Kl99] Frank Kleine: <strong>Lexikalische</strong> <strong>Analyse</strong> und Stoplisten,<br />
http://talks.frankkleine.de/ir/, 1999.<br />
[LMB92] John Levine, Tony Mason, Doug Brown: lex & yacc, 2. Auflage, O`Reilly,<br />
1992.<br />
[LS06] M. E. Lesk and E. Schmidt: Lex – A Lexical Analyzer Generator,<br />
http://dinosaur.compilertools.net/lex/index.html, 2006.<br />
[Pa95] Vern Paxson: Flex, version 2.5: A fast scanner generator,<br />
http://dinosaur.compilertools.net/flex/index.html, 1995<br />
[Sc08] Uwe Schöning: Theoretische Informatik – kurz gefasst, 5. Auflage, Spektrum<br />
Akademischer Verlag, 2008.<br />
[VW06] Gottfried Vossen, Kurt-Ulrich Witt: Grundkurs Theoretische Informatik,<br />
3. Auflage, Vieweg+Teubner Verlag, 2006.<br />
[Vö96] Reinhard Völler: Formale Sprachen und Compiler, http://users.informatik.hawhamburg.de/~voeller/fc/comp/comp.html,<br />
1996.<br />
[WH09] Christian Wagenknecht, Michael Hielscher: Formale Sprachen, abstrakte<br />
Automaten und Compiler, Vieweg+Teubner Verlag, 2009.<br />
[Wi02] Arnold Willemer: Compilerbau,<br />
http://www.willemer.de/informatik/compiler/index.htm, 2002<br />
[Wi08] Niklaus Wirth: Grundlagen und Techniken des Compilerbaus, 2. Auflage,<br />
Oldenbourg Wissenschaftsverlag, 2008.<br />
23