Grundlagen der Programmierung 2 (Comp-C) Prof. Dr. Manfred Schmidt-Schauÿ Künstliche Intelligenz und Softwaretechnologie 25. Juni 2008 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) - 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) - 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) - 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) - 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) - 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) - 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) - 7 - Beispiel S A B ::= ::= ::= AB 0 | 1 8 | 9 Frage: Kann 09“ aus dieser Grammatik hergeleitet werden? ” Grundlagen der Programmierung 2 (Comp-C) - 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) - 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) ~~ ~~ ~ ~ ~ ~ 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 Grundlagen der Programmierung 2 (Comp-C) 02 B 0B 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) - 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) - 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) - 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) - 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 (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) - 16 - Eigenschaften eines rekursiv-absteigenden Parsers • • • • • Liefert alle Linksherleitungen für alle Präfixe (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) - 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) - 18 - 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, R)] Präfix der Eingabe 7→ (Rest der Eingabe, Resultat (z.B. Syntaxbaum) ) ..... Liste aller Möglichkeiten Grundlagen der Programmierung 2 (Comp-C) - 19 - 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) - 20 - 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) - 21 - 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) - 22 - 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) - 23 - 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) - 24 - 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) - 25 - 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) - 26 - 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: natural :: Parser Char Int natural = many1 digit <@ foldl f 0 where f a b = a*10 + b Grundlagen der Programmierung 2 (Comp-C) - 27 - 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) - 28 - 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) <*> many (psymb *> parg)) <@ list - 29 - 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) - 30 - 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) - 31 - 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) - 32 - Kombinatorparser; Beispiele AT NOARNX NOAR TCT KLRUND KLECK ::= ::= ::= ::= ::= ::= NOAR { NOARNX | ε } -> AT Var | TCT | KLRUND | KLECK TC NOAR . . . NOAR (AT,. . . ,AT) Mindestens 2-Tupel [AT] pKLRUND = (symbol ’(’ *> (pINKLRUND <*! ") erwartet") (symbol ’)’)) <@ id pINKLRUND = pAT <*> (manyex (((symbol ’,’) *>! "Typ nach , erwartet") pAT)) <@ \(t1,t2) -> if null t2 then t1 else Fn (show ((length t2) +1) ++"-Tup") (t1:t2) Grundlagen der Programmierung 2 (Comp-C) - 33 - Kombinatorparser mit Fehlerbehandlung Programme und Vorführung typeUnifErr combParserWithError Grundlagen der Programmierung 2 (Comp-C) - 34 - Rekursiv-prädiktive Parser 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 der Resteingabe (Lookahead-Symbol) • • Aber: 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) - 35 - Rekursiv-prädiktive Parser Zweidimensionale Tabelle: (Lookahead-Symbol, Nichtterminal) ⇒ 7→ Regel oder Fehlereintrag Tabellengesteuerter rekursiv-prädiktiver Parser: Grundlagen der Programmierung 2 (Comp-C) - 36 - Rekursiv-prädiktive Parser Eindeutigkeitsbedingung: Wenn A → w1 | . . . | wn alle Regeln zu A sind: Falls Parser im Zustand A und a das erste Symbol der Eingabe: Dann darf nur eine Regel A → wi anwendbar sein! Beispiel: A → bCD | aEF | cG | H H → dabc ... Grundlagen der Programmierung 2 (Comp-C) - 37 - 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) - 38 - 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) - 39 - 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) - 40 - 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) - 41 - 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) - 42 - 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) - 43 - 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) - 44 - Beispielparser zur Grammatik E R T ::= ::= ::= T R + T R | - T R | ε 0 | ...| 9 Die Parsefunktionen haben als Wert: (Resteingabe, Pbaum) data Pbaum = Pblatt Int | PExp Pbaum Pbaum | PRest Char Pbaum Pbaum | PLeer deriving (Show, Eq) Grundlagen der Programmierung 2 (Comp-C) - 45 - Beispielparser zur Grammatik (2) parseTerm [] = error "Ziffer erwartet; rest = nil" parseTerm eingabe@(symbol:rest) = if not (symbol ‘elem‘ [’0’..’9’] ) then error ("Ziffer erwartet; rest = " ++ eingabe) else (rest,Pblatt (fst (head (reads [symbol])))) parseRest [] = ([],PLeer) parseRest eingabe@(operator: rest) = if not (operator ‘elem‘ [’+’,’-’] ) then error ("+ oder - erwartet; rest = " ++ eingabe) else let (rest1,resultat1) = parseTerm rest (rest2,resultat2) = parseRest rest1 in (rest2, PRest operator resultat1 resultat2) Grundlagen der Programmierung 2 (Comp-C) - 46 - Beispielparser zur Grammatik (3) parseExp :: [Char] -> (String,Pbaum) parseExp [] = error ("Ziffer erwartet; rest = nil") parseExp (eingabe@(symbol: rest)) = if not (symbol ‘elem‘ [’0’..’9’] ) then error ("Ziffer erwartet; rest = " ++ eingabe) else let (rest1,resultat1) = parseTerm eingabe (rest2,resultat2) = parseRest rest1 in (rest2, PExp resultat1 resultat2) Grundlagen der Programmierung 2 (Comp-C) - 47 - Beispielparser zur Grammatik (4) Testbeispiele: test1::(String,Pbaum) test1 = parseExp "1 + 2 - 3" test2::(String,Pbaum) test2 = parseExp "1+2-3" Main> test2 ("",PExp (Pblatt 1) (PRest ’+’ (Pblatt 2) (PRest ’-’ (Pblatt 3) PLeer))) :: (String,Pbaum) Grundlagen der Programmierung 2 (Comp-C) - 48 - Beispielparser zur Grammatik (3) 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) - 49 - 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 Parser kann meist nur melden: keine Alternativen mehr Grundlagen der Programmierung 2 (Comp-C) - 50 -