Programmiersprachen und Übersetzer Sommersemester 2009 27. April 2009 Aufbau von lexikalen Scannern Beispiel Festlegung der Tokenklassen für eine Programmiersprache“, die ” nur einfache Wertzuweisungen mit arithmetischen Ausdrücken und if-Anweisungen enthält. Zunächst definiert man sich zweckmäßigerweise Abkürzungen <letter> = [A - Z] | [a - z] <digit> = [0 - 9] Die Tokenklassen ident (Identifier) und number (vorzeichenlose Integer- und Real-Zahlen) wären dann: <letter> ( <letter> | <digit> )∗ <digit>+ ( .<digit>+ )? Die Tokenklasse relop (Vergleichsoperator) ist durch den regulären Ausdruck: = | <= | <> | < | > | >= definiert und entsprechend arithop durch: + | - | * | / Die reservierten Wörter if, then und else bilden jeweils für sich eine Tokenklasse. Verwendet man einen Scanner-Generator wie etwa LEX, FLEX oder JFLEX, so reicht es aus, reguläre Ausdrücke zu definieren und geeignete Aktionen zu konstruieren, die beim Erkennen eines Tokens ausgeführt werden sollen. Es ist wichtig, dass alle Eingabezeichen zu Token zusammengefasst werden. Aus diesem Grund sollte man dann noch eine weitere Tokenklasse ws (white space) schaffen, die das Überlesen von Leerzeichen usw. steuert. <delim> = <blank> | <tab> | <newline> und die Tokenklasse ws wird definiert durch <delim>+ Die zugeordnete Aktion wäre dann natürlich eine Null-Operation! Arbeitsweise von Scanner-Generatoren (LEX) Lex-Quellenprogramm- Lex-Compiler lex.l lex.yy.c - C-Compiler EingabeProgramm - a.out - lex.yy.c - a.out - Folge von Token Aufbau eines Lex-Programms %{ I Deklarationen von Konstanten, Variablen usw. %} Aufbau eines Lex-Programms %{ I Deklarationen von Konstanten, Variablen usw. %} I Deklaration von Abkürzungen für reguläre Teilausdrücke Aufbau eines Lex-Programms %{ I Deklarationen von Konstanten, Variablen usw. %} I Deklaration von Abkürzungen für reguläre Teilausdrücke %% I reguläre Ausdrücke mit zugeordneten Aktionen (Übersetzungsregeln) %% Aufbau eines Lex-Programms %{ I Deklarationen von Konstanten, Variablen usw. %} I Deklaration von Abkürzungen für reguläre Teilausdrücke %% I reguläre Ausdrücke mit zugeordneten Aktionen (Übersetzungsregeln) %% I Hilfsfunktionen Aufbau der Übersetzungsregeln Die Übersetzungsregeln haben die Form: reg. Ausdrucki { Aktioni } Aktionen werden in LEX als Codefragmente in C geschrieben. Arbeitsweise des durch LEX generierten Scanners Von der momentanen Position aus liest das Unterprogramm yylex Zeichen für Zeichen der Eingabe, bis es das längste Wort gefunden hat, das zu einem der regulären Ausdrücke passt Dann wird die zugeordnete Aktion ausgeführt, die üblicherweise ein Rücksprung zum Parser mit der Rückgabe der Tokenklasse ist. Bemerkung Gibt es mehr als einen passenden regulären Ausdruck zu diesem längsten Anfangsstück, wird der in der Reihenfolge des Lex-Programms erste reguläre Ausdruck gewählt. Kommunikation mit dem Parser Die Übergabe der Information von yylex an den Parser geschieht meist wie folgt: Kommunikation mit dem Parser Die Übergabe der Information von yylex an den Parser geschieht meist wie folgt: - der Rückgabewert von yylex gibt die Tokenklasse an, Kommunikation mit dem Parser Die Übergabe der Information von yylex an den Parser geschieht meist wie folgt: - der Rückgabewert von yylex gibt die Tokenklasse an, - der Tokenwert wird in einer globalen Variable yylval (ist vom Parser zu liefern!) gespeichert, Kommunikation mit dem Parser Die Übergabe der Information von yylex an den Parser geschieht meist wie folgt: - der Rückgabewert von yylex gibt die Tokenklasse an, - der Tokenwert wird in einer globalen Variable yylval (ist vom Parser zu liefern!) gespeichert, - yytext ist ein Zeiger auf das erste Zeichen des Tokens und - yyleng ist die Länge des Tokens in der Eingabe. Kommunikation mit dem Parser Die Übergabe der Information von yylex an den Parser geschieht meist wie folgt: - der Rückgabewert von yylex gibt die Tokenklasse an, - der Tokenwert wird in einer globalen Variable yylval (ist vom Parser zu liefern!) gespeichert, - yytext ist ein Zeiger auf das erste Zeichen des Tokens und - yyleng ist die Länge des Tokens in der Eingabe. Damit können die Hilfsfunktionen des Scanners oder aber der Parser auf das gefundene Token zugreifen und eventuell weitere Aktionen ausführen. Beispieleingabe für LEX /**************************************************************/ /* scanner.l */ /**************************************************************/ /* Definition von Konstanten, Variablen und Typen */ %{ /* Hier werden die Tokenklassen numerisch codiert */ #define IDENT 1 #define NUMBER 2 #define RELOP 3 #define ARITHOP 4 #define ASSIGNOP 5 #define IF 6 #define THEN 7 #define ELSE 8 #define SEMIK 9 /* Weitere Definitionen z.B. fuer einzelne Operationen */ /* Dies sind die #define ADD #define SUB #define MULT #define DIV Tokenwerte zur Tokenklasse 4 */ 41 42 43 44 /* Dies sind die Tokenwerte zur Tokenklasse 3 */ #define GT 31 #define GE 32 #define LT 33 #define LE 34 #define EQ 35 #define NE 36 %} /* Definition regulaerer Ausdruecke */ /* LEX-Konvention: bei der Definition eines Ausdrucks wird name, */ /* beim Gebrauch {name} verwendet. */ delim ws letter digit ident number [ \t\n] /* \t fuer Tabulator, \n für Zeilenende */ {delim}+ [A-Za-z] [0-9] {letter}({letter}|{digit})* {digit}+(\.{digit}+)? /* . steht in LEX für ein beliebiges Zeichen, daher %% \.*/ {ws} { /* keine Aktion und kein Ruecksprung */ } "if" "then" "else" /* zuerst kommen die Schluesselwoerter */ { return(IF); } { return(THEN); } { return(ELSE); } {ident} {number} { yylval = install_id(); return(IDENT); } { yylval = install_num(); return(NUMBER); } "*" "/" "+" "-" "<=" "<" "=" ">" ">=" "<>" ":=" ";" %% { { { { { { { { { { { { yylval = MULT; return(ARITHOP); } yylval = DIV; return(ARITHOP); } yylval = ADD; return(ARITHOP); } yylval = SUB; return(ARITHOP); } yylval = LE; return(RELOP); } yylval = LT; return(RELOP); } yylval = EQ; return(RELOP); } yylval = GT; return(RELOP); } yylval = GE; return(RELOP); } yylval = NE; return(RELOP); } return(ASSIGNOP); } return(SEMIK); } /* Hier wuerden Hilfsfunktionen wie install_id() usw. stehen */ /* Will man vielleicht nur eine Liste der Token ausgeben, so koennte man an dieser Stelle auch ein Hauptprogramm definieren, z.B.: */ main() { int c; while (c = yylex()) /* Rueckgabewert von yylex */ /* ist 0 falls eof gelesen */ printf( "TOKENKLASSE: %d %s\n", c, yytext); } Hat man keinen Generator für einen Scanner zur Verfügung oder ist die Verwendung eines automatisch erzeugten Scanner zu aufwendig, stellt sich die Frage, wie man einen Scanner für eine gegebene Menge regulärer Ausdrücke konstruieren kann. Dazu 3 Schritte: Hat man keinen Generator für einen Scanner zur Verfügung oder ist die Verwendung eines automatisch erzeugten Scanner zu aufwendig, stellt sich die Frage, wie man einen Scanner für eine gegebene Menge regulärer Ausdrücke konstruieren kann. Dazu 3 Schritte: 1. Transformation regulärer Ausdrücke in Transitionsgraphen (Darstellung eines nicht-deterministischen Automaten) Hat man keinen Generator für einen Scanner zur Verfügung oder ist die Verwendung eines automatisch erzeugten Scanner zu aufwendig, stellt sich die Frage, wie man einen Scanner für eine gegebene Menge regulärer Ausdrücke konstruieren kann. Dazu 3 Schritte: 1. Transformation regulärer Ausdrücke in Transitionsgraphen (Darstellung eines nicht-deterministischen Automaten) 2. Transformation des nicht-deterministischen in einen deterministischen Automaten Hat man keinen Generator für einen Scanner zur Verfügung oder ist die Verwendung eines automatisch erzeugten Scanner zu aufwendig, stellt sich die Frage, wie man einen Scanner für eine gegebene Menge regulärer Ausdrücke konstruieren kann. Dazu 3 Schritte: 1. Transformation regulärer Ausdrücke in Transitionsgraphen (Darstellung eines nicht-deterministischen Automaten) 2. Transformation des nicht-deterministischen in einen deterministischen Automaten 3. Implementation des Automaten Transitionsgraphen Transitionsgraphen sind eine graphische Darstellung der Arbeitsweise endlicher Automaten, die eine Eingabe Zeichen für Zeichen lesen und dabei verschiedene Zustände durchlaufen. Transitionsgraphen Transitionsgraphen sind eine graphische Darstellung der Arbeitsweise endlicher Automaten, die eine Eingabe Zeichen für Zeichen lesen und dabei verschiedene Zustände durchlaufen. I Zustände werden durch Knoten und Überführungen durch Kanten dargestellt. Transitionsgraphen Transitionsgraphen sind eine graphische Darstellung der Arbeitsweise endlicher Automaten, die eine Eingabe Zeichen für Zeichen lesen und dabei verschiedene Zustände durchlaufen. I Zustände werden durch Knoten und Überführungen durch Kanten dargestellt. I Es gibt es einen Startzustand, in dem der Automat startet, und mindestens einen Endzustand. Transitionsgraphen Transitionsgraphen sind eine graphische Darstellung der Arbeitsweise endlicher Automaten, die eine Eingabe Zeichen für Zeichen lesen und dabei verschiedene Zustände durchlaufen. I Zustände werden durch Knoten und Überführungen durch Kanten dargestellt. I Es gibt es einen Startzustand, in dem der Automat startet, und mindestens einen Endzustand. I Gelangt der Automat durch Lesen einiger Zeichen der Eingabe vom Startzustand aus in einen Endzustand, so hat er das gelesene Wort in der Eingabe akzeptiert. Transitionsgraphen Transitionsgraphen sind eine graphische Darstellung der Arbeitsweise endlicher Automaten, die eine Eingabe Zeichen für Zeichen lesen und dabei verschiedene Zustände durchlaufen. I Zustände werden durch Knoten und Überführungen durch Kanten dargestellt. I Es gibt es einen Startzustand, in dem der Automat startet, und mindestens einen Endzustand. I Gelangt der Automat durch Lesen einiger Zeichen der Eingabe vom Startzustand aus in einen Endzustand, so hat er das gelesene Wort in der Eingabe akzeptiert. Satz Die Menge aller so akzeptierten Wörter bildet eine reguläre Sprache Beispiel Ein Übergang der Form i c j hat folgende Bedeutung: Befindet sich der Automat im Zustand i und liest er als nächstes Zeichen ein c, so geht er in den Zustand j über (Folgezustand). Ein doppelter Kreis markiert einen Endzustand. Prinzipielle Arbeitsweise eines endlichen Automaten Eingabeband Lesekopf - Steuerwerk mit Zuständen Akzeptieren von Eingabewörtern Es gibt zwei alternative Möglichkeiten, ein Wort zu akzeptieren: 1. Erreicht der Automat einen Endzustand, so hält er an. Die gelesenen Zeichen bilden ein Wort der Sprache. Ist der Endzustand zusätzlich mit einem ∗ markiert, so gehört das zuletzt gelesene Zeichen nicht zum akzeptierten Wort und muss daher in die Eingabe zurückgestellt werden. Akzeptieren von Eingabewörtern Es gibt zwei alternative Möglichkeiten, ein Wort zu akzeptieren: 1. Erreicht der Automat einen Endzustand, so hält er an. Die gelesenen Zeichen bilden ein Wort der Sprache. Ist der Endzustand zusätzlich mit einem ∗ markiert, so gehört das zuletzt gelesene Zeichen nicht zum akzeptierten Wort und muss daher in die Eingabe zurückgestellt werden. 2. Der Automat liest so lange aus der Eingabe, bis er in einen Zustand z gerät, in dem das nächste Zeichen nicht gelesen werden kann. Ist z ein Endzustand, so bilden die gelesenen Zeichen bilden ein Wort der Sprache. Beispiel Der Automat erkennt die Wörter, die durch den regulären Ausdruck [0-9]+ (.[0-9]+ )? bezeichnet werden 0-9 start 0 0-9 1 0-9 . 2 ≠ 0-9 ≠. 5 0-9 * 3 ≠ 0-9 4 * Fordert man, daß der Automat immer solange weiterarbeitet, bis er auf ein Zeichen trifft, für das er keinen Übergang vom momentanen Zustand hat, so vereinfacht sich der TG zu: 0-9 start 0 0-9 1 0-9 . 2 0-9 3 Ein Wort wird in diesem Fall akzeptiert, wenn der Automat in einem Endzustand stehenbleibt. Definition Ein Transitionsgraph (TG) heißt deterministisch, falls er 1. keine Übergänge besitzt, die mit dem leeren Wort ε markiert sind und 2. keine Zustände besitzt, von denen aus es mehr als einen mit einem Zeichen a des Alphabets markierten Übergang gibt. Verletzt ein Transitionsgraph eine der beiden Bedingungen, wird er nichtdeterministisch genannt. Beispiel a start 0 a 1 b 2 b 3 b Die Menge der von diesem Automaten erkannten Worte wird durch den regulären Ausdruck (a|b)∗ abb bezeichnet. Beispiel b 1 a 2 ε start 0 a ε 3 b 4 Die Menge der von diesem Automaten erkannten Worte wird durch den regulären Ausdruck ab∗ |ba∗ bezeichnet. Erzeugung von Scannern aus deterministischen Transitionsgraphen Tabellengesteuerter Scanner Transitionstabelle T für Transitionsgraphen aus dem vorangehenden Beispiel Zustand 0 1 2 3 Eingabezeichen 0-9 . 1 – 1 2 3 – 3 – Aufbau eines tabellengesteuerten Scanners Eingabe $ H Y H H Ende-Markierung Lesefenster Steuerwerk (mit Zuständen) ? erkannte Token - Transitionstabelle T Arbeitsweise des tabellengesteuerten lexical Scanners I Initiale Situation: Auf dem Eingabeband steht das zu lesende Wort, eventuell gefolgt von einer Endmarkierung. Das Lesefenster steht auf dem ersten Buchstaben, und die Steuerung ist im Startzustand. Arbeitsweise des tabellengesteuerten lexical Scanners I I Initiale Situation: Auf dem Eingabeband steht das zu lesende Wort, eventuell gefolgt von einer Endmarkierung. Das Lesefenster steht auf dem ersten Buchstaben, und die Steuerung ist im Startzustand. Arbeitsschritt: Abhängig vom momentanen Zustand z der Steuerung und dem Zeichen c unter dem Lesefenster wird in der Tansitionstabelle unter dem Eintrag T (z, c) nachgesehen. a) T (z, c) enthält einen Zustand z 0 . Dann geht die Steuerung in den Zustand z 0 über, und das Lesefenster wird um ein Zeichen nach rechts geschoben. b) T (z, c) enthält keinen Zustand. Ist z ein Endzustand, dann wurde ein Token erkannt. Ansonsten ist ein Fehler in der Eingabe aufgetreten. Bemerkung 1. Um das nächste Token in der Eingabe zu erkennen, wird das Steuerwerk auf den Startzustand gesetzt und der Automat erneut gestartet. Bemerkung 1. Um das nächste Token in der Eingabe zu erkennen, wird das Steuerwerk auf den Startzustand gesetzt und der Automat erneut gestartet. 2. Diese Arbeitsweise ist gültig für Transitionsgraphen von Automaten, die solange weiterarbeiten, bis sie auf ein Zeichen treffen, für das kein Übergang definiert ist. Bemerkung 1. Um das nächste Token in der Eingabe zu erkennen, wird das Steuerwerk auf den Startzustand gesetzt und der Automat erneut gestartet. 2. Diese Arbeitsweise ist gültig für Transitionsgraphen von Automaten, die solange weiterarbeiten, bis sie auf ein Zeichen treffen, für das kein Übergang definiert ist. 3. Für die Automaten, die beim Erreichen eines Endzustands anhalten, muss die Arbeitsweise des tabellengesteuerten Scanners leicht modifiziert werden! Direkt erzeugter Scanner für die Tokenklasse int scanner () { int zustand; char zeichen; zustand = 0; while (TRUE) { if (feof(stdin)) return (EOF); zeichen = getchar(); switch (zustand) { case 0: if (isdigit(zeichen)) zustand = 1; else error(); break; case 1: if (zeichen == ’.’) zustand = 2; else if (!isdigit(zeichen)) { ungetc(zeichen); return (NUMBER); } break; case 2: if (isdigit(zeichen)) zustand = 3; else error(); break; case 3: if (!isdigit(zeichen)) { ungetc(zeichen); return(NUMBER); } break; } } } Erzeugung von Transitionsgraphen aus regulären Ausdrücken 1. Ist der reguläre Ausdruck von der Form ε, so ist der zugehörige TG von der Form: start ε 2. Ist der reguläre Ausdruck von der Form a, so ist der zugehörige TG von der Form: start a 3. Ist der reguläre Ausdruck von der Form α1 | α2 , so ist der zugehörige TG von der Form: ε TG für α 1 ε start ε TG für α 2 ε 4. Ist der reguläre Ausdruck von der Form α1 α2 , so ist der zugehörige TG von der Form: start TG für α 1 ε TG für α 2 5. Ist der reguläre Ausdruck von der Form α∗ oder α+ , so ist der zugehörige TG von der Form: ε start ε TG für α ε ε entfällt bei α+ Umwandlung eines nichtdeterministischen Transitionsgraphen T in einen deterministischen 1. Schritt: Entfernen der ε-Übergänge. Idee: Befindet sich der Automat nach Lesen eines Buchstabens im Zustand z, so befindet“ er sich ebenso in jedem Zustand z 0 , der ” von z aus durch eine Folge von ε-Übergängen erreichbar ist. Die Menge der Knoten in dem nichtdeteministischen Transitionsgraphen T sei K . Es wird ein neuer Transitionsgraph T 0 folgendermaßen konstruiert: I T 0 enthält alle Knoten aus K , auf die wenigstens ein Übergangspfeil zeigt, der nicht mit ε markiert ist, sowie den Startknoten von T . Die Menge dieser Knoten werde mit K 0 bezeichnet. I T 0 enthält alle Knoten aus K , auf die wenigstens ein Übergangspfeil zeigt, der nicht mit ε markiert ist, sowie den Startknoten von T . Die Menge dieser Knoten werde mit K 0 bezeichnet. I In T 0 wird ein Übergangspfeil vom Knoten k1 zum Knoten k2 mit der Markierung a eingetragen, wenn man in T vom Knoten k1 mit einer Folge von ε-Übergängen zu einem Knoten k gelangen kann, von dem ein mit a markierter Übergangspfeil zum Knoten k2 führt. (Diese Folge der ε-Übergänge darf auch leer sein.) I T 0 enthält alle Knoten aus K , auf die wenigstens ein Übergangspfeil zeigt, der nicht mit ε markiert ist, sowie den Startknoten von T . Die Menge dieser Knoten werde mit K 0 bezeichnet. I In T 0 wird ein Übergangspfeil vom Knoten k1 zum Knoten k2 mit der Markierung a eingetragen, wenn man in T vom Knoten k1 mit einer Folge von ε-Übergängen zu einem Knoten k gelangen kann, von dem ein mit a markierter Übergangspfeil zum Knoten k2 führt. (Diese Folge der ε-Übergänge darf auch leer sein.) I Der Startknoten in T ist auch Startknoten in T 0 . I T 0 enthält alle Knoten aus K , auf die wenigstens ein Übergangspfeil zeigt, der nicht mit ε markiert ist, sowie den Startknoten von T . Die Menge dieser Knoten werde mit K 0 bezeichnet. I In T 0 wird ein Übergangspfeil vom Knoten k1 zum Knoten k2 mit der Markierung a eingetragen, wenn man in T vom Knoten k1 mit einer Folge von ε-Übergängen zu einem Knoten k gelangen kann, von dem ein mit a markierter Übergangspfeil zum Knoten k2 führt. (Diese Folge der ε-Übergänge darf auch leer sein.) I Der Startknoten in T ist auch Startknoten in T 0 . I Endknoten sind alle Knoten k aus K 0 , von denen aus in T ein Endknoten mit einer (auch leeren) Folge von ε-Übergängen erreicht werden kann. 2. Schritt: Vom nichtdeterministischen TG T ohne ε-Übergänge zum deterministischen TG. Idee: Befindet sich der Automat in einem Zustand z und könnte er nach Lesen eines Zeichens a sowohl in den Zustand z1 als auch in den Zustand z2 gehen, so geht“ er in beide Zustände. ” Gegeben sei ein nichtdeterministischer Transitionsgraph TN ohne ε-Überführungen. Die Menge der Knoten von TN sei mit KN bezeichnet. Konstruiert wird ein deterministischer Transitionsgraph TD , dessen Knoten aus gewissen Teilmengen von Knoten aus KN bestehen. Die Menge der Knoten KD von TD wird dabei sukzessive bestimmt. KD enthält zu Beginn nur die Menge, die den Startknoten von TN enthält. Anschließend wird folgendes solange ausgeführt, bis keine neuen Knoten mehr zu KD hinzugefügt und keine neuen Überführung mehr in TD eingetragen werden: Anschließend wird folgendes solange ausgeführt, bis keine neuen Knoten mehr zu KD hinzugefügt und keine neuen Überführung mehr in TD eingetragen werden: I Ist P ein Knoten aus KD (d.h. P = {z1 , . . . , zr } mit zi ∈ KN ), so wird für jedes Eingabezeichen a die Menge Q aller Knoten in KN bestimmt, für die in TN eine Überführung von einem Knoten aus P in einen Knoten aus Q mit der Markierung a vorhanden ist. Anschließend wird folgendes solange ausgeführt, bis keine neuen Knoten mehr zu KD hinzugefügt und keine neuen Überführung mehr in TD eingetragen werden: I Ist P ein Knoten aus KD (d.h. P = {z1 , . . . , zr } mit zi ∈ KN ), so wird für jedes Eingabezeichen a die Menge Q aller Knoten in KN bestimmt, für die in TN eine Überführung von einem Knoten aus P in einen Knoten aus Q mit der Markierung a vorhanden ist. I Ist Q 6= ∅ und existiert ein Zustand Q noch nicht in KD , so wird Q zu den bisher schon in KD vorhandenen Knoten hinzugefügt. Anschließend wird folgendes solange ausgeführt, bis keine neuen Knoten mehr zu KD hinzugefügt und keine neuen Überführung mehr in TD eingetragen werden: I Ist P ein Knoten aus KD (d.h. P = {z1 , . . . , zr } mit zi ∈ KN ), so wird für jedes Eingabezeichen a die Menge Q aller Knoten in KN bestimmt, für die in TN eine Überführung von einem Knoten aus P in einen Knoten aus Q mit der Markierung a vorhanden ist. I Ist Q 6= ∅ und existiert ein Zustand Q noch nicht in KD , so wird Q zu den bisher schon in KD vorhandenen Knoten hinzugefügt. I Außerdem wird eine mit a markierte Überführung in TD von P nach Q eingetragen. I Der Startknoten in TD ist die Menge, die nur den Startknoten von TN enthält. I Der Startknoten in TD ist die Menge, die nur den Startknoten von TN enthält. I Ein Knoten {z1 , . . . , zr } in TD wird Endknoten in TD , wenn einer der Knoten zi ∈ KN mit i ∈ [1 : r ] ein Endknoten in TN ist.