Grundlagen der Programmierung 2 (Comp-C) Prof. Dr. Manfred Schmidt-Schauß Künstliche Intelligenz und Softwaretechnologie 25. Mai 2011 Syntaktische Analyse (Parsen) Gegeben: eine kontextfreie Grammatik G und ein String w. Fragen: gehört w zu L(G)? Welche Bedeutung hat w? Vorgehen: Konstruiere Herleitungsbaum zu w Grundlagen der Programmierung 2 (Comp-C) 2011 - 1 - Syntaktische Analyse eines Programms Gegeben: Syntax einer Programmiersprache und der Quelltext eines Programms. Frage: ist dies ein syntaktisch korrektes Programm? Was soll dieses Programm bewirken ? Aufgabe: Ermittle Bedeutung“ des Programms, ” Konstruktionsverfahren für Herleitungsbäume (bzw. Syntaxbäume) Grundlagen der Programmierung 2 (Comp-C) 2011 - 2 - Syntaktische Analyse bzgl einer CFG • 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 (Comp-C) 2011 - 3 - Syntaktische Analyse bzgl einer CFG 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 (Comp-C) 2011 - 4 - Parse-Methoden und Beschränkungen 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 (Comp-C) 2011 - 5 - Parse-Methoden: Vorgehensweisen: Top-Down: Es wird versucht eine Herleitung vorwärts, vom Startsymbol aus, zu bilden ( forward-chaining“) ” Bottom-Up: Es wird versucht eine Herleitung rückwärts, vom Wort aus, zu bilden ( backward-chaining“). ” Grundlagen der Programmierung 2 (Comp-C) 2011 - 6 - Parse-Methoden: Vorgehensweisen: 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 (Comp-C) 2011 - 7 - Beispiel S A B ::= ::= ::= AB 0 | 1 8 | 9 Frage: Kann 09“ aus dieser Grammatik hergeleitet werden? ” Grundlagen der Programmierung 2 (Comp-C) 2011 - 8 - 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. Ziel NT-Wort Herleitung 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 (Comp-C) 2011 - 9 - 09-Beispiel: Bottom-up: Vorgehen: Regeln rückwärts auf den gegebenen String anwenden das Startsymbol der Grammatik ist zu erreichen 09 ← A9 ← AB ← S 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 A 1 Grundlagen der Programmierung 2 (Comp-C) 2011 ~~ ~~ ~ ~ ~ ~ S ??? ?? ?? ? B 2 - 10 - Beispiel: Suche nach der Herleitung S A B ::= ::= ::= A |B 0A | 1 0B | 2 Kann 002“ hergeleitet werden? ” Ziel NT-Wort Herleitung 002 S S 002 A A 02 A 0A 2 A 00A ? 002“ kann nur aus B hergeleitet werden: ” Ziel NT-Wort Herleitung 002 S S 002 B B 02 B 0B Grundlagen der Programmierung 2 (Comp-C) 2011 2 B 00B 002 - 11 - Beispiel: Bemerkungen 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 (Comp-C) 2011 - 12 - Parsemethoden Wir betrachten im folgenden: • rekursive absteigende Parser: – Allgemeine – optimierte: rekursive-prädiktive Parser (LL-Parser) • Bottom-Up-Parser (LR-Parser) Grundlagen der Programmierung 2 (Comp-C) 2011 - 13 - Rekursiv absteigende 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 (Comp-C) 2011 - 14 - Struktur eines rekursiv absteigenden Parsers • Top-Down bzgl. der Grammatik. • Eingabewort von links nach rechts • Backtracking, falls Sackgasse • Konstruktion einer Linksherleitung Grundlagen der Programmierung 2 (Comp-C) 2011 - 15 - Struktur eines rekursiv absteigenden Parsers • Pro Nichtterminal N existiert ein Parser PN Eingabe: String 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 = wi1wi2 . . . wim von links nach rechts durchgehen Jeweils Parser Pwij aufrufen und Reststring weitergeben I.a. rekursiver Aufruf, falls wij Nichtterminal. Grundlagen der Programmierung 2 (Comp-C) 2011 - 16 - Eigenschaften: rekursiv-absteigender 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 (Comp-C) 2011 - 17 - Rekursiv-absteigende 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 • Umbau der Grammatik (Optimierung der Grammatik) Grundlagen der Programmierung 2 (Comp-C) 2011 - 18 - Funktionale Kombinator-Parser Programmierung Grundlagen der Programmierung 2 (Comp-C) 2011 - 19 - Funktionale Kombinator-Parser Implementierung von rekursiv-absteigenden Parsern in Haskell Vorteile relativ leicht verständliche Programmierung 1-1-Übersetzung der Regeln in Programmcode Pro Nichtterminal N eine Funktion parserN:: String -> [(String, Syntaxbaum)] bzw. parserN:: [Token] -> [([Token], Syntaxbaum)] Präfix der Eingabe 7→ (Rest der Eingabe, Resultat (z.B. Syntaxbaum) ) ..... Liste aller Möglichkeiten Grundlagen der Programmierung 2 (Comp-C) 2011 - 20 - Funktionale Kombinator-Parser Um Backtracking zu implementieren: Liste von erfolgreichen Ergebnissen verzögerte Auswertung ergibt richtige Reihenfolge der Abarbeitung. Grundlagen der Programmierung 2 (Comp-C) 2011 - 21 - Haskell-Implementierung Kombinatoren der Parser- Kombinator (kombiniert Parser) Z.B. Alternative, Sequenz, Resultat-Umbau 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 (Comp-C) 2011 - 22 - Haskell: Parser-Kombinatoren (2) 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] epsilon :: Parser s () epsilon xs = [(xs,())] Grundlagen der Programmierung 2 (Comp-C) 2011 - 23 - Haskell: Parser-Kombinatoren (3) immer erfolgreich: succeed :: r -> Parser s r succeed v xs = [(xs,v)] immer fehlschlagend: pfail :: Parser s r pfail xs = [] Grundlagen der Programmierung 2 (Comp-C) 2011 - 24 - 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] xs: xs2 p1 | p2 {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 (Comp-C) 2011 - 25 - Haskell: Parser-Kombinatoren (4b) 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 Eingaben 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 (Comp-C) 2011 - 26 - 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] ignoriert rechtes Ergebnis: (<*) :: Parser s a -> Parser s b -> Parser s a p <* q = p <*> q <@ fst ignoriert linkes Ergebnis: (*>) :: Parser s a -> Parser s b -> Parser s b p *> q = p <*> q <@ snd Grundlagen der Programmierung 2 (Comp-C) 2011 - 27 - Haskell: Parser-Kombinatoren (7) erkennt Folge. d.h. entspricht *: 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: Grundlagen der Programmierung 2 (Comp-C) 2011 - 28 - Haskell: Parser-Kombinatoren (7) natural :: Parser Char Int natural = many1 digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 (Comp-C) 2011 - 29 - Haskell: Parser-Kombinatoren (8) Nimmt nur die erste (maximale) Alternative des many: nur erlaubt, wenn der Parser diese 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: naturalex :: Parser Char Int naturalex = many1ex digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 (Comp-C) 2011 - 30 - Haskell: Parser-Kombinatoren (9) Erkennt Klammerung und ignoriert den Wert der Klammern: 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 Grundlagen der Programmierung 2 (Comp-C) 2011 <*> many (psymb *> parg)) <@ list - 31 - Beispiel: Polymorphe Typ-Ausdrücke Grammatik AT ::= AT -> AT | (AT) | [AT] | Var | TCA TCA ::= TC | (TC AT . . . AT) | (AT1,. . . ,ATn), n > 1 Grammatik ist linksrekursiv! Grundlagen der Programmierung 2 (Comp-C) 2011 - 32 - Beispiel: Polymorphe Typ-Ausdrücke umgebaute Grammatik; nicht linksrekursiv und optimiert für den Parser AT NOARNX NOAR TCT KLRUND KLECK ::= ::= ::= ::= ::= ::= NOAR { NOARNX | ε } -> AT Var | TCT | KLRUND | KLECK TC NOAR . . . NOAR (AT,. . . ,AT) Mindestens 2-Tupel [AT] Grundlagen der Programmierung 2 (Comp-C) 2011 - 33 - Kombinatorparser Mit Fehlerbehandlung 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 (Comp-C) 2011 - 34 - Kombinatorparser; Beispiele AT NOARNX NOAR TCT KLRUND KLECK ::= ::= ::= ::= ::= ::= NOAR { NOARNX | ε } -> AT Var | TCT | KLRUND | KLECK TC NOAR . . . NOAR (AT,. . . ,AT) Mindestens 2-Tupel [AT] parseKLRUND = (parseSymbol ’(’ <@ id *> (parseINKLRUND <*! ") erwartet") (parseSymbol ’)’)) 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 (Comp-C) 2011 - 35 - Kombinatorparser mit Fehlerbehandlung Programme und Vorführung typeUnifErr combParserWithError parseEquation parseAT . prelex printUnif "(a,a) = (b,[b])“ Grundlagen der Programmierung 2 (Comp-C) 2011 - 36 - Kombinatorparser mit Fehlerbehandlung Programme und Vorführung html-parser.hs main prelex (linPosNumbering "<D> xxx </D>\n<br> text </br>") Grundlagen der Programmierung 2 (Comp-C) 2011 - 37 - Rekursiv-prädiktive Parser Optimierte rekursiv absteigende Parser für eingeschränkte Grammatiken ( LL(1) ). Eigenschaften: • • • Aber: 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 man kann nicht für jede eindeutige kontextfreie Grammatik einen rekursiv-prädiktiven Parser konstruieren. Grundlagen der Programmierung 2 (Comp-C) 2011 - 38 - Rekursiv-prädiktive Parser Zweidimensionale Tabelle: (Lookahead-Symbol, Nichtterminal) ⇒ 7→ Regel oder Fehlereintrag Tabellengesteuerter rekursiv-prädiktiver Parser: Grundlagen der Programmierung 2 (Comp-C) 2011 - 39 - 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! Beispiel: A → bCD | aEF | cG | H H → dabc ... Grundlagen der Programmierung 2 (Comp-C) 2011 - 40 - Rekursiv-prädiktive Parser Sonderfall: Es gibt eine Regel A → wi mit wi →∗ ε: 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 Grundlagen der Programmierung 2 (Comp-C) 2011 - 41 - Rekursiv-prädiktive Parser, ε-Fall Beispiel: S A H ... B C → AB | AC → bCD | aEF | cG | H → ε → dA → eA Im Zustand A und bei Eingabesymbol d: A → H wird ausgewählt. Grundlagen der Programmierung 2 (Comp-C) 2011 - 42 - FIRST- und FOLLOW-Mengen 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. Grundlagen der Programmierung 2 (Comp-C) 2011 - 43 - Beispiel für first Ex Plus PlusRest SigZ B Z ::= ::= ::= ::= ::= ::= Plus SigZ | SigZ Plusrest + SigZ PlusRest | ε B | - B Z | ( Ex ) 0 | ... | 9 Man erhält als first-Mengen: Ex Plus Plus Rest SigZ B Z 0,...,9, (,- 0,...,9, (,- +, ε 0,...,9, (,- 0,...,9, ( 0,...,9 Grundlagen der Programmierung 2 (Comp-C) 2011 - 44 - Beispiel für follow : Ex Plus PlusRest SigZ B Z ::= ::= ::= ::= ::= ::= Plus SigZ | SigZ Plusrest + SigZ PlusRest | ε B | - B Z | ( Ex ) 0 | ... | 9 Man erhält als follow- Mengen: Ex Plus Plus Rest SigZ B Z ) ) ) +,) +,) +,) Grundlagen der Programmierung 2 (Comp-C) 2011 - 45 - Vorgehen des LL(1)-Parsers Bei Symbol a, und aktuellem Nichtterminal A: • • • Ist a ∈ first(wi) für eine Regel A ::= wi, dann nehme diese Regel. 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. Vorteil: genaue und frühe Fehlererkennung Grundlagen der Programmierung 2 (Comp-C) 2011 - 46 - Beispiel: vereinfachte Grammatik für Ausdrücke Expr Rest Term • • ::= ::= ::= Term Rest + Term Rest | − Term Rest | ε 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. Grundlagen der Programmierung 2 (Comp-C) 2011 - 47 - Beispielparser zur Grammatik Parsebaum: Syntaxbaum: PExpLL 1 xx xx x xx x |x LLL LLL LL& PRestNNNN qqq + qq qqq q q q x q 2 − NNN NNN NN& 1 PRestNNNN ppp pp ppp p p p x pp 3 NNN NNN NN& + AA AA AA A 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 (Comp-C) 2011 - 48 - Fehler-Erkennung und Meldung Die Fehlererkennung und -meldung sollte spezifisch sein und möglichst genau die Ursache und Stelle melden. Schlecht: Gut Keine Alternativen mehr gefunden in Zeile... “ ” Fehler in Zeile ... Spalte... Möglicher Grund: ... “ ” Bei deterministischen Parsern (und Kombinatorparser mit Fehlerbehandlung) Der Fehlerort ist klar; die Fehlerursache ist auch meist spezifisch genug Bei Parsern mit Backtracking und ohne Fehlerbehandlung Der richtige Fehlerort ist meist unklar Der Backtracking-Parser kann meist nur melden: keine Alternativen mehr Grundlagen der Programmierung 2 (Comp-C) 2011 - 49 -