Syntaktische Analyse (Parsen) Grundlagen der Programmierung Compiler: Parser (5C) Prof. Dr Manfred Schmidt-Schauß Gegeben: eine kontextfreie Grammatik G und ein String w. Fragen: (1) gehört w zu L(G)? (2) Welchen Syntaxbaum hat w? (3) Welche Bedeutung hat w? Vorgehen: Konstruiere Herleitungsbaum zu w Sommersemester 2013 1 Grundlagen der Programmierung 2 Parser Syntaktische Analyse eines Programms Gegeben: Syntax einer Programmiersprache und der Quelltext eines Programms. Fragen: Ist das Programm syntaktisch korrekt? Was soll dieses Programm bewirken? Aufgabe: Ermittle Bedeutung“ des Programms, ” Konstruktionsverfahren für Herleitungsbäume (bzw. Syntaxbäume) Grundlagen der Programmierung 2 Parser – 2/53 – Syntaktische Analyse bzgl einer CFG – 3/53 – • Für jede CFG gibt es einen Parse-Algorithmus mit worst case Laufzeit O(n3 ) (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 – 4/53 – Syntaktische Analyse bzgl einer CFG Parse-Methoden und Beschränkungen Beschränkung in dieser Vorlesung auf Praxis: Für jede Programmiersprache gibt es einen Parser, der effizient arbeitet, d.h. in O(n), oder in O(n ∗ log(n)) Grundlagen der Programmierung 2 Parser – 5/53 – Parse-Methoden: Vorgehensweisen: • 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 – 6/53 – Parse-Methoden: Vorgehensweisen: Weiteres Unterscheidungsmerkmal: Top-Down: Es wird versucht eine Herleitung vorwärts, vom Startsymbol aus, zu bilden ( forward-chaining“) ” L : Konstruktion einer Linksherleitung Gängige Kombinationsmöglichkeiten: Bottom-Up: Es wird versucht eine Herleitung rückwärts, vom Wort aus, zu bilden ( backward-chaining“). ” Grundlagen der Programmierung 2 Parser R : Konstruktion einer Rechtsherleitung • • – 7/53 – Top-Down-Verfahren zur Konstruktion einer Linksherleitung Bottom-Up-Verfahren zur Konstruktion einer Rechtsherleitung Grundlagen der Programmierung 2 Parser – 8/53 – Beispiel S A B ::= ::= ::= 09-Beispiel: Top-down: Start mit Startsymbol S Rate die Produktionen; Nutze den zu parsenden String zur Steuerung Bilde Restproblem Ziel: Eingabestring bis zum Ende verarbeiten. AB 0|1 8|9 Ziel NT-Wort Herleitung Frage: Kann 09“ aus dieser Grammatik hergeleitet werden? ” 09 S S → 09 AB AB → 9 B 0B ε → 09 Das ergibt eine Linksherleitung. Beachte 09“ wird von links nach rechts bearbeitet ” Jedes Eingabezeichen bestimmt eindeutig die Produktion Grundlagen der Programmierung 2 Parser – 9/53 – 09-Beispiel: Bottom-up: Vorgehen: Regeln rückwärts auf den gegebenen String anwenden das Startsymbol der Grammatik ist zu erreichen Eine Rechtsherleitung wurde konstruiert 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 1 Grundlagen der Programmierung 2 Parser – 10/53 – Beispiel: Suche nach der Herleitung 09 ← A9 ← AB ← S Beachte: Grundlagen der Programmierung 2 Parser B S A B ::= ::= ::= A |B 0A | 1 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 ” Ziel 002 NT-Wort S Herleitung S 002 B hergeleitet werden: 002 B B 02 B 0B 2 B 00B 2 – 11/53 – Grundlagen der Programmierung 2 Parser – 12/53 – Beispiel: Bemerkungen Parsemethoden Ein deterministischer Top-Down-Parser muss beim ersten Zeichen von 002“ entscheiden, ” ob A, oder B. Wir betrachten im folgenden: rekursive absteigende Parser: Allgemeine optimierte: rekursive-prädiktive Parser (LL-Parser) Diese Wahl kann falsch sein. Misslingt eine Herleitung, so muss der Parser zurücksetzen: Bottom-Up-Parser (LR-Parser) Backtracking“ ” Grundlagen der Programmierung 2 Parser – 13/53 – Rekursiv absteigende Parser Grundlagen der Programmierung 2 Parser Struktur eines rekursiv absteigenden Parsers • Top-Down bzgl. der Grammatik. Rekursiv absteigender Parser / Syntaxanalyse ist an der Form der Regeln der Grammatik orientiert. • Eingabewort von links nach rechts Methode: Top-Down-Prüfung der Anwendbarkeit der Regeln • Backtracking, falls Sackgasse • Konstruktion einer Linksherleitung Grundlagen der Programmierung 2 Parser – 14/53 – – 15/53 – Grundlagen der Programmierung 2 Parser – 16/53 – Struktur eines rekursiv absteigenden Parsers Eigenschaften: rekursiv-absteigender Parser • Pro Nichtterminal N wird ein Parser PN programmiert. Eingabe: String (bzw. Tokenstrom) Ausgabe: Syntaxbaum zum Prefix der Eingabe; und Reststring • N → w1 | . . . | wn (das sind alle Regeln zu N ) PN probiert alle wi aus • Prüfung, ob ein wi passt: wi = wi1 wi2 . . . wim von links nach rechts durchgehen Jeweils Parser Pwij aufrufen und Reststring weitergeben I.a. rekursiver Aufruf, falls wij Nichtterminal. Grundlagen der Programmierung 2 Parser – 17/53 – 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) → . . . Grundlagen der Programmierung 2 Parser Funktionale Kombinator-Parser 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 • – 18/53 – Programmierung Umbau der Grammatik (Optimierung der Grammatik) Grundlagen der Programmierung 2 Parser – 19/53 – Grundlagen der Programmierung 2 Parser – 20/53 – Funktionale Kombinator-Parser Funktionale Kombinator-Parser 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. Um Backtracking zu implementieren: Liste von erfolgreichen Ergebnissen Pro Nichtterminal N eine Funktion parserN:: String -> [(String, Syntaxbaum)] bzw. parserN:: [Token] -> [([Token], Syntaxbaum)] verzögerte Auswertung ergibt richtige Reihenfolge der Abarbeitung. Präfix der Eingabe 7→ (Rest der Eingabe, Resultat (z.B. Syntaxbaum) ) ..... Liste aller Möglichkeiten Grundlagen der Programmierung 2 Parser – 21/53 – Haskell-Implementierung der Parser-Kombinatoren – 22/53 – Haskell: Parser-Kombinatoren (2) Kombinator (kombiniert Parser) Z.B. Alternative, Sequenz, Resultat-Umbau erkennt einen String: module CombParser where --- bzw. CombParserWithError import Char infixr 6 <*>, <*, *> infixr 4 <|>, <!> infixl 5 <@ type Parser a b = [a] -> [([a],b)] 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] 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 Grundlagen der Programmierung 2 Parser epsilon :: Parser s () epsilon xs = [(xs,())] – 23/53 – Grundlagen der Programmierung 2 Parser – 24/53 – Haskell: Parser-Kombinatoren (3) Haskell: Parser-Kombinatoren (4) 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] immer erfolgreich: succeed :: r -> Parser s r succeed v xs = [(xs,v)] xs: immer fehlschlagend: p2 | pfail :: Parser s r pfail xs = [] • • • Grundlagen der Programmierung 2 Parser xs2 p1 – 25/53 – Haskell: Parser-Kombinatoren (4b) {z } 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 – 26/53 – Haskell: Parser-Kombinatoren (6) Operation auf dem Ergebnis des Parse : Alternativkombinator : (<@) :: Parser s a -> (a-> b) -> Parser s b (<|>) :: Parser s a -> Parser s a -> Parser s a (p1 <|> p2) xs = p1 xs ++ p2 xs (p <@ f) xs = [(ys, f v) | (ys,v) <- p xs] ignoriert rechtes Ergebnis: Es werden beide Parser p1 und p2 auf die gleiche Eingaben angewendet Alternativkombinator-2: nur das erste Ergebnis: (<*) :: Parser s a -> Parser s b -> Parser s a p <* q = p <*> q <@ fst (<!>) :: Parser s a -> Parser s a -> Parser s a (p1 <!> p2) xs = take 1 (p1 xs ++ p2 xs) ignoriert linkes Ergebnis: (*>) :: Parser s a -> Parser s b -> Parser s b p *> q = p <*> q <@ snd Grundlagen der Programmierung 2 Parser – 27/53 – Grundlagen der Programmierung 2 Parser – 28/53 – Funktionale Kombinator-Parser Haskell: Parser-Kombinatoren (7) erkennt Folge. d.h. entspricht *: many :: Parser s a -> Parser s [a] many p = p <*> many p <@ list <|> succeed [] Aufgaben der Kombinatoren 1 Lesen und Prüfen, 2 Kombinatoren zum Aufbau des Syntaxbaums many1 p = p <*> many p <@ list Beispiel p <* q = p <*> q <@ fst digit :: Parser Char Int digit = satisfy isDigit <@ f where f c = ord c - ord ’0’ <*> ist ein Kombinator zum Lesen und Prüfen. <@ bewirkt die Nachbearbeitung. erkennt Zahl: natural :: Parser Char Int natural = many1 digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 Parser – 29/53 – Haskell: Parser-Kombinatoren (8) Grundlagen der Programmierung 2 Parser – 30/53 – Haskell: Parser-Kombinatoren (9) Nimmt nur die erste (maximale) Alternative des many: nur erlaubt, wenn der Parser die weggelassenen Alternativen nicht benötigt Erkennt Klammerung; Klammern kommen nicht in den Syntaxbaum: manyex :: Parser s a -> Parser s [a] manyex p = p <*> many p <@ list <!> succeed [] pack:: Parser s a -> Parser s b -> Parser s c -> Parser s b pack s1 p s2 = s1 *> p <* s2 many1ex p = p <*> manyex p <@ list option p = p <@ (\x->[x]) <!> epsilon <@ (\x-> []) Erkennt Infix-Folge wie z.B. (1+2+3+4+5): Liste der Argumente: opSeqInf psymb parg = (parg <*> many (psymb *> parg)) <@ list Nimmt nur die erste (maximale) Alternative bei Zahlen: naturalex :: Parser Char Int naturalex = many1ex digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 Parser – 31/53 – Grundlagen der Programmierung 2 Parser – 32/53 – Einfaches Beispiel Leicht komplexeres Beispiel S → AB A → aA B → bB parse S = parse A <*> parse B parse A = (symbol ’a’) <*> parse A parse B = (symbol ’b’) <*> parse B Grammatik-Regeln: S → AB A→a B→b parse S = parse A <*> parse B parse A = (symbol ’a’) parse B = (symbol ’b’) Grammatik-Regeln: Programm: Grundlagen der Programmierung 2 Parser Programm: Typgerecht programmieren mit Syntaxbaum-Erzeugung: parse S = parse A <*> parse B <@ (\(x,y) -> [x,y]) parse A = many (symbol ’a’) parse B = many (symbol ’b’) – 33/53 – Beispiel: Polymorphe Typ-Ausdrücke ::= umgebaute Grammatik; nicht linksrekursiv und optimiert für den Parser AT -> AT | (AT) AT NOARNX NOAR TCT KLRUND KLECK | [AT] | Var | TCA TCA ::= TC | (TC AT . . . AT) | (AT1 ,. . . ,ATn ), n > 1 Grammatik ist linksrekursiv! Grundlagen der Programmierung 2 Parser – 34/53 – Beispiel: Polymorphe Typ-Ausdrücke Grammatik AT Grundlagen der Programmierung 2 Parser – 35/53 – Grundlagen der Programmierung 2 Parser ::= ::= ::= ::= ::= ::= NOAR { NOARNX | ε } -> AT Var | TCT | KLRUND | KLECK TC NOAR . . . NOAR (AT,. . . ,AT) Mindestens 2-Tupel [AT] – 36/53 – Kombinatorparser Mit Fehlerbehandlung Kombinatorparser; Beispiele AT NOARNX NOAR TCT KLRUND KLECK Erweiterte Bibliothek mit neuen Kombinatoren ((p1 <*>!) errStr) p2 Ergibt Fehler mit Text errStr Wenn p2 fehlschlägt ((p1 *>!) errStr) p2 Wie <*>! aber nur Ergebnis von p2 ((p1 *<!) errStr) p2 Wie <*>! aber nur Ergebnis von p1 Grundlagen der Programmierung 2 Parser – 37/53 – Kombinatorparser mit Fehlerbehandlung NOAR { NOARNX | ε } -> AT Var | TCT | KLRUND | KLECK TC NOAR . . . NOAR (AT,. . . ,AT) Mindestens 2-Tupel [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 – 38/53 – Kombinatorparser mit Fehlerbehandlung Programme und Vorführung typeUnifErr combParserWithError parseEquation parseAT . prelex printUnif "(a,a) = (b,[b])“ Grundlagen der Programmierung 2 Parser ::= ::= ::= ::= ::= ::= Programme und Vorführung html-parser.hs main prelex (linPosNumbering "<D> xxx </D>\n<br> text </br>") – 39/53 – Grundlagen der Programmierung 2 Parser – 40/53 – Fehler-Meldungen: Bemerkungen Rekursiv-prädiktive Parser Die Fehlererkennung und -meldung sollte spezifisch sein und möglichst genau die Ursache und Stelle melden. Optimierte rekursiv absteigende Parser für eingeschränkte Grammatiken ( LL(1) ). Schlecht: Gut Keine Alternativen mehr gefunden in Zeile... “ ” 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 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 Bei Parsern mit Backtracking und ohne Fehlerbehandlung Der richtige Fehlerstelle ist meist unklar Der Backtracking-Parser kann meist nur melden: keine Alternativen mehr Grundlagen der Programmierung 2 Parser Aber: – 41/53 – Rekursiv-prädiktive Parser ⇒ Grundlagen der Programmierung 2 Parser – 42/53 – Rekursiv-prädiktive Parser Eindeutigkeitsbedingung: Wenn A → w1 | . . . | wn alle Regeln zu A sind: Falls Parser im Zustand A Für jedes erste Symbol a der Eingabe: nur eine Regel A → wi darf anwendbar sein! Zweidimensionale Tabelle: (Lookahead-Symbol, Nichtterminal) man kann nicht für jede eindeutige kontextfreie Grammatik einen rekursiv-prädiktiven Parser konstruieren. 7→ Regel oder Fehlereintrag Tabellengesteuerter rekursiv-prädiktiver Parser: Beispiel: Grundlagen der Programmierung 2 Parser – 43/53 – Grundlagen der Programmierung 2 Parser A → bCD | aEF | cG | H H → dabc ... – 44/53 – Rekursiv-prädiktive Parser Rekursiv-prädiktive Parser, ε-Fall Beispiel: Sonderfall: Es gibt eine Regel A → wi mit wi Diese wird ausgewählt, wenn: • • • →∗ S A H ... B C ε: 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 → AB | AC → bCD | aEF | cG | H → ε → dA → eA Im Zustand A und bei Eingabesymbol d: A → H wird ausgewählt. Grundlagen der Programmierung 2 Parser – 45/53 – FIRST- und FOLLOW-Mengen := 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. Ex Plus PlusRest SigZ B Z ::= ::= ::= ::= ::= ::= Plus SigZ Plusrest + SigZ PlusRest | ε B|-B Z | ( Ex ) 0 | ... | 9 Man erhält als first-Mengen: Diese Mengen kann man in allen rekursiv-absteigenden Parsern zur Eindämmung, evtl. zur Vermeidung, von Backtracking verwenden. Grundlagen der Programmierung 2 Parser – 46/53 – Beispiel für first Wenn Grammatik G gegeben ist: first(A) Grundlagen der Programmierung 2 Parser – 47/53 – Ex Plus Plus Rest SigZ B Z 0,...,9, (,- 0,...,9, (,- +, ε 0,...,9, (,- 0,...,9, ( 0,...,9 Grundlagen der Programmierung 2 Parser – 48/53 – Beispiel für follow : Ex Plus PlusRest SigZ B Z ::= ::= ::= ::= ::= ::= Vorgehen des LL(1)-Parsers Plus SigZ Plusrest + SigZ PlusRest | ε B|-B Z | ( Ex ) 0 | ... | 9 Bei Symbol a, und aktuellem Nichtterminal A: • Ist a ∈ first(wi ) für eine Regel A ::= wi , dann nehme diese Regel. (ε 6∈ first(wi ) für alle i muss gelten. ) • Ist a 6∈ first(wi ) für alle Regeln A ::= wi , 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. Man erhält als follow- Mengen: Ex ) Plus ) PlusRest ) SigZ +,) B +,) Z +,) Vorteil: genaue und frühe Fehlererkennung Grammatik ist LL(1) parsebar, da: First-Mengen zu Regelalternativen passen und first(PlusRest) ∩ follow(PlusRest) = ∅ Grundlagen der Programmierung 2 Parser – 49/53 – Beispiel: vereinfachte Grammatik für Ausdrücke Expr Rest Term • • • • • • • ::= ::= ::= – 50/53 – Beispielparser zur Grammatik Parsebaum: Term Rest + Term Rest | − Term Rest | ε 0 | ... | 9 Syntaxbaum: PExp 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. Grundlagen der Programmierung 2 Parser Grundlagen der Programmierung 2 Parser – 51/53 – 1 + % | PRest + y & 2 − 1 y PRest 3 % 2 − ~ 3 PLeer 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 – 52/53 – 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 – 53/53 –