Document

Werbung
2. Kapitel:
Syntax- und Typanalyse
Lernziele:
• Aufgaben der verschiedenen Syntaxanalysephasen
• Zusammenwirken der Syntaxanalysephasen
• Spezifikationstechniken für Syntaxanalyse
• Generierungstechniken
• Anwendung einschlägiger Werkzeuge
• Lexikalische Analyse
• Kontextfreie Analyse
• Kontextabhängige Analyse
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
29
Aufgaben der Syntaxanalyse:
• Prüfen, ob Eingabe syntaktisch korrekt
• je nach Ergebnis:
- Ausgabe von Fehlermeldung
- Aufbau geeigneter Datenstruktur für
Weiterverarbeitung
Syntaxanalysephasen:
Quellcode als Zeichenreihe
Lexikalische Analyse:
Wortfolge
Zeichenfolge
Scanner
Symbolfolge
Kontextfreie Analyse:
Wortfolge
Baumstruktur
Parser
Syntaxbaum
Kontextabhängige Analyse:
Baumstruktur
Baumstruktur
mit Querbezügen
Namensbinden
Typisieren
Attributierter Baum
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
30
Gründe für Trennung der Phasen:
• lexikalischer von kontextfreier Analyse:
- Entlastung der kontextfreien Analyse
- Baumstruktur der Wortanalyse wird
später nicht benötigt
• kontextfreier von kontextabhängiger Analyse:
- kontextabhängige Analyse setzt auf
Baumstruktur und nicht auf Symbolfolge
auf und wird dementsprechend mit
anderen Techniken spezifiziert
- Vorteile für den Aufbau der Zieldatenstruktur
• in beiden Fällen:
- Effizienzsteigerung
- natürliches Vorgehen (vgl. natürliche Sprache)
- läßt sich gezielter durch Werkzeug
unterstützen
Leseempfehlung:
Appel:
• Chap. 2, S. 16
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
31
2.1 Lexikalische Analyse
Aufgabenstellung:
• Zerlegen der Eingabezeichenreihe in Symbole
gemäß Sprachspezifikation
• Gruppieren der Symbole in Symbolklassen
• Geeignete Darstellung der Symbole:
- Hashen von Bezeichnern
- Umwandlung von Konstanten
• Elimination von
- Whitespace (Leerzeichen, Kommentaren,...)
- sprachfremden Elementen (Übersetzungsanweisungen,...)
Begriffsklärung:
• Symbol: Wort über einem Alphabet von Zeichen
(oft mit weiterer Information: Symbolklasse,
Codierung, Quelltextposition)
• Symbolklasse: repräsentiert eine Menge von
Symbolen (Bezeichner, int-Konstanten,...); sie
entspricht den Terminalen der kontextfreien
Grammatik
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
32
Beispiel: (lexikalische Analyse)
Zeile 23 der Eingabedatei:
if( A <= 3.14)
B = B---
Ergebnis der lexikalischen Analyse:
Symbolklasse
String
IF
“if“
OPAR
“(“
ID
“A“
RELOP
“<=“
FLOATCONST “3.14“
CPAR
“)“
ID
“B“
...
Wert der
Konstanten
Codierung Zeile:Spalte
23:3
23:5
23:7
23:9
23:12
23:16
23:20
72
4
3,14
84
Hashcode des
Identifiers
Codierung für
Operator <=
Symbolinformation
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
33
2.1.1 Spezifikation
Die Spezifikation der lexikalischen Syntax ist Teil
der Spezifikation der Programmiersprache.
Sie besteht üblicherweise aus zwei Teilen:
• Scan-Algorithmus (oft implizit)
• Spezifikation der Symbole und Symbolklassen
Beispiele: (Scannen)
1. Folgende Anweisung aus obigem C-Fragment:
B = B---A;
Problem: Trennung (-- und – sind Symbole)
Lösung: längstes Symbol hat Vorrang, d.h.
äquivalent zu
B = B-- - A;
bzw: ID EQ ID DECROP MINUS ID SEMI
2. Folgendes Fragment aus Java-Quellcode:
class public { public m(){ ...} }
Problem: Mehrdeutigkeit (Schlüsselwort/Bezeichner)
Lösung: über Vorrangregeln
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
34
Standard Scan-Algorithmus (konzeptionell):
Scannen wird meistens als Coroutine realisiert:
- Zustand ist der Eingaberest
- die Coroutine liefert jeweils das nächste Symbol,
in undefinierten bzw. Fehlersituationen das
Symbol UNDEF und aktualisiert die Eingabe
String
eingaberest := eingabe;
Symbol nächstesSymbol() {
Symbol aktSymbol :=
längsterSymbolPräfix( eingaberest );
eingaberest :=
abschneiden(aktSymbol,eingaberest);
return aktSymbol;
}
wobei
- die Zeichenreiche zum aktuellen Symbol vom
Eingaberest abschneidet, wenn aktSymbol != UNDEF
- den Eingaberest unverändert lässt, sonst.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
35
Symbol längsterSymbolPräfix(String egr){
// länge(egr) > 0
int
aktLänge := 0;
String aktPräfix:= präfix(aktLänge,egr);
Symbol längstesSymbol := UNDEF;
while( aktLänge <= länge(egr)
&& istSymbolPräfix(aktPräfix)
) {
if( istSymbol(aktPräfix) ){
längstesSymbol := aktPräfix;
}
aktLänge++;
aktPräfix := präfix(aktLänge,egr);
}
return längstesSymbol;
}
Diese Prozedur benötigt nur noch die Prädikate:
- istSymbolPräfix : String
- istSymbol :
25.04.2007
String
bool
bool
© A. Poetzsch-Heffter, TU Kaiserslautern
36
Bemerkungen:
• Obiger Scan-Algorithmus wird bei vielen modernen
Sprachen verwendet; aber funktioniert z.B. nicht
für Fortran:
DO 7 I = 1.25
DO 7 I = 1,25
• Fehlersituationen sind im Algorithmus nicht behandelt.
• Eine vollständige Realisierung der Prozedur
längsterSymbolPräfix wird unten erläutert.
Spezifikation der Symbole:
• Symbole einer Sprache werden mit regulären
Ausdrücken spezifiziert.
• Symbolklassen werden informell beschrieben.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
37
Definition: (reguläre Ausdrücke)
Sei Σ ein Alphabet, d.h. endliche nichtleere Menge
von Zeichen (Σ∗ bezeichnet die Menge der Worte/
Zeichenreihen über Σ, ε das leere Wort).
Reguläre Ausdrücke und die durch sie spezifizierten
regulären Sprachen sind rekursiv wie folgt definiert:
• ε ist ein reg. Ausdruck und spezifiziert die Sprache {ε}
• Jedes a in Σ ist ein reg. Ausdruck und spezifiziert
die Sprache { a }
• Sind r und s reg. Ausdrücke, die die Sprachen
R und S spezifizieren, dann sind folgende Ausdrücke
regulär und spezifizieren die angegebenen Sprachen:
(r|s) mit R U S
(Vereinigung)
(rs) mit { vw | v in R, w in S }
(Konkatenation)
r*
mit { v1 ... vn | vi in R, 0 <= i <= n }
(Kleenesche Hülle)
U
M
Σ∗ heißt regulär, wenn es einen reg. Ausdruck
gibt, der M spezifiziert.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
38
Bemerkungen:
• L = {} ist gemäß obiger Definition nicht regulär,
wird aber manchmal als regulär angesehen.
• Häufig werden weitere Operatoren betrachtet:
^ , + , ? , . , [ ], etc.
Die weiteren Operatoren lassen sich mit Hilfe der
elementaren Operatoren aus der obigen Definition
definieren.
Beispiele:
• r+ ist gleichbedeutend mit (r r*)
spezifiziert also die Sprache r* \ { ε }
• [aBd] ist gleichbedeutend mit a|B|d
• [a-g]
ist gleichbedeutend mit a|b|c|d|e|f|g
Zur Beachtung:
Im Zusammenhang der Scannergenerierung gilt:
Die regulären Ausdrücke spezifizieren nicht die
Programme/Übersetzungseinheiten einer
Programmiersprache, sondern nur die erlaubten
Symbole!
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
39
2.1.2 Implementierung von Scannern
Mit Scannergeneratoren:
Sequenz regulärer Ausdrücke + Aktionen
(Eingabesprache des Scannergenerators)
Scannergenerator
Scannerprogramm
(meist in einer Programmiersprache)
Beispiele: Lex, Flex, JLex, JFlex, MLlex
Typische Verwendung, hier von Lex:
% lex source.l
% cc lex.yy.c –ll
// erzeugt Datei lex.yy.c
Aktionen werden bei Lex in C geschrieben werden.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
40
Beispiele:
1. Typischer regulärer Ausdruck in Lex:
[a-zA-Z_][a-zA-Z_0-9]*
2. Lex-Eingabe mit Abkürzungen:
BU
BUZI
%%
{BU}{BUZI}*
[a-zA-Z_]
[a-zA-Z_0-9]
{ eineAktion(); }
3. Interessanteres Beispiel:
Meta-Pünktchen
ZI
[0-9]
BU
BUZI
ZE
%%
[a-zA-Z_]
[a-zA-Z_0-9]
[a-zA-Z_0-9!?\]\[\.\t ...]
[ \t]*
"do"
"double"
{BU}{BUZI}*
{ZI}+\.{ZI}+
\"({ZE}|\\\")*\"
/* whitespace */
{ return DO; }
{ return DOUBLE; }
{ return IDENT; }
{ return FLOATCONST;
{ return STRING; }
ZE enthält kein Anführungszeichen und kein ‘\‘ !
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
41
Scannergenerierung:
• Die Konstruktionstechnik der Scannergenerierung
basiert auf der konstruktiven Äquivalenz von
- regulären Ausdrücken,
- nichtdeterministischen endlichen Automaten (NEA),
- deterministischen endlichen Automaten (DEA)
• Die Konstruktionstechnik besteht konzeptionell aus
zwei Schritten:
1. Reguläre Ausdrücke
NEA
2.
DEA
NEA
Erläuterung der Schritte hier exemplarisch für die
regulären Ausdrücke des obigen Beispiels 3.
• Mit der Konstruktionstechnik wird u. A. die Prozedur
längsterSymbolPraefix entwickelt.
• Die vorgestellten Schritte sind auch bei der
Scanner-Entwicklung von Hand hilfreich.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
42
1. Schritt: Reguläre Ausdrücke NEA
Übersetzungsschema:
Prinzip: Konstruiere für jeden regulären Teilausdruck
NEA mit genau einem Start- und Endzustand,
der die gleiche Sprache akzeptiert.
•ε
s0
•a
s0
• (r|s)
a
ε
s1 R
f1
ε
s0
f0
ε
• (rs)
f0
s1 R
s2 S
f1
f2
ε
ε
s2 S
f2
ε
• r*
s0
ε
s1 R
f1
ε
f0
ε
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
43
Übersetzung am Beispiel
von Folie 41:
LZ, TAB
s1
s2 d s3 o
ε
s4
ε
s5 d s6 o s7 u s8 b s9 l
s0
BUZI
ε
BU
s13
ε
s 14
s18
“
25.04.2007
s11
ε
s12
ε
s10 e
ZI
ε
s 19
ZI
ZI
ε
s 15
s20
.
s 16 ZI
ZE
s21
ε
s22 \
s23
“
s17
ε
s25
ε
s24
“
s26
ε
© A. Poetzsch-Heffter, TU Kaiserslautern
44
Bestimmung des längsten Symbolpräfixes mit NEA:
Symbol längsterSymbolPräfix(char[] egr)
// laenge( egr ) > 0
{
Zustandsmenge aktZustand:= huelle({s0});
int aktLänge
:= 0;
int symbolLänge
:= undefiniert;
while( aktLänge <= länge(egr)
&& !istLeereMenge( aktZustand ) ) {
if( aktZustand enthält Endzustand ){
symbolLänge := aktLänge;
}
aktZustand := huelle(
nachfolger(aktZustand,egr[aktLänge]));
aktLänge++;
}
return
symbol(präfix(symbolLänge,egr));
}
wobei die Funktion huelle die ε -Hülle zu einer
Menge von Zuständen { t0,...,tn } berechnet.
Die ε -Hülle von { t0,...,tn } ist die Vereinigung von
{ t0,...,tn } mit der Menge der Zustände, die von
den si aus über ε –Kanten erreichbar sind.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
45
Bemerkung:
Problem der Mehrdeutigkeit ist hier nicht gelöst,
d.h. wenn es zum längsten Eingabepräfix, zu
dem es ein Symbol gibt, auch andere Symbole gibt,
liefert die Funktion symbol eines davon.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
46
2. Schritt: NEA DEA
Zu jedem NEA lässt sich ein DEA konstruieren, der
die gleiche Sprache akzeptiert. Dies gilt im Allg.
nicht für NEA mit Ausgabe.
• Konstruktionsprinzip (Myhill-Konstruktion)
Zustände des DEA entsprechen Mengen von
Zuständen des NEA. (Funktioniert, da Potenzmenge
von endl. Mengen wieder endliche Menge.)
• Startzustand des DEA ist ε –Hülle des Startzustands
des NEA.
• Endzustände sind die Zustände des DEA, deren
zugeordnete Menge von NEA-Zuständen einen
NEA-Endzustand enthält.
• Beachte beim Arbeiten mit Zeichenklassen:
Zeichenklassen und Zeichen müssen an
Ausgangskanten disjunkt sein.
• Vervollständigung des Automaten:
- Füge zusätzlichen Zustand ks (kein Symbol) ein
- Füge von jedem Zustand s für alle Zeichen z,
für die s keine ausgehende Kante besitzt, eine
mit z markierte Kante von s nach ks ein.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
47
Wg. Übersichtlichkeit
Kanten zu ks nur
angedeutet.
ks
e
s11,13
s10,13
LZ,
TAB
b
o
BUZI\{b}
s 4,7,13
s3,6,13
BUZI\{u}
BUZI
BU\{d}
ZI
s 0,1,2,5,12,14,18
ZI
ZI
s 15
“
.
s 16 ZI
s17
ZE
s 19,20,22,25
\
ZE
s 19,20,21,22,25
ZE
s 19,20,21,22,23,25
25.04.2007
BUZI
s13
BUZI\{o}
d
“
BUZI\{l}
s 8,13
u
LZ,
TAB
s 9,13
BUZI\{e}
s1
l
\
\
\
“
“
ZE
“
s26
s 19,20,22,24,25,26
© A. Poetzsch-Heffter, TU Kaiserslautern
48
Bestimmung des längsten Symbolpräfixes mit DEA:
Symbol längsterSymbolPräfix(char[] egr)
// laenge( egr ) > 0
{
Zustand aktZustand := startzustand;
int
aktLänge
:= 0;
int symbolLänge
:= undefiniert;
while( aktLänge <= länge(egr)
&& aktZustand != ks
) {
if( aktZustand ist ein Endzustand ){
symbolLänge := aktLänge;
}
aktZustand :=
nachfolger(aktZustand,egr[aktLänge]);
aktLänge++;
}
return
symbol(präfix(symbolLänge,egr));
}
Bemerkungen:
• Hüllenbildung zur Generierungszeit, nicht mehr zur
Laufzeit (Prinzip: Tue soviel wie möglich statisch!)
• Problem der Mehrdeutigkeit ist immer noch nicht
gelöst. Die meisten Scannergeneratoren benutzen
dazu die Reihenfolge der Regeln.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
49
Implementierungsaspekte:
• Konstruierter DEA kann minimiert werden.
• gute Eingabepufferung ist wichtig:
- häufig in zyklisch verwaltetem Feld
- beachte maximale Symbollänge
(z.B. bei der Behandlung von Kommentaren)
• Codierung des DEA als Tabelle
• wähle geeignete Partitionierung des Alphabets
zur Verringerung der Kantenanzahl
bzw. Verkleinerung der Tabelle
• Schnittstelle zum Parser: Üblicherweise ist der
Parser aktiv und fragt schrittweise nach dem
nächsten Symbol (Coroutinenprinzip).
Lesen Sie zu Abschnitt 2.1:
Wilhelm, Maurer:
• Kap. 7, Lexikalische Analyse (S. 239 - 269)
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
50
2.2 Kontextfreie Analyse
Aufgabenstellung:
• Prüfen, ob die (vom Scanner gelieferte) Symbolfolge
der kontextfreien Syntax der Sprache entspricht:
- Im Fehlerfall: Fehlerbehandlung
- Bei Korrektheit: Liefern eines geeigneten Baums
Symbolfolge
Parser
Konkreter / abstrakter Syntaxbaum
Bemerkung:
• Teilweise wird Parsen mit Aktionen zur weiteren
Bearbeitung des Programms verschränkt,
etwa mit Aktionen zur Attributierung.
• Syntaxbaum steuert wichtige Teile der weiteren
Übersetzung, darum geeignete Wahl wichtig:
- konkreter Syntaxbaum entspricht der für das
Parsen benutzten kontextfreien Grammatik
- abstrakter Syntaxbaum ist eine auf die weitere
Verarbeitung ausgerichtete, oft kompaktere
Baumrepräsentation.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
51
2.2.1 Spezifikation
Im Wesentlichen zwei Spezifikationstechniken:
• Syntaxdiagramme
• kontextfreie Grammatiken (meist in erweiterter Form)
Definition: (kontextfreie Grammatik)
U
Seien N und T Alphabete mit N T = {},
Π eine endl. Teilmenge von N x (N U T)* und S in N.
Dann heißt Γ = ( N, T, Π, S )
eine kontextfreie Grammatik, kurz KFG.
T heißen die Terminale, N die Nichtterminale und Π
die Produktionen oder Regeln von Γ, S wird das
Startsymbol oder Axiom genannt.
Notationen/Konventionen:
• Im Folgenden seien A,B,C, ... aus N, a,b,c aus T
x,y,z aus T* und α,β,γ,ψ,φ,σ,τ aus (N U T)* .
• Produktion werden in der Form A
• Abkürzend steht A
A
α, Α β, Α
25.04.2007
α notiert.
α | β | γ | ... für
γ , Α ...
© A. Poetzsch-Heffter, TU Kaiserslautern
52
Definition: (Begriffe zur Ableitbarkeit)
Sei Γ = ( N, T, Π, S ) gegeben.
• ψ ist mit Γ aus φ direkt ableitbar (man sagt auch φ
erzeugt ψ direkt), in Zeichen φ => ψ, wenn es Zerlegungungen σΑτ von φ und σατ von ψ gibt und A α in Π.
• ψ ist mit Γ aus φ ableitbar, in Zeichen φ =>* ψ,
wenn es φ0, ... , φn gibt mit φ = φ0, φn = ψ und
für alle i in {0,...n-1}: φi => φi+1 .
φ0, ... , φn heißt dann Ableitung von ψ aus φ.
• Eine Ableitung φ0, ... , φn heißt Linksableitung
(bzw. Rechtsableitung ), wenn in φi jeweils nur
das am weitesten links (bzw. rechts) stehende
Nichtterminal ersetzt wird. Linksableitungsschritte
notieren wir als φ => ψ, Rechtsabl.schritte als φ => ψ.
lm
rm
• Die baumartige Darstellung einer Ableitung nennen
wir Syntaxbaum.
• L(Γ) = { z in T* | S =>* z } heißt die von Γ erzeugte
Sprache.
• x in L(Γ) heißt ein Satz von Γ.
ψ in (NUT)* mit S =>* ψ heißt eine Satzform von Γ.
• Ein Satz heißt mehrdeutig, wenn er mehr als einen
Syntaxbaum besitzt. Eine Grammatik heißt mehrdeutig,
wenn sie einen mehrdeutigen Satz besitzt; andernfalls
eindeutig.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
53
Beispiele:
(Mehrdeutigkeit)
1. Beispiel einer Ausdrucksgrammatik:
Γ0: S
E, E
E + E, E
E * E, E
( E ), E
ID
Betrachte die Eingabe: (av+av) * bv + cv +dv)
Eingabe zur kf-Analyse: ( ID + ID ) * ID + ID + ID
S
Ε
Ε
Ε
Ε
E
Ε
E
( ID + ID )
*
E
E
ID
+ ID
E
+
ID
- Syntaxbaum entspricht nicht den üblichen
Rechenregeln.
- Es gibt mehrere Syntaxbäume gemäß Γ0,
insbesondere ist die Grammatik mehrdeutig.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
54
2. Mehrdeutigkeit beim if-then-else-Konstrukt:
if B1 then if B2 then A:=8 else A:= 7
IFTHENELSE
ANW
IFTHEN
ANW
ANW
ZW
ZW
IF ID THEN IF ID THEN ID EQ CO ELSE ID EQ CO
ZW
ANW
ZW
ANW
IFTHENELSE
ANW
IFTHEN
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
55
Bemerkungen:
• Jeder Ableitung entspricht genau ein Syntaxbaum.
Umgekehrt kann es zu einem Syntaxbaum mehrere
Ableitungen geben.
• Anstatt von Syntaxbaum spricht man häufig auch von
Struktur- oder Ableitungsbaum.
• Zusammenhang zwischen Sprache und Grammatik:
Sprache ist
Die Abbildung L : Grammatik
im Allg. nicht injektiv; d.h. zu einer Sprache gibt es
im Allg. mehrere erzeugende Grammatiken.
• =>* ist die reflexive und transitive Hülle von => .
• Fakt: Ein Satz ist genau dann eindeutig, wenn er
genau eine Linksableitung (bzw. Rechtsableitung)
besitzt.
• Bei Programmiersprachen spielen die eindeutigen
Grammatiken die zentrale Rolle, da die Semantik
(und Übersetzung) der Sprache über die syntaktische
Struktur definiert wird.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
56
Beispiel: (Mehrdeutigkeit als Grammatikeig.)
Mehrdeutigkeit ist zunächst einmal eine Grammatikeigenschaft.
Die obige Ausdrucksgrammatik
Γ0: S
E, E
E+E | E*E | E
(E) | E
ID
ist ein Beispiel für eine mehrdeutige Grammatik:
S
E
E
E
ID
E
E
+
ID
*
E
E
ID
E
E
E
S
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
57
Aber es gibt eine eindeutige Grammatik für die Sprache:
Γ1: S
E, E
T + E | T, T
S
F * T | F, F
( E ) | ID
E
E
T
F
T
T
F
F
E
T
E
T
F
(
F
ID + ID
)
*
ID
+
ID
(Es gibt aber auch kontextfreie Sprachen, die nur durch
mehrdeutige Grammatiken beschrieben werden.)
Lesen Sie zu Abschnitt 2.2.1:
Wilhelm, Maurer:
• aus Kap. 8, Syntaktische Analyse, die S. 271 - 283
Appel:
• aus Chap. 3, S. 40 - 47
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
58
2.2.2 Implementierung von Parsern
Übersicht:
• Verfahren zur kontextfreien Analyse
• Top-down-Analyse (2.2.2.1)
• Bottom-up-Analyse (2.2.2.2)
• Fehlerbehandlung (2.2.2.3)
• Parsergeneratoren (2.2.2.4)
• Baumaufbau und Repräsentation (2.2.2.5)
Verfahren zur kontextfreien Analyse:
• „von Hand“ entwickelte, grammatikspezifische
Implementierung (wenig flexibel, fehleranfällig)
• mittels Backtracking: einfach, aber ineffizient
• Cocke-Younger-Kasami-Algorithmus (1967):
- funktioniert für alle KFG‘s
- Zeitkomplexität O(n 3 )
- Ziel aber meist: Analyse mit linearem Aufwand
• Top-down-Verfahren: vom Axiom zur Symbolfolge
• Bottum-up-Verfahren: von der Symbolfolge zum Axiom
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
59
Beispiel: (Top-down-Analyse)
S
Gemäß Γ1 :
E
T
F
=>
+
E
=>
*
T
+
E
=>
(
E
)
*
T
+
E
=>
(
T + E
)
*
T
+
E
=>
(
F + E
)
*
T
+
E
=>
(
ID + E
)
*
T
+
E
=>
(
ID + T
)
*
T
+
E
=>
(
ID + F
)
*
T
+
E
=>
(
ID + ID
)
*
T
+
E
=>
(
ID + ID
)
*
F
+
E
=>
(
ID + ID
)
*
ID
+
E
=>
(
ID + ID
)
*
ID
+
T
=>
(
ID + ID
)
*
ID
+
F
=>
(
ID + ID
)
*
ID
+
ID
Ergebnis der td-Analyse ist eine Linksableitung.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
60
Beispiel:
(Bottom-up-Analyse)
Gemäß Γ1 :
(
ID + ID
)
*
ID
+
ID
<=
(
F + ID
)
*
ID
+
ID
<=
(
T + ID
)
*
ID
+
ID
<=
(
T + F
)
*
ID
+
ID
<=
(
T +
T
)
*
ID
+
ID
<=
(
T +
E
)
*
ID
+
ID
<=
)
*
ID
+
ID
<=
F
*
ID
+
ID
<=
F
*
F
+
ID
<=
F
*
T
+
ID
<=
T
+
ID
<=
T
+
F
<=
T
+
T
<=
T
+
E
<=
(
E
E
<=
S
<=
Ergebnis der bu-Analyse ist eine Rechtsableitung.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
61
Kontextfr. Analyse mit linearer Komplexität:
• durch Einschränkung bei den Grammatiken
(nicht jede kf. Grammatik besitzt lineare Parser)
• Verwendung von Kellerautomaten bzw. geeignetem
System rekursiver Prozeduren
• durch Lösen des Auswahlproblems („Welche
Produktion soll angewendet werden?“) mittels
Vorausschau (lookahead) in den Eingaberest.
Warum kf. Syntaxanalyseverfahren lernen,
wenn es Parsergeneratoren gibt?
• Grundkenntnisse sind auch für die Anwendung von
Parsergeneratoren unabdingbar.
• Parsergeneratoren sind nicht immer verwendbar.
• Fehlerbehandlung muss oft von Hand dazu gebaut
werden.
• Zugrunde liegende Methodik ist das beste Beipiel
für generische Techniken (und ein Highlight der
Informatik!).
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
62
2.2.2.1 Top-down-Syntaxanalyse
Lernziele:
• Generelle Einführung in die td-Syntaxanalyse
• Exemplarische Erläuterung der Methode des
rekursiven Abstiegs (recursive descent)
• Mächtigkeit von Top-down-Verfahren
• Grundbegriffe der LL(k)-Theorie
Grundidee des rekursiven Abstiegs:
• Ordne jedem Nichtterminal A eine Prozedur zu;
diese Prozedur akzeptiert den aus A abgeleiteten
Teilsatz.
• Die Prozedur implementiert einen endlichen
Automaten, der aus den Produktionen zu A
konstruiert wird.
• Die Rekursivität der Grammatik wird dabei auf
verschränkt rekursive Prozeduren abgebildet, so
dass der Kellermechanismus der Implementierung
höherer Programmiersprachen ausgenutzt werden
kann.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
63
Konstruktion eines Parsers mit der Methode
des rekursiven Abstiegs (exemplarisch):
Sei Γ‘1 wie Γ1, aber mit Randzeichen #, d.h.
S
E #, E
T + E | T, T
F * T | F, F
( E ) | ID
Konstruiere für jedes Nichtterminal A den sogenannten
Item-Automaten. Er beschreibt die Analyse derjenigen
Produktionen, deren linke Seite A ist:
[S .E #]
[E .T+E]
[E .T ]
E
T
[T .F*T]
[ T .F ]
F
[F .(E)]
[F .ID ]
(
#
[S E.# ]
[E T.+E]
[E T.]
[T F.*T]
[ T F.]
[F (.E)]
[S E#.]
+
E
[E T+.E]
*
E
[T F*.T]
[F (E.)]
[E T+E.]
T
)
[T F*T.]
[F (E).]
ID
[F ID.]
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
64
Recursive-descent-Parser: Die Automaten lassen sich
wie folgt in rekursive Prozeduren umsetzen.
Die Eingabe ist ein Strom von Symbolen terminiert
durch das Symbol #. Die Variable aktSymbol enthält
das Zeichen für die Vorausschau, also das jeweils
erste Symbol des Stroms:
void S() {
E();
if( aktSymbol == ‘#‘ ) {
akzeptieren();
} else {
fehler();
}
}
void E() {
T();
if( aktSymbol == ‘+‘ ) {
lesenSymbol();
E();
}
}
void T() {
F();
if( aktSymbol == ‘*‘ ) {
lesenSymbol();
T();
}
}
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
65
void F() {
if( aktSymbol ==‘(‘ ) {
lesenSymbol();
E();
if( aktSymbol ==‘)‘ ) {
lesenSymbol();
} else {
fehler();
} else if( aktSymbol =‘ID‘ ) {
lesenSymbol();
} else {
fehler();
}
}
Bemerkungen:
• Rekursiver Abstieg ist
- relativ einfach zu implementieren;
- leicht mit anderen Aufgaben zu koppeln (s.u.).
- ein typisches Beispiel für syntaxgesteuerte
Verfahren (siehe auch folgendes Beispiel).
• Das Beispiel arbeitet mit einem Zeichen
Vorausschau.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
66
Beispiel: (Rek. Abstieg & Berechnung)
Interpreter für Ausdrücke mit rekursivem Abstieg:
int belegung( Ident );
// ID -> int
// lokale Variable zwerg speichert im
// Folgenden das aktuelle Zwischenergebnis
int S() {
int zwerg := E();
if( aktSymbol == ‘#‘ ) {
return zwerg;
} else {
fehler();
return dummy_ergebnis;
}
}
int E() {
int zwerg := T();
if( aktSymbol == ‘+‘ ) {
lesenSymbol();
return zwerg + E();
}
}
int T() {
int zwerg := F();
if( aktSymbol == ‘*‘ ) {
lesenSymbol();
return zwerg * T();
}
}
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
67
int F() {
int zwerg;
if( aktSymbol ==‘(‘ ) {
lesenSymbol();
zwerg := E();
if( aktSymbol ==‘)‘ ) {
lesenSymbol();
return zwerg;
} else {
fehler();
return dummy_ergebnis;
}
} else if( aktSymbol =‘ID‘ ) {
lesenSymbol();
return belegung( code(ID) );
} else {
fehler();
return dummy_ergebnis;
}
}
Bemerkungen:
• Anreichern des Parsers mit Aktionen/Berechnungen
ist einfach zu implementieren, vermischt aber
konzeptionell unterschiedliche Aufgaben und führt
leicht zu schlecht wartbaren Programmen.
• Frage: Funktioniert die Methode immer?
Für welche Grammatiken funktioniert sie?
Antwort liefert die LL(k)-Theorie.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
68
Grundbegriffe der LL-Theorie:
Die LL-Theorie liefert die Grundlagen für td-Analyse.
Das erste L steht für die Leserichtung von links nach
rechts, das zweite dafür, dass Linksableitungen gesucht
werden.
Definition: ( LL(k)-Grammatik )
Sei Γ = ( N, T, Π, S ) eine kf. Grammatik, k in |N.
Γ heißt LL(k)-Grammatik, wenn für zwei beliebige
Linksableitungen
S =>*
lm
gilt:
S =>*
lm
uAα =>
uβα =>*
ux
lm
lm
uAα =>
uγα =>*
uy
lm
lm
Ist präfix(k,x) = präfix(k,y), dann ist β = γ .
Bemerkungen:
• Eine Grammatik ist LL(k), wenn man in einer
Linksableitung durch k Symbole Vorausschau
die „richtige“ Produktion für den nächsten
Ableitungsschritt auswählen kann.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
69
U
• Ein Sprache L Σ* heißt LL(k), wenn es eine
LL(k)-Grammatik Γ gibt mit L(Γ) = L .
• Die LL(k)-Definition liefert noch kein Testverfahren
zum Prüfen, ob eine Grammatik LL(k) ist.
• Von Interesse sind die Präfixe, die aus βα bzw. γα
ableitbar sind.
Beispiele:
(nicht LL(k)-Grammatiken)
1. Grammatik mit Linksrekursion:
Γ2:
S
T
E#, E
T * F | F,
E+T | T,
F
( E ) | ID
Elimination der Linksrekursion:
Aα | β ,
Ersetze Produktionen der Form A
wobei β nicht mit A beginnen darf, durch
A
β Α‘ und A‘
αA‘ | ε .
Im Beispiel ergibt sich:
Γ3:
25.04.2007
S
E#, E
T E‘ , E‘
T
F T‘ , T‘
* F T‘ | ε, F
+ T E‘ | ε
( E ) | ID
© A. Poetzsch-Heffter, TU Kaiserslautern
70
2. Grammatik mit unbeschränkter Vorausschau:
Γ3:
STM
VAR := VAR | ID( IDLIST ),
VAR
ID | ID( IDLIST ) ,
IDLIST
ID | ID, IDLIST
Γ3 ist für kein k eine LL(k)-Grammatik
(Beweis siehe [WM], Beispiel 8.3.4, S. 319)
Transformation in eine LL(2)-Grammatik:
Ersetze die Produktionen für STM durch
STM
ASS_CALL | ID := VAR
ASS_CALL
ID( IDLIST ) ASS_CALL_REST
ASS_CALL_REST
:= VAR | ε
Bemerkung:
• Die transformierten Grammatiken akzeptieren
zwar die gleiche Sprache, liefern aber andere
Syntaxbäume. Aus Sicht der Theorie der formalen
Sprachen ist das akzeptabel, aus Sicht der
Implementierung von Programmiersprachen im
Allg. nicht!
• Es gibt Sprachen L, für die es keine
LL(k)-Grammatik Γ gibt mit L(Γ) = L .
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
71
Beispiel:
(nicht LL(k)-Sprache)
Γ4 : S
A|B, A
aAb | 0 , B
(s. [WM], Beispiel 8.3.5, S. 320)
aBbb | 1
Es gilt: Für L(Γ4) gibt es keine LL(k)-Grammatik.
Wir zeigen hier nur:
Γ4 ist für kein k eine LL(k)-Grammatik.
Beweis:
Sei k beliebig.
Beweisidee: Wähle zwei Ableitungen entsprechend
der LL(k)-Definition und zeige, dass trotz gleicher
Präfixe der Länge k die β und γ entsprechenden
Zeichenreihen ungleich sind:
k
k
k
2k
S =>*
S => A =>* a 0b
S =>*
S => B =>* a 1b
lm
lm
dann ist:
lm
lm
lm
lm
k
k
k
k
2k
aber
präfix(k,a 0b ) = a = präfix(k,a 1b )
β = A =/ B = γ .
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
72
Definition: (FIRSTk- und FOLLOW k -Mengen)
Sei Γ = ( N, T, Π, S ) eine kf. Grammatik, k in |N und
bezeichne T<k = { u in T* | |u| < k }:
Pot( T <k )
FIRSTk : (N U T)*
FIRSTk( α ) = { präfix(k,u) | α =>* u }
wobei präfix(n,u) = u für alle u mit |u| < n .
FOLLOW k : (N U T)*
<k
Pot( T )
FOLLOW k( α ) = { w | S =>* βαγ und w in FIRSTk( γ ) }
Definition: (reduzierte KFG)
Eine KFG Γ = ( N, T, Π, S ) heißt reduziert, wenn
jedes Nichtterminal in einer Ableitung vorkommt und
aus jedem Nichtterminal mindestens ein Wort abgeleitet
werden kann.
Lemma: (Charakterisierung von LL(1)-Gram.)
Eine reduzierte KFG ist genau dann LL(1), wenn für je
β,A
γ gilt:
zwei verschiedene Produktionen A
FIRST1( β )
1
FOLLOW 1( Α )
FIRST1( γ )
1
FOLLOW 1( Α )
U
wobei L1
25.04.2007
1 L2
= {}
= { präfix(1,vw) | v in L1 , w in L2 }
© A. Poetzsch-Heffter, TU Kaiserslautern
73
Bemerkung:
FIRST und FOLLOW-Mengen lassen sich berechnen,
so dass das Kriterium automatisch geprüft werden kann.
Beispiele:
(FIRSTk und FOLLOWk)
Um zu prüfen, ob die modifizierte Ausdrucksgrammatik
Γ3:
+ T E‘ | ε
S
E#, E
T E‘ , E‘
T
F T‘ , T‘
* F T‘ | ε, F
( E ) | ID
LL(1) ist, wenden wir das Kriterium des obigen Lemmas
auf Produktionen mit Alternativen an (wir schreiben
abkürzend: FI1 für FIRST1 und FO1 für FOLLOW 1 ):
U
= {*}
1
1
FO1( T‘)
FO1( T‘)
1
FO1( T‘) = { * }
FO1( F )
1
FO1( E‘ )
FO1( E‘)
{ #, ) } = { }
FI1( ε )
{ε}
1
FO1( F ) = { }
FI1( ε )
{ε}
FO1( E‘) = { + }
FI1( *FT‘ )
= {*}
25.04.2007
FO1( E‘)
U
T‘:
U
= {+}
1
FO1( E‘)
1
1
U
= {+}
{ ID }
1
U
FI1( +TE‘ )
FI1( ID )
U
FO1( F )
U
E‘:
1
FO1( F )
U
= {(}
1
U
FI1( ( E ) )
U
F:
1
FO1( T‘ )
FO1( T‘)
{ +, # , ) } = { }
© A. Poetzsch-Heffter, TU Kaiserslautern
74
Beweis: (Charakterisierungslemma)
1. Γ ist LL(1) impliziert FIFO-Charakterisierung.
Wiederspruchsbeweis:
Annahme:
A
β,A
γ zwei Produktionen, β =/ γ, mit
FIRST1(β) 1 FOLLOW 1(A)
FIRST1(γ) 1 FOLLOW 1(A) =/ { }
U
Dann gibt es ein z in dem Durchschnitt. Wir betrachten
hier nur den Fall |z| = 1. Da Γ reduziert ist,
gibt es Ableitungen:
S =>* ψAα => ψβα =>* ψzx
S =>*
ψAα => ψγα =>*
ψzy
Daraus lassen sich folgende Linksableitungen
konstruieren:
S lm
=>*
S =>*
lm
uAα =>
=>* uzx
lm uβα lm
uAα => uγα =>* uzy
lm
lm
mit präfix(1,zx) = z = präfix(1,zy) im Widerspruch
zur LL(1)-Eigenschaft von Γ.
2. FIFO-Charakterisierung impliziert Γ ist LL(1).
( siehe Vorlesung )
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
75
Zur Parsergenerierung für LL(k)-Sprachen:
Grammatik
LL(k)-Parsergenerator
Fehler:
Grammatik
nicht LL(k)
Tabelle für Kellerautomat/
Parserprogramm
Bemerkungen:
• Benutzt wird ein Kellerautomat mit Vorausschau
(ähnlich wie bei bu-Verfahren; siehe dort)
• Produktionsauswahl erfolgt über Tabellen
• td-Verfahren besitzen gegenüber bu-Verfahren
Vorteile bei der Fehleranalyse und -behandlung
Lesen Sie zu Unterabschnitt 2.2.2.1:
Wilhelm, Maurer:
• aus Kap. 8, Abschnitt 8.3.1 bis einschl. 8.3.4,
S. 312 – 329.
25.04.2007
© A. Poetzsch-Heffter, TU Kaiserslautern
76
Herunterladen