Syntaktische Analyse (Parsen) Grundlagen der Programmierung Compiler: Parser (5C) Gegeben: eine kontextfreie Grammatik G und ein String w. Prof. Dr. Manfred Schmidt-Schauß Sommersemester 2015 Fragen: Vorgehen: (1) gehört w zu L(G)? (2) Welchen Syntaxbaum hat w? (3) Welche Bedeutung hat w? Konstruiere Herleitungsbaum zu w Grundlagen der Programmierung 2 Parser 2/57 Syntaktische Analyse eines Programms Syntaktische Analyse bzgl einer CFG Gegeben: Fragen: Aufgabe: Syntax einer Programmiersprache und der Quelltext eines Programms. Ist das Programm syntaktisch korrekt? Was soll dieses Programm bewirken? Ermittle Bedeutung des Programms, Konstruktionsverfahren für Herleitungsbäume (bzw. Syntaxbäume) Für jede CFG gibt es einen Parse-Algorithmus mit worst case Laufzeit O(n 3 ) (n : Anzahl der Eingabesymbole) CYK: Cocke, Younger, Kasami, falls Grammatik in Chomsky-Normalform (Alle Regeln von der Form N W mit W 2 oder Earley-Algorithmus CYK benutzt dynamisches Programmieren. erzeugt eine Tabelle: pro Paar (N, w) von Nichtterminal N und Subwort w der Eingabe ein Eintrag True wenn N G w, sonst False Grundlagen der Programmierung 2 Parser 3/57 Grundlagen der Programmierung 2 Parser 4/57
Syntaktische Analyse bzgl einer CFG Parse-Methoden und Beschränkungen Praxis: Für jede Programmiersprache gibt es einen Parser, der effizient arbeitet, d.h. in O(n), oder in O(n log(n)) Beschränkung in dieser Vorlesung auf einfach implementierbare oder effiziente Parser Nur für eingeschränkte CFGs Verarbeitung des Zeichenstroms bzw. des Eingabewortes von links nach rechts evtl. auch mit Vorausschau um einige Zeichen. Grundlagen der Programmierung 2 Parser 5/57 Grundlagen der Programmierung 2 Parser 6/57 Parse-Methoden: Vorgehensweisen: Parse-Methoden: Vorgehensweisen: Top-Down: Es wird versucht eine Herleitung vorwärts, vom Startsymbol der Grammatik aus, zu bilden ( forward-chaining ) Bottom-Up: Es wird versucht eine Herleitung rückwärts, vom Wort aus, zu bilden; bis das Startsymbol der Grammatik erreicht ist. ( backward-chaining ). Weiteres Unterscheidungsmerkmal: R : Konstruktion einer Rechtsherleitung L : Konstruktion einer Linksherleitung Gängige Kombinationsmöglichkeiten: Top-Down-Verfahren zur Konstruktion einer Linksherleitung Bottom-Up-Verfahren zur Konstruktion einer Rechtsherleitung Grundlagen der Programmierung 2 Parser 7/57 Grundlagen der Programmierung 2 Parser 8/57
Beispiel 09-Beispiel: Top-down: S ::= AB A ::= 0 1 B ::= 8 9 Frage: Kann 09 aus dieser Grammatik hergeleitet werden? Start mit Startsymbol S Rate die Produktionen; Nutze den zu parsenden String zur Steuerung Bilde Restproblem Ziel: Eingabestring bis zum Ende verarbeiten. Grundlagen der Programmierung 2 Parser 9/57 Grundlagen der Programmierung 2 Parser 10/57 Beispiel 09-Beispiel: Bottom-up: Vorgehen: Regeln rückwärts auf den gegebenen String anwenden das Startsymbol der Grammatik ist zu erreichen S ::= AB A ::= 0 1 B ::= 8 9 Eingabe 09 09 9 ε (N + T ) -Wort S AB B Herleitung S AB 0B 09 Das ergibt eine Linksherleitung. Beachte 09 wird von links nach rechts bearbeitet Jedes Eingabezeichen bestimmt eindeutig die Produktion 09 A9 AB S anders geschrieben: S AB A9 0 Eine Rechtsherleitung wurde konstruiert Beachte: Manchmal sind mehrere Regeln anwendbar zudem muss man i.a. den Teilstring raten, auf den eine Produktion (rückwärts) anzuwenden ist Im Beispiel: Gleicher Herleitungsbaum S A B Grundlagen der Programmierung 2 Parser 11/57 1 2 Grundlagen der Programmierung 2 Parser 12/57
Beispiel: Suche nach der Herleitung Beispiel: Bemerkungen S ::= A B A ::= 0A 1 B ::= 0B 2 Kann 002 hergeleitet werden? Ziel 002 002 02 2 NT-Wort S A A A Herleitung S A 0A 00A? 002 kann nur aus B hergeleitet werden: Ziel 002 002 02 2 NT-Wort S B B B Herleitung S B 0B 00B 002 S ::= A B A ::= 0A 1 B ::= 0B 2 Ein deterministischer Top-Down-Parser muss beim ersten Zeichen von 002 entscheiden, ob A, oder B. Diese Wahl kann falsch sein. Misslingt eine Herleitung, so muss der Parser zurücksetzen: Backtracking Grundlagen der Programmierung 2 Parser 13/57 Grundlagen der Programmierung 2 Parser 14/57 Parsemethoden Rekursiv absteigende Parser Wir betrachten im folgenden: rekursive absteigende Parser: Allgemeine optimierte: rekursive-prädiktive Parser (LL-Parser) Bottom-Up-Parser (LR-Parser) Rekursiv absteigender Parser / Syntaxanalyse ist an der Form der Regeln der Grammatik orientiert. Methode: Top-Down-Prüfung der Anwendbarkeit der Regeln Grundlagen der Programmierung 2 Parser 15/57 Grundlagen der Programmierung 2 Parser 16/57
Struktur eines rekursiv absteigenden Parsers Struktur eines rekursiv absteigenden Parsers Top-Down bzgl. der Grammatik. Eingabewort von links nach rechts Backtracking, falls Sackgasse Konstruktion einer Linksherleitung Pro Nichtterminal N wird ein Parser P N programmiert. Eingabe: String (bzw. Tokenstrom) Ausgabe: Syntaxbaum zum Prefix der Eingabe; und Reststring N w 1... w n (das sind alle Regeln zu N) P N probiert alle w i aus Prüfung, ob ein w i passt: w i = w i1 w i2... w im von links nach rechts durchgehen Jeweils Parser P wij aufrufen und Reststring weitergeben I.a. rekursiver Aufruf, falls w ij Nichtterminal. Grundlagen der Programmierung 2 Parser 17/57 Grundlagen der Programmierung 2 Parser 18/57 Eigenschaften: rekursiv-absteigender Parser Rekursiv-absteigende Parser Liefert alle Linksherleitungen für alle Präfixe des Tokenstroms (wenn der Parser terminiert) Leicht implementierbar Leicht erweiterbar auf weitere Einschränkungen I.a. exponentiell oder sogar: Terminiert nicht für bestimmte (linksrekursive) Grammatiken, obwohl eine Herleitung existiert: Beispiel A ::= A+A A-A 1... 9 Eingabe: 1+1 : Aber: nur die erste Regel wird (jeweils rekursiv) versucht: (A,1+1) (A+A,1+1) ((A+A)+A, 1+1)... Programme von Programmiersprachen kann man i.a. in O(n) oder O(n log(n)) parsen, Effiziente rekursiv-absteigende Parser benötigen i.a.: Erweiterungen wie Vorausschau Umbau der Grammatik (Optimierung der Grammatik) Grundlagen der Programmierung 2 Parser 19/57 Grundlagen der Programmierung 2 Parser 20/57
Funktionale Kombinator-Parser Funktionale Kombinator-Parser Programmierung Implementierung von rekursiv-absteigenden Parsern in Haskell Vorteile relativ leicht verständliche Programmierung 1-1-Übersetzung der Regeln in Programmcode Nach Erweiterung und Optimierung kann der Parser Fehler gut erkennen und deterministisch werden. Pro Nichtterminal N eine Funktion parsern:: String -> [(String, Syntaxbaum)] bzw. parsern:: [Token] -> [([Token], Syntaxbaum)] Präfix der Eingabe (Rest der Eingabe, Resultat (z.b. Syntaxbaum) )... Liste aller Möglichkeiten Grundlagen der Programmierung 2 Parser 21/57 Grundlagen der Programmierung 2 Parser 22/57 Funktionale Kombinator-Parser Haskell-Implementierung der Parser-Kombinatoren Kombinator (kombiniert Parser) Z.B. Alternative, Sequenz, Resultat-Umbau Um Backtracking zu implementieren: Liste von erfolgreichen Ergebnissen verzögerte Auswertung ergibt richtige Reihenfolge der Abarbeitung. module CombParser where --- bzw. CombParserWithError import Char infixr 6 <*>, <*, *> infixr 4 < >, <!> infixl 5 <@ type Parser a b = [a] -> [([a],b)] erkennt ein Zeichen: symbol :: Eq s => s -> Parser s s symbol a [] = [] symbol a (x:xs) a ==x = [(xs,x)] otherwise = [] Grundlagen der Programmierung 2 Parser 23/57 Grundlagen der Programmierung 2 Parser 24/57
Haskell: Parser-Kombinatoren (2) Haskell: Parser-Kombinatoren (3) erkennt einen String: token :: Eq s => [s] -> Parser s [s] -- token :: Eq s => [s] -> Parser s [s] token k xs k == (take n xs) = [(drop n xs, k)] otherwise = [] where n = length k testet ein Zeichen der Eingabe: satisfy :: (s -> Bool) -> Parser s s satisfy p [] = [] satisfy p (x:xs) = [(xs,x) p x] immer erfolgreich: succeed :: r -> Parser s r succeed v xs = [(xs,v)] immer fehlschlagend: pfail :: Parser s r pfail xs = [] epsilon :: Parser s () epsilon xs = [(xs,())] Grundlagen der Programmierung 2 Parser 25/57 Grundlagen der Programmierung 2 Parser 26/57 Haskell: Parser-Kombinatoren (4) Haskell: Parser-Kombinatoren (4b) Sequenzkombinator : (<*>) :: Parser s a -> Parser s b -> Parser s (a,b) (p1 <*> p2) xs = [(xs2, (v1,v2)) (xs1,v1) <- p1 xs, (xs2,v2) <- p2 xs1] xs: xs2 p1 p2 } {{ } p1 <*> p2 p1 parst den Anfang der Eingabe; gibt den Reststring xs1 weiter an p2 p2 parst danach den Anfang des Reststrings gibt den Reststring zurück Gesamtresultat = Tupel aus den zwei Resultaten Grundlagen der Programmierung 2 Parser 27/57 Alternativkombinator : (< >) :: Parser s a -> Parser s a -> Parser s a (p1 < > p2) xs = p1 xs ++ p2 xs Es werden beide Parser p1 und p2 auf die gleiche Eingabe angewendet Alternativkombinator-2: nur das erste Ergebnis: (<!>) :: Parser s a -> Parser s a -> Parser s a (p1 <!> p2) xs = take 1 (p1 xs ++ p2 xs) Grundlagen der Programmierung 2 Parser 28/57
Haskell: Parser-Kombinatoren (6) Haskell: Parser-Kombinatoren (6) Operation auf dem Ergebnis des Parse : (<@) :: Parser s a -> (a->b) -> Parser s b (p <@ f) xs = [(ys, f v) (ys,v) <- p xs] Kombinatoren, die die Nachverarbeitung miterledigen: ignoriert rechtes Ergebnis: (<*) :: Parser s a -> Parser s b -> Parser s a p <* q = p <*> q <@ fst f ist der Modifikator des Ergebnisses v des Parsers p. Typischer Fall: p < > q kann ungetypt sein, wenn p, q verschiedene Ergebnis-Typen liefern. Dann z.b. (p <@ f) < > (q <@ g) benutzen. ignoriert linkes Ergebnis: (*>) :: Parser s a -> Parser s b -> Parser s b p *> q = p <*> q <@ snd Grundlagen der Programmierung 2 Parser 29/57 Grundlagen der Programmierung 2 Parser 30/57 Funktionale Kombinator-Parser Haskell: Parser-Kombinatoren (7) erkennt Folge. d.h. entspricht *: Aufgaben der Kombinatoren 1 Lesen und Prüfen, 2 Kombinatoren zum Aufbau des Syntaxbaums Beispiel p <* q = p <*> q <@ fst <*> ist ein Kombinator zum Lesen und Prüfen. <@ bewirkt die Nachbearbeitung. many :: Parser s a -> Parser s [a] many p = p <*> many p <@ list < > succeed [] many1 p = p <*> many p <@ list digit :: Parser Char Int digit = satisfy isdigit <@ f where f c = ord c - ord 0 erkennt Zahl: natural :: Parser Char Int natural = many1 digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 Parser 31/57 Grundlagen der Programmierung 2 Parser 32/57
Haskell: Parser-Kombinatoren (8) Nimmt nur die erste (maximale) Alternative des many: nur erlaubt, wenn der Parser die weggelassenen Alternativen nicht benötigt manyex :: Parser s a -> Parser s [a] manyex p = p <*> many p <@ list <!> succeed [] many1ex p = p <*> manyex p <@ list option p = p <@ (\x->[x]) <!> epsilon <@ (\x-> []) Nimmt nur die erste (maximale) Alternative bei Zahlen: Haskell: Parser-Kombinatoren (9) Erkennt Klammerung; Klammern kommen nicht in den Syntaxbaum: pack:: Parser s a -> Parser s b -> Parser s c -> Parser s b pack s1 p s2 = s1 *> p <* s2 Erkennt Infix-Folge wie z.b. (1+2+3+4+5): Liste der Argumente: opseqinf psymb parg = (parg <*> many (psymb *> parg)) <@ list naturalex :: Parser Char Int naturalex = many1ex digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 Parser 33/57 Grundlagen der Programmierung 2 Parser 34/57 Einfaches Beispiel Leicht komplexeres Beispiel Grammatik-Regeln: S AB A a B b Programm: parse S = parse A <*> parse B parse A = (symbol a ) parse B = (symbol b ) Grammatik-Regeln: S AB A aa a B bb b Programm: parses = parsea <*> parseb parsea = (symbol a ) <*> parsea < > (symbol a ) (parseb = (symbol b ) <*> parseb) < > (symbol b ) Grundlagen der Programmierung 2 Parser 35/57 Grundlagen der Programmierung 2 Parser 36/57
Leicht komplexeres Beispiel Leicht komplexeres Beispiel Grammatik-Regeln: S AB A aa a B bb b Typgerecht programmieren mit Modifikatoren und Syntaxbaum-Erzeugung: parses = (parsea <*> parseb) <@ (\(x,y)-> [x,y]) parsea = ((symbol a ) <*> parsea) <@ (list) < > (symbol a ) <@ (\x -> (x:[])) parseb = (((symbol b ) <*> parseb) <@ (list)) < > (symbol b ) <@ (\x -> (x:[])) list (x,xs) = x:xs Zusätzliche Verwendung des Kombinators many um neue Parser zu generieren: parse S = parsea <*> parseb <@ (\(x,y) -> [x,y]) parsea = many (symbol a ) parseb = many (symbol b ) oder, noch besser: parse S = parsea <*> parseb <@ (\(x,y) -> [x,y]) parsea = manyex (symbol a ) parseb = manyex (symbol b ) Grundlagen der Programmierung 2 Parser 37/57 Grundlagen der Programmierung 2 Parser 38/57 Beispiel: Polymorphe Typ-Ausdrücke Beispiel: Polymorphe Typ-Ausdrücke Grammatik AT ::= AT -> AT (AT) [AT] Var TCA TCA ::= TC (TC AT... AT) (AT 1,...,AT n ), n > 1 Grammatik ist linksrekursiv! umgebaute Grammatik; nicht linksrekursiv und optimiert für den Parser AT ::= NOAR { NOARNX ε } NOARNX ::= -> AT NOAR ::= Var TCT KLRUND KLECK TCT ::= TC NOAR... NOAR KLRUND ::= (AT,...,AT) Mindestens 2-Tupel KLECK ::= [AT] Grundlagen der Programmierung 2 Parser 39/57 Grundlagen der Programmierung 2 Parser 40/57
Kombinatorparser mit Fehlerbehandlung Kombinatorparser; Beispiele Erweiterte Bibliothek mit neuen Kombinatoren ((p1 <*>!) errstr) p2 ((p1 *>!) errstr) p2 ((p1 *<!) errstr) p2 Ergibt Fehler mit Text errstr Wenn p2 fehlschlägt Wie <*>! aber nur Ergebnis von p2 Wie <*>! aber nur Ergebnis von p1 AT ::= NOAR { NOARNX ε } NOARNX ::= -> AT NOAR ::= Var TCT KLRUND KLECK TCT ::= TC NOAR... NOAR KLRUND ::= (AT,...,AT) Mindestens 2-Tupel KLECK ::= [AT] parseklrund = (parsesymbol ( *> (parseinklrund <*! ") erwartet") (parsesymbol ) )) <@ id parseinklrund = (parseat <*> (manyex (((parsesymbol, ) *>! "Typ nach, erwartet") parseat))) <@@ (\(t1,t2) er -> if null t2 then t1 else (Fn ("Tup"++(show ((length t2) +1))) (t1:t2) er)) Grundlagen der Programmierung 2 Parser 41/57 Grundlagen der Programmierung 2 Parser 42/57 Kombinatorparser mit Fehlerbehandlung Kombinatorparser mit Fehlerbehandlung Programme und Vorführung typeuniferr combparserwitherror parseequation parseat. prelex printunif " (a,a) = (b,[b]) Programme und Vorführung html-parser.hs main prelex (linposnumbering "<DD> xxx </DD>\n<br> text </br>") Grundlagen der Programmierung 2 Parser 43/57 Grundlagen der Programmierung 2 Parser 44/57
Fehler-Meldungen: Bemerkungen Rekursiv-prädiktive Parser Die Fehlererkennung und -meldung sollte spezifisch sein und möglichst genau die Ursache und Stelle melden. Schlecht: Keine Alternativen mehr gefunden in Zeile... Gut Fehler in Zeile... Spalte... Möglicher Grund:... Bei deterministischen Parsern (und Kombinatorparser mit Fehlerbehandlung) Die Fehlerstelle ist klar; die Fehlerursache ist auch meist spezifisch genug Bei Parsern mit Backtracking und ohne Fehlerbehandlung Der richtige Fehlerstelle ist meist unklar Der Backtracking-Parser kann meist nur melden: keine Alternativen mehr Optimierte rekursiv absteigende Parser für eingeschränkte Grammatiken ( LL(1) ). Eigenschaften: Die anzuwendende Produktion ist immer eindeutig festgelegt abhängig vom aktuellen Nichtterminal und dem nächsten Symbol (Lookahead-Symbol) der Resteingabe kein Zurücksetzen notwendig, deterministische Abarbeitung der Eingabe von links nach rechts Aber: man kann nicht für jede eindeutige kontextfreie Grammatik einen rekursiv-prädiktiven Parser konstruieren. Grundlagen der Programmierung 2 Parser 45/57 Grundlagen der Programmierung 2 Parser 46/57 Rekursiv-prädiktive Parser Rekursiv-prädiktive Parser Zweidimensionale Tabelle: (Lookahead-Symbol, Nichtterminal) Regel oder Fehlereintrag Eindeutigkeitsbedingung: Wenn A w 1... w n alle Regeln zu A sind: Falls Parser im Zustand A Für jedes erste Symbol a der Eingabe: nur eine Regel A w i darf anwendbar sein! Tabellengesteuerter rekursiv-prädiktiver Parser: Beispiel: A bcd aef cg H H dabc... Grundlagen der Programmierung 2 Parser 47/57 Grundlagen der Programmierung 2 Parser 48/57
Rekursiv-prädiktive Parser Rekursiv-prädiktive Parser, ε-fall Sonderfall: Es gibt eine Regel A w i mit w i ε: Diese wird ausgewählt, wenn: keine passende rechte Seite für das Lookahead-Symbol und das Lookahead-Symbol kann auf A folgen und es gibt nur eine solche Regel für A Beispiel: S AB AC A bcd aef cg H H ε... B da C ea Im Zustand A und bei Eingabesymbol d: A H wird ausgewählt. Grundlagen der Programmierung 2 Parser 49/57 Grundlagen der Programmierung 2 Parser 50/57 FIRST- und FOLLOW-Mengen Beispiel für first Wenn Grammatik G gegeben ist: first(a) := Terminal-Symbole die am Anfang eines erkannten A-Wortes stehen können. (auch ε) follow(a) := Terminal-Symbole die auf ein erkanntes A-Wort folgen können. Diese Mengen kann man in allen rekursiv-absteigenden Parsern zur Eindämmung, evtl. zur Vermeidung, von Backtracking verwenden. Ex ::= Plus Plus ::= SigZ Plusrest PlusRest ::= + SigZ PlusRest ε SigZ ::= B - B B ::= Z ( Ex ) Z ::= 0... 9 Man erhält als first-mengen: Ex Plus Plus SigZ B Z Rest 0,...,9, (,- 0,...,9, (,- +, ε 0,...,9, (,- 0,...,9, ( 0,...,9 Grundlagen der Programmierung 2 Parser 51/57 Grundlagen der Programmierung 2 Parser 52/57
Beispiel für follow : Vorgehen des LL(1)-Parsers Ex ::= Plus Plus ::= SigZ Plusrest PlusRest ::= + SigZ PlusRest ε SigZ ::= B - B B ::= Z ( Ex ) Z ::= 0... 9 Man erhält als follow- Mengen: Ex Plus PlusRest SigZ B Z ) ) ) +,) +,) +,) Grammatik ist LL(1) parsebar, da: First-Mengen zu Regelalternativen passen und first(plusrest) follow(plusrest) = Bei Symbol a, und aktuellem Nichtterminal A: Ist a first(w i ) für eine Regel A ::= w i, dann nehme diese Regel. (ε first(w i ) für alle i muss gelten. ) Ist a first(w i ) für alle Regeln A ::= w i, dann gibt es maximal eine Regel A ::= w mit first(w) = Falls a follow(a), dann diese Regel. Wenn auch dann keine passende Alternative existiert, wird mit Fehler abgebrochen. Vorteil: genaue und frühe Fehlererkennung Grundlagen der Programmierung 2 Parser 53/57 Grundlagen der Programmierung 2 Parser 54/57 Beispiel: vereinfachte Grammatik für Ausdrücke Beispielparser zur Grammatik Expr ::= Term Rest Rest ::= + Term Rest Term Rest ε Term ::= 0... 9 first(term Rest) = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} first(+ Term Rest) = {+}, first( Term Rest) = { } first(expr ) = first(term ) = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} first(rest) = {+,, ε} follow(expr) =. follow(rest) =. follow(term) = {+, }. Diese Grammatik hat somit die LL(1)-Eigenschaft. Parsebaum: PExp 1 PRest + 2 PRest Syntaxbaum: 3 PLeer + 1 2 3 Der Parsebaum entspricht der Grammatik, aber noch nicht der gewünschten Struktur des arithmetischen Ausdrucks. Man braucht eine Nachbearbeitung des Parsebaumes. Grundlagen der Programmierung 2 Parser 55/57 Grundlagen der Programmierung 2 Parser 56/57
Prädiktiv vs. Kombinatoren Meistens kann man für Grammatiken die geeignet sind für rekursiv-prädiktive Parser (LL(1)) auch einen deterministischen Kombinator-Parser schreiben. (Nach etwas Analyse und Nachdenken) Dabei ist im Parserprogramm überall der Parserkombinator < > durch <!> ersetzt. und man kann teilweise die um Fehlermeldungen erweiterten Kombinatoren verwenden D.h der Parser ist frei von Backtracking. Grundlagen der Programmierung 2 Parser 57/57