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