28.10.2013 Aufrufe

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

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!