Programmiersprachen und Übersetzer

Werbung
Programmiersprachen und Übersetzer
Sommersemester 2011
3. Mai 2011
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 dabei 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.
%}
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:
- 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:
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.
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 der 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 bei Scannern
Für einen Scanner muss das Akzeptanzverhalten ein wenig geändert
werden, da die Eingabe aus einer Folge von Lexemen besteht.
Von der momentanen Position aus werden Eingabezeichen gelesen,
bis man in einen Zustand z gerät, in dem keine Überführung mehr
durchgeführt werden kann.
I
Ist z ein Endzustand, dann gehört das gelesene Wort zu einer
Tokenklasse, die durch den Endzustand bestimmt ist.
I
Ist z kein Endzustand und wurde während des Lesens
mindestens ein Endzustand erreicht, so bestimmt der letzte
dieser erreichten Endzustände die Tokenklasse. Die gefundene
Zeichenkette besteht also aus allen bis hierher gelesenen
Zeichen und der Lesekopf muss auf das nachfolgende Zeichen
zurückgesetzt werden.
I
Ist z kein Endzustand und wurde während des Lesens
zwischendurch kein Endzustand erreicht, liegt offensichtlich
ein Fehler vor.
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
3
Beispiel
Die Tokenklasse T1 sei durch ab, die Tokenklasse T2 sei durch
abc+ d und die Tokenklasse T3 sei durch c+ b definiert. Ein
zugehöriger Transitionsgraph könnte folgendes Aussehen haben:
b
a
start
1
c
2
d
3
c
0
c
c
5
b
6
In der Eingabe befinde sich das Wort abcccb,
4
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
I
Initiale Situation: Auf dem Eingabeband steht das zu lesende
Wort 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 Transitionstabelle 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. Ist z 0 ein Endzustand, so merkt man
sich den Zustand und die Position des Lesefensters.
Arbeitsweise des tabellengesteuerten lexical Scanners
b) T (z, c) enthält keinen Zustand und man hat sich unter a)
einen Endzustand gemerkt. Dann bestimmt der letzte
gemerkte Endzustand, welches Token erkannt wurde und der
Lesezeiger wird auf die entsprechende Position gesetzt.
c) T (z, c) enthält keinen Zustand und man hat sich unter a)
keinen Endzustand gemerkt.
Dann ist ein Fehler in der Eingabe aufgetreten.
Bemerkung
Um das nächste Token in der Eingabe zu erkennen, wird das
Steuerwerk auf den Startzustand gesetzt und der Automat erneut
gestartet.
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
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:
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
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.
Herunterladen