Übersetzerbau fldit-www.cs.tu-dortmund.de/∼peter/CbauFolien.pdf Webseite: http://ls1-www.cs.uni-dortmund.de/∼padawitz/ueb.html Peter Padawitz, TU Dortmund 1. Februar 2010 1 ` Inhalt ` Compiler et al. ` Signaturen und Terme ` Algebren ` Initiale Algebren ` Compiler-Korrektheit ` Reguläre Ausdrücke und die Algebra der Sprachen ` Kontextfreie Grammatiken ` Term-, Wort- und Ableitungsalgebra ` Korrektheit monadischer Parser ` Das Gleichungssystem einer CFG ` Coalgebren = Algebren einer Destruktorsignatur ` Erkennende Automaten als Coalgebren ` Parser für reguläre Ausdrücke ` Vom LL-Parser zum LL-Compiler 2 ` LR-Parser und -Compiler ` Haskell: Funktionen ` Haskell: Listen ` Haskell: Datentypen ` Typklassen ` Rund um Expr ` Datentypen mit Attributen ` Ein Haskell-Datentyp für Algebren ` Attributierte Grammatiken ` Haskell: Monaden ` Transitionsmonaden ` Monadische String-Parser ` Monadische Compiler ` Mehrpässige Compiler ` Korrektheit des JavaLight-Compilers* 3 ` Deklarationen und ihre Adressierung* ` Literatur über algebraische Methoden im Compilerbau ` Index (In der Regel sind nur die Seiten angegeben, auf denen die jeweiligen Begriffe oder Abkürzungen eingeführt werden.) Die gesternten Abschnitte werden in dieser LV nur gestreift! Interne Links zu den jeweiligen Definitionen sind an ihrer hellblauen Färbung erkennbar. Namen von Haskell-Modulen wie Java.hs sind mit den jeweiligen Programmdateien verknüpft. 4 ` Compiler et al. Ein Scanner (Programm zur lexikalischen Analyse) fasst Zeichen zu Symbolen zusammen, übersetzt also eine Zeichenfolge in eine Symbolfolge. Bezüglich der Grammatik, die der nachfolgenden Syntaxanalyse zugrundeliegt, heißen die Symbole auch Terminale. Man muss unterscheiden zwischen • Symbolen, die Komponenten von Operatoren sind (if, then, =, etc.), und • Symbolen, die der Scanner nicht nur erkennt, sondern auch einem Basistyp (Int, Float, Bool, Identifier, etc.) zuordnet. In der Grammatik kommen diese Symbole nicht vor, jedoch ihre jeweiligen Typen. Ein Parser (Programm zur Syntaxanalyse) übersetzt eine Symbolfolge in einen Syntaxbaum, der den für ihre Übersetzung in eine Zielsprache notwendigen Teil ihrer Bedeutung (Semantik) wiedergibt. Der Parser scheitert, wenn die Symbolfolge in diesem Sinne bedeutungslos ist. Er orientiert sich dabei an einer (kontextfreien) Grammatik, die korrekte von bedeutungslosen Symbolfolgen trennt und damit die Quellsprache definiert. 5 Ein Compiler (Programm zur semantischen Analyse und Codeerzeugung) übersetzt ein Programm einer Quellsprache in ein semantisch äquivalentes Programm einer Zielsprache. Ein Interpreter (Programm zur Auswertung oder Ausführung von Ausdrücken bzw. Prozeduren) • wertet einen Ausdruck in Abhängigkeit von einer Belegung seiner Variablen aus, • führt eine Prozedur in Abhängigkeit von einer Belegung ihrer Parameter aus. Im Grunde sind auch Scanner und Parser Compiler, denn auch sie tun nichts anderes als Objekte von einem Format in ein anderes zu übersetzen. Und auch ein Interpreter ist ein Compiler: Sein jeweiliges Zielobjekt ist die Funktion, die später auf Eingabedaten angewendet wird. Es liegt deshalb nahe, alle o.g. Programme nach den gleichen Prinzipien zu entwerfen, die bei Compilern im engeren Sinne Anwendung finden. Die Entwicklung dieser Prinzipien und ihrer formalen Grundlagen wurzelt in der mathematischen Logik und dort vor allem in der Theorie formaler Sprachen (siehe GTI) und der universellen Algebra, deren Anwendung im Compilerbau in dieser LV eine zentrale Rolle spielen wird. Die Aufgabe, Übersetzer zu schreiben, hat auch entscheidend die Weiterentwicklung von Programmiersprachen, insbesondere der logischen und funktionalen, vorangetrieben. 6 Insbesondere Konzepte der universellen Algebra und der funktionalen Programmierung im Übersetzerbau erlauben es, bei Entwurf und Implementierung von Scannern, Parsern, Compilern und Interpretern ähnliche Methoden einzusetzen. So ist z.B. ein wie oben definierter Interpreter gleichzeitig ein Compiler, der den Ausdruck oder die Prozedur in eine Funktion übersetzt, die eine Belegung auf einen Wert des Ausdrucks bzw. eine vom Aufruf der Prozedur erzeugte Zustandsfolge abbildet. Der algebraische Ansatz klärt nicht nur die Bezüge zwischen den oben genannten Programmen, sondern auch eine Reihe weiterer in der Compilerbauliteratur meist sehr speziell definierter und daher im Kern vager Begriffe wie attributierter Grammatiken und mehrpässiger Compilation. Mehr Beachtung als die nicht essentielle Trennung zwischen Scanner, Parser, Compiler und Interpreter verdienen die Ansätze, Compiler generisch (neu-informatisch: agil) zu entwerfen, so dass sie mit verschiedenen Quellsprachen und - wichtiger noch - mehreren Zielsprachen instanziiert werden können. Es handelt sich dann im Kern um einen Parser, der keine Symbol-, sondern Zeichenfolgen verarbeitet und ohne den klassischen Umweg über Syntaxbäume gleich die gewünschten Zielobjekte/programme aufbaut. Sein front end kann mit verschiedenen Scannern verknüpft werden, die mehr oder weniger detaillerte Symbol- und Basistypinformation erzeugen, während sein back end mit verschiedenen Interpretationen ein und derselben - üblicherweise als kontextfreie Grammatik eingegebenen - Signatur instanziiert werden kann, die verschiedenen Zielsprachen entsprechen. 7 Die beiden folgenden Graphiken skizzieren die Struktur des algebraischen Ansatzes und seine Auswirkung auf die Generizität der Programme. Auf die Details wird in den darauffolgenden Kapiteln eingegangen. Bool Auswertungsfunktionen Σ-Formeln Verifizierer Parser Algebren Quellsprache L(G) Unparser konkrete Syntax CFG G abstrakte Syntax Signatur Σ(G) Syntaxbäume Termalgebra TΣ(G) Ableitungsbäume Abl(G) Transformer Compiler/ Interpreter Normalformen optimierte Programme Unteralgebra von TΣ(G) Zielsprache Z Von konkreter über abstrakte Syntax zu Algebren 8 Menge_1 Compiler_1 kontextfreie Sprache Parser Termalgebra Compiler_i Menge_i Compiler_n Menge_n Algebra_1 kontextfreie Sprache Parser Termalgebra Compiler Algebra_i Algebra_n Algebra_1 kontextfreie Sprache parsierender Compiler Algebra_i Algebra_n Der Weg zum Compiler als interpretierendem Parser 9 ` Signaturen und Terme Um Modelle schrittweise aufzubauen, wird die Menge der Signaturen induktiv definiert: • (∅, ∅, ∅, ∅) ist eine Signatur, • Σ = (S, F, BΣ, BA) ist eine Signatur, falls BΣ = (BS, BF, BΣ0, BA0) eine Signatur (die Basissignatur von Σ), BA eine BΣ-Algebra (die Basisalgebra von Σ), S eine endliche Menge von Sorten mit BS ⊆ S und F eine endliche Menge typisierter Funktionssymbole f :: w → s ist, wobei s ∈ S, w ∈ S ∗ und BF ⊆ F gilt. Für alle f :: w → s ∈ F ist dom(f ) =def w der Domain und ran(f ) =def s der Range von f . f heißt Konstante, wenn der Domain von f das leere Wort ist. f ∈ F \ BF heißt Konstruktor, wenn der Range von f zu S \ BS gehört. f ∈ F \ BF heißt Destruktor, wenn der Domain von f bis auf genau eine Sorte aus Basisorten besteht. Σ heißt Konstruktorsignatur bzw. Destruktorsignatur, falls alle Funktionssymbole von F \ BF Konstruktoren bzw. Destruktoren sind. 10 Die Basissignatur einer Konstruktor- bzw. Destruktorsignatur ist beliebig, im Rahmen ihres schrittweisen Entwurfs aber oft vom jeweils entgegengesetztem Typ, also eine Destruktor- bzw. Konstruktorsignatur. In der LV Logik für Informatiker wurde nur der einsortige Fall behandelt. Im Rahmen der dort behandelten Signaturen lässt sich Mehrsortigkeit mit Hilfe einstelliger Prädikate simulieren: Mehrere Sorten werden durch mehrere Prädikate ersetzt, die jeweils die Zugehörigkeit zu der durch die Sorte bezeichneten Menge ausdrücken. Sei S eine Menge. Eine S-sortierte Menge ist eine Mengenfamilie A = {As | s ∈ S}. Man sagt “Mengenfamilie” und nicht, was die Schreibweise nahelegt, “Menge von Mengen”, um Antinomien (logische Widersprüche) wie die Menge aller Mengen zu vermeiden. Sortierung ist hier nur im Sinne einer Indizierung der Elemente von A gemeint, ohne dass damit eine bestimmte partielle oder totale Ordnung der Elemente von A. Falls die Mengen As paarweise disjunkt sind, wird A auch als Bezeichnung für die Vereinigung aller As verwendet. Sei A eine S-sortierte Menge und s1, . . . , sn ∈ S. Dann bezeichnet As1...sn das kartesische Produkt As1 × . . . × Asn . 11 Sei X eine S-sortierte Menge logischer Variablen. Die S-sortierte Menge TΣ(X) der Σ-Terme über X ist induktiv definiert: • Für alle s ∈ S und x ∈ Xs ∪ BAs ist x ∈ TΣ(X)s. • Für alle f :: w → s ∈ F und t ∈ TΣ(X)w ist f t ∈ TΣ(X)s. TΣ =def TΣ(∅) ist die S-sortierte Menge der Σ-Grundterme. 12 ` Algebren Sei Σ = (S, F, BΣ, BA) eine Signatur. Eine Σ-Algebra (A, OP ) besteht aus einer S-sortierten Menge A und einer Menge OP von - auch Operationen genannten - Konstanten und Funktionen, den Interpretationen von F , mit folgenden Eigenschaften: • Für alle f :: → s ∈ F gibt es genau einen Wert f A ∈ As. • Für alle w ∈ S + und f :: w → s ∈ F gibt es genau eine Funktion f A : Aw → As ∈ OP . • Das BΣ-Redukt von A (s.u.) stimmt mit BA überein. BΣ wird also in allen Σ-Algebren gleich interpretiert. Sei s ∈ S. Die Menge As heißt Trägermenge von s (carrier set; in der LV Logik für Informatiker Grundmenge genannt). Wie die hier verwendeten Schreibweisen zeigen, wird (A, OP ), falls sich aus dem jeweiligen Kontext keine Konflikte ergeben, oft mit A gleichgesetzt! 13 In der klassischen Theorie formaler Sprachen kommen Algebren im Rahmen von Baumautomaten vor (siehe z.B. die grundlegenden Arbeiten [8, 12]). Neben einer Σ-Algebra A enthält ein Σ-Baumautomat Aut zu jeder Sorte s von Σ eine ausgezeichnete Teilmenge Bs von As, die dann auch als Menge der (s-)Endzustände von Aut bezeichnet wird. Eine Menge T von Σ-Termen der Sorte s heißt Σ-erkennbar (recognizable), wenn evalA(T ) mit der Menge der s-Endzustände eines Σ-Baumautomaten übereinstimmt. Sei Σ0 = (S 0, F 0, BΣ, BA) eine weitere Signatur mit S 0 ⊆ S und F 0 ⊆ F und A eine Σ-Algebra. Das Σ0-Redukt von A, geschrieben: A|Σ0 , ist die Σ0-Algebra, die BΣ genauso interpretiert wie A: • Für alle s ∈ S 0, A|Σ0,s = As. • Für alle f :: w → s ∈ F , f A|Σ0 = f A. Die Trägermengen eines Redukts von A sind also keine echten Teilmengen der entsprechenden Trägermengen von A. 14 Seien A und B S-sortierte Mengen. Eine S-sortierte Funktion h : A → B ist eine Familie von Funktionen: h : A → B = {hs : As → Bs | s ∈ S}. Wieder wird die Sortierung auf Produkte fortgesetzt: Sei w = s1 . . . sn ∈ S +. Dann bezeichnet hw die Funktion von Aw nach Bw , die jedes Tupel (a1, . . . , an) ∈ Aw auf das Tupel (hs1 (a1), . . . , hsn (an)) ∈ Bw abbildet. Sind A und B Σ-Algebren, dann ist h ein Σ-Homomorphismus, falls • h mit den Interpretationen der Funktionssymbole von Σ in A bzw. B verträglich ist, d.h. für alle f :: w → s ∈ F gilt: hs ◦ f A = f B ◦ hw , • für alle s ∈ BS und a ∈ As ist hs(a) = a. Die Übereinstimmung von hs : As → Bs für Basissorten s mit der entsprechenden Identitätsfunktion kann gefordert werden, weil A und B Σ-Algebren sind und daher As = BAs = Bs gilt! A und B Σ-isomorph, falls es einen bijektiven Σ-Homomorphismus von A nach B gibt. 15 ` Initiale Algebren Sei Σ = (S, F, BΣ, BA) eine Konstruktorsignatur mit Basissignatur BΣ. Die S-sortierte Menge TΣ der Σ-Grundterme lässt sich zu einer Σ-Algebra, der Σ-Termalgebra, erweitern: • Für alle f :: w → s ∈ F \ BF und t ∈ TΣ,w , f TΣ (t) =def f t. • TΣ|BΣ =def BA. Ohne die Einschränkung auf Konstruktoren könnte es Σ-Terme einer Basissorte geben, die nicht zu BA gehören, womit A = TΣ die Bedingung A|BΣ = BA an eine Σ-Algebra A verletzen würde. Allerdings ist jede Signatur mit leerer Basissignatur eine Konstruktorsignatur. TΣ ist eine initiale Σ-Algebra, d.h. zu jeder Σ-Algebra A gibt es einen eindeutigen ΣHomomorphismus evalA : TΣ → A. Alle initialen Σ-Algebren sind zueinander Σ-isomorph. Man spricht deshalb von der initialen Algebra. 16 evalA ist induktiv über TΣ definiert und beschreibt die Auswertung von Σ-Termen in A (in der LV Logik für Informatiker wurde [ t|]A für evalA(t) geschrieben): • Für alle s ∈ BS und a ∈ As ist evalsA(a) = a. • Für alle f :: w → s ∈ F und t ∈ TΣ,w ist evalsA(f t) = f A(evalwA(t)). Enthält ein Term t Variablen (aus der Menge X), dann müssen diese vor der Auswertung von t in A mit Elementen von A belegt werden. evalA hat dann den Typ (X → A) → (TΣ(X) → A) und folgende Definition: Sei β : X → A eine S-sortierte Funktion, genannt Variablenbelegung (valuation). • Für alle s ∈ S und x ∈ Xs ist evalsA(β)(x) = β(x). • Für alle s ∈ BS und a ∈ As ist evalsA(β)(a) = a. • Für alle f :: w → s ∈ F und t ∈ TΣ(X)w ist evalsA(β)(f t) = f A(evalwA(β)(t)). 17 Nur im Falle einer Konstruktorsignatur Σ ist TΣ eine Σ-Algebra und daher evalA ein Σ-Homomorphismus. Eine totale Funktion liefert die obige Definition von evalA aber auch, wenn man sie auf Terme anderer Signaturen anwendet. Mit diesen Begriffen können wir nun in einer Algebra rechnen, z.B. Gleichungen lösen: Sei n > 0. Ein Σ-Gleichungssystem ist eine Konjunktion GLS = (t1 = u1 ∧ . . . ∧ tn = un) mit ti, ui ∈ TΣ(X)si für alle 1 ≤ i ≤ n und s1, . . . , sn ∈ S. Da GLS eine Konjunktion ist, nennt man ti = ui, 1 ≤ i ≤ n, einen Faktor von GLS . Sei A eine Algebra. Eine Variablenbelegung β : X → A heißt Lösung von GLS in A, wenn für alle 1 ≤ i ≤ n evalsAi (β)(ti) = evalsAi (β)(ui) gilt. 18 ` Compiler-Korrektheit Mit TΣ als Quellsprache und einer Formulierung der Zielsprache als Σ-Algebra (Ziel) ist ein Compiler nichts anderes als eine Implementierung der Funktion evalZiel , die Syntaxbäume in Ziel auswertet. Ist die Semantik der Quellsprache ebenfalls als Σ-Algebra (Sem) gegeben, dann benötigen wir zum Nachweis der Korrektheit des Compilers ein - unabhängig von Σ definiertes Modell M ach der Zielsprache, meist in Form einer abstrakten Maschine, einen Interpreter execute, der, intuitiv gesprochen, Zielprogramme in M ach ausführt, sowie eine Funktion encode, die Sem auf M ach abbildet und die gewünschte Arbeitsweise des Compilers auf semantischer Ebene reflektiert. Der Compiler ist korrekt, wenn das folgende Funktionsdiagramm kommutiert: TΣ evalSem evalZiel - = execute ? ? Sem Ziel - encode 19 M ach Die Initialität von TΣ erlaubt es uns, den Korrektheitsbeweis auf folgende Schritte zu reduzieren: • M ach wird - wie Sem und Ziel - ebenfalls als Σ-Algebra formuliert. Das ist in der Regel einfach, weil dazu nur die Schemata, nach denen die Definition von Ziel Zielsprachenoperationen zu Σ-Operationen zusammensetzt, mittels execute auf M ach homomorph übertragen werden müssen, d.h. für alle Funktionssymbole f :: w → s muss gelten: executes ◦ f Ziel = f M ach ◦ executew . • encode wird ebenfalls als Σ-Homomorphismus formuliert. Am Ende sind alle vier Abbildungen in obigem Diagramm und damit auch die beiden Kompositionen execute ◦ evalZiel und encode ◦ evalM ach Σ-Homomorphismen. Da TΣ die initiale Σ-Algebra ist, sind beide Kompositionen gleich. Also kommutiert das Diagramm. 20 ` Reguläre Ausdrücke und die Algebra der Sprachen Reguläre Ausdrücke sind Terme der folgenden Konstruktorsignatur: Reg = ( S, F, BΣ, BA ) = ( {reg, word, symbol}, {∅, :: → reg, _ :: symbol → reg, _ | _ :: reg reg → reg, _ · _ :: reg reg → reg, plus, star :: reg → reg, [] :: → word, _ : _ :: symbol word → word}, ({symbol}, ∅, ∅, ∅), ({Z}, ∅) ) (sorts) (regular constants) (regular operators) (word constructors) (base signature) (base algebra) Die Reg-Grundterme der Sorte reg sind offenbar genau die regulären Ausdrücke über Z. Sie bilden daher die Reg-Termalgebra TReg . Was die Klammerung von Teilausdrücken angeht, so hat wie üblich ein Produktoperator wie · eine höhere Priorität als ein Summenoperator wie |. 21 Um nicht bei jeder einzelnen Algebra unnötig viel interpretieren zu müssen, ist das Funktionssymbol star hier als Abkürzung für den Funktionsterm plus(_)| :: reg → reg gemeint. Man nennt solche Funktionsterme auch abgeleitete Funktionssymbole. Sie werden in jeder Σ-Algebra durch die Komposition der Interpretationen ihrer Komponenten interpretiert. Die übliche Interpretation eines regulären Ausdrucks R als Wortmenge L(R) ⊆ Z ∗ erhält man durch Auswertung von R in der Wortmengenalgebra Lang. Die Signatur Reg wird dort folgendermaßen interpretiert: Langreg = ℘(Z ∗) Langword = Z ∗ Langsymbol = Z Sei a ∈ Z und L, L0 ∈ ℘(Z ∗). ∅Lang Lang _Lang (a) L|Lang L0 L ·Lang L0 plusLang (L) []Lang a :Lang w = = = = = = = = ∅ {} {a} L ∪ L0 {vw | v ∈ L, w ∈ L0} {w1 . . . wn | n > 0, ∀ 1 ≤ i ≤ n : wi ∈ L} [] aw 22 Lang (R). Es gilt also L(R) = evalreg Auch Bool ist eine Reg-Algebra: Boolreg = {True, False} Boolword = Z ∗ Boolsymbol = Z Sei a ∈ Z und b, b0 ∈ Bool. ∅Bool Bool _Bool (a) b|Bool b0 b ·Bool b0 plusBool (b) []Bool a :Bool w = = = = = = = = False True False b ∨ b0 b ∧ b0 b [] aw Wie man leicht durch Induktion über den Aufbau von R nachrechnet, beantwortet Bool evalreg (R) die Frage, ob in L(R) enthalten ist oder nicht. 23 ` Kontextfreie Grammatiken ordnen einer Signatur Σ eine konkrete Syntax und damit eine vom Compiler verstehbare Quellsprache zu, so dass er diese in eine - als Σ-Algebra formulierte - Zielsprache übersetzen kann. Eine kontextfreie Grammatik (CFG) G = (N, Z, P, BΣ, BA) besteht aus • einer endlichen Menge N von Nichtterminalen, • einer Menge Z von Terminalen oder Zeichen, • einer endlichen Menge P von Regeln oder Produktionen der Form A → w mit A ∈ N \ BS und w ∈ (N ∪ Z \ BA)∗, • einer Signatur BΣ mit Sortenmenge BS ⊆ N und einer BΣ-Algebra BA, deren Trägermengen zu Z gehören. Die Menge der Zeichen, die zu keiner Trägermenge von BA gehören, wird als endlich vorausgesetzt. Häufig werden n Regeln A → w1, . . . , A → wn mit Hilfe des regulären Summenoperators | zur Regel A → w1| . . . |wn zusammengefasst. Dies ist aber nur als abkürzende Schreibweise zu verstehen und bedeutet nicht etwa die Einführung eines neuen Regeltyps! 24 Meistens genügt es, bei der Definition einer CFG nur deren Regeln und Basissorten anzugeben. Automatisch bilden dann die Symbole, die auf der linken Seite einer Regel vorkommen, die Menge N \ BS. Die Menge der Symbole auf den rechten Seiten, die nicht zu N gehören, bilden – zusammen mit den in P nicht auftretenden Elementen der Basisalgebra (!) – die Menge der Terminale. Die Elemente der Basisalgebra muss erst ein Parser von G kennen, weil sie nur im zu analysierenden Wort vorkommen können. Beispiel FPF (functional programming fragment [5]) pdigit digit digitList const constList fun → → → → → → funList → 1 | 2 | ... | 9 0 | pdigit digit digitList | pdigit digitList | <> | < constList > const, constList | const id | head | tail | num | + | = | ≡ const | fun ◦ fun | [funList] | if fun then fun else fun | αfun | /fun fun, funList | fun 25 Beispiel JavaLight Commands → Command → IntE SumSect P roduct P rodSect F actor BoolE Conjunct Literal → → → → → → → → Command Commands | Command ; | String = IntE; | if BoolE {Commands} | if BoolE {Commands} else {Commands} | while BoolE {Commands} P roduct SumSect + P roduct SumSect | − P roduct SumSect | F actor P rodSect ∗ F actor P rodSect | / F actor P rodSect | Int | String | (IntE) Conjunct || BoolE | Conjunct Literal && Conjunct | Literal Bool | IntE IntE | !Literal | (BoolE) für alle ∈ {<, >, <=, >=, ==, ! =} Die Basissorten sind hier offenbar Int, Bool und String. Die zugehörigen Trägermengen der Basisalgebra BA sind Z, {True, False} bzw. die Menge aller Zeichenfolgen außer den auf der rechten Seite einer Regel auftretenden Terminalen (if, else, while, etc.). Ein Element der von JavaLight erzeugten Sprache: fact = 1; while x > 0 {fact = fact*x; x = x-1;} 26 Abstrakte Syntax einer CFG Sei G = (N, Z, P, BΣ, BA) eine CFG. Die Konstruktorsignatur Σ(G) = (N, F, BΣ, BA) mit F = {fp : A1 . . . An → A | p = (A → w0A1w1 . . . Anwn) ∈ P, } w0, . . . , wn ∈ Z ∗, A1, . . . , An ∈ N } heißt abstrakte Syntax von G. Regeln mit n Nichtterminalen auf der rechten Seite werden also zu n-stelligen Funktionssymbolen, Regeln mit terminaler rechter Seite zu Konstanten. Erlaubt man Mixfixnotation und bezieht man den Typ in die Namen der Funktionssymbole bzw. Konstanten ein, dann ist w0_w1 . . . _wn :: A1 . . . An → A eine alternative - eindeutige - Notation für das Funktionssymbol fA,w0A1w1...Anwn , das der Regel A → w0A1w1 . . . Anwn mit w0, . . . , wn ∈ Z ∗ und A1, . . . , An ∈ N entspricht. Σ(G)-Terme werden auch Syntaxbäume von G genannt. Sie dürfen nicht mit Ableitungsbäumen verwechselt werden: Die Knoten eines Syntaxbaums sind mit Funktionssymbolen markiert, die Knoten eines Ableitungsbaums mit Terminalen oder Nichtterminalen (siehe Term-, Wort- und Ableitungsalgebra). 27 Beispiel SAB Die Grammatik SAB = Regeln p1 p4 p6 (N, Z, P, ∅, ∅) besteht aus N = {S, A, B}, Z = {a, b} und den = S → aB, p2 = S → bA, p3 = S → , = A → aS, p5 = A → bAA, = B → bS, p7 = B → aBB (vgl. GTI-Folie 9/20). Demnach besteht die abstrakte Syntax von SAB aus den Sorten S, A, B und den Funktionssymbolen fp1 :: B → S, fp2 :: A → S, fp3 :: → S, fp4 :: S → A, fp5 :: AA → A, fp6 :: S → B, fp7 :: BB → B Ein Syntaxbaum von SAB: 28 Beispiel Σ(FPF) N = { pdigit, digit, digits, const, consts, fun, funs } F = { 1, 2 . . . , 9 :: → pdigit, 0 :: → digit, pos :: pdigit → digit, [] :: → digits, _:_ :: digit digits → digits, int :: pdigit digits → const, [] :: → const, list :: const consts → const, [_] :: const → consts, _:_ :: const consts → consts, id, head, tail, num, add, eq :: → fun, cfun :: const → fun, comp :: fun fun → fun, prod :: fun funs → fun, [_] :: fun → funs, _:_ :: fun funs → funs, cond :: fun fun fun → fun, map, f old :: fun → fun } 29 Beispiel Σ(JavaLight) N = { Commands, Command, IntE, SumSect, P roduct, P rodSect, F actor, BoolE, Conjunct, Literal } ∪ BS = {Int, Bool, String} F = { seq :: Command Commands → Commands, embedC :: Command → Commands, skip :: → Command, assign :: String IntE → Command, cond :: BoolE Commands Commands → Command, cond1, loop :: BoolE Commands → Command, sum :: P roduct SumSect → IntE, addSect, subSect :: P roduct SumSect → SumSect, emptyS :: → SumSect, prod :: F actor P rodSect → P roduct, mulSect, divSect :: F actor P rodSect → P rodSect, emptyP :: → P rodSect, embedI :: Int → F actor, var :: String → F actor, encloseI :: IntE → F actor, or :: Conjunct BoolE → BoolE, embedA :: Conjunct → BoolE, and :: Literal Conjunct → Conjunct, embedL :: Literal → Conjunct, embedB :: Bool → Literal, rel() :: IntE IntE → Literal für alle ∈ {<, >, <=, >=, ==, ! =}, not :: Literal → Literal, encloseB :: BoolE → Literal } 30 Syntaxbaum des JavaLight-Programms fact = 1; while x > 0 {fact = fact*x; x = x-1;} 31 ` Term-, Wort- und Ableitungsalgebra Sei G = (N, Z, P, BΣ, BA) eine CFG. Da Σ(G) eindeutig aus G konstruiert ist, nennen wir eine Σ(G)-Algebra und einen Σ(G)-Homomorphismus schlicht G-Algebra bzw. GHomomorphismus. Neben der G-Termalgebra bilden auch • die Menge der Z ∗ der terminalen Wörter und • die Menge der Ableitungsbäume von G jeweils eine G-Algebra. Word (G), die Wortalgebra von G Sei A ∈ N . Word (G)A =def BAA falls A ∈ BS Z ∗ sonst Die Trägermengen von Word (G) hängen offenbar nicht von den Regeln von G ab. Diese gehen jedoch in die Interpretation der Funktionssymbole ein: Sei A → w0A1w1 . . . Anwn ∈ P mit w0, . . . , wn ∈ Z ∗ und A1, . . . , An ∈ N . 32 Sei v1, . . . , vn ∈ Z ∗. Word (G) fA,w0A1w1...Anwn : Word (G)A1 × . . . × Word (G)An → Word (G)A (v1, . . . , vn) 7→ w0v1w1 . . . vnwn t ∈ TΣ(G) ist ein Syntaxbaum für w ∈ Z ∗, falls evalWord (G)(t) = w gilt. G ist eindeutig, wenn evalWord (G) injektiv ist. Die Sprache L(G) von G ist das Bild von TΣ(G) unter evalWord (G). L(G) ist selbst eine G-Algebra und zwar die kleinste G-Unteralgebra von Word (G). Für eine beliebige Signatur Σ ist eine Σ-Unteralgebra B einer Σ-Algebra A bzgl. ihrer Trägermengen eine Teilmenge von A mit der Eigenschaft, dass für alle Konstanten c :: s von Σ, cB = cA und für alle Funktionssymbole f :: w → s und a ∈ Bw f B (a) ∈ Bs gilt. Die Bildmenge evalA(TΣ) bildet stets die kleinste Σ-Unteralgebra von A. Für CFGs ohne Basissignatur entspricht diese Definition der Sprache einer CFG der in der Theorie formaler Sprachen verwendeten, die auf der aus GTI bekannten, aus G gebildeten Wortersetzungsrelation →G basiert: Für alle A ∈ N , ∗ L(G)A = {w ∈ Z ∗ | A →G w}. 33 Beispiel SAB Alle drei Trägermengen der Wortalgebra alg = Word (SAB) sind durch {a, b}∗ gegeben. Ihre Operationen sind wie folgt definiert: Sei v, w ∈ {a, b}∗. alg (w) fS,aB alg = fA,aS (w) = aw alg fS,bA (w) alg = fB,bS (w) = bw fSalg = alg fA,bAA (v, w) = bvw alg fB,aBB (v, w) = avw Für alle x ∈ Z und w ∈ Z ∗ bezeichne w#x die Anzahl der Vorkommen von x in w. o 34 ` Korrektheit monadischer Parser Die Wortalgebra erlaubt es uns, den Begriff der Compiler-Korrektheit nicht erst auf der Zwischensprache TΣ(G) der Syntaxbäume, sondern auf der Quellsprache Z ∗, die auch syntaktisch inkorrekte Eingaben umfasst, m.a.W. auf der Wortalgebra aufzusetzen. Wir schalten dazu eine Funktion parseG : Z ∗ → M (TΣ(G)) vor den Compiler evalZiel der Zwischensprache TΣ(G). M gehört zu einer (Plus-)Monade (M, return, bind, mzero, mplus). Hierbei ist M eine Funktion, die, intuitiv gesprochen, jede Menge auf einen bestimmten Typ von Berechnungen ausgewählter Elemente dieser Menge abbildet. Die jeweilige Instanz von M legt fest, wie die Berechnungen ablaufen und mit welchen Ergebnissen sie enden. Typische Instanzen von M werden im Abschnitt Haskell: Monaden behandelt. Demnach beschreibt das Bild M (TΣ(G)) von parseG Berechnungen von Syntaxbäumen. 35 Die anderen Komponenten einer Monade (M, return, bind, mzero, mplus) sind für alle Mengen A und B die Einbettung returnA : A → M (A), die Extension _∗ : (A → M (B)) → (M (A) → M (B)), die parallele Komposition mplusA : M (A) × M (A) → M (A) und die stets scheiternde Berechnung mzero : M (A) derart, dass mplusA assoziativ ist, mzeroA das links- und rechtsneutrale Element bzgl. mplusA ist und die folgenden Gleichungen gelten: Für alle m ∈ M (A), f : A → M (B) und g : B → M (C), return∗A = idM (A), f ∗ ◦ returnA = f, f ∗(mzeroA) = mzeroB , (λx.mzeroB )∗(m) = mzeroB , g∗ ◦ f ∗ = (g ∗ ◦ f )∗. Ausdrücke der Form λx.e heißen λ-Abstraktionen und bezeichnen Funktionen: λx.e bezeichnet die durch die Gleichung f (x) = e definierte Funktion f . Ihre Anwendung auf einen Wert a des Typs der Variablen x entspricht der λ-Applikation (λx.e)(a). Sie ist semantisch äquivalent zum Wert des Ausdrucks e, in dem alle Vorkommen von x durch a ersetzt wurden. In Haskell wird λ als backslash dargestellt (siehe Haskell: Funktionen). x kann auch ein Tupel mehrerer Variablen sein. 36 Die Haskell-Implementierung von Monaden (siehe Haskell: Monaden) verwendet anstelle der Extension die sequentielle Komposition _ >>= _ : (M (A) × (A → M (B)) → M (B). Für alle m ∈ M (A) und f : A → M (B), m >>= f =def f ∗(m). Weitere häufig benutzte abgeleitete Funktionen sind die Wächter guard : (A → Bool) → (M (A) → M (A)). Für alle m ∈ M (A) und f : A → Bool, guardA(f ) =def (λx.if f (x) then return(x) else mzero)∗. 37 Die Funktion parseG kann also unabhängig von der konkreten Instanz von M definiert werden. Sie ist ein korrekter Parser für L(G), wenn die Teildiagramme (1) und (2) des folgenden Diagramms kommutieren: ∗ Z \L(G) λw.mzero - (returnZiel ◦ evalZiel )∗- M (TΣ(G)) M (Ziel) - 6 6 inc (1) rse G (2) returnTΣ(G) pa evalZiel ? Z ∗ eval Word (G) returnZiel (3) TΣ(G) - evalSem (4) Ziel execute ? ? Sem - encode (1) ∀ w ∈ Z ∗ \L(G) : parseG(w) = mzero (2) parseG ◦ evalWord (G) = return (3) ... folgt aus obiger Gleichung f ∗ ◦ returnA = f (4) ... siehe Compiler-Korrektheit 38 M ach Alle Funktionen im Korrektheitsdiagramm sind N -sortiert, d.h. dass u.a. der Parser parseG als auch der Compiler evalZiel aus mehreren Funktionen bestehen – für jedes Nichtterminal eine –, die sich entsprechend den Regeln von G gegenseitig aufrufen. Außerdem lässt das Diagramm vermuten, dass die beiden Schritte: rekursive Konstruktion von Syntaxbäumen durch parseG und deren nachfolgende rekursive Übersetzung/Auswertung durch evalZiel , zu einem zusammengezogen werden können, indem man die Komposition beider Funktionen als eine einzige rekursive Funktion compileZiel : Z ∗ → M (Ziel) G definiert, so dass nicht erst Syntaxbäume, sondern gleich deren Werte in Ziel konstruiert werden. Der Compiler compileZiel G ist korrekt, wenn parseG ein korrekter Parser für L(G) ist (s.o.) und (4) kommutiert. 39 Da das Rekursionsschema der Definition von evalZiel nicht von der konkreten Zielalgebra abhängt, kann compileZiel ähnlich wie evalZiel generisch für alle G-Algebren Ziel G definiert werden. wegen evalTΣ(G) = id - was aus der Initialität von Für Ziel = TΣ(G) stimmt compileZiel G TΣ(G) folgt! - und obiger Gleichung return∗ = id mit parseG überein: T compileGΣ(G) = (return ◦ evalTΣ(G) )∗ ◦ parseG = return∗ ◦ parseG = parseG. Demnach ist parseG genau dann ein korrekter Parser für L(G), wenn compileZiel ein G korrekter Compiler ist. 40 Abl(G), die Ableitungsalgebra von G Sei A ∈ N . ( B ∈ Abl(G)A ⇐⇒def B ist ein Baum mit Knoteneinträgen aus N \ BS ∪ Z und Wurzeleintrag A Sei A → w0A1w1 . . . Anwn ∈ P mit w0, . . . , wn ∈ Z ∗ und A1, . . . , An ∈ N . Für alle 1 ≤ i ≤ n sei Bi ∈ Abl(G)Ai . Abl(G) fA,w0A1w1...Anwn : Abl(G)A1 × . . . × Abl(G)An → Abl(G)A A w0 B1 Bn w1 B1 wn Bn Die Blätter eines Ableitungsbaums tragen Terminalzeichen (oder ), die inneren Knoten Nichtterminale. Es handelt sich dabei jeweils um den Wertebereich der Funktion, die an entsprechender Stelle im Syntaxbaum steht. 41 Ableitungsbaum des JavaLight-Programms fact = 1; while x > 0 {fact = fact*x; x = x-1;} 42 ` Das Gleichungssystem einer CFG Sei M = {R1, . . . , Rn} eine Menge regulärer Ausdrücke. P P Wir schreiben R∈M R für R1| . . . |Rn und ∅ für ∅. Sei G = (N, Z, P, BΣ, BA) eine CFG, BS die Menge der Basissorten (Sorten von BΣ) und {A1, . . . , An} = N \ BS. Das Reg-Gleichungssystem X X GLS (G) = (A1 = w ∧ · · · ∧ An = w) A1 →w∈P An →w∈P heißt Gleichungssystem von G. In GLS (G) werden alle Nichtterminale von G als Variablen aufgefasst! Satz Die Sprache von G ist die kleinste Lösung β : N → Lang von GLS (G) mit β(A) = BAA für alle A ∈ BS. Beweis: Die Existenz der kleinsten Lösung von GLS (G) in Lang folgt aus dem Fixpunktsatz von Knaster und Tarski. Seine einzige Bedingung an das zu lösende Gleichungssystem ist die Monotonie der darin auftretenden Operatoren. Die aber gilt aber für alle Operationen von Reg. Für den Komplementoperator compl :: reg → reg mit complLang (L) =def Z ∗ \ L gilt sie z.B. nicht! 43 Zusätzlich zur Existenz liefert der Fixpunktsatz von Kleene eine iterative Konstruktion der kleinsten Lösung. Allerdings verlangt dieser die - im Vergleich zur Monotonie stärkere Stetigkeit der im jeweiligen Gleichungssystem auftretenden Operationen, die bei GLS (G) aber auch gegeben ist. Beginnend mit dem |N |-Tupel leerer Sprachen vergrößert jeder Iterationsschritt der Lösungskonstruktion die Komponenten eines Sprachtupels (L1, . . . , Ln): Für alle 1 ≤ i ≤ n besteht Li nach dem k-ten Iterationsschritt aus allen Wörtern, deren Urbild unter evalWord (G) ein Syntaxbaum ist, dessen Höhe k nicht übersteigt. Beide Fixpunktsätze werden nicht nur auf GLS(G) angewendet. Sie dienen ganz allgemein der Lösung regulärer Gleichungssysteme, die sich dadurch auszeichnen, dass eine Seite jeder Gleichung eine Variable ist. Jeder knotenmarkierte Graph lässt sich als reguläres Gleichungssystem darstellen. Im Übersetzerbau werden z.B. häufig imperative Zielprogramme in Datenflussgraphen umgesetzt und diese durch Gleichungen repräsentiert. Deren Lösungen in geeigneten Wertebereichen liefern wichtige semantische Informationen über die Programme, die wiederum die Grundlage für Optimierungen bilden (siehe z.B. Kapitel 9 meines Übersetzerbau-Skripts). 44 Nach Definition der Lösung eines Reg-Gleichungssystems ist eine Variablenbelegung β : N → Lang genau dann eine Lösung von GLS (G), wenn für alle Faktoren A = R von GLS (G) β(A) = evalLang (β)(R) gilt. Wir zeigen zunächst, dass β mit ( L(G)A falls A ∈ N \ BS, β(A) = BAA sonst eine Lösung von GLS (G) ist. Sei also A = R ein Faktor von GLS (G). Daraus folgt β(A) = L(G)A = evalWord (G)(TΣ(G),A) = Word (G) {fA,A1...An (v1, . . . , vn) | A → w0A1w1 . . . Anwn ∈ P, vi ∈ L(G)Ai , wj ∈ Z ∗} = {w0v1w1 . . . vnwn | A → w0A1w1 . . . Anwn ∈ P, vi ∈ L(G)Ai , wj ∈ Z ∗} = {w0v1w1 . . . vnwn | A → w0A1w1 . . . Anwn ∈ P, vi ∈ β(Ai), wj ∈ Z ∗}, (1) also auch P S evalLang (β)(R) = evalLang (β)( A→w∈P w) = A→w∈P evalLang (β)(w) = S Lang (β)(A1)w1 . . . evalLang (β)(An)wn = A→w0 A1 w1 ...An wn ∈P, wj ∈Z ∗ w0 eval S A→w0 A1 w1 ...An wn ∈P, wj ∈Z ∗ w0 β(A1 )w1 . . . β(An )wn . (2) Da (1) und (2) dieselben Mengen darstellen, ist β tatsächlich eine Lösung von GLS (G). 45 β ist die kleinste Lösung von GLS (G) in Lang, weil L(G) das Bild von TΣ(G) unter dem GHomomorphismus evalWord (G) ist und damit die kleinste G-Unteralgebra von Word (G). Demnach bleibt zu zeigen, dass sich jede Lösung β : N → Lang von GLS (G) als GUnteralgebra β von Word (G) darstellen lässt. Wir definieren β wie folgt: • Für alle A ∈ N , β A = β(A). • Für jede Regel A → w0A1w1 . . . Anwn ∈ P (wi ∈ Z ∗, Ai ∈ N ) Word (G) β und (v1, . . . , vn) ∈ β(A1) × . . . × β(An), fA,A (v , . . . , v ) = f (v1, . . . , vn). 1 n ...A A,A n 1 1 ...An Wegen Word (G) fA,A1...An (v1, . . . , vn) = w0v1w1 . . . vnwn ∈ w0β(A1)w1 . . . β(An)wn = w0evalLang (β)(A1)w1 . . . evalLang (β)(An)wn = evalLang (β)(w0A1w1 . . . Anwn) U P ⊆ A→w∈P evalLang (β)(w) = evalLang (β)( A→w∈P w) = β(A) o ist β eine G-Unteralgebra von Word (G). 46 Beispiel SAB Für alle w ∈ Z ∗ und x ∈ Z bezeichne w#x die Anzahl der Vorkommen von x in w. Man sieht fast auf Anhieb, dass GLS (SAB) = (S = aB | bA | ∧ A = aS | bAA ∧ B = bS | aBB) von β : N → ℘(Z ∗) mit β(S) = {w ∈ Z ∗ | w#a = w#b}, β(A) = {w ∈ Z ∗ | w#a = w#b + 1}, β(B) = {w ∈ Z ∗ | w#a = w#b − 1} o gelöst wird. 47 Eine CFG G = (N, Z, P, BΣ, BA) heißt bewacht, wenn sich N ∪ Z \ BA so ordnen lässt, dass die Elemente von BS ∪ Z \ BA minimal sind und für alle A → ϕ ∈ P , ϕ = gilt oder ϕ mit einem Zeichen x < A beginnt. Beispiele Die CFG G1 = ({A, B}, {a, b}, {A → BA, A → a, B → b}, ∅, ∅) ist bewacht. GLS (G1) hat genau eine Lösung in ℘({a, b}∗), nämlich die Sprache des regulären Ausdrucks b∗a. Demgegenüber ist die CFG G2, die sich von G1 nur darin unterscheidet, dass die Regel B → b durch B → ersetzt ist, nicht bewacht und jede Funktion β : {A, B} → ℘({a, b}∗) mit a ∈ β(A) und β(B) = {} löst GLS (G2). JavaLight ist bewacht: Wähle < als transitiven Abschluss folgender Relation: {(x, A) | x ∈ BS ∪ Z \ BA, A ∈ N \ BS} ∪ {(F actor, P roduct), (P roduct, IntE), (IntE, Literal), (Literal, Conjunct), (Conjunct, BoolE), (Command, Commands)} Bewachtheit motiviert die Form der Regeln von JavaLight, insbesondere derjenigen für arithmetische und Boolesche Operationen. Die Regeln reflektieren die üblichen Operatorprioritäten sowie die Linksassoziativität arithmetischer und die Rechtassoziativität Boolescher Verknüpfungen. Ist G bewacht, dann ist G nicht linksrekursiv, was garantiert, dass LL-Parser für G terminieren (s.u.). Außerdem impliziert Bewachtheit die eindeutige Lösbarkeit von GLS (G): 48 Fixpunktsatz für CFGs Ist G bewacht, dann hat GLS (G) genau eine Lösung in Lang mit β(A) = BAA für alle A ∈ BS. Beweis: Die Existenz einer Lösung von GLS (G) folgt aus dem Fixpunktsatz von Knaster und Tarski. Wir erweitern Belegungen β : N → ℘(Z ∗) zu Funktionen von (N ∪ Z)∗ nach ℘(Z ∗) durch β(x) = x für alle x ∈ Z und β(x1 . . . xn) = β(x1) · · · · · β(xn) für alle x1, . . . , xn ∈ N ∪ Z. Seien β, γ : N → ℘(Z ∗) zwei Lösungen von GLS (G) in Lang mit β(A) = BAA für alle A ∈ BS. Wir zeigen ∀ w ∈ Z ∗ ∀ A ∈ N \ BS : w ∈ β(A) ⇔ w ∈ γ(A) und damit β = γ durch Induktion über (length(w), A). Sei ∈ β(A). Da G bewacht ist und β GLS (G) löst, enthält P die Regel A → . Da auch γ eine Lösung von GLS (G) ist, folgt ∈ γ(A). Sei x ∈ Z, w ∈ Z ∗ und xw ∈ β(A). Da β GLS (G) löst, gibt es A → ϕ ∈ P mit xw ∈ β(ϕ). Da G bewacht ist, gibt es y ∈ N ∪ Z \ BA und ψ ∈ (N ∪ Z)∗ mit yψ = ϕ und y < A. Daraus folgt xw ∈ β(yψ). 49 Fall 1: y ∈ BS. Dann ist xw ∈ β(yψ) = β(y) · β(ψ) ∈ BAA · β(ψ), also x ∈ BAA und w ∈ β(ψ). Nach Induktionsvoraussetzung folgt w ∈ γ(ψ), also xw ∈ x · γ(ψ) = γ(y) · γ(ψ) = γ(ϕ). Fall 2: y ∈ Z. Dann ist xw ∈ β(yψ) = β(y) · β(ψ) ∈ y · β(ψ), also x = y und w ∈ β(ψ). Nach Induktionsvoraussetzung folgt w ∈ γ(ψ), also xw ∈ x · γ(ψ) = γ(y) · γ(ψ) = γ(ϕ). Fall 3: y ∈ N \ BS. Wegen xw ∈ β(yψ) = β(y) · β(ψ) gibt es w0, w00 ∈ Z ∗ mit w0w00 = w, xw0 ∈ β(y) und w00 ∈ β(ψ). Nach Induktionsvoraussetzung folgt xw0 ∈ γ(y) und w00 ∈ γ(ψ), also xw = xw0w00 ∈ γ(y) · γ(ψ) = γ(ϕ). o 50 Beispiel SAB Da SAB bewacht ist, stimmt die oben angebene Lösung β von GLS (SAB) mit der von SAB erzeugten Sprache überein. Ein zu kurzer Blick auf GLS (SAB) könnte auch die Belegung β mit β(S) = Z ∗ und β(A) = β(B) = Z + als Lösung erscheinen lassen. Das stimmt jedoch nicht. Z.B. gehört {a} zu β(S), aber weder zu evalLang (β)(aB) noch zu evalLang (β)(bA) noch stimmt {a} mit Lang = {} überein, d.h. β erfüllt die erste Gleichung von GLS (SAB) nicht! o Man braucht also - im Gegensatz zu entsprechenden, in GTI vorgeführten Beweisen keine Induktion, weder über Wortlängen noch über Ableitungslängen, um zu zeigen, dass eine bewachte CFG eine bestimmte Sprache erzeugt. Das liegt daran, dass wir den Fixpunktsatz für CFGs verwenden konnten, der selbst durch Induktion über Wortlängen bewiesen wurde. Im nächsten Abschnitt wird Lang coalgebraisch charakterisiert und damit als alternatives Beweisverfahren für Sprachäquivalenzen die Coinduktion zur Verfügung gestellt. Der Hauptzweck der coalgebraischen Charakterisierung besteht allerdings in der daraus herleitbaren Konstruktion von Parsern für reguläre Ausdrücke bzw. bewachte CFGs. 51 ` Coalgebren = Algebren einer Destruktorsignatur Eine Destruktorsignatur Σ = (S, F, BΣ, BA) (siehe Signaturen und Terme) spezifiert eine Klasse von (nicht notwendig endlichen) Automaten: Die Nicht-Basissorten einer Destruktorsignatur nennen wir deshalb Zustandssorten. Entsprechend stehen die Funktionssymbole f :: w → s von F \ BF für Zustandsübergangsfunktionen (falls s ∈ S \ BS) bzw. Ausgabefunktionen (falls s ∈ BS). Folglich eignen sich Destruktorsignaturen auch zur objektorientierten Spezifikation: s ∈ S \ BS bezeichnet eine Klasse, f :: sw → s0 ∈ F mit Basissorte bzw. Zustandssorte s0 ein Attribut bzw. eine Methode der Klasse s. Σ kann auch ein ganzes Klassendiagramm beschreiben - mit Benutzt-Beziehungen, aber ohne Vererbungsrelationen. Unterklassen ließen sich durch Signaturerweiterungen beschreiben. 52 Sei s eine Zustandssorte und x eine feste Variable der Sorte s. Ein Σ-Term [ TΣ,s0 ({x}) t∈ s0 ∈BS heißt s-Beobachter (observer). Sei A eine Σ-Algebra. Belegt man x mit einem Element a ∈ As und wertet t unter dieser Belegung in A aus, dann beschreibt das Ergebnis der Auswertung einen Teil des Verhaltens (behavior) von a. Das gesamte Verhalten von a ist durch das – i.d.R. unendliche – Tupel der Werte aller s-Beobachter gegeben. Die Menge der s-Beobachter bezeichnen wir mit Obss. Obs bildet demnach eine (S \BS)sortierte Menge. Die Typbedingung an die Funktionssymbole einer Destruktorsignatur impliziert, dass jeder s-Beobachter die Form fn(. . . (f1(x, a11, . . . , a1k1 ) . . . ), an1, . . . , ankn ) hat, wobei a11, . . . , a1k1 , . . . , an1, . . . , ankn Elemente von Trägermengen der Basisalgebra sind. Die Menge aller Tupel ausgewerteter Beobachter ist selbst eine Σ-Algebra, die Σ-Verhaltensalgebra genannt und mit BehΣ bezeichnet wird. 53 BehΣ ist eine finale Σ-Algebra, d.h. zu jeder Σ-Algebra A gibt es einen eindeutigen Σ-Homomorphismus unfold A : A → BehΣ. Im Vergleich mit evalA : TΣ → A verläuft unfold A : A → BehΣ in umgekehrter Richtung. Deshalb nennt man Finalität die zur Initialität duale Eigenschaft. Die Bezeichnung Coalgebren für Algebren einer Destruktorsignatur rührt ebenso von dieser Dualität her wie der Name “unfold” für einen Homomorphismus in eine finale Coalgebra: Während die Auswertung eines Terms mit evalA diesen zu einem Wert in A faltet, so entfaltet die Funktion unfold A ein Element von A, indem sie die Ergebnisse der o.g. Beobachterauswertungen auflistet. Auch die unterschiedlichen Aufgaben der Funktionen einer Coalgebra einerseits und einer Algebra einer Konstruktorsignatur andererseits machen beide dual zueienander: letztere benutzt ihre Funktionen zur Synthese, erstere zur Analyse (der Elemente) ihrer Trägermengen. Wie die initialen Algebren einer Konstruktorsignatur, so sind auch die finalen Coalgebren einer Destruktorsignatur zueinander isomorph. Man spricht deshalb von der finalen Coalgebra. 54 Definition von BehΣ • Für alle s ∈ S \ BS, Y BehΣ,s =def BAsort(t), t∈Obss 0 • für alle s ∈ S \ BS, w ∈ BS ∗, f :: sw → s ∈ F , (at(x))t∈Obss ∈ BehΣ,s und (b1, . . . , bn) ∈ BAw , ( af (x,b1,...,bn) falls s ∈ BS, f BehΣ ((at)t∈Obss , b1, . . . , bn) =def (at(f (x,b1,...,bn)))t∈Obss0 sonst. • BehΣ|BΣ =def BA. Sei A eine Σ-Algebra und b : X → A eine Variablenbelegung. Funktionen in ein Produkt wie BehΣ,s definiert man oft durch Angabe der einzelnen Komponenten des Bildtupels: Für alle s ∈ S \ BS, x ∈ Xs und t ∈ Obss, unfold A(b(x))t =def evalA(b)(t). unfold A bildet das Element b(x) ∈ A auf das Tupel seiner ausgewerteten Beobachter t ab. 55 Wir fassen zusammen: • Für jede Konstruktorsignatur Σ ist die Σ-Termalgebra die initiale Σ-Algebra, • Für jede Destruktorsignatur Σ ist die Σ-Verhaltensalgebra die finale Σ-Algebra. Die initiale Algebra einer Destruktorsignatur macht genausowenig Sinn wie die finale Algebra einer Konstruktorsignatur: Erstere hat nur leere Trägermengen, weil es unter Destruktoren keine Konstanten gibt und deshalb die Basis zum Aufbau von Grundtermen fehlt; letztere hat nur einelementige Trägermengen, was mit der Charakterisierung der Gleichheit in finalen Algebren zusammenhängt (s.u.). Obgleich Destruktoren immer durch totale Funktionen interpretiert werden, können auch nichtdeterministische Automaten als Algebren einer Destruktorsignatur dargestellt werden. Man könnte z.B. die Übergangsfunktion mit einer natürlichen Zahl parametrisieren, die angibt, welcher der möglichen Nachfolger ausgewählt werden soll. δ hätte dann einen Typ der Form δ :: state × nat → state. Eleganter lassen sich nichtdeterministische und andere Funktionen mit mehreren “Ausgängen” als Destruktoren einführen, die in eine Summe von Sorten der Signatur abbilden: f :: sw → s1 + · · · + sn. Das Verhaltensmodell einer solchen Destruktorsignatur ist aber um einiges komplizierter als das oben definierte und wird deshalb nicht hier, sondern in meinen LVs über LogischAlgebraischen Systementwurf behandelt. 56 Eine Σ-Algebra A umfasst in der Regel mehrere Automaten, da zunächst kein Startzustand ausgewählt ist. Die Menge der von einem Startzustand st durch wiederholte Anwendung von Destruktoren erreichbaren Zustände von A bildet ebenfalls eine Σ-Algebra, die von st erzeugte Unteralgebra von A genannt und mit hsti bezeichnet wird. Aus der Homomorphie von unfold A folgt die Übereinstimmung des Bildautomaten unfold A(hsti) mit der von unfold A(st) erzeugten Unteralgebra von A. Ist der Automat hsti endlich, dann ist sein Bild unter unfold A natürlich auch endlich, mehr noch: es ist der minimale Automat unter allen Automaten mit Startzustand und gleichem Verhalten, d.h. gleichem unfold A-Bild des jeweiligen Startzustandes. 57 ` Erkennende Automaten als Coalgebren Spracherkennende Automaten (language acceptors) sind Algebren der folgenden Destruktorsignatur: Accept = ( S, F, BΣ, BA ) = ( {state, symbol, Bool}, {δ :: state symbol → state, (sorts) (transition function) accepting :: state → Bool, ({symbol, Bool}, Boolean operators, ∅, ∅), (base signature) ({Z, {True, False}}, standard Boolean arithmetic ) (base algebra) 58 Lang ist Accept-isomorph zur Accept-Verhaltensalgebra BehAccept Wir haben Lang als Algebra der Konstruktorsignatur Reg eingeführt. Jetzt wird wir Lang zur Accept-Algebra erweitert: Langstate = ℘(Z ∗), Langsymbol = Z, LangBool Sei x ∈ Z, w ∈ Z ∗ und L, L0 ∈ ℘(Z ∗). = {True, False}. = {w ∈ Z ∗ | xw ∈ L}, ( True falls ∈ L, accepting Lang (L) = False sonst. Tatsächlich ist Lang isomorph zu BehAccept: Jeder state-Beobachter hat die Form δ Lang (L, x) accepting(δ(. . . (δ(x, a1), . . . ), an), ist also eindeutig durch ein Wort (hier: a1 . . . an) beschrieben. Sein Wert liegt in Bool. Umgekehrt lässt sich jedes Wort w ∈ Z ∗ eindeutig als state-Beobachter repräsentieren. Es gibt also eine Bijektion zwischen Obsstate und Z ∗. Wegen BABool = {True, False} ist daher jedes Element von BehAccept,state die charakteristische Funktion einer Wortmenge, m.a.W.: BehAccept,state ∼ (2) = ℘(Z ∗) = Langstate. 59 Die Accept-Homomorphie dieser Bijektion ergibt sich ebenfalls aus der zwischen Obsstate und Z ∗. Lang ist also die finale Accept-Algebra, so dass sich die obigen Aussagen über die Verhaltensalgebra einer beliebigen Destruktorsignatur vermöge der Bijektion (2) auf Lang übertragen lassen: Sei A ein erkennender Automat, st ∈ Astate und L ⊆ Z ∗. • unfold A(st) ist die von A erkannte Sprache, wenn A im Zustand st losläuft. • unfold A(hsti) ist der minimale Automat, der diese Sprache erkennt. • Die Unteralgebra hLi von Lang ist der minimale Automat, der L erkennt. Charakterisierung der Zustände von hLi Regulär ist eine Sprache L laut GTI genau dann, wenn sie von einem endlichen Automaten erkannt wird, was nach dem oben Gesagten genau dann gilt, wenn hLi endlich ist. Daraus folgt der in GTI behandelte Satz von Myhill und Nerode: L ist genau dann regulär, wenn ∼L⊆ Z ∗ × Z ∗ endlich viele Äquivalenzklassen hat, wobei ∼L folgendermaßen definiert ist: v ∼L w ⇐⇒def ∀ u ∈ Z ∗ : vu ∈ L ⇔ wu ∈ L. Wie man leicht nachrechnet, sind die Äquivalenzklassen von ∼L nämlich gerade die Zustände von hLi. 60 Zum Rechnen und Beweisen in Coalgebren Das Rechnen in einer Algebra wird wesentlich vom dort verwendeten Gleichheitsbegriff bestimmt. In einer initialen bedeutet Gleichheit Termidentität, in einer finalen bedeutet sie Verhaltensgleichheit. Auch hier gibt es wieder eine Dualität: Termidentität ist die kleinste, Verhaltensgleichheit die größte Σ-Kongruenz (binäre Relation, die mit den Funktionen von Σ verträglich ist. Im Verhaltensfall werden Kongruenzen auch Bisimulationen genannt. Auf den Trägermengen der Basissorten ist Gleichheit natürlich immer Identität. Die beiden Charakterisierungen der Gleichheit werden verwendet, um die Gültigkeit von Gleichungen in einer initialen bzw. finalen Algebra zu zeigen. Die entsprechenden Beweisregeln heißen Induktion bzw. Coinduktion. Die Charakterisierung der Gleichheit in der finalen Σ-Algebra A als größte Σ-Kongruenz begründet auch die o.g. Behauptung, dass A nur einelementige Trägermengen hat, falls Σ eine Konstruktorsignatur ist: Nach Definition ist ∼ ⊆ A2 eine Σ-Kongruenz, wenn für alle f :: w → s ∈ Σ und a, b ∈ Aw a ∼ b ⇒ f A(a) ∼ f A(b) (3) gilt. Ist s keine Basissorte, dann gilt (3) für die Allrelation ∼s= {(a, b) | a, b ∈ As}. Da die Gleichheit in A mit der größten Σ-Kongruenz auf A übereinstimmt und es keine größere Σ-Kongruenz als die Allrelation geben kann, sind alle Elemente von As gleich. 61 Beispiel Da δ und accepting die Destruktoren von Accept sind, ist ∼ ⊆ ℘(Z ∗)2 genau dann eine Accept-Kongruenz auf einer Accept-Algebra, wenn diese die folgende Implikation erfüllt: ∀ x, y, z : (x ∼ y =⇒ δ(x, z) ∼ δ(y, z) ∧ accepting(x) = accepting(y)) (4) Da Lang die finale Accept-Algebra ist, sind zwei Sprachen L und L0 genau dann gleich, wenn sie in der größten Accept-Kongruenz liegen. Da die Vereinigung von Kongruenzen selbst eine solche ist (Beweis!), ist sie die größte und wir folgern, dass L und L0 genau dann übereinstimmen, wenn (L, L0) zu irgendeiner Accept-Kongruenz auf ℘(Z ∗) gehört. Diese zu finden ist das Ziel eines Beweises durch Coinduktion. Man definiert ∼ zunächst als die Menge genau der Paare von Sprachen, deren Gleichheit gezeigt werden soll. Ist ∼ keine Kongruenz, dann werden Sprachpaare hinzugefügt, die sich im Laufe des Beweises ergeben, bis eine Kongruenzrelation gefunden ist. Die Termination des Verfahrens ist nicht gesichert. Die Vergrößerung von ∼ entspricht der Verallgemeinerung einer Behauptung, die durch Induktion bewiesen werden soll. Auch dort kann nicht garantiert werden, dass jemals eine gültige Behauptung erreicht wird. o 62 Beispiel Seien x, y, z Variablen. Durch Coinduktion wollen wir zeigen, dass die Interpretation von | in Lang kommutativ ist, m.a.W., dass die Gleichung x|y = y|x in Lang gilt. Benutzt werden sollen hierzu nur die beiden in Lang gültigen Gleichungen: (eq1) δ(x|y, z) = δ(x, z)|δ(y, z), (eq2) accepting(x|y) = accepting(x) ∨ accepting(y), nicht jedoch die Definition von |Lang als Mengenvereinigung! Wir setzen ∼= {(L|L0, L0|L) | L, L0 ∈ ℘(Z ∗)} und zeigen, dass ∼ eine Accept-Kongruenz ist (siehe (4)). Sei L1, L2 ∈ ℘(Z ∗). L1 ∼ L2 ⇐⇒ ∃ L, L0 : L1 = L|L0 ∧ L2 = L0|L eq1 eq1 =⇒ ∃ L, L0 ∀ a ∈ Z : δ(L1, a) = δ(L, a)|δ(L0, a) ∼ δ(L0, a)|δ(L, a) = δ(L2, a) ∧ eq2 accepting(L1) = accepting(L) ∧ accepting(L0) eq2 = accepting(L0) ∧ accepting(L) = accepting(L2) ⇐⇒ ∀ a ∈ Z : δ(L1, a) ∼ δ(L2, a) ∧ accepting(L1) = accepting(L2). ∼ ist also in der größten Accept-Kongruenz enthalten, die – wie oben ausgeführt – mit der Gleichheit in der finalen Accept-Algebra Lang übereinstimmt, d.h. für alle L, L0 ∈ ℘(Z ∗) gilt tatsächlich L|L0 = L0|L. o 63 ` Parser für reguläre Ausdrücke Bool T accepting Reg Automat A Zustandsmenge S accepting Lang TReg (3) (reguläre Ausdrücke) unfold inclusion A eval Lang ∪w∈Z* δ*A(st,w) evalLang δLang <st> (2) TReg (4) (Sprachen) erreichbarer Teilautomat <st> Zustandsmenge δTReg T unfold Reg (1) unfold Lang Lang inclusion nf evalΣLang minimization 64 TParse/~ParseE nat TParse Automat <L(<st>)> ≅ Blaue Pfeile bezeichnen Reg-Homomorphismen. Rote Pfeile bezeichnen Accept-Homomorphismen. Schwarze Pfeile bezeichnen Funktionen der Accept-Algebren TReg bzw. Lang. Wir fassen zusammen: • TReg ist die initiale Reg-Algebra, • Lang ist eine Reg-Algebra und gleichzeitig die finale Accept-Algebra. Die Finalität von Lang impliziert die Kommutativität der Diagramme (1) und (2). Im Folgenden wollen wir zeigen, dass • TReg eine Accept-Algebra ist und • evalLang Accept-homomorph ist (Kommutativität der Diagramme (3) und (4)). Wegen der Eindeutigkeit eines Accept-Homomorphismus von TReg in die finale AcceptAlgebra Lang folgt daraus die Gleichheit von evalLang und unfold TReg . Im Abschnitt Reguläre Ausdrücke... haben wir Bool als Reg-Algebra definiert und darauf hingewiesen, Bool Lang dass evalreg (R) genau dann True ist, wenn zu L(R) = evalreg (R) gehört, was wiederum genau dann Lang Lang Bool gilt, wenn accepting (evalreg (R)) = True ist. Aus (3) folgt daher die Übereinstimmung von evalreg mit accepting TReg . Umgekehrt lässt sich jede auf einer Termalgebra (induktiv) definierte Funktion (wie z.B. accepting TReg ) als Auswertungsfunktion in eine passende Algebra darstellen! 65 Die Signatur Parse Wir führen eine dritte Signatur ein und verwenden etwas Termersetzungstheorie (siehe z.B. [?, ?, P] Kapitel 5), um die Accept-Homomorphie von evalLang zu zeigen. Da Termersetzung - neben dem λ-Kalkül - die Grundlage funktionaler, gleichungsbasierter Programme bildet, ist es nützlich, auch in diese Theorie ein wenig hineinzuschnuppern. Parse besteht aus den Sorten und Konstruktoren von Reg, den Sorten und Destruktoren von Accept (wobei state mit reg gleichgesetzt wird) sowie den Funktionssymbolen bzw. Konstanten parse :: state word → Bool δ ∗ :: state word → state a :: → symbol für alle a ∈ Z True, False :: → Bool = :: symbol symbol → Bool ∨, ∧ :: Bool Bool → Bool if _then_else_ :: Bool Bool Bool → Bool Die Basissignatur von Parse ist leer. Damit ist Parse eine Konstruktorsignatur und die Termalgebra TParse die initiale Σ-Algebra. 66 Die Menge ParseE folgender Gleichungen zwischen Parse-Termen definiert einen Parser für reguläre Ausdrücke: Seien R, R0, w, x, y, z Variablen. parse(R, w) δ ∗(R, x : w) δ ∗(R, []) δ(∅, x) δ(, x) δ(x, y) δ(R|R0, x) δ(R · R0, x) δ(plus(R), x) accepting(∅) accepting() accepting(a) accepting(R|R0) accepting(R · R0) accepting(plus(R)) = = = = = = = = = = = = = = = accepting(δ ∗(R, w)) δ ∗(δ(R, x), w) R ∅ ∅ if x = y then else ∅ δ(R, x)|δ(R0, x) δ(R, x) · R0|(if accepting(R) then δ(R0, x) else ∅) δ(R, x)|δ(R, x) · plus(R) False True False für alle a ∈ Z accepting(R) ∨ accepting(R0) accepting(R) ∧ accepting(R0) accepting(R) 67 a=a a=b x ∨ True x ∨ False x ∧ True x ∧ False if True then y else z if False then y else z = = = = = = = = True für alle a ∈ Z False für alle a, b ∈ Z mit a 6= b True x x False y z Die kleinste Kongruenzrelation auf TParse , die alle Grundterminstanzen von ParseE enthält, heißt ParseE-Äquivalenz und wird mit ∼ParseE bezeichnet. Der daraus gebildete Quotient TParse /∼ParseE ist initial in der Klasse aller Parse-Algebren A, die ParseE erfüllen, m.a.W. Existenz und Eindeutigkeit eines Parse-Homomorphismus von TParse /∼ParseE nach A können nur garantiert werden, wenn A ParseE erfüllt. 68 Hier kommt Termersetzung ins Spiel: Durch wiederholte Links-Rechts-Anwendung von Gleichungen aus ParseE läßt sich jeder Parse-Grundterm in genau einen Reg-Term transformieren. Zum Beweis von Existenz und Eindeutigkeit solcher Normalformen stellt die Termersetzungstheorie sog. Konfluenzkriterien zur Verfügung. Für ParseE gelten die folgenden drei: • Jede Folge von Links-Rechts-Anwendungen von Gleichungen aus ParseE auf einen Parse-Grundterm terminiert, d.h. irgendwann sind keine Gleichungen mehr anwendbar. • Die linken Seiten der Gleichungen von ParseE sind paarweise überlappungsfrei. • Auf einen Parse-Grundterm sind genau dann keine Gleichungen anwendbar, wenn t aus Reg-Symbolen und Bool- oder symbol-Konstanten besteht. Die letzten beiden Bedingungen lassen sich allein durch Ansehen von ParseE überprüfen. Für die erste gibt es ein in Beweisen der Programmtermination oft verwendetes Kriterium, das in unserem Fall folgendermaßen lautet: Die Argumente von nicht zu Reg gehörigen Funktionen müssen auf der linken Seite jeder Gleichung von ParseE größer sein als auf der rechten. Wozu eindeutige Normalformen? Nun, wenn es sie gibt, dann bilden sie eindeutige Repräsentanten der Äquivalenzklassen von ∼ParseE und damit eine zu TParse /∼ParseE isomorphe Parse-Algebra, die Normalformalgebra NF ParseE . 69 Der Parser parseTReg für reguläre Ausdrücke ist korrekt Da das Reg-Redukt von NF Parse,ParseE mit TReg übereinstimmt, können auch die Funktionssymbole von Parse \ Reg in TReg interpretiert werden und zwar so, dass ParseE in TReg gilt. Damit ist TReg eine Parse-Algebra, also insbesondere eine Accept-Algebra. Bleibt zu zeigen, dass evalLang : TReg → Lang Accept-homomorph ist. Die Gleichungen für parse und δ ∗ und lassen sich direkt in eine Interpretation dieser Symbole in Lang überführen, die ParseE erfüllt. Damit ist die Funktion Lang evalParse : TParse → Lang, die jeden Parse-Grundterm in Lang auswertet, Parse-homomorph. Die surjektiven Funktionen nat : TParse → TParse /∼ParseE und nf : TParse → TReg , die jeden Parse-Grundterm auf seine ParseE-Äquivalenzklasse bzw. seine eindeutige Normalform abbilden, sind ebenfalls Parse-homomorph. 70 Lang entDa Lang ParseE erfüllt, ist die ParseE-Äquivalenzrelation im Kern von evalParse 0 halten, d.h. für alle t, t ∈ TΣ gilt: t ∼ParseE t0 =⇒ Lang Lang 0 evalParse (t) = evalParse (t ). Außerdem ist jeder Parse-Grundterm zu seiner Normalform ParseE-äquivalent. Also gilt Lang Lang evalParse (t) = evalParse (nf (t)) = evalLang (nf (t)), Lang kurz: evalLang ◦ nf = evalParse . Lang Die Anwendung des folgenden Lemmas auf f = evalParse , g = evalLang und h = nf liefert schließlich die Parse-Homomorphie und damit auch die Accept-Homomorphie von evalLang . Lemma Für alle Funktionen f, g, h gilt: f = g ◦ h ∧ f, h homomorph ∧ h surjektiv 71 =⇒ g homomorph. Damit können wir die Korrektheit von ParseE zeigen: Sei R ∈ TReg und w = a1 . . . an ∈ Z ∗. parseTReg (R, w) = True ⇐⇒ w ∈ evalLang (R) = L(R). Beweis: Sei T = TReg . unfold TBool =id True = parse (R, w) = unfold T (parseT (R, w)) = unfold T (accepting T (δ ∗T (R, w))) = unfold T (accepting T (δ T (. . . (δ T (R, a1), . . . ), an)) T unfold T Accept−hom. = accepting Lang (δ Lang (. . . (δ Lang (unfold T (R), a1), . . . ), an)) = accepting Lang (δ ∗Lang (unfold T (R), w)) = parseLang (unfold T (R), w) unfold T =evalLang = parseLang (evalLang (R), w) ⇔ w ∈ evalLang (R). o Wie wir später sehen werden, führt ParseE direkt zu einer Haskell-Implementierung von parseTReg . 72 Jeder Leseschritt des Parsers führt eine Transition des Automaten TReg aus. Diese ist jedoch nicht besonders effizient, da sie ihren Folgezustand nicht wie üblich in einer Tabelle abliest, sondern aus dessen Vorgänger konstruiert. Deshalb werden endliche Automaten bevorzugt, deren Zustände einfache Namen haben, z.B. natürliche Zahlen, so dass die zugehörige Transitionsfunktion als Tabelle dargestellt und die Transitionen als Tabellenzugriffe implementiert werden können. Um zu einem solchen, L(R) erkennenden Automaten zu gelangen, konstruiert man zunächst durch Auswertung von R in der Algebra Aut einen nichtdeterministischen Automaten, macht ihn deterministisch (z.B. mit der Haskell-Funktion makeDet; s.u.) und minimiert die determininistische Version. Letzteres geht am schnellsten mit dem in Abschnitt 2.3 meines Übersetzerbau-Skripts beschriebenen Paull-Unger-Verfahren. 73 Das Prinzip, komplexe Daten durch Tabellenindizes zu ersetzen und eine ursprünglich rekursiv definierte Funktion als Tabelle zu implementieren, nennt man dynamische Programmierung. Es reduziert die Komplexität einer rekursiven Funktion oft erheblich und zeichnet Automatenmodelle vor anderen aus. Auch wenn es am Ende nur auf endliche Datenmengen anwendbar ist, kann am Anfang der Entwicklung auch eine unendliche Menge stehen wie hier die Menge der regulären Ausdrücke. Ob der Übergang von einer unendlichen zu einer endlichen Menge möglich ist, hängt von der jeweils zu realisierenden Funktion ab. Hat ihr Kern, d.i. die Menge aller Argumentpaare mit gleichem Wert, nur endlich viele Äquivalenzklassen, dann lässt sie sich als Tabelle implementieren. So ergeben sich z.B. auch die Tabellen, die von LR-Parsern benutzt werden (s.u.). 74 In der Algebra spricht man von Quotientenbildung, wenn (viele) Daten durch (wenige) Äquivalenzklassen ersetzt und Funktionen dementsprechend umformuliert werden. In der Systemmodellierung ist Quotientenbildung das Mittel zur Abstraktion. In diesem Sinne ist z.B. die kleinste Unteralgebra einer Σ-Algebra A eine Abstraktion von TΣ und evalA : TΣ → A die Funktion, aus deren Kern der Quotient gebildet wird. Man nennt den Quotienten auch die Faktorisierung von TΣ nach dem Kern von evalA. Für das rechnergestützte Rechnen mit Abstraktionen ist entscheidend, ob und wie die zugrundeliegende Äquivalenzrelation axiomatisiert wird, z.B. wie oben durch die Gleichungen von ParseE. Der zur Abstraktion duale Entwicklungsschritt ist die Restriktion: Eine Datenmenge A wird zunächst eingeschränkt auf eine Teilmenge B von A und dann abgeschlossen unter Funktionsanwendungen, d.h. erweitert um alle Elemente von A, die durch endliche Folgen von Konstruktor- bzw. Destruktoranwendungen auf Elemente von B aus diesen entstehen. Restriktion heißt Bildung einer Unteralgebra wie z.B. der oben behandelten Extraktion der von einem Startzustand erreichbaren Elemente einer Coalgebra. 75 Aus Sicht der Modellierung mittels Quotienten von Termalgebren stellt sich die Frage, ob sich ParseE so zu einer Gleichungsmenge ParseE 0 erweitern lässt, dass TParse /∼ParseE 0 zu der Unteralgebra von Lang, die aus allen regulären Sprachen über Z besteht, isomorph ist. Dann wäre evalLang die natürliche Abbildung, die Parse-Terme auf ihre EÄquivalenzklassen abbildet. Um aus diesem Modell einen Parser (für reguläre Ausdrücke) abzuleiten, bräuchten wir allerdings eine zu TParse / ∼ParseE 0 isomorphe Normalformalgebra. Sie müsste für jede reguläre Sprache L genau einen regulären Ausdruck R mit L(R) = L enthalten. Der entsprechende Parser müsste einen gegebenen Ausdruck zunächst in seine Normalform überführen und würde dann wie der obige weiterarbeiten, d.h. die Gleichungen von ParseE anwenden. Die coalgebraische Charakterisierung von Lang wäre damit vollständig umgangen. Leider gibt es aber nicht für jede reguläre Sprache eine eindeutige Normalform in TReg . Selbst wenn es sie gäbe, wäre der Nachweis der Eindeutigkeit weitaus komplizierter als der analoge, oben skizzierte, dass ∼ParseE -Äquivalenzenklassen eindeutige Repräsentanten in TReg haben. Einige Gleichungen von ParseE 0 müssten nämlich in Lang gültige Eigenschaften regulärer Operatoren ausdrücken wie z.B. die - oben durch Coinduktion gezeigte - Kommutativität von |. Solche Gleichungen erfüllen aber i.d.R. nicht die drei o.g. Konfluenzkriterien. 76 ` Vom LL-Parser zum LL-Compiler parseTReg lässt sich auch für die Syntaxanalyse bewachter CFGs oder ECFGs (s.u.) verwenden. Sei G = (N, Z, P, BΣ, BA) eine bewachte CFG. Man braucht nur • die Nichtterminale von G als Konstanten der Sorte reg zu den Signaturen Reg und Σ sowie • eine Destruktorversion des Gleichungssystems von G zu ParseE hinzufügen. Reg(N ) und Parse(N ) bezeichnen die um N erweiterten Signaturen Reg bzw. Parse. Um die Korrektheit von parseTReg(N ) ähnlich der von parseTReg begründen zu können, ist es nicht ratsam, GLS (G) in unveränderter Form zu ParseE hinzuzunehmen. Die obige Argumentation basiert nämlich u.a. auf der Endlichkeit aller Grundtermreduktionen mit Hilfe von ParseE. Da G bewacht ist, kann aber GLS (G) so in Gleichungen für δ und accepting überführt werden, dass die o.g. Kriterien für Existenz und Eindeutigkeit von Normalformen und die Gültigkeit in Lang erhalten bleiben. Zusammen mit ParseE bilden sie das Gleichungssystem ParseE(G). Sie lauten wie folgt: 77 Sei x eine Variable. δ(A, a) = δ(A, a) = ∅ δ(A, x) = δ(R, x) accepting(A) = accepting(R) für alle A ∈ BS und a ∈ BAA “a ∈ BAA” zu prüfen ist Aufgabe des Scanners. für alle A ∈ BS und a ∈ Z \ BAA für alle A = R ∈ GLS (G) Die Bewachtheit von G garantiert die Termination (der Interpretationen) von δ und accepting (in TReg(N )): Zwar vergrößert die Anwendung einer der letzten beiden Gleichungen den regulären Ausdruck, auf den δ rekursiv angewendet wird. Die im Folgenden anwendbaren Gleichungen von ParseE reduzieren aber die Argumente von δ bzw. accepting auf , ∅ oder solche der Form xR mit x < A. 78 Beispiel SAB Aus GLS (SAB) = (S = aB | bA | ∧ A = aS | bAA ∧ B = bS | aBB) ergibt sich die Gleichungsmenge ParseE(SAB): {δ(S, x) = δ(aB|bA|, x) | x ∈ {a, b}} ∪ {accepting(S) = accepting(aB|bA|)} ∪ {δ(A, x) = δ(aS|bAA, x) | x ∈ {a, b}} ∪ {accepting(A) = accepting(aS|bAA)} ∪ {δ(B, x) = δ(bS|aBB, x) | x ∈ {a, b}} ∪ {accepting(B) = accepting(bS|aBB)} o Wie wir später sehen werden, kann bei der Haskell-Implementierung von ParseE(G) auf die Umformung von GLS (G) in Gleichungen für δ verzichtet werden, da die lazyevaluation-Strategie, nach der Haskell Terme auswertet, sicherstellt, dass auch die Gleichungen von GLS (G) nicht unendlich oft angewendet werden. 79 Man sieht sofort, dass die Konstruktion von ParseE(G) nicht funktioniert, wenn G unbewacht ist. Eine besonders unangenehme mögliche Konsequenz von Unbewachtheit ist + die Linksrekursion, d.h. die Existenz von Ableitungen der Form A −→G Av. Sie kann dazu führen, dass die Auswertung eines Terms parse(A, w) nicht terminiert, weil dieser Term in einem Zwischenergebnis seiner eigenen Auswertung vorkommt. Zum Glück kann jede linksrekursive Grammatik G durch Einführung neuer Nichtterminale folgendermaßen in eine nicht-linksrekursive transformiert werden: Ordne N total. Eliminiere alle Regeln der Form A → A. • Ersetze jede Regel A → Bα mit A > B, für die eine Regel B → β existiert, durch die Regel A → βα. Wiederhole diesen Schritt, solange seine Bedingung erfüllt ist. + Am Ende gibt es für alle A ∈ N mit Ableitungen der Form A −→G Av eine Regel der Form A → Aα. • Ersetze jedes Regelpaar (A → Aα, A → β), bei dem A kein Präfix von β ist, durch die drei Regeln A → βA0, A0 → αA0, A0 → , wobei A0 ein neues Nichtterminal ist. Ähnlich lassen sich auch andere Verletzungen der Bewachtheit eliminieren. Man braucht dabei nicht bis zur Greibach-Normalform gehen, die nur ein einziges Terminal in jedem Summanden der rechten Seite einer Regel zulässt. Bewachtheit ist eine schwächere Forderung: Jeder Summand muss lediglich mit einem Terminal beginnen oder mit übereinstimmen. 80 Der Parser parseTReg(N ) für bewachte CFGs korrekt Sei β : N → ℘(Z ∗) die kleinste Lösung von GLS (G) in Lang mit β(A) = BAA für alle A ∈ BS. Durch ALang =def β(A) für alle A ∈ N wird Lang zu einer Reg(N )Algebra, die die Gleichungen von GLS (G), also auch ParseE(G) erfüllt. Die Gültigkeit von ParseE in Lang wurde im vorigen Abschnitt gezeigt. Für die restlichen Gleichungen von ParseE(G) ist die Gültigkeit schnell gezeigt: Sei A ∈ BS. Dann gilt für alle a ∈ Z, evalLang (δ(A, a)) = δ Lang (evalLang (A), a) =δ Lang (β(A), a) = = evalLang () falls a ∈ BAA, Lang ∗ δ (BAA, a) = {w ∈ Z | aw ∈ BAA} = ∅ = evalLang (∅) sonst. Sei A = R ∈ GLS (G). Dann gilt für alle a ∈ Z, evalLang (δ(A, a)) = δ Lang (evalLang (A), a) = δ Lang (ALang , a) = δ Lang (β(A), a) = δ Lang (evalLang (β)(R), a) = evalLang (β)(δ(R, a)) und evalLang (accepting(A)) = accepting Lang (evalLang (A)) = accepting Lang (ALang ) = accepting Lang (β(A)) = accepting Lang (evalLang (β)(R)) = evalLang (β)(accepting(R)). 81 Die Erweiterung von Reg, Parse und ParseE zu Reg(N ), Parse(N ) bzw. ParseE(G) erhält also alle Eigenschaften von Lang, die im vorigen Abschnitt zur Accept-Homomorphie von evalLang und damit zur Korrektheit des Parsers parseTReg für reguläre Ausdrücke geführt haben. Also ist auch parseTReg(N ) korrekt: Sei R ∈ TReg(N ) und w ∈ Z ∗. parseTReg(N ) (R, w) = True ⇐⇒ w ∈ evalLang (R) = L(R). Daraus folgt nämlich insbesondere für alle A ∈ N : parseTReg(N ) (A, w) = True w ∈ evalLang (A) = β(A) = L(G)A. ⇐⇒ parseTReg(N ) ist ein LL-Parser. LL-Parser lesen ein Wort w von links nach rechts und versuchen dabei (implizit) eine Linksableitung von w zu konstruieren. Da dies dem schrittweisen, mit der Wurzel (dem “Startsymbol”) beginnenden Aufbau eines Ableitungsbaums entspricht, werden LL-Parser auch top-down-Parser genannt. 82 Erweiterte kontextfreie Grammatiken Der Fixpunktsatz für CFGs bleibt gültig, wenn man ihn mit einer entsprechend angepassten Bewachtheitsbedingung auf erweiterte kontextfreie Grammatiken (ECFGs) verallgemeinert, deren rechte Regelseiten beliebige reguläre Ausdrücke sein dürfen. ECFGs werden für Dokumenttypdefinitionen (DTDs) verwendet und kommen dort u.a. als Baumgrammatiken vor, deren rechten Regelseiten Terme sind. ECFGs mögen sich zur reinen Syntaxdefinition von Dokumenttypen eignen und damit auch für Parser, die auf die Erkennung syntaktischer Korrektheit beschränkt sind. Als Grundlage eines echten Übersetzers, der seine Eingaben nicht nur prüft, sondern im Erfolgsfall in eine mehr oder weniger komplexe Datenstruktur transformiert, sind sie jedoch wenig hilfreich, weil sie keine abstrakte Syntax haben und damit das Bindeglied zwischen Quell- und Zielsprache fehlt, ohne das die semantische Äquivalenz zwischen Quell- und Zielobjekten nicht formalisierbar, also auch nicht überprüfbar ist. 83 Die Frage, warum Dokumenttypen überhaupt durch ECFGs und nicht durch CFGs und deren adäquate algebraische Interpretation dargestellt werden, lässt sich einfach beantworten: Weil die meisten Leute, die XML und DTDs geschaffen haben, in der Theorie formaler Sprachen (und der Datenbanktheorie) zuhause sind und den - auch aus Sicht der programmiersprachlichen Realisierung - adäquateren algebraischen Zugang schlichtweg ignorieren. Eine Folge davon ist die unpräzise Typisierung von Daten, die entsteht, wenn konkrete und abstrakte Syntax gleichgesetzt werden, indem Typkonstruktoren (wie z.B. die Bildung des Typs der Listen von Elementen eines anderen Typs) auf reguläre Operatoren reduziert (wie z.B. den Sternoperator) reduziert werden. 84 LL-Compiler mit Backtracking Sei G = (N, Z, P, BΣ, BA) eine bewachte CFG. parseTReg(N ) bildet Eingabewörter auf Boolesche Werte ab. Demgegenüber wurde parseG im Abschnitt Korrektheit monadischer Parser als eine Funktion nach M (TΣ(G)) eingeführt. Dort hatten wir bereits bemerkt, dass die zwei Schritte eines Compilers: • die rekursive Konstruktion von Syntaxbäumen mit parseG und • deren rekursive Auswertung in einer G-Algebra Ziel mit evalZiel zu einer N -sortierten Funktion parse G ∗ compileZiel : Z −→ M (TΣ(G)) G (returnZiel ◦ evalZiel )∗ −→ M (Ziel), die Eingabewörter direkt in Elemente von Ziel übersetzt, zusammengezogen werden können. Da diese Funktion ihre Argumente schrittweise liest, also auch schrittweise ihre Werte erzeugt, definieren wir sie mit Hilfe von Schleifenfunktionen compStepZiel : Z ∗ → M (ZielA × Z ∗), A A ∈ N ∪ Z \ BA die nach dem Lesen eines akzeptierbaren Präfixes der Eingabe neben Zielobjekten die in der Eingabe verbliebenen Restwörter ausgibt. Für alle x ∈ Z \ BA definieren wir Zielx als {x}. 85 compileG wird wie folgt auf compStep zurückgeführt: Für alle A ∈ N , ∗ Ziel compileZiel G,A = (λ(a, w).return(a)) ◦ guard(λ(a, w).w = ) ◦ compStepA . Scheitert die Ausführung von compStepZiel (w), d.h. endet sie mit dem Wert mzero, diesen Wert zurück, weil f ∗(mzero) = mzero für alle f : dann gibt auch compileZiel G A → M (B) gilt. Letzteres passiert auch, wenn compStepZiel nicht scheitert, aber nur Elemente (a, v) ∈ Ziel × Z ∗ mit v = liefert. Aus den von compStepZiel berechneten Paaren (a, v) ∈ Ziel × Z ∗ mit v 6= filtert compileZiel die Ziel-Komponenten heraus. G Sei P = { p11 = (A1 → v11), . . . , p1n1 = (A1 → v1n1 ), ... pk1 = (Ak → vk1), . . . , pknk = (Ak → vknk )}. Für alle 1 ≤ i ≤ k und w ∈ Z ∗, compStepZiel Ai (w) =def mplus(trypi1 (w), mplus(trypi2 (w), ..., mplus(trypi(n −1) (w), i trypini (w)))), 86 wobei für alle 1 ≤ j ≤ ni trypij : Z ∗ → M (ZielAi × Z ∗) folgendermaßen definiert ist: return(fpZiel , w) falls vij = ij (λ(a1, w). falls ∃ n > 0 : (λ(a2, w). vij = x1 . . . xn ∈ (N ∪Z \BA)n ... i1 < · · · < im (λ(an−1, w). {i1, . . . , im} = {i | ai ∈ N \Z} (λ(an, w). trypij (w) = (ai1 , . . . , aim ), w)))∗ return(fpZiel ij ∗ (compStepZiel xn (w))) ∗ Ziel (w))) (compStep x n−1 ... ∗ (compStepZiel x2 (w))) (compStepZiel x1 (w)). Für alle x ∈ BS ∪Z \BA, z ∈ Z und w ∈ Z ∗, if z ∈ BAA then return(z, w) else mzero falls A ∈ BS, compStepZiel (zw) = def x if z = x then return(z, w) else mzero sonst, Ziel compStepx () =def mzero. 87 compStepAi (w) prüft sequentiell alle Regeln für Ai auf ihre Anwendbarkeit auf w: Gibt es ein Präfix von w, das aus der rechten Seite einer Regel für Ai ableitbar ist? Da alle Funktionen trypij auf dieselbe Eingabe w angewendet werden, handelt es sich hier um einen Backtrack-Parser. Demgegenüber sind klassische LL-Parser auf LL(k)-Grammatiken zugeschnitten, die verlangen, dass bei jedem Anwendungsschritt die ersten k Zeichen von w bestimmen, ob und, wenn ja, welche - einzig (!) mögliche - Regel angewendet werden muss. Diese findet der Parser in der sog. Aktionstabelle. Im nächsten Abschnitt werden wir eine entsprechende Aktionstabelle für LR-Parser definieren. Sei p = (A → x1 . . . xn). tryp(w) ruft sequentiell compStepx1 (w1), . . . , compStepxn (wn) auf, wobei w1 = w gilt und für alle 1 < i ≤ n wi das von compStepxi−1 (wi−1) nicht verarbeitete Suffix von wi−1 ist. Gibt es kein aus xi ableitbares (bzw. mit xi übereinstimmendes Präfix von wi, dann scheitert compStepxi (wi) und gibt mzero aus, was aufgrund der Gültigkeit der Gleichung f ∗(mzero) = mzero (siehe Abschnitt Korrektheit monadischer Parser) dazu führt, dass tryp selbst diesen Wert zurückgibt. Andernfalls berechnet tryp alle Argumente ai1 , . . . , aim der Interpretation fpZiel des aus p gebildeten Funktionssymbols fp und gibt das Element fpZiel (ai1 , . . . , aim ) von ZielA zurück. compStep arbeitet nach dem Prinzip des recursive descent, d.h. steigt - implizit - im Ableitungsbaum ab, baut sein Ergebnis aber rekursiv von den Blättern her auf. 88 T Der Parser parseG = compileGΣ(G) ist korrekt. Dazu ist zu zeigen, dass die Funktionsdiagramme (1) und (2) im Abschnitt Korrektheit monadischer Parser kommutieren, d.h. (1) ∀ w ∈ Z ∗ \L(G) : parseG(w) = mzero, (2) parseG ◦ evalWord (G) = return. Da die Definition von parseG auf den Hilfsfunktionen cst =def compStepTΣ(G) und try aufbaut, müssen wir Eigenschaften dieser Funktionen formulieren und beweisen, aus denen (1) und (2) folgen. Alle drei Funktionen verwenden die monadischen Konstanten bzw. Operatoren bind, return, mplus und mzero. Um Aussagen über die Funktionen zu beweisen, müssen wir deshalb neben den Gleichungen für die Extension _∗ (siehe Korrektheit monadischer Parser) einen logischen Operator voraussetzen, der Eigenschaften von Elementen einer Menge A in Eigenschaften von Elementen von M (A) überführt. Da er den modallogischen Box-Operator 2 verallgemeinert, wollen wir ihn ebenso nennen: 2A : (A → Bool) → (M (A) → Bool) Er muss die folgenden Gleichungen erfüllen: Für alle f : A → Bool, m, m1, m2 ∈ M (A) 89 und m3, m4 ∈ M (B), 2A(f )(m) ⇒ (λx.(if f (x) then m3 else m4)∗(m) = (λx.m3)∗(m), 2A(f )(mzeroA) = False, 2A(f )(mplusA(m1, m2)) = 2A(f )(m1) ∨ 2A(f )(m2). Mit Hilfe von _∗- und 2-Gleichungen und der Definition von parseG: parseG = (λ(a, w).return(a))∗ ◦ guard(λ(a, w).w = ) ◦ cst lassen sich (1) und (2) auf die folgenden Eigenschaften von cst zurückführen: (3) ∀ w ∈ Z ∗ \L(G) : 2(λ(a, w).w 6= )(cst(w)), (4) ∀ t ∈ TΣ(G) : cst(evalWord (G)(t)) = return(t). ******** 90 Beispiel SAB Die Regeln p1 = S → aB, p2 = S → bA, p3 = S → , p4 = A → aS, p5 = A → bAA, p6 = B → bS, p7 = B → aBB von SAB liefern nach obigem Schema den folgenden LL-Compiler von L(SAB) nach Ziel: compStep_S(w) = mplus(try_p1(w),mplus(try_p2(w),mplus(try_p3(w)))) where try_p1(w) = bind(compStep_a(w), λ(x,w).bind(compStep_B(w), λ(c,w).return(f_p1^Ziel(c),w))) try_p2(w) = bind(compStep_b(w), λ(x,w).bind(compStep_A(w), λ(c,w).return(f_p2^Ziel(c),w))) try_p3(w) = return(f_p3^Ziel,w) compStep_A(w) = mplus(try_p4(w),try_p5(w)) where try_p4(w) = bind(compStep_a(w), λ(x,w).bind(compStep_S(w), λ(c,w).return(f_p4^Ziel(c),w))) try_p5(w) = bind(compStep_b(w), λ(x,w).bind(compStep_A(w), λ(c,w).bind(compStep_A(w), λ(d,w).return(f_p5^Ziel(c,d),w)))) 91 compStep_B(w) = mplus(try_p6(w),compStep_p7(w)) where try_p6(w) = bind(compStep_b(w), λ(x,w).bind(compStep_S(w), λ(c,w).return(f_p6^Ziel(c),w))) try_p7(w) = bind(compStep_a(w) of λ(x,w).bind(compStep_B(w), λ(c,w).bind(compStep_B(w), λ(d,w).return(f_p7^Ziel(c,d),w)))) compStep_a(zw) = if z = a then return(a,w) else mzero compStep_b(zw) = if z = b then return(b,w) else mzero compStep_a() = mzero compStep_b() = mzero 92 ` LR-Parser und -Compiler LR-Parser lesen ein Wort w von links nach rechts und versuchen dabei (implizit) eine Rechtsreduktion von w, d.h. die Umkehrung einer Rechtsableitung zu konstruieren. Da dies dem schrittweisen, mit den Blättern (die das Eingabewort bilden) beginnenden Aufbau eines Ableitungsbaums entspricht, werden LR-Parser auch bottom-up-Parser genannt. Im Gegensatz zur obigen Funktion compile ist die entsprechende Übersetzungsfunktion eines LL-Compilers iterativ. Die Umkehrung der Ableitungsrichtung (Reduktion statt Ableitung) macht es möglich, die Compilation durch einen Automaten, also eine iterativ definierte Funktion, zu steuern. Auf der anderen Seite kann compile alle nicht-linksrekursiven CFGs verarbeiten, ein LR-Compiler aber nur LR(k)-Grammatiken (s.u.). In eine bottom-up-Analyse lässt sich auch nicht so einfach Backtracking einbauen wie in eine top-down-Analyse, so dass die Möglichkeit, auf diese Weise die Klasse der von LR-Parsern analysierbaren Sprachen zu erweitern, praktisch entfällt. 93 Außerdem ist die Konstruktion des o.g. Automaten, der die LR-Analyse steuert, – wie wir unten sehen werden – nicht ganz trivial. Da gebräuchliche LR-Parsergeneratoren wie yacc ihren Benutzern diese Arbeit abnehmen, ist das natürlich nur dann ein Argument gegen die LR-Analyse, wenn die jeweils vorliegende CFG keine LR(k)-Grammatik ist. Voraussetzungen Sei G = (N, Z, P, BΣ, BA) eine CFG. Wir setzen voraus, dass N \ BS ein Startsymbol S enthält und dieses nicht auf der rechten Seite einer Regel von G auftritt. ∗ Sei −→ der reflexiv-transitive Abschluss von {(wAv, wϕv) | w, v ∈ (N ∪ Z)∗ ∧ (A → ϕ ∈ P ∨ (A ∈ BS ∧ ϕ ∈ BAA))}. Ablauf der LR-Analyse auf Ableitungsbäumen S φ ε vw Ableitungsbäume Eingabe v w Ableitungsbäume Eingabe 94 vw ε Ableitungsbaum Eingabe first- und follow-Wortmengen Sei k > 0, α ∈ (N ∪ Z)∗ und A ∈ N . first k (α) bezeichnet die k-elementigen Präfixe der aus α ableitbaren terminalen Wörter. follow k (A) bezeichnet die k-elementigen Präfixe der in einem aus S ableitbaren Wort auf A folgenden terminalen Wörter. ∗ ∗ first k (α) = {w ∈ Z k | ∃v ∈ Z ∗ : α −→ wv} ∪ {w ∈ Z <k | α −→ w} ∗ follow k (A) = {w ∈ Z k | ∃u, v ∈ Z ∗ : S −→ uAwv} ∪ ∗ {w ∈ Z <k | ∃u ∈ Z ∗ : S −→ uAw} first(α) = first 1(α) follow (A) = follow 1(A) Induktive Definition von first(α), α ∈ (N ∪ Z)∗ ∈ first() (1) x ∈ Z ⇒ x ∈ first(xα) (2) (A → α) ∈ P ∧ x ∈ first(αβ) ⇒ x ∈ first(Aβ) (3) 95 Eine CFG G, die die o.g. Voraussetzungen erfüllt, heißt LR(k)-Grammatik, wenn das Vorauslesen von k noch nicht verarbeiteten Eingabesymbolen genügt, um zu entscheiden, ob ein weiteres Zeichen gelesen oder eine Reduktion durchgeführt werden soll und, wenn ja, welche. Beispiel Die CFG G = ({S, A}, {∗, b, c}, P, ∅, ∅) mit den Regeln S A A A → → → → A A∗A b c ist für kein k eine LR(k)-Grammatik. Nach der Reduktion der Eingabe auf A ∗ A liegt ein shift-reduce-Konflikt vor. Soll man zuerst weiterlesen (das Präfix ∗b der Resteingabe) und dann b zu A reduzieren oder sofort die Regel A ∗ A zu A reduzieren? Das Vorauslesen von k Zeichen der Resteingabe nimmt uns diese Entscheidung nicht ab. o Der folgende LR-Parser liefert genau dann eindeutige Ergebnisse, wenn G eine LR(1)Grammatik ist. 96 Der LR-Parser in zwei Schritten Schritt 1 parse1 : (N ∪ Z)∗ × Z ∗ → Bool Anforderung ∗ parse1(ϕ, w) = True ⇐⇒ S → ϕw, Definition Sei γ, α ∈ (N ∪ Z \ BA)∗, x ∈ Z und w ∈ Z ∗. ∗ parse1(γα, xw) = parse1(γαx, w) falls S → γAv, A → αβ ∈ P, shift β 6= , x ∈ first(βfollow (A)) ∗ parse1(γα, w) = parse1(γA, w) falls S → γAv, A 6= S, reduce A → α ∈ P, first(w) ∈ follow (A) parse1(ϕ, ) = True falls S → ϕ ∈ P accept parse1(ϕ, w) = False sonst error 97 Sei ϕ ∈ (N ∪ Z)∗, M = (N ∪ Z \ BA)∗ und die vierstellige Relation state(ϕ) ⊆ N × (M ∪ BA) × M × (Z ∪ {}) definiert durch: ∗ (A, α, β, first(w)) ∈ state(γα) ⇐⇒ S → γAw ∧ A → αβ ∈ P Die Bedingungen in der Definition von parse1 werden durch Abfragen der Form (A, α, β, x) ∈ state(ϕ) ersetzt: parse1(γα, xw) = parse1(γαx, w) falls (A, α, β, y) ∈ state(γα), shift β 6= , x ∈ first(βy) parse1(γα, w) = parse1(γA, w) falls (A, α, , first(w)) ∈ state(γα), reduce A 6= S parse1(ϕ, ) = True falls (S, α, , ) ∈ state(ϕ) accept parse1(ϕ, w) = False sonst error 98 Schritt 2 Die Menge Q = {state(ϕ) | ϕ ∈ (N ∪Z)∗} wird zur Zustandsmenge des LR-Automaten für G. N ∪ Z ist sein Eingabealphabet, state() ist sein Startzustand, alle Zustände state(ϕ) ∗ mit S −→ ϕ bilden die Menge seiner Endzustände und seine Übergangsfunktion ist für alle x ∈ N ∪ Z wie folgt definiert: δ(state(ϕ), x) ⇐⇒ state(ϕx) Im Parser wird ϕ durch eine Liste von Zuständen ersetzt, die er wie einen Keller verwaltet: parse1 wird so zu parse2 : Q∗ × Z ∗ → Bool umgeformt, dass für alle x1, . . . , xn ∈ N ∪ Z parse1(x1 . . . xn, w) = parse2(state(x1 . . . xn)state(x1 . . . xn−1) . . . state(x1)state(), w) gilt. 99 Sei q, qi ∈ Q, qs ∈ Q∗, x ∈ Z und w ∈ Z ∗. parse2(q : qs, xw) parse2(q1 : · · · : q|α| parse2(q : qs, ) parse2(qs, w) = parse2(δ(q, x) : q : qs, w) shift falls (A, α, β, y) ∈ q, β 6= , x ∈ first(βy) : q : qs, w) = parse2(δ(q, A) : q : qs, w) reduce falls A 6= S, (A, α, , first(w)) ∈ q1 = True falls (S, α, , ) ∈ q accept = False sonst error 100 Induktive Definition von Q und δ S → α ∈ P ⇒ (S, , α, ) ∈ q0 (1) (A, α, Bβ, x) ∈ q ∧ B → γ ∈ P ∧ y ∈ first(βx) ⇒ (B, , γ, y) ∈ q (2) (A, α, xβ, y) ∈ q ⇒ (A, αx, β, y) ∈ δ(q, x) (3) Aktionstabelle actLR : Q × (Z ∪ {}) → P ∪ {shift, error } falls (A, α, β, y) ∈ q, β 6= , x ∈ first(βy) shift actLR (q, x) = A → α falls A 6= S, (A, α, , x) ∈ q error sonst S → α falls (S, α, , ) ∈ q actLR (q, ) = error sonst 101 Unter Verwendung von δ und actLR erhält man schließlich eine kompakte Definition von parse2: Sei q, qi ∈ Q, qs ∈ Q∗, x ∈ Z und w ∈ Z ∗. parse2(q : qs, xw) parse2(q1 : · · · : q|α| parse2(q : qs, ) parse2(q : qs, w) = parse2(δ(q, x) : q : qs, w) falls actLR (q, x) = shift : q : qs, w) = parse2(δ(q, A) : q : qs, w) falls actLR (q1, first(w)) = A → α = True falls actLR (q, ) = S → α = False falls actLR (q, first(w)) = error 102 Beispiel SAB2 G = ({S, A, B}, {c, d, ∗}, P, ∅, ∅) P = {S → A, A → A ∗ B, A → B, B → c, B → d} LR-Automat für G (δ wird auch goto-Tabelle genannt) q0 = {(S, , A, ), (A, , A ∗ B, ), (A, , B, ), (A, , A ∗ B, ∗), (A, , B, ∗), (B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)} q1 = δ(q0, A) = {(S, A, , ), (A, A, ∗B, ), (A, A, ∗B, ∗)} q2 = δ(q0, B) = {(A, B, , ), (A, B, , ∗)} q3 = δ(q0, c) = {(B, c, , ), (B, c, , ∗)} = δ(q5, c) q4 = δ(q0, d) = {(B, d, , ), (B, d, , ∗)} = δ(q5, d) q5 = δ(q1, ∗) = {(A, A∗, B, ), (A, A∗, B, ∗), (B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)} q6 = δ(q5, B) = {(A, A ∗ B, , ), (A, A ∗ B, , ∗)} A B c d mul 0 1 2 3 4 1 5 5 6 3 4 103 Aktionstabelle c d ∗ q0 q1 q2 q3 q4 shift error error error error shift error error error error error shift A → B B → c B → d error S → A A → B B → c B → d Ein Parserlauf parse2(q0, c∗d) = parse2(q3q0, ∗d) = parse2(q2q0, ∗d) = parse2(q1q0, ∗d) = parse2(q5q1q0, d) = parse2(q4q5q1q0, ) = parse2(q6q5q1q0, ) = parse2(q1q0, ) = True wegen wegen wegen wegen wegen wegen wegen wegen q5 q6 shift error shift error error A → A ∗ B error A → A ∗ B actLR (q0, c) = shift und δ(q0, c) = q3 actLR (q3, ∗) = B → c und δ(q0, B) = q2 actLR (q2, ∗) = A → B und δ(q0, A) = q1 actLR (q1, ∗) = shift und δ(q1, ∗) = q5 actLR (q5, d) = shift und δ(q5, d) = q4 actLR (q4, ) = B → d und δ(q5, B) = q6 actLR (q6, ) = A → A ∗ B und δ(q0, A) = q1 actLR (q1, ) = S → A 104 LR-Compiler Wir erweitern parse2 zum LR-Compiler compile : Q∗ × Z ∗ × alg ∗ → M (alg), der alle Wörter von L(G) parallel zu ihrer Analyse in Elemente einer beliebigen G-Algebra alg übersetzt. Im Gegensatz zum LL-Compiler übersetzt der LR-Compiler nur Worte, die aus dem Startsymbol S ableitbar sind. Er startet mit q0 = state() im ansonsten leeren Zustandskeller und mit der leeren Liste von alg-Werten. Anforderung an compile ( compile([q0], w, []) = Word (G) return(evalalg (t)) =⇒ evalS (t) = w Word (G) mzero =⇒ ∀ t ∈ TΣ(G),S : evalS (t) 6= w Definition von compile Sei q, qi ∈ Q, qs ∈ Q∗, x ∈ Z, w ∈ Z ∗, ai ∈ alg, as ∈ alg ∗ und ||α|| die Anzahl der Nichtterminale von α ∈ (N ∪ Z \ BA)∗. compile(q : qs, xw, as) = compile(δ(q, x) : q : qs, w, as) falls actLR (q, x) = shift 105 compile(q1 : · · · : q|α| : q : qs, w, as ++[a1, . . . , a||α||]) = alg (a1, . . . , a||α||)]) compile(δ(q, A) : q : qs, w, as ++[fA,α falls actLR (q1, first(w)) = A → α Ist alg = TΣ(G), dann ist as ++[a1, . . . , a||α||] eine Baumliste, die ein Reduktionsschritt folgendermaßen verändert: a1 a||α|| as fA -> α a1 a||α|| as alg compile(q : qs, , [a1, . . . , a||α||]) = return(fS→α (a1, . . . , a||α||) falls actLR (q, ) = S → α compile(qs, w, as) = mzero sonst 106 Zum Nachweis der obigen Anforderung an compile müssen die Trägermengen der GTermalgebra um Syntaxbäume mit Variablen aus N und die Trägermengen der Wortalgebra von G um Wörter mit Nichtterminalen erweitert werden. Dann lässt sich zeigen, dass die folgende Bedingung eine Invariante der compile-Schleife ist und die obige Anforderung an compile impliziert: compile(state(w0A1w1 . . . Anwn) : qs, w, [a1, . . . , an]) = return(evalalg (t(a1, . . . , an))) Word (G) (t(x1, . . . , xn)) = w0A1w1 . . . Anwnw. =⇒ evalS 107 Beispiel SAB2 Hier ist noch einmal der obige Parserlauf mit schrittweiser Konstruktion des Syntaxbaums (als Klammerausdruck im drittem Argument von parse). Demnach ist die Zielalgebra hier die SAB2-Termalgebra. parse([c,mul,d],[0], []) parse([mul,d], [3,0], []) parse([mul,d], [2,0], [f_{B,c}]) parse([mul,d], [1,0], [f_{A,B}(f_{B,c})]) parse([d], [5,1,0], [f_{A,B}(f_{B,c})]) parse([], [4,5,1,0],[f_{A,B}(f_{B,c})]) parse([], [6,5,1,0],[f_{A,B}(f_{B,c}), f_{B,d}]) parse([], [1,0], [f_{A,A*B}(f_{A,B}(f_{B,c}),f_{B,d})]) parse([], [1,0], [f_{S,A}(f_{A,A*B}(f_{A,B}(f_{B,c}),f_{B,d}))]) 108 Auf den nächsten drei Folien stehen die Aktions- und goto-Tabelle der folgenden Grammatik G für imperative straight-line-Programme (also ohne Konditionale und Schleifen) in der Form, in der sie der in C geschriebene Compilergenerator yacc (“yet another compilercompiler”) erzeugt. prog statems assign exp -> -> -> -> statems exp . statems assign ; | ID : = exp exp + exp | exp * exp | (exp) | NUMBER | ID Auf der vierten Folie werden diese Regeln um C-Code erweitert, der die Funktionen einer G-Algebra definiert. In diesem Fall handelt es sich um einen Interpreter der durch G definierten Programmiersprache. Die entsprechende Algebra für JavaLight heißt javaState und wird unten im Abschnitt Ein Haskell-Datentyp für Algebren definiert. 109 110 111 112 113 LR(k) LALR(k) SLR(k) LL(k) eindeutige kf. Gramm. A aAa|a nicht LR(k) S B E C D aB|eE Cc|Dd Cd|Dc b b LR(1), nicht LALR(1) S B C D Ba|aCd Cc|Dd b b LALR(1), nicht SLR(k) S A B C D E aA|bB Ca|Db Cc|Da E E ε LL(1), nicht LALR(k) Die Hierarchie der CFG-Klassen 114 eindeutige kontextfreie det. kf. Sprachen =LR(1)-Spr. =SLR(1)-Spr. =LALR(1)-Spr. LL(k)Spr. Sprachen Die Hierarchie der entsprechenden Sprachklassen 115 ` Haskell: Funktionen Neben • Produkten A1 × . . . × An (Haskell-Notation: (A1,...,An)) und • Summen A1 + · · · + An (Datentypen mit n Konstruktoren; s.u.) werden in Haskell auch Funktionen als Objekte betrachtet und z.B. als λ-Abstraktionen \p -> e dargestellt. Programmtechnisch gesprochen ist p der formale Parameter - also eine einzelne Variable oder, allgemeiner, ein Muster (pattern; s.u.) - der dargestellten Funktion und e ihr Rumpf. Ihre Definition ist eine Gleichung: f = \p -> e Meistens wird jedoch die äquivalente applikative Definition bevorzugt: f p = e Damit ist auch f eine Repräsentation der durch sie definierten Funktion. Funktionen, die andere Funktionen als Argumente oder Werte haben, heißen Funktionen höherer Ordnung. In Haskell ist schon die Addition eine solche: 116 (+) :: Int -> (Int -> Int) oder (+) :: Int -> Int -> Int wegen der Rechtsassoziativität von -> Sie wird zunächst als Präfixfunktion verwendet: ((+) 5) 6 oder (+) 5 6 wegen der Linksassoziativität der Applikation (s.u.) Ohne die runden Klammern kann sie wie üblich infix verwendet werden: 5 + 6 Eine Funktion eines Typs a -> b -> c, deren Name mit einem Buchstaben beginnt, kann ebenfalls infix verwendet werden, muss dann aber in ‘ eingeschlossen werden, z.B.: mod :: Int -> Int -> Int mod 11 5 oder 11 `mod` 5 Die Infixnotation wird auch verwendet, um die in einer Funktion eines Typs a -> b -> c enthaltenen Sektionen zu benennen. Z.B. sind die Ausdrücke (+ 5) (+) 5 (11 +) mod 5 (`mod` 5) (11 `mod`) Funktionen des Typs Int -> Int. Hier sind alle Klammern zwingend! 117 Der Ausdruck, der die Anwendung einer λ-Abstraktion auf ein Argument repräsentiert, heißt λ-Applikation, z.B. (\p -> e)(e’). Die Applikation führt nur dann zu einem Ergebnis, wenn der Ausdruck e’ das Pattern e matcht (s.u.). Die Applikationsfunktion ($) :: (a -> b) -> a -> b f $ a = f a führt die Anwendung einer gegebenen Funktion auf ein gegebenes Argument durch. Sie ist rechtsassoziativ und hat unter allen Operationen die niedrigste Priorität. Daher kann durch Benutzung von $ manch schließende Klammer vermieden werden: f1 (f2 (...(fn a)...))) ist äquivalent zu f1 $ f2 $ ... $ fn a Demgegenüber ist der Kompositionsoperator (.) :: (b -> c) -> (a -> b) -> a -> c (g . f) a = g (f a) zwar auch rechtsassoziativ, hat aber - nach den Präfixoperationen - die höchste Priorität. Man benutzt ihn u.a., um in einer applikativen Definition Argumentvariablen einzusparen: f a b = g (h a) b ist äquivalent zu f a = g $ h a ist äquivalent zu f = g . h f a b = g $ h a b ist äquivalent zu f a = g . h a ist äquivalent zu f = (g.).h 118 Weitere nützliche Funktionsgeneratoren, -transformatoren und kombinatoren: const :: a -> b -> a const a _ = a konstante Funktion update :: Eq a => (a -> b) -> a -> b -> a -> b update f a b a' = if a == a' then b else f a' Funktionsupdate flip :: (a -> b -> c) -> b -> a -> c flip f b a = f a b Vertauschung der Argumente curry :: ((a,b) -> c) -> a -> b -> c curry f a b = f (a,b) Kaskadierung (Currying) uncurry :: (a -> b -> c) -> (a,b) -> c uncurry f (a,b) = f a b Dekaskadierung 119 (***) :: (a -> b) -> (a -> c) -> a -> (b,c) (f *** g) x = (f x,g x) Funktionsprodukt (&&&), (|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool Lifting Boolescher Operationen (f &&& g) x = f x && g x (f ||| g) x = f x || g x Ein Typ mit Typvariablen wie z.B. a -> b -> c heißt polymorph. Andernfalls ist er monomorph. Eine Funktion mit poly/monomorphem Typ heißt poly/monomorph. Ein Typ t heißt Instanz eines Typs u, wenn man t durch Ersetzung der Typvariablen aus u erhält. 120 ` Haskell: Listen data [a] = [] | a : [a] (++) :: [a] -> [a] -> [a] (a:s)++s' = a:(s++s') _++s = s (!!) :: [a] -> Int -> a (a:_)!!0 = a (_:s)!!n | n > 0 = s!!(n-1) head :: [a] -> a head (a:_) = a tail :: [a] -> [a] tail (_:s) = s init :: [a] -> [a] init [_] = [] init (a:s) = a:init s last :: [a] -> a last [a] = a last (_:s) = last s 121 take take take take :: Int -> [a] -> [a] 0 _ = [] n (x:s) | n > 0 = x:take (n-1) s _ [] = [] drop drop drop drop :: Int -> [a] -> [a] 0 s = s n (_:s) | n > 0 = drop (n-1) s _ [] = [] takeWhile :: (a -> Bool) -> [a] -> [a] takeWhile f (x:s) = if f x then x:takeWhile f s else [] takeWhile f _ = [] dropWhile :: (a -> Bool) -> [a] -> [a] dropWhile f s@(x:s') = if f x then dropWhile f s' else s dropWhile f _ = [] updList :: [a] -> Int -> a -> [a] updList s i a = take i s++a:drop (i+1) s 122 map :: (a -> b) -> [a] -> [b] map f (a:s) = f a:map f s map _ _ = [] [f1 a,f2 a,...,fn a] ist äquivalent zu map ($ a) [f1,f2,...,fn] zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] zipWith f (a:s) (b:s') = f a b:zipWith f s s' zipWith _ _ _ = [] zip :: [a] -> [b] -> [(a,b)] zip = zipWith $ \x y -> (x,y) repeat :: a -> [a] repeat a = a:repeat a replicate :: Int -> a -> [a] replicate n a = take n $ repeat a iterate :: (a -> a) -> a -> [a] iterate f a = a:iterate f $ f a 123 Beispiel Listenteilung splitAt splitAt splitAt splitAt :: Int -> [a] -> ([a],[a]) 0 s = ([],s) _ [] = ([],[]) n (a:s) | n > 0 = (a:s1,s2) where (s1,s2) = splitAt (n-1) s span :: (a -> Bool) -> [a] -> ([a],[a]) span f s@(x:s') = if f x then (x:s1,s2) else ([],s) where (s1,s2) = span f s' span _ [] = ([],[]) Beispiel Listenintervall sublist sublist sublist sublist sublist :: [a] -> Int -> Int -> [a] (a:_) 0 0 = (a:s) 0 j | j > 0 = (_:s) i j | i > 0 && j > 0 = _ _ _ = 124 [a] a:sublist s 0 $ j-1 sublist s (i-1) $ j-1 [] Beispiel Pascalsches Dreieck = Binomialkoeffizienten pascal :: Int -> [Int] pascal 0 = [1] pascal n = zipWith (+) (0:s) $ s++[0] where s = pascal $ n-1 binom :: Int -> Int -> Int binom n k = product[1..n]/(product[1..k]*product[1..n-k]) Für alle n ∈ N und n ≤ k gilt: binom(n)(k) = pascal(n)!!k 125 Strings sind Listen von Zeichen Strings werden als Listen von Zeichen betrachtet, m.a.W.: die Typen String und [Char] sind identisch. words :: String -> [String] und unwords :: [String] -> String zerlegen bzw. konkatenieren Strings, wobei Leerzeichen, Zeilenumbrüche ('\n') und Tabulatoren (’ ’)als Trennsymbole fungieren. unwords fügt Leerzeichen zwischen die zu konkatenierenden Strings. lines :: String -> [String] und unlines :: [String] -> String zerlegen bzw. konkatenieren Strings, wobei nur Zeilenumbrüche als Trennsymbole fungieren. unlines fügt '\n' zwischen die zu konkatenierenden Strings. 126 Beschränkte Iterationen (for-Schleifen) entsprechen oft Listenfaltungen. Faltung einer Liste von links her f f f f f a b1 b2 b3 b4 b5 foldl :: (a -> b -> a) -> a -> [b] -> a foldl f a (b:s) = foldl f (f a b) s foldl _ a _ = a foldl1 :: (a -> a -> a) -> [a] -> a foldl1 f (a:s) = foldl f a s sum and minimum concat = = = = foldl (+) 0 foldl (&&) True foldl1 min foldl (++) [] product or maximum concatMap 127 = = = = foldl (*) 1 foldl (||) False foldl1 max (concat .) . map Parallele Faltung zweier Listen von links her f f f f f a b1 c1 b2 c2 b3 c3 b4 c4 b5 c5 fold2 :: (a -> b -> c -> a) -> a -> [b] -> [c] -> a fold2 f a (b:s) (c:s') = fold2 f (f a b c) s s' fold2 _ a _ _ = a listsToFun :: b -> [a] -> [b] -> a -> b listsToFun = fold2 update . const Mit listsToFun können funktionale Objekte mit endlichem Wertebereich in Haskell implementiert werden: bs!!i falls i bzgl. a = as!!i maximal ist, listsToFun b as bs a = b sonst. 128 Faltung einer Liste von rechts her f f f f f b1 b2 b3 b4 foldr :: (b -> a -> a) -> a -> [b] -> a foldr f a (b:s) = f b $ foldr f a s foldr _ a _ = a 129 b5 a Listenlogik any :: (a -> Bool) -> [a] -> Bool any = (or .) . map elem :: a -> [a] -> Bool elem a = any (a ==) all :: (a -> Bool) -> [a] -> Bool all = (and .) . map notElem :: a -> [a] -> Bool notElem a = all (a /=) filter :: (a -> Bool) -> [a] -> [a] filter f (a:s) = if f a then a:filter f s else filter f s filter f _ = [] 130 Jeder Aufruf von map, zipWith, filter oder einer Komposition dieser Funktionen entspricht einer Listenkomprehension: map f s = [f a | a <- s] zipWith f s s' = [uncurry f a | a <- zip s s'] filter f s = [a | a <- s, f a] allgemein: [e(x1, . . . , xn) | x1 ← s1, . . . , xn ← sn, be(x1, . . . , xn)] :: [a] • x1, . . . , xn sind Variablen, • s1, . . . , sn sind Listen, • e(x1, . . . , xn) ist ein Ausdruck des Typs a, • xi ← si heißt generator und steht für xi ∈ si, • be(x1, . . . , xn) heißt guard und ist ein Boolescher Ausdruck. Mit der Listenkomprehension lassen sich u.a. sehr einfach Relationen (Teilmengen eines kartesischen Produktes) A1 × . . . × An definieren, sofern die Ai als Listen gegeben sind, z.B. die folgende Menge aller Elemente von A1 × A2 × A3, die p : A1 × A2 × A3 → Bool erfüllen: rel = [(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)] 131 ` Haskell: Datentypen Beispiel Arithmetische Ausdrücke data Expr = Con Int | Var String | Sum [Expr] | Prod [Expr] | Expr :- Expr | Int :* Expr | Expr :^ Int Z.B. lautet der Ausdruck 5*11+6*12+x*y*z als Objekt vom Typ Expr folgendermaßen: Sum [5 :* Con 11,6 :* Con 12,Prod [Var "x",Var "y",Var "z"]] 132 Die Haskell-Programme zum Datentyp Expr stehen auch in der Datei Expr.hs. Funktionen mit Expr-Argumenten werden in Abhängigkeit von deren Muster (pattern) definiert: Beispiel Symbolische Differentiation diff diff diff diff diff :: String -> Expr -> Expr x (Con _) = zero x (Var y) = if x == y then one else zero x (Sum es) = Sum $ map (diff x) es x (Prod es) = Sum $ map f [0..length es-1] where f i = Prod $ updList es i $ diff x $ es!!i diff x (e :- e') = diff x e :- diff x e' diff x (i :* e) = i :* diff x e diff x (e :^ i) = i :* Prod [diff x e,e:^(i-1)] zero = Con 0 one = Con 1 133 Beispiel Expr-Interpreter type Store = String -> Int (Belegung der Variablen) evalE evalE evalE evalE evalE evalE evalE evalE :: Expr -> Store -> Int (Con i) _ = i (Var x) st = st x (Sum es) st = sum $ map (flip evalE st) es (Prod es) st = product $ map (flip evalE st) es (e :- e') st = evalE e st - evalE e' st (i :* e) st = i * evalE e st (e :^ i) st = evalE e st ^ i 134 Beispiel Parser für reguläre Ausdrücke infixr 7 :* infixr 6 :+ data Reg = Empty | Eps | Sym String | Reg :+ Reg | Reg :* Reg | Plus Reg Die Gleichungen für parse, δ ∗, δ und accepting im Abschnitt Ein Parser für reguläre Ausdrücke liefern Haskell-Definitionen dieser Funktionen: parse :: Reg -> [String] -> Bool parse r = accepting . dstar r dstar :: Reg -> [String] -> Reg dstar r (x:w) = dstar (d r x) w dstar r _ = if accepting r then Eps else Empty 135 d d d d d d d :: Reg -> String -> Reg Empty _ = Empty Eps _ = Empty (Sym x) y = if x == y then Eps else Empty (r1:+r2) x = d r1 x:+d r2 x (r1:*r2) x = d r1 x:*r2:+if accepting r1 then d r2 x else Empty (Plus r) x = d r x:+d r x:*Plus r accepting accepting accepting accepting accepting accepting accepting :: Reg -> Bool Empty = False Eps = True (Sym _) = False (r1:+r2) = accepting r1 || accepting r2 (r1:*r2) = accepting r1 && accepting r2 (Plus r) = accepting r 136 Um die durch Anwendungen von d erzeugten regulären Ausdrücke nicht zu groß werden zu lassen, reduzieren wir sie nach jeder Anwendung gemäß in Lang gültiger Gleichungen: dstar :: Reg -> [String] -> Reg dstar r (x:w) = dstar (reduce $ d r x) w dstar r _ = if accepting r then Eps else Empty reduce reduce reduce reduce reduce reduce reduce reduce :: Reg -> Reg (r:+Empty) = r (Empty:+r) = r (r:*Eps) = r (Empty:*r) = Empty (r:*Empty) = Empty (Eps:*r) = r r = r 137 Im Abschnitt Vom LL-Parser zum LL-Compiler haben wir das Gleichungssystem einer bewachten CFG G in weitere Gleichungen für δ und accepting überführt und damit parse zu einem Parser für L(G) gemacht. Wenn wir ihn in Haskell implementieren, können wir uns die Umformung der Gleichungen sparen. GLS (G) ist nämlich selbst eine HaskellDefinition, nicht von Funktionen, sondern von Konstanten. Da man ihre Gleichungen unendlich oft anwenden kann, repräsentieren sie unendliche Objekte. Beispiel SAB s,a,b :: Reg s = Sym "a":*b:+Sym "b":*a:+Eps a = Sym "a":*s:+Sym "b":*a:*a b = Sym "b":*s:+Sym "a":*b:*b Z.B. wird mit dem Aufruf parse s $ words "a b a b b a b a a b" geprüft, ob das Wort ababbabaab zu L(SAB)s gehört. 138 (1) Da die Auswertung von (1) von links nach rechts voranschreitet, werden zunächst nicht die Gleichungen für parse, dstar und d angewendet, was u.a. zum Teilausdruck d s $ Sym "a". (2) führt. Da es keine Gleichung für d gibt, die auf diesen Teilausdruck anwendbar ist, wendet der Haskell-Interpreter - seiner lazy-evaluation-Strategie folgend - erst jetzt die Gleichung für s an und ersetzt damit (2) durch d (Sym "a":*b:+Sym "b":*a:+Eps) $ Sym "a". (3) Auf (3) können wieder Gleichungen für d angewendet werden. Da nur in solchen Fällen Gleichungen von GLS (SAB) angewendet werden, terminiert z.B. die Auswertung der folgenden Ausdrücke mit dem jeweils rechts stehenden Ergebnis: parse parse dstar dstar s s s s $ $ $ $ words words words words "a "a "a "a b b b b a a a a b b b b b b b b a a a a b b b b a b a b a a a a b" b" b" b" ; ; ; ; True False Eps Empty Diese und verwandte Haskell-Programme stehen in der Datei Lazy.hs. 139 ` Typklassen stellen Bedingungen an die Instanzen einer Typvariablen. Die Bedingungen bestehen in der Existenz bestimmter Funktionen und bestimmten Beziehungen zwischen ihnen. class Eq a where (==), (/=) :: a -> a -> Bool (/=) = (not .) . (==) Eine Instanz einer Typklasse besteht aus den Instanzen ihrer Typvariablen sowie Definitionen der von ihr geforderten Funktionen. instance Eq (Int,Bool) where (x,b) == (y,c) = x == y && b == c instance Eq a => Eq [a] where s == s' = length s == length s' && and $ zipWith (==) s s' Auch (/=) könnte hier definiert werden. Die Definition von (/=) in der Klasse Eq als Negation von (==) ist nur ein Default! 140 Beispiele insert :: Eq a => a -> [a] -> [a] insert x s@(y:s') = if x == y then s else y:insert x s' insert x _ = [x] remove :: Eq a => a -> [a] -> [a] remove x = filter (/= x) union :: Eq a => [a] -> [a] -> [a] union = foldl $ flip insert Mengenvereinigung diff :: Eq a => a -> [a] -> [a] diff = foldl $ flip remove Mengendifferenz inter :: Eq a => [a] -> [a] -> [a] inter s = filter (`elem` s) Mengendurchschnitt unionMap :: Eq a => (a -> [b]) -> [a] -> [b] unionMap = (foldl union [] .) . map concatMap für Mengen 141 Unterklassen Typklassen können wie Objektklassen in OO-Sprachen andere Typklassen erben. Die jeweiligen Oberklassen werden vor dem Erben vor dem Pfeil => aufgelistet. class Eq a => Ord a where (<), (<=), (>=), (>) :: a -> a -> Bool max, min :: a -> a -> a max x y = if x >= y then x else y min x y = if x <= y then x else y class Ord a => Bounded a where minBound, maxBound :: a quicksort :: Ord a => [a] -> [a] quicksort (x:s) = quicksort [z | z <- s, z <= x]++x: quicksort [z | z <- s, z > x] quicksort s = s 142 Beispiel Mergesort mergesort :: Ord a => [a] -> [a] mergesort (x:y:s) = merge (mergesort $ x:s1) (mergesort $ y:s2) where (s1,s2) = split s mergesort s = s split :: [a] -> ([a],[a]) split (x:y:s) = (x:s1,y:s2) where (s1,s2) = split s split s = (s,[]) merge :: Ord a => [a] -> [a] -> [a] merge s1@(x:s3) s2@(y:s4) = if x <= y then x:merge s3 s2 else merge s3 s2 merge [] s = s merge s _ = s 143 Beispiel Binäre Bäume data Bintree a = Empty | Branch (Bintree a) a (Bintree a) subtrees :: Bintree a -> [Bintree a] subtrees (Branch left _ right) = [left,right] subtrees _ = [] leaf :: a -> Bintree a leaf = flip (Branch Empty) Empty insertTree :: Ord a => a -> Bintree a -> insertTree a t@(Branch t1 b t2) | a == b | a < b | True insertTree a _ Bintree a = t = Branch (insertTree a t1) b t2 = Branch t1 b $ insertTree a t2 = leaf a instance Eq a => Ord (Bintree a) where Empty <= _ = True _ <= Empty = False Branch t1 a t2 <= Branch t3 b t4 = t1 <= t3 && a == b && t2 <= t4 144 Vor der Ein- bzw. Ausgabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion read bzw. show aufgerufen, die zur Typklasse Read a bzw. Show a gehört: type ReadS a = String -> [(a,String)] type ShowS = String -> String class Read a where read :: String -> a read s = case [x | (x,t) <- reads s, ("","") <- lex t] of [x] -> x [] -> error "PreludeText.read: no parse" _ -> error "PreludeText.read: ambiguous parse" reads :: ReadS a reads = readsPrec 0 readsPrec :: Int -> ReadS a class Show a where show :: a -> String show x = shows x "" 145 shows :: a -> ShowS shows = showsPrec 0 showsPrec :: Int -> a -> ShowS Das Argument einer Funktion vom Typ ReadS a ist der jeweilige Eingabestring s. Ihr Wert ist eine Liste von Paaren, bestehend aus dem als Objekt vom Typ a erkannten Präfix von s und der jeweiligen Resteingabe (= Suffix von s). Das Argument einer Funktion vom Typ ShowS ist der an die Ausgabe eines Objektes vom Typ a anzufügende String. Ihr Wert ist die dadurch entstehende Gesamtausgabe. lex :: ReadS String ist eine Standardfunktion, die ein evtl. aus mehreren Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und das Paar (Symbol,Resteingabe) ausgibt (siehe PreludeHugs.hs). Wird deriving Read bzw. deriving Show ans Ende der Definition eines Datentyps T gesetzt, dann werden T-Objekte in der Darstellung eingelesen bzw. ausgegeben, in der sie auch in Programmen vorkommen. Will man das Ein- bzw. Ausgabeformat von T-Objekten selbst bestimmen, dann muss die T-Instanz von readsPrec bzw. show oder showsPrec definiert werden. 146 Beispiel Binäre Bäume readTree :: Read a => ReadS (Bintree a) readTree s = [(Branch left a right,s6) | (a,s1) <- reads s, ("(",s2) <- lex s1, (left,s3) <- readTree s2, (",",s4) <- lex s3, (right,s5) <- readTree s4, (")",s6) <- lex s5] ++ [(Branch left a Empty,s4) | (a,s1) <- reads s, ("(",s2) <- lex s1, (left,s3) <- readTree s2, (")",s4) <- lex s3] ++ [(leaf a,s1) | (a,s1) <- reads s] showTree0 showTree0 showTree0 showTree0 :: Show (Branch (Branch (Branch a => Bintree a Empty a Empty) left a Empty) left a right) -> String = show a = show a++'(':showTree0 left++")" = show a++'(':showTree0 left++',': showTree0 right++")" 147 oder effizienter, weil iterativ: showTree :: Show a => Bintree a -> ShowS showTree (Branch Empty a Empty) = shows a showTree (Branch left a Empty) = shows a . ('(':) . showTree left . (')':) showTree (Branch left a right) = shows a . ('(':) . showTree left . (',':) . showTree right . (')':) showTree0 und showTree sind partielle Funktionen, die nur auf den mit read eingelesenen Bäumen definiert sind! instance Read a => Read (Bintree a) where readsPrec _ = readTree instance Show a => Show (Bintree a) where show = showTree0 oder showsPrec _ = showTree rb1 = read "5(7(3, 8),6( 2) ) " :: Bintree Int ; 5(7(3,8),6(2)) rb2 = reads "5(7(3, 8),6( 2) ) " :: [(Bintree Int,String)] ; [(5(7(3,8),6(2))," "),(5,"(7(3, 8),6( 2) ) ")] rb3 = read "5(7(3,8),6(2))hh" :: Bintree Int ; Exception: ...: no parse rb4 = reads "5(7(3,8),6(2))hh" :: [(Bintree Int,String)] ; [(5(7(3,8),6(2)),"hh"),(5,"(7(3,8),6(2))hh")] 148 Als Listen dargestellte Mengen newtype Set a = Set {list :: [a]} newtype kann data ersetzen, falls der Datentyp genau einen Konstruktor hat list = Zugriffsfunktion auf das Argument von Set instance Eq a => Eq (Set a) instance Eq a => Ord (Set a) where s == s' = s <= s' && s' <= s where s <= s' = all (`elem` list s') $ list s instance Show a => Show (Set a) where show = show . list Set[1,2,3] <= Set[3,4,2,5,1,99] Set[1,2,3] >= Set[3,4,2,5,1,99] ; ; mkSet :: Eq a => [a] -> Set a mkSet = Set . union [] True False eliminiert Mehrfachvorkommen 149 Beispiel Vom nichtdeterministischen (erkennenden) Automaten mit -Übergängen zum deterministischen Automaten ohne -Übergänge Wir transformieren Automaten mit ganzzahligen Zuständen, Startzustand 0 und Endzustand 1, wie sie z.B. die Auswertung regulärer Ausdrücke in der Reg-Algebra NDA (s.u.) liefert. makeDet :: (Int -> String -> [Int]) -> ([Int], [Int] -> String -> [Int], [Int] -> Bool) makeDet delta = (epsHull [0], delta', (1 `elem`)) where delta' qs x = epsHull $ unionMap (flip delta x) qs epsHull qs = if all (`elem` qs) qs' then qs else epsHull $ qs `union` qs' where qs' = unionMap (flip delta "eps") qs 150 ` Rund um Expr Eine Expr-Instanz von Show instance Show Expr where showsPrec _ = showE showE :: Expr -> ShowS showE (Con i) = (show i++) showE (Var x) = (x++) showE (Sum es) = foldShow '+' es showE (Prod es) = foldShow '*' es showE (e :- e') = ('(':) . showE e . ('-':) . showE e' . (')':) showE (n :* e) = ('(':) . (show n++) . ('*':) . showE e . (')':) showE (e :^ n) = ('(':) . showE e . ('^':) . (show n++) . (')':) foldShow :: Char -> [Expr] -> ShowS foldShow op (e:es) = ('(':) . showE e . foldr trans id es . (')':) where trans e h = (op:) . showE e . h foldShow _ _ = id Prod[Con 3,Con 5,x,Con 11] ; (3*5*x*11) Sum [11 :* (x :^ 3),5 :* (x :^ 2),16 :* x,Con 33] ; ((11*(x^3))+(5*(x^2))+(16*x)+33) 151 Beispiel Normalisierung arithmetischer Ausdrücke Die folgende Funktion reduce wendet die folgenden Gleichungen auf einen arithmetischen Ausdruck an: 0+e=e e+e=2∗e (m ∗ e) + (n ∗ e) = (m + n) ∗ e m ∗ (n ∗ e) = (m ∗ n) ∗ e reduce reduce reduce reduce reduce reduce reduce reduce reduce reduce :: Expr -> Expr (e :- e') (0 :* e) (1 :* e) (i :* Con j) (i :* (j :* e)) (e :^ 0) (e :^ 1) (Con i :^ j) ((e :^ i) :^ j) = = = = = = = = = 0∗e=0 e ∗ e = e2 em ∗ en = em+n (em)n = em∗n reduce $ Sum [e,(-1):*e'] zero reduce e Con $ i*j reduce $ (i*j) :* e one reduce e Con $ i^j reduce $ e :^ (i*j) 152 1∗e=e e0 = 1 e1 = e reduce (Sum es) = mkSum $ map mkScal dom ++ if c == 0 then [] else [Con c] where (c,dom,sc) = foldl f (0,[],const 0) $ map reduce es f st (Con 0) = st f (c,dom,sc) (Con i) = (c+i,dom,sc) f (c,dom,sc) (i:*e) = (c,insert e dom,update sc e $ sc e+i) f st e = f st (1:*e) mkScal e = reduce $ sc e :* e mkSum [] = zero mkSum [e] = e mkSum es = Sum es reduce (Prod es) = mkProd $ map mkExpo dom ++ if c == 1 then [] else [Con c] where (c,dom,ex) = foldl f (1,[],const 0) $ map reduce es f st (Con 1) = st f (c,dom,ex) (Con i) = (c*i,dom,ex) f (c,dom,ex) (e:^i) = (c,insert e dom,update ex e $ ex e+i) f st e = f st (e:^1) mkExpo e = reduce $ e :^ ex e mkProd [] = one mkProd [e] = e mkProd es = Prod es reduce e = e reduce übersetzt Ausdrücke in Ausdrücke und ist damit in der Compiler-Terminologie ein front-end-Optimierer. Diese optimieren Quell- oder Zwischencode, back-end-Optimierer optimieren Zielcode. 153 Beispiel Expr-Compiler von Syntaxbäumen in Assemblerprogramme und deren Ausführung in einer Kellermaschine Das Prinzip der Erzeugung von Zielcode aus Syntaxbäumen lässt sich am Beispiel des Datentyps Expr (dessen Elemente ja nichts anderes als Syntaxbäume sind) und einer kleinen Assemblersprache mit Kellerbefehlen ganz gut demonstrieren. Die Zielkommandos sind durch folgenden Datentyp gegeben: data StackCom = Push Int | Load String | Sub | Add Int | Mul Int | Pow Die (virtuelle) Zielmaschine besteht aus einem Keller für ganze Zahlen und einem Speicher (Menge von Variablenbelegungen) wie beim Interpreter arithmetischer Ausdrücke. Genaugenommen beschreibt ein Typ für diese beiden Objekte nicht diese selbst, sondern die Menge ihrer möglichen Zustände: type StateE = ([Int],Store) 154 Die Bedeutung der einzelnen Zielkommandos wird durch einen Interpreter auf State definiert: executeCom executeCom executeCom executeCom executeCom executeCom executeCom :: StackCom -> StateE -> (Push a) (stack,store) = (Load x) (stack,store) = Sub st = (Add n) st = (Mul n) st = Pow st = StateE (a:stack,store) (store x:stack,store) executeOp (foldl1 (-)) 2 st executeOp sum n st executeOp product n st executeOp (foldl1 (^)) 2 st Die Ausführung eines arithmetischen Kommandos besteht in der Anwendung der jeweiligen arithmetischen Operation auf die obersten n Kellereinträge, wobei n die Stelligkeit der Operation ist: executeOp :: ([Int] -> Int) -> Int -> StateE -> StateE executeOp f n (stack,store) = (f (reverse as):bs,store) where (as,bs) = splitAt n stack 155 Die Ausführung einer Befehlsliste besteht in der Hintereinanderausführung ihrer Elemente: executeE :: [StackCom] -> StateE -> StateE executeE = flip $ foldl $ flip executeCom Die Übersetzung eines arithmetischen Ausdrucks von seiner Baumdarstellung in eine Befehlsliste erfolgt wie die Definition aller Funktionen auf Expr-Daten induktiv: compileE compileE compileE compileE compileE compileE compileE compileE :: Expr -> [StackCom] (Con i) = [Push i] (Var x) = [Load x] (Sum es) = concatMap compileE es++[Add $ length es] (Prod es) = concatMap compileE es++[Mul $ length es] (e :- e') = compileE e++compileE e'++[Sub] (i :* e) = Push i:compileE e++[Mul 2] (e :^ i) = compileE e++[Push i,Pow] 156 Beginnt die Ausführung des Zielcodes eines Ausdrucks e im Zustand ([],store), dann endet sie im Zustand ([a],store), wobei a der Wert von e unter der Variablenbelegung store ist: executeE (compileE e) ([],store) = ([evalE e store],store). Die Gleichung lässt sich durch Induktion über den Aufbau von e zeigen. Wählt man executeE als Zielinterpreter, dann beschreibt sie im Sinne des Abschnitts CompilerKorrektheit die Korrektheit von compileE. 157 Die Ausdrücke 5*11+6*12+x*y*z und 11*x^3+5*x^2+16*x+33 als Expr-Objekte Z.B. übersetzt compileE die obigen Expr-Objekte in folgende Kommandosequenzen: 0: 1: 2: 3: 4: 5: 6: 7: Push 5 Push 11 Mul 2 Push 6 Push 12 Mul 2 Load "x" Load "y" 8: Load "z" 9: Mul 3 10: Add 3 0: 1: 2: 3: 4: 5: 6: 7: Push 11 Load "x" Push 3 Pow Mul 2 Push 5 Load "x" Push 2 8: 9: 10: 11: 12: 13: 14: Pow Mul 2 Push 16 Load "x" Mul 2 Push 33 Add 4 Eine Testumgebung für evalE, executeE und compileE wird im Abschnitt Monadische String-Parser vorgestellt. 158 ` Datentypen mit Attributen data DT a1 ... am = Constructor_1 e_11 ... e_1n_1 | ... | Constructor_k e_k1 ... e_kn_k e_11,...,e_kn_k sind beliebige Typausdrücke, die aus Typkonstanten (z.B. Int), Typvariablen (a1,...,am) und Typkonstruktoren (Produktbildung, Listenbildung, Datentypen) zusammengesetzt sind. Kommt DT selbst in einem dieser Typausdrücke vor, spricht man von einem rekursiven Datentyp. Die Elemente von DT a1 ... am sind alle funktionalen Ausdrücke, die aus den Konstruktoren Constructor_1,...,Constructor_k und Elementen von Instanzen von a1,...,am zusammengesetzt sind. Als Funktion hat Constructor_i den Typ e_i1 -> ... -> e_in_i -> DT a1 ... am. Da Konstruktoren ihre Argumente nicht verändern, sondern nur kapseln, möchte man oft auf diese direkt zugreifen. Das erreicht man durch Einführung von Attributen, eines für jede Argumentposition des Konstruktors. In der Definition von DT wird Constructor_i e_i1 ... e_in_i ersetzt durch Constructor_i {attribute_i1 :: e_i1, ..., attribute_in_i :: e_in_i}. 159 Z.B. lassen sich die aus anderen Programmiersprachen bekannten Recordtypen und Objektklassen als Datentypen mit genau einem Konstruktor, aber mehreren Attributen implementieren. Wie ein Konstruktor, so ist auch ein Attribut eine Funktion. Als solche hat attribute_ij den Typ attribute_ij :: DT a1 ... am -> e_ij Attribute sind also invers zu Konstruktoren. Man nennt sie deshalb heute auch Destruktoren. attribute_ij (Constructor_i t1 ... tn_i) hat den Wert tj. Die Objektdefinition obj = Constructor_i t1 ... tn_i ist äquivalent zu obj = Constructor_i {attribute_i1 = t1, ..., attribute_in_i = tn_i}. Update von attribute_ij: obj' = obj {attribute_ij = t} 160 Rekursive Definitionen funktionaler Attribute sind verboten. Folglich kann attribute_ij nur dann innerhalb des Ausdrucks tk verwendet werden, wenn es unter diesem Namen noch ein anderes Objekt gibt, das nicht als Attribut deklariert wurde. In O’Haskell gibt es ein eigenes Typkonstrukt (struct) für Recordtypen: struct Record = attribute_1 ... attribute_n Objektdefinition: record struct attribute_1 = ... attribute_n = Attributaufruf: a = record.attribute_i :: e1 :: en t1 tn Auch hier sind rekursive Attributdefinitionen verboten. Allerdings dürfen funktionale Attribute zumindest applikativ definiert werden wie z.B. in attribute_i p = t. Derselbe Konstruktor oder dasselbe Attribut darf nicht in mehreren Daten- oder Recordtypen verwendet werden! 161 ` Ein Haskell-Datentyp für Algebren Sei Σ = (S, F, BΣ, BA) eine Signatur, BΣ = (BS, BF, BΣ0, BA0), S \ BS = {s1, . . . , sk }, F \ BF = { f11 :: w11 → s1, . . . , f1n1 :: w1n1 → s1, ... fk1 :: wk1 → sk , . . . , fknk :: wknk → sk }. Jede Σ-Algebra kann als Element des folgenden polymorphen Datentyps implementiert werden: data SigAlg s1 . . . sk = SigAlg { (f11 :: w11 → s1)0, . . . , (f1n1 :: w1n1 → s1)0, ... (fk1 :: wk1 → sk )0, . . . , (fknk :: wknk → sk )0 } wobei (f :: → s)0 =def f :: s (f :: s1 . . . sn → s)0 =def f :: s1 → . . . → sn → s Die Sorten von S \ BS sind also als Typvariablen implementiert, die durch die (Typen der) entsprechenden Trägermengen der jeweiligen Algebra instanziiert werden. Die Funktionssymbole von F \ BF sind als Attribute implementiert, die durch die entsprechenden Operationen der jeweiligen Algebra instanziiert werden. 162 Beispiel Die Klasse der JavaLight-Algebren als Datentyp data JavaAlg commands command intE sumSect product prodSect factor boolE conjunct literal = JavaAlg {seq :: command -> commands -> commands, skip :: command, embedC :: command -> commands, assign :: String -> intE -> command, cond :: boolE -> commands -> commands -> command, cond1, loop :: boolE -> commands -> command, sum :: product -> sumSect -> intE, addSect, subSect :: product -> sumSect -> sumSect, emptyS :: sumSect, prod :: factor -> prodSect -> product, mulSect, divSect :: factor -> prodSect -> prodSect, emptyP :: prodSect, embedI :: Int -> factor, var :: String -> factor, encloseI :: intE -> factor, or :: conjunct -> boolE -> boolE, embedA :: conjunct -> boolE, and :: literal -> conjunct -> conjunct, embedL :: literal -> conjunct, embedB :: Bool -> literal, rel :: String -> intE -> intE -> literal, not :: literal -> literal, encloseB :: boolE -> literal} 163 Die JavaLight-Termalgebra data Commands = Seq Command Commands | EmbedC Command data Command = Skip | Assign String IntE | Cond BoolE Commands Commands | Cond1 BoolE Commands | Loop BoolE Commands data IntE = Sum Product SumSect data SumSect = AddSect Product SumSect | SubSect Product SumSect | EmptyS data Product = Prod Factor ProdSect data ProdSect = MulSect Factor ProdSect | DivSect Factor ProdSect | EmptyP data Factor = EmbedI Int | Var String | EncI IntE data BoolE = Or Conjunct BoolE | EmbedA Conjunct data Conjunct = And Literal Conjunct | EmbedL Literal data Literal = EmbedB Bool | Rel String IntE IntE | Not Literal | EncB BoolE javaTerm :: JavaAlg Commands Command IntE SumSect Product ProdSect Factor BoolE Conjunct Literal javaTerm = JavaAlg EmbedC Seq Skip Assign Cond Cond1 Loop Sum AddSect SubSect EmptyS Prod MulSect DivSect EmptyP EmbedI Var EncI EmbedA Or EmbedL And EmbedB Rel Not EncB 164 Beispiel (siehe Kontextfreie Grammatiken) 165 JavaLight-Interpreter evalCS :: JavaAlg a b c d e f g h i k -> Commands -> a evalCS alg (Seq c d) = seq alg (evalC alg c) $ evalCS alg d evalCS alg (EmbedC c) = embedC alg $ evalC alg c Analog werden die Interpreter von Termen der anderen Nicht-Basis-Sorten von JavaLight, also evalC evalI evalST evalP evalPT evalF evalB evalCJ evalL :: :: :: :: :: :: :: :: :: JavaAlg JavaAlg JavaAlg JavaAlg JavaAlg JavaAlg JavaAlg JavaAlg JavaAlg a a a a a a a a a b b b b b b b b b c c c c c c c c c d d d d d d d d d e e e e e e e e e f f f f f f f f f g g g g g g g g g h h h h h h h h h i i i i i i i i i k k k k k k k k k -> -> -> -> -> -> -> -> -> Command IntE SumSect Product ProdSect Factor BoolE Conjunct Literal -> -> -> -> -> -> -> -> -> b c d e f g h i k induktiv definiert (siehe evalA). Eine Testumgebung für die folgenden JavaLight-Algebren wird im Abschnitt JavaLight-Parser vorgestellt. Diese und alle anderen Haskell-Programme zu JavaLight (außer obigem Interpreter) stehen auch in der Datei Java.hs. 166 Die Wortalgebra von JavaLight javaWord :: JavaAlg String String String String String String String String String String javaWord = JavaAlg {seq = (++), embedC = id, skip = "; ", assign = \x e -> x++" = "++e++"; ", cond = \be c d -> "if "++be++cPars c++ " else"++cPars d, cond1 = \be c -> "if " ++be++cPars c, loop = \be c -> "while "++be++cPars c, sum = (++), addSect = (('+':) .) . (++), subSect = (('-':) .) . (++), emptyS = "", prod = (++), mulSect = (('*':) .) . (++), divSect = (('/':) .) . (++), emptyP = "", embedI = show, var = id, encloseI = rPars, or = (++) . (++" || "), embedA = id, and = (++) . (++" && "), embedL = id, embedB = show, rel = ((++) .) . flip (++), not = ('!':), encloseB = rPars} where cPars = (" {"++) . (++"}"); rPars = ('(':) . (++")") 167 Die Ableitungsalgebra von JavaLight javaDeri :: JavaAlg (Tree String) (Tree String) (Tree String) (Tree String) (Tree String) (Tree String) (Tree String) (Tree String) (Tree String) (Tree String) javaDeri = JavaAlg {seq_ = (F "commands" .) . two, embedC = F "command" . one, skip = F "command" [leaf ";"], assign = \x e -> F "command" [leaf x,leaf "=",e,leaf ";"], cond = \be c d -> F "command" $ leaf "if":be:cPars c++ leaf "else":cPars d, cond1 = \be c -> F "command" $ leaf "if":be:cPars c, loop = \be c -> F "command" $ leaf "while":be:cPars c, sum_ = (F "intE" .) . two, addSect = (F "sumSect" .) . three (leaf "+"), subSect = (F "sumSect" .) . three (leaf "-"), emptyS = leaf "sumSect", prod = (F "product" .) . two, mulSect = (F "prodSect" .) . three (leaf "*"), divSect = (F "prodSect" .) . three (leaf "/"), emptyP = leaf "prodSect", embedI = F "factor" . one . leaf . show, var = F "factor" . one . leaf, encloseI = F "factor" . rPars, 168 or_ = (F "boolE" .) . flip three (leaf "||"), embedA = F "boolE" . one, and_ = (F "conjunct" .) . flip three (leaf "&&"), embedL = F "conjunct" . one, embedB = F "literal" . single . leaf . show, rel = \rel -> (F "literal" .) . flip three (leaf rel), not_ = F "literal" . two (leaf "!"), encloseB = F "literal" . rPars} where one x = [x] two x y = [x,y] three x y z = [x,y,z] leaf = flip F [] cPars e = [leaf "",e,leaf ""] rPars e = [leaf "(",e,leaf ")"] 169 Beispiel (siehe Term-, Wort- und Ableitungsalgebra) 170 Das Zustandsmodell von JavaLight javaState :: JavaAlg (Store -> Store) (Store -> Store) (Store -> Int) (Store -> Int -> Int) (Store -> Int) (Store -> Int -> Int) (Store -> Int) (Store -> Bool) (Store -> Bool) (Store -> Bool) javaState = JavaAlg {seq_ = flip (.), embedC = id, skip = id, assign = \x e st -> update st x $ e st, cond = \be c d st -> if be st then c st else d st, cond1 = \be c st -> if be st then c st else st, loop = loop, sum_ = \e sect st -> sect st $ e st, addSect = \e sect st i -> sect st $ i+e st, subSect = \e sect st i -> sect st $ i-e st, emptyS = const id, prod = \e sect st -> sect st $ e st, mulSect = \e sect st i -> sect st $ i*e st, divSect = \e sect st i -> sect st $ i`div`e st, emptyP = const id, embedI = const, var = flip ($), encloseI = id, or_ = (|||), embedA = id, and_ = (&&&), embedL = id, embedB = const, rel = \rel e e' st -> evalRel rel (e st) $ e' st, not_ = (not .), encloseB = id} 171 where loop :: (Store -> Bool) -> (Store -> Store) -> Store -> Store loop be c st = if be st then loop be c $ c st else st (1) evalRel :: String -> Int -> Int -> Bool evalRel rel = case rel of "<" -> (<); ">" -> (>); "<=" -> (<=) ">=" -> (>=); "==" -> (==); "!=" -> (/=) Die Auswertung arithmetischer Ausdrücke (ohne Skalarmultiplikation und Exponentiation) im Zustandsmodell von JavaLight liefert dieselben Ergebnisse wie die Funktion evalE :: Expr -> (Store -> Store) im Abschnitt Haskell: Datentypen, d.h. evalE = evalI javaState. loop ist eine partielle Funktion, denn bekanntlich terminieren Schleifen nicht immer. Um zu zeigen, dass der im nächsten Abschnitt definierte JavaLight-Compiler korrekt bzgl. javaState ist, muss die mathematische Definition von loop, die hinter der Gleichung (1) steckt, genauer betrachtet werden. Wir werden das im Abschnitt Korrektheit des JavaLight-Compilers nachholen. 172 ` Attributierte Grammatiken Nach traditioneller Terminologie sind die Funktion evalE und evalCS javaState Interpreter, weil sie neben dem Quellprogramm eine “Eingabe”, hier: eine Variablenbelegung vom Typ Store, verlangen. An den Typen von evalE und evalCS javaState wird jedoch deutlich, dass die Unterscheidung zwischen Compiler und Interpreter keine substanzielle ist. Die Argumente dieser Funktionen sind nämlich keine Paare, bestehend aus Syntaxbaum und Variablenbelegung, sondern es sind - wie immer bei einem Compiler - allein die Syntaxbäume (oder Quellprogramme), die abgebildet werden, hier auf Funktionen von Store nach Store. Die Typen sind also Instanzen des polymorphen Typs a -> b -> c. Jede Funktion dieses Typs kann aber in eine Funktion des Typs (a,b) -> c umgewandelt werden und umgekehrt: uncurry :: (a -> b -> c) -> (a,b) -> c uncurry f (a,b) = f a b curry :: ((a,b) -> c) -> a -> b -> c curry f a b = f (a,b) (siehe Haskell: Funktionen). curry macht demnach aus dem Interpreter von a mit Eingabetyp b und Zielsprache c einen Compiler von a mit Zielsprache b -> c. 173 Parameter wie die Eingaben vom Typ b bezeichnet man auch als vererbte Attribute (inherited attributes) des Compilers. Allgemein bildet der Definitionsbereich einer funktionalen Zielalgebra alg = (b1 -> ... -> bk -> c) die Wertebereiche der vererbten Attribute des Compilers nach alg. Auch der Wertebereich von alg kann neben der Menge c der eigentlichen Zielobjekte weitere Komponenten enthalten, die dann abgeleitete Attribute (derived, synthesized attributes) genannt werden. Die Trägermengen von alg haben dann die Form b1 -> ... -> bk -> (c,c1,...,cn). Abgeleitete Attribute sind z.B. der Typ eines Ausdrucks, der Platzbedarf für seinen Wert, die Größe eines Zielobjekts c, etc. Ihre jeweiligen Werte werden bei der Berechnung von c aus Attributen von Komponenten von c ermittelt. Vererbte Attribute sind z.B. Positionen, Adressen, Schachtelungstiefen, etc. Diese müssen umgekehrt an das Zielobjekt oder an einzelne Komponenten desselben übergeben werden. Transiente Attribute sind gleichzeitig vererbt und abgeleitet. Ihre jeweiligen Werte bilden die Zustände, die einen Compiler zur Übergangsfunktion eines Automaten machen. In den folgenden Grammatikinterpretationen werden die Attributkomponenten der Trägermengen und ihre jeweilige Bedeutung besonders herausgestellt. 174 Beispiel Binärdarstellung rationaler Zahlen konkrete Syntax G rat → nat. | rat0 | rat1 abstrakte Syntax Σ(G) mkRat :: nat → rat app0, app1 :: rat → rat 0, 1 :: nat app0, app1 :: nat → nat nat → 0 | 1 | nat0 | nat1 G-Algebra A Arat enthält ein abgeleitetes Attribut mit Wertebereich R, welches das Inkrement liefert, um das sich der Dezimalwert einer rationalen Zahl erhöht, wenn an die Mantisse ihrer Binärdarstellung eine 1 angefügt wird. Arat = R × R Anat = N mkRatA : Anat → Arat mkRatA (val) = (1, val) app0A : Arat → Arat app0A (inc, val) = (inc/2, val) 0A : Anat 0A = 0 app0A : Anat → Anat app0A (n) = n ∗ 2 app1A : Arat → Arat app1A (inc, val)) = (inc/2, val + inc/2) 1A : Anat 1A = 1 app1A : Anat → Anat app1A (n) = n ∗ 2 + 1 175 Beispiel Strings mit Hoch- und Tiefstellungen konkrete Syntax G text → text box | text ↑ box | text ↓ box | box → (text)|a|b|c abstrakte Syntax Σ(G) app :: text box → text up :: text box → text down :: text box → text empty :: text mkBox :: text → box a, b, c :: box G-Algebra A Atext und Abox enthalten • ein vererbtes Attribut mit Wertebereich N2, das die Koordinaten der linken unteren Ecke des Rechtecks liefert, in das der eingelesene String geschrieben werden soll, • ein abgeleitetes Attribut mit Wertebereich N3, das die Länge sowie - auf eine feste Grundlinie bezogen - Höhe und Tiefe des Rechtecks liefert. 176 Atext = Abox = N2 → N3 × String appA : Atext × Abox → Atext appA(f, g)(x, y) = ((l + l0, max h h0, max t t0), str · str0) where ((l, h, t), str) = f (x, y) ((l0, h0, t0), str0) = g(x + l, y) upA : Atext × Abox → Atext 0 upA(f, g)(x, y) = ((l + l0, h + h0 − 1, max t (t0 − h + 1)), str·str ) where ((l, h, t), str) = f (x, y) ((l0, h0, t0), str0) = g(x + l, y + h − 1) downA : Atext × Abox → Atext downA(f, g)(x, y) = ((l + l0, max h (h0 − t − 1), t + t0 − 1), str·str0 ) where ((l, h, t), str) = f (x, y) ((l0, h0, t0), str0) = g(x + l, y − t + 1) empty A : Atext mkBoxA : Atext → Abox xA : Abox x ∈ {a, b, c} empty A(x, y) = ((0, 0, 0), t) mkBoxA = id xA(x, y) = ((1, 2, 0), x) 177 Beispiel Übersetzung regulärer Ausdrücke in erkennende Automaten Die folgende Graphgrammatik beschreibt die Übersetzung eines regulären Ausdrucks R in einen nichtdeterministischen Automaten, der L(R) erkennt: s ∅ R 0 s' s R 1 Regel 1 s' Regel 2 s' Regel 3 R s R|R' s' s R' s R.R' s' s R R' Regel 4 s' ε s plus(R) s' s ε R 178 ε s' Regel 5 Zunächst wird die erste Regel auf den gegebenen Ausdruck R angewendet. Es entsteht ein Graph mit zwei Knoten und einer mit R markierten Kante. Dieser wird nun mit den anderen Regeln schrittweise vergrößert, bis an allen seinen Kanten nur noch Alphabetsymbole stehen. Dann stellt er den Transitionsgraphen eines Automaten dar, der R erkennt. Mit obiger Graphgrammatik aus dem regulären Ausdruck R = ((a+|(bc)+)∗ konstruierter Automat zur Erkennung von L(R) 179 Während die obige Graphgrammatik von einer einzelnen (Übergangs-)Kante ausgeht und daraus schrittweise den Automaten erzeugt, beginnt die Auswertung eines regulären Ausdrucks in der folgenden Reg-Algebra mit der leeren Übergangsfunktion und fügt schrittweise Übergänge hinzu bzw. fasst die Übergangsfunktionen für Teilausdrücke zur Übergangsfunktion für ihre Verknüpfung mit einem regulären Operator zusammen. Reg-Algebra NDA (siehe Reguläre Ausdrücke...) Zustände werden als natürliche Zahlen dargestellt, Z ist das Eingabealphabet und δ : N × Z ∪ {} → [N] die Übergangsfunktion des Automaten. NDAreg enthält • die Übergangsfunktion als transientes Attribut, das mit der konstanten Funktion const[] initialisiert wird, • zwei vererbte Attribute mit Wertebereich N, die mit 0 bzw. 1 initialisiert werden und den Start- bzw. Endzustand des Automaten bezeichnen, • ein transientes Attribut mit Wertebereich N, das mit 2 initialisiert wird und die nächste ganze Zahl liefert, die als Zustandsname vergeben werden kann. 180 NDAreg = (N × Z ∪ {} → [N]) × N3 → N × (N × Z ∪ {} → [N]) NDAword = Z ∗ NDAsymbol = Z ∅NDA : NDAreg ∅NDA(s, s0, next, δ) = (next, delete(δ, s, s0)) (siehe Regel 2) NDA : NDAreg NDA(s, s0, next, δ) = (next, add(δ, s, , s0)) _NDA : NDAsymbol → NDAreg _NDA(a)(s, s0, next, δ) = (next, add(δ, s, a, s0)) _|NDA_ : NDAreg × NDAreg → NDAreg (f |NDAg)(s, s0, next, δ) = g(s, s0, next0, δ 0) where (next0, δ 0) = f (s, s0, next, δ) (siehe Regel 3) _ ·NDA _ : NDAreg × NDAreg → NDAreg (f ·NDA g)(s, s0, next, δ) = g(next, s0, next0, δ 0) (siehe Regel 4) where (next0, δ 0) = f (s, next, next + 1, δ) 181 plusNDA : NDAreg → NDAreg plusNDA(f )(s, s0, next, δ) = (next2, add(δ3, next1, , s0)) (siehe Regel 5) where next1 = next + 1 (next2, δ1) = f (next, next1, next1 + 1, δ) δ2 = add(δ1, s, , next) δ3 = add(δ2, next1, , next) x add(δ, s, x, s0) fügt den Übergang s → s0 zur Übergangsfunktion δ hinzu. delete(δ, s, s0) beseitigt alle Übergange von s nach s0. Haskell-Implementierung: add :: (Eq a,Eq b,Eq c) => (a -> b -> [c]) -> a -> b -> c -> a -> b -> [c] add f a b c = update f a $ update (f a) b $ insert c $ f a b delete :: (Eq a,Eq b) => (a -> b -> [c]) -> a -> b -> a -> b -> [c] delete f a b = update f a $ update (f a) b [] 182 Darstellung von Σ(G)-Termen als hierarchische Listen Beispiel Mit der unten definierten JavaLight-Algebra javaList wird der Syntaxbaum javaTerm des Programms fact = 1; while x > 0 {fact = fact*x; x = x-1;} in folgende Form gebracht: (Seq (Assign "fact" (Sum (Prod (EmbedI 1)) EmptyP) EmptyS)) (EmbedC (Loop (EmbedA (EmbedL (Rel ">" (Sum (Prod (Var "x")) EmptyP) EmptyS) (Sum (Prod (EmbedI 0)) EmptyP) EmptyS))) (Seq (Assign "fact" (Sum (Prod (Var "fact")) (MulSect (Var "x")) EmptyP) EmptyS)) (EmbedC (Assign "x" (Sum (Prod (Var "x")) EmptyP) (SubSect (Prod (EmbedI 1)) EmptyP) EmptyS))) Alle Trägermengen von javaList enthalten zwei vererbte Attribute vom Typ Bool bzw. Int. Der Boolesche Wert gibt an, ob der jeweilige Teilstring str hinter bzw. unter den vorangehenden geschrieben wird. Im zweiten Fall wird str um den Wert des zweiten Attributs eingerückt. 183 type BIS = Bool -> Int -> String javaList :: JavaAlg BIS BIS BIS BIS BIS BIS BIS BIS BIS BIS javaList = JavaAlg {seq_ = indent2 "Seq", embedC = indent1 "EmbedC", skip = indent0 "Skip", assign = indent1 . ("Assign "++) . show, cond = indent3 "Cond", cond1 = indent2 "Cond1", loop = indent2 "Loop", sum_ = indent2 "Sum", addSect = indent2 "AddSect", subSect = indent2 "SubSect", emptyS = indent0 "EmptyS", prod = indent2 "Prod ", mulSect = indent2 "MulSect", divSect = indent2 "DivSect", emptyP = indent0 "EmptyP", embedI = indentA "EmbedI", var = indentA "Var", encloseI = indent1 "EncloseI", or_ = indent2 "Or", embedA = indent1 "EmbedA", and_ = indent2 "And", embedL = indent1 "EmbedL ", embedB = indentA "EmbedB",, rel = indent2 . ("Rel "++) . show, not_ = indent1 "Not", encloseB = indent1 "EncloseB"} where indent0 x = blanks x [] indentA x a = indent0 $ enclose x $ show a indent1 x f = blanks x [f] indent2 x f g = blanks x [f,g] indent3 x a b c = blanks x [a,b,c] enclose x y = '(':x++' ':y++")" 184 blanks :: String -> [BIS] -> BIS blanks x fs b n = if b then str else '\n':replicate n ' '++str where str = case fs of [] -> x [f] -> first f [f1,f2] -> first f1++f2 False k [f1,f2,f3] -> first f1++f2 False k++f3 False k first f = enclose x $ f True k k = n+length x+2 Eine Testumgebung der Listenalgebra wird im Abschnitt JavaLight-Parser vorgestellt. 185 Beispiel Übersetzung von JavaLight in eine Assemblersprache Wir erweitern zunächst die Zielsprache StackCom des Expr-Compilers compileE um einen Speicherbefehl (Save), arithmetische und Boolesche Befehle sowie Sprungbefehle: data StackCom = Push Int | Load String | Save String | Add | Sub | Mul | Div | Or_ | And_ | Inv | Cmp String | Jump Int | JumpT Int | JumpF Int Zur Ausführung der Sprungbefehle muss der Zustandstyp der Kellermaschine um einen Befehlszähler (program counter) erweitert werden: type State = ([Int],Store,Int) Die Bedeutung der einzelnen Zielkommandos wird wieder durch einen Interpreter auf State definiert: 186 executeCom :: StackCom -> State -> State executeCom com (stack,store,pc) = case com of Push a -> (a:stack,store,pc) Load x -> (store x:stack,store,pc) Save x -> (s,update store x a,pc) where a:s = stack Add -> (a+b:s,store,pc) where a:b:s = stack Sub -> (a-b:s,store,pc) where a:b:s = stack Mul -> (a*b:s,store,pc) where a:b:s = stack Div -> (a`div`b:s,store,pc) where a:b:s = stack Or_ -> (signum (a+b):s,store,pc) where a:b:s = stack And_ -> (a*b:s,store,pc) where a:b:s = stack Inv -> ((a+1)`mod`2:s,store,pc) where a:s = stack Cmp rel -> (c:s,store,pc) where a:b:s = stack c = if evalRel rel a b then 1 else 0 Jump lab -> (stack,store,lab) JumpT lab -> (stack,store,if a == 1 then lab else pc) where a:s = stack JumpF lab -> (stack,store,if a == 0 then lab else pc) where a:s = stack 187 Der jeweilige Inhalt des Befehlszählers bestimmt die Reihenfolge, in der die Elemente einer Befehlsliste ausgeführt und ggf. wiederholt werden: execute :: [StackCom] -> State -> State execute cs = cycle where cycle state@(stack,store,pc) = if pc < length cs then cycle $ executeCom (cs!!pc) (stack,store,pc+1) else state Um Sprungziele (die Int-Parameter der Sprungbefehle) korrekt berechnen zu können, muss der Compiler die Position jedes erzeugten Befehls innerhalb des gesamten Zielprogramms kennen. Dieses erzeugt er durch Interpretation des Quellprogramms in der folgenden JavaLight-Algebra, deren Trägermengen • ein vererbtes Attribut vom Typ Int für die Nummer des ersten Befehls des Zielcodes und • ein abgeleitetes Attribut vom Typ Int für die Länge des Zielcodes enthalten: type ISI = Int -> ([StackCom],Int) 188 javaStack :: JavaAlg ISI ISI ISI (ISI -> ISI) ISI (ISI -> ISI) ISI ISI ISI ISI javaStack = JavaAlg {seq_ = concatCode, embedC = id, skip = const ([],0), assign = append . Save, cond = \be c d lab -> let ((code1,lg1),exit) = cond1 1 be c lab (code2,lg2) = d exit in (code1++Jump (exit+lg2):code2,lg1+lg2+1), cond1 = \be c -> fst . cond1 0 be c, loop = \be c lab -> append (Jump lab) (fst . cond1 1 be c) lab, sum_ = flip ($), addSect = \e -> (. flip (appendF Add) e), subSect = \e -> (. flip (appendF Sub) e), emptyS = id, prod = flip ($), mulSect = \e -> (. flip (appendF Mul) e), divSect = \e -> (. flip (appendF Div) e), emptyP = id, embedI = \i -> const ([Push i],1), var = \x -> const ([Load x],1), encloseI = id, or_ = appendF Or_, embedA = id, and_ = appendF And_, embedL = id, embedB = \b -> const ([Push $ if b then 1 else 0],1), rel = appendF . Cmp, not_ = append Inv, encloseB = id} 189 where append :: StackCom -> ISI -> ISI append com f lab = (code++[com],lg+1) where (code,lg) = f lab appendF :: StackCom -> ISI -> ISI -> ISI appendF com = (append com .) . concatCode concatCode :: ISI -> ISI -> ISI concatCode f g lab = (code1++code2,lg1+lg2) where (code1,lg1) = f lab (code2,lg2) = g $ lab+lg1 cond1 :: Int -> ISI -> ISI -> Int -> (([StackCom],Int),Int) cond1 n f g lab = ((code1++JumpF exit:code2,lg1+lg2+1),exit) where (code1,lg1) = f lab lab1 = lab+lg1+1 (code2,lg2) = g lab1 exit = lab1+lg2+n Eine Testumgebung des Compilers wird im Abschnitt JavaLight-Parser vorgestellt. 190 ` Haskell: Monaden class Monad m where (>>=) :: m a -> (a -> m b) -> m b sequentielle Komposition (>>) :: m a -> m b -> m b sequentielle Komposition ohne Wertübergabe return :: a -> m a Einbettung in monadischen Typ fail :: String -> m a Wert im Fall eines Matchfehlers m >> m' = m >>= const m' (m >>= f) >>= g = m >>= \x -> f x >>= g m >>= return = m return a >>= f = f a (1) class Monad m => MonadPlus m where steht im Modul Monad.hs mzero :: m a scheiternde Berechnung heißt zero in hugs mplus :: m a -> m a -> m a parallele Komposition heißt (++) in hugs mzero >>= f = mzero (2) m >>= \x -> mzero = mzero mzero `mplus` m = m m `mplus` mzero = m 191 Instanzen von Monad bzw. MonadPlus sollen die aufgeführten Gleichungen erfüllen. Sie wurden - mit _∗ anstelle von >>= bereits im Abschnitt Korrektheit monadischer Parser genannt. Alle bisher deklarierten Typen sind Typen erster Ordnung und haben den Kind ∗. Kinds sind Typen von Typen. Alle Kinds sind aus ∗ und → gebildet. Die Typvariable m (s.o.) und der Ausdruck Bintree (siehe Typklassen) sind Typen zweiter Ordnung und haben daher den Kind ∗ → ∗. Sie bezeichnen also Funktionen, die Typen erster Ordnung auf Typen erster Ordnung abbilden. JavaAlg bildet Typen erster Ordnung auf Typen siebenter Ordnung ab und hat deshalb den Kind ∗ → ∗ → ∗ → ∗ → ∗ → ∗ → ∗ → ∗. 192 Die do-Notation Ein Ausdruck m >>= \x -> m' vom Typ m a wird ersetzt durch do x <- m; m' do x <- m m' oder Wird die “Ausgabe” x von m nicht benötigt, dann genügt m anstelle der Zuweisung x <- m. Außerdem schreibt man do x <- m; y <- m'; m'' do x <- m; (do y <- m'; m'') anstelle von Der return-Operator ist im Sinn der Gültigkeit folgender Gleichungen neutral bzgl. des Komposition ; (siehe (1)): do x <- m; return x do x <- return a; m = = m m[a/x] (a substituiert für x) 193 Monaden-Kombinatoren (<-) :: Monad m => a -> m a -> m () (;) :: Monad m => m a -> m b -> m b when :: Monad m => Bool -> m () -> m () when b m = if b then m else return () guard :: MonadPlus m => Bool -> m () guard b = if b then return () else mzero (1) (2) ⇒ ⇒ (do guard True; m1; ...; mn) = (do m1; ...; mn) (do guard False; m1; ...; mn) = mzero msum :: MonadPlus m => [m a] -> m a msum = foldr mplus mzero 194 heißt concat in hugs sequence :: Monad m => [m a] -> m [a] sequence (m:ms) = do a <- m; as <- sequence ms; return $ a:as sequence _ = return [] heißt accumulate in hugs sequence_ :: Monad m => [m a] -> m () sequence_ = foldr (>>) $ return () heißt sequence in hugs mapM :: Monad m => (a -> m b) -> [a] -> m [b] mapM f = sequence . map f mapM_ :: Monad m => (a -> m b) -> [a] -> m () mapM_ f = sequence_ . map f zipWithM :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m [c] zipWithM f as = sequence . zipWith f as zipWithM_ :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m () zipWithM_ f as = sequence_ . zipWith f as lookupM :: (Eq a,MonadPlus m) => a -> [(a,b)] -> m b lookupM a ((a',b):s) = if a == a' then return b `mplus` lookupM a s else lookupM a s lookupM _ _ = mzero 195 Beispiel Der LL-Compiler für SAB (siehe Vom LL-Parser zum LL-Compiler lautet unter Verwendung von Monaden-Kombinatoren und der do-Notation wie folgt: data SABalg s a b = SABalg {f_p1 :: b -> s, f_p2 :: a -> s, f_p3 :: s, f_p4 :: s -> a, f_p5 :: a -> a -> a, f_p6 :: s -> b, f_p7 :: b -> b -> b} compStep_S :: MonadPlus m => Alg s a b -> String -> m s compStep_S alg w = msum [try_p1(w), try_p2(w), try_p3(w)] where try_p1 w = do (_,w) <- compStep_a w; (c,w) <- compStep_B w return (f_p1 alg c,w) try_p2 w = do (_,w) <- compStep_b w; (c,w) <- compStep_A w return (f_p2 alg c,w)) try_p3 w = (f_p3 alg,w) 196 compStep_A :: MonadPlus m => Alg s a b -> String -> m a compStep_A(w) = msum [try_p4(w), try_p5(w)] where try_p4 w = do (_,w) <- compStep_a w; (c,w) <- compStep_S w return (f_p4 alg c,w) try_p5 w = do (_,w) <- compStep_b w; (c,w) <- compStep_A w (d,w) <- compStep_A w; return (f_p5 alg c d,w) compStep_B :: MonadPlus m => Alg s a b -> String -> m b compStep_B w = msum [try_p6 w, try_p7 w ] where try_p6 w = do (_,w) <- compStep_b w; (c,w) <- compStep_S w return (f_p6 alg c,w) try_p7 w = do (_,w) <- compStep_a w; (c,w) <- compStep_B w (d,w) <- compStep_B w; return (f_p7 alg c d),w) compStep_a(z:w) compStep_b(z:w) compStep_a[] compStep_b[] = = = = do guard $ z == a; return (a,w) do guard $ z == b; return (b,w) mzero mzero 197 Die Identitätsmonade newtype Id a = Id {entry :: a} instance Monad Id where Id a >>= f = f a return = Id Mit dieser Monade kann man nicht allzuviel anfangen, aber ganz gut die Verwandtschaft zwischen monadischer und imperativer Programmierung erkennen. So lautet z.B. eine monadische - imperative Programmierer würden sagen: prozedurale - Version der Funktion sumTree :: Bintree Int -> Int sumTree (Branch left a right) = a+sumTree left+sumTree right sumTree _ = 0 folgendermaßen: sumTree = entry . f where f (Branch left a right) = do b <- f left c <- f right return $ a+b+c f _ = return 0 198 Die Maybe-Monade data Maybe a = Just a | Nothing instance Monad Maybe where Just a >>= f = f a _ >>= _ = Nothing return = Just fail _ = mzero erfüllt (1) instance MonadPlus Maybe where mzero = Nothing erfüllt (2) Nothing `mplus` m = m m `mplus` _ = m 199 Verwendung der Maybe-Monade Rechnen mit partiellen Funktionen Eine partielle Funktion f : A → B wird in Haskell durch f :: A -> Maybe B implementiert. Das Ergebnis der Komposition zweier partieller Funktionen f und g wird durch f ‘comp‘ g implementiert: comp f g :: (a -> Maybe b) -> (b -> Maybe c) -> a -> Maybe c comp f g = do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b Nach Definition von >>= in der Maybe-Monade ist (f ‘comp‘ g) a genau dann definiert (hat also einen Wert der Form Just c), wenn f (a) und f (g(a) definiert sind. c ist dann gleich f (g(a), m.a.W. case f a of Just b -> g b; _ -> Nothing ist äquivalent zu: do b <- f a; g b Nach Definition von mzero bzw. mplus in der Maybe-Monade gilt z.B.: case f a of Just b | p b -> g b; _ -> Nothing ist äquivalent zu: do b <- f a; guard $ p b; g b case f a of Nothing -> g a; b -> b ist äquivalent zu: f a `mplus` g a 200 Beispiel Die folgende Variante split2 von filter wendet zwei Boolesche Funktionen f und g auf die Elemente einer Liste s und ist genau dann definiert, wenn für jedes Listenelement x f (x) oder g(x) gilt. Im definierten Fall liefert split2 das Listenpaar, bestehend aus f ilter(f )(s) und f ilter(g)(s): split2 :: (a -> Bool) -> (a -> Bool) -> [a] -> Maybe ([a],[a]) split2 f g (x:s) = do (s1,s2) <- split2 f g s if f x then Just (x:s1,s2) else do guard $ g x; Just (s1,x:s2) split2 _ _ _ = Just ([],[]) 201 Eine Exception-Monade für differenzierte Fehlermeldungen (vom Typ a) data Either a b = Left a | Right b instance (Ord err,Bounded err) => Monad (Either a) where Left a >>= _ = Left a Right b >>= f = f b return = Right fail _ = mzero erfüllt (1) instance (Ord err,Bounded err) => MonadPlus (Either a) where mzero = Left minBound erfüllt (2) Left a `mplus` Left a' = Left $ max a a' Left _ `mplus` m = m m `mplus` _ = m 202 Die Listenmonade instance Monad [ ] where as >>= f = concatMap f as return a = [a] fail _ = mzero instance MonadPlus [ ] where mzero = [] mplus = (++) 203 erfüllt (1) erfüllt (2) Verwendung der Listenmonade Rechnen mit nichtdeterministischen Funktionen Eine nichtdeterministische Funktion f : A → ℘(B) kann in Haskell durch f :: A -> [B] implementiert werden. Das Ergebnis der Komposition zweier nichtdeterministischer Funktionen f und g wird durch f ‘comp‘ g implementiert: comp f g :: (a -> [b]) -> (b -> [c]) -> a -> [c] comp f g = do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b Nach Definition von >>= in der Listenmonade gilt: (f `comp` g) a ist äquivalent zu: concat [g b | b <- f a] Die Listeninstanz der Monadenfunktion sequence :: [m a] -> m [a] hat den Typ [[a]] -> [[a]] und liefert das - als Liste von Listen dargestellte - kartesische Produkt seiner Argumentlisten as1, . . . , asn: sequence[as1, . . . , asn] = [[a1, . . . , an] | ai ∈ asi, 1 ≤ i ≤ n] = as1 × · · · × asn. 204 Nichtmonadisch ist sequence demnach folgendermaßen definiert: sequence (as:bss) = [a:bs | a <- as, bs <- sequence bss] sequence _ = [[]] Beispiel sequence $ replicate(3)[1..4] ; [[1,1,1],[1,1,2],[1,1,3],[1,1,4],[1,2,1],[1,2,2],[1,2,3],[1,2,4], [1,3,1],[1,3,2],[1,3,3],[1,3,4],[1,4,1],[1,4,2],[1,4,3],[1,4,4], [2,1,1],[2,1,2],[2,1,3],[2,1,4],[2,2,1],[2,2,2],[2,2,3],[2,2,4], [2,3,1],[2,3,2],[2,3,3],[2,3,4],[2,4,1],[2,4,2],[2,4,3],[2,4,4], [3,1,1],[3,1,2],[3,1,3],[3,1,4],[3,2,1],[3,2,2],[3,2,3],[3,2,4], [3,3,1],[3,3,2],[3,3,3],[3,3,4],[3,4,1],[3,4,2],[3,4,3],[3,4,4], [4,1,1],[4,1,2],[4,1,3],[4,1,4],[4,2,1],[4,2,2],[4,2,3],[4,2,4], [4,3,1],[4,3,2],[4,3,3],[4,3,4],[4,4,1],[4,4,2],[4,4,3],[4,4,4]] Nach Definition von mzero bzw. mplus in der Listenmonade gilt z.B.: do b <- f a; guard (p b); g b ist äquivalent zu: concat [g b | b <- f a, p b] do a <- s; let b = f a; guard (p b); [h b] ist äquivalent zu: [h b | a <- s, let b = f a, p b] 205 ` Transitionsmonaden Trans state a state -> (a,state) total functions Parser m nondeterministic functions String/state [ ]/m TransM state m a state -> m (a,state) Either a/m Maybe/m partial functions functions with exceptions of type a 206 Die Monade totaler Transitionsfunktionen newtype Trans state a = T {trans :: state -> (a,state)} instance Monad (Trans state) where T m >>= f = T $ \st -> let (a,st') = m st in trans (f a) st' return a = T $ \st -> (a,st) Die IO-Monade type IO a = Trans STATE a STATE ist hier der Typ interner Systemzustände. 207 Verwendung der IO-Monade Ein/Ausgabe von Funktionsargumenten bzw. -werten test :: (Read a,Show b) => (a -> b) -> IO() test f = do str <- readFile "source" writeFile "target" $ show $ f $ read str readFile :: String -> IO String readFile "source" liest den Inhalt der Datei source und gibt ihn als String zurück. read :: Read a => String -> a read übersetzt einen String in ein Objekt vom Typ a. show :: Show b => b -> String show übersetzt ein Objekt vom Typ b in einen String. writeFile :: String -> String -> IO () writeFile "target" schreibt einen String in die Datei target. 208 Ein/Ausgabe-Schleifen loop :: IO () loop = do putStrLn "Hello!" putStrLn "Enter an integer x!" str <- getLine let x = read str lokale Definition, gilt in allen darauffolgenden Kommandos. Nicht mit einer lokalen Definition let a = e in e' verwechseln, die nur in e' gilt! if x < 5 then do putStr "x < 5" else do putStrLn ("x = "++show x) loop then und else müssen bzgl. der Spalte ihres ersten Zeichen hinter if stehen! putStr :: String -> IO () putStr str schreibt str ins Konsolenfenster. putStrLn :: String -> IO () putStrLn str schreibt str ins Konsolenfenster und geht in die nächste Zeile. getLine :: IO String getLine liest den eingebenen String und geht in die nächste Zeile. 209 Datei lesen mit Fehlerbehandlung und Inhaltsübergabe an Folgeprozedur readFileAndDo :: String -> (String -> IO ()) -> IO () readFileAndDo file continue = do str <- readFile file `catch` const (return "") if null str then putStrLn $ file++" does not exist" else continue str test f = readFileAndDo "source" $ writeFile "target" . show . f . read 210 Die Funktionen der Graphikprogramme PainterHGL.hs und PainterSVG.hs rufen die folgende Schleife auf, die bei jedem Durchlauf ein Objekt vom Typ a mit interaktiv eingegebenen Skalierungsfaktoren zeichnet: readFileAndDraw :: Read a => String -> (Float -> Float -> a -> (Window -> IO (),Pos)) -> IO () readFileAndDraw file draw = readFileAndDo file $ scale . read where scale a = do putStrLn "Enter a horizontal and a vertical scaling factor!" str <- getLine; let strs = words str when (length strs == 2) $ do let [hor,ver] = map read strs (act,size) = draw hor ver a runGraphics $ bzw. writeFile (file++".svg") do w <- openWindow file size $ svg act size back w white scale a frame w size; act w getLBP w; closeWindow w scale a 211 Die Huckepack-Monade monadenabhängiger Transitionsfunktionen hat in Haskell-Bibliotheken den Namen StateT (state transformer) und unterscheidet sich von der Zustandsmonade dadurch, das deren Wertetyp (a,state) in eine Monade eingebettet wird: newtype TransM state m a = TM {transM :: state -> m (a,state)} instance MonadPlus m => Monad (TransM state m) where TM m >>= f = TM $ \st -> m st >>= \(a,st) -> transM (f a) st return a = TM $ \st -> return (a,st) fail _ = mzero instance MonadPlus m => MonadPlus (TransM state m) where mzero = TM $ const mzero TM m `mplus` TM m' = TM $ \st -> m st `mplus` m' st 212 TransM state m liftet die Operationen >>=, mplus, return, fail und mzero von m zum Funktionenraum state -> m (a,state) und zwar so, dass die Gültigkeit der Gleichungen (1) und (2) von m auf TransM state m übertragen werden. m >>= f führt zunächst die Transition m aus. Ihr Endzustand und ihre Ausgabe(n) werden zum Anfangszustand bzw. zur Eingabe der zweiten Transition f. Das Ergebnis von m ‘mplus‘ m’ hängt von der Instanz der Typvariablen m ab: Bei Instanziierung durch die Maybe- oder Exception-Monade arbeitet ‘mplus‘ deterministisch: Liefert m ein definiertes Ergebnis, so ist dieses auch das Ergebnis von m ‘mplus‘ m’. Scheitert m jedoch, dann wird die Ausgabe von m’ zur Ausgabe von m ‘mplus‘ m’. Bei Instanziierung durch die Listenmonade arbeitet ‘mplus‘ nichtdeterministisch: die Ausgabe von m ‘mplus‘ m’ ist die Konkatenation der Ausgaben von m und m’. Demnach lassen sich mit der Huckepackmonade deterministische wie auch nichtdeterministische Automaten implementieren: Die Zustandsmenge ist eine Instanz von state, Übergangs- und Ausgabefunktion sind durch fst . transM bzw. snd . transM gegeben. 213 Weitere TransM-Kombinatoren sat trans f (“satisfies”) bildet jeden Zustand, der bei Anwendung von trans eine Ausgabe produziert, welche die Bedingung f verletzt, auf das Nullelement mzero von m ab: sat :: MonadPlus m => TransM state m a -> (a -> Bool) -> TransM state m a sat trans f = do a <- trans; guard $ f a; return a some trans und many trans wiederholen trans solange wie diese Funktion ein Ergebnis ungleich mzero liefert. Alle durch die aufeinanderfolgenden Anwendungen von trans erzeugten Ausgaben werden aufgelistet. some trans scheitert (liefert mzero), falls schon die erste Anwendung von trans scheitert. many trans gibt in diesem Fall den Ausgangszustand und die leere Ausgabeliste zurück. some, many :: MonadPlus m => TransM state m a -> TransM state m [a] some trans = do a <- trans; as <- many trans; return $ a:as many trans = some trans `mplus` return [] 214 ` Monadische String-Parser Ein String-Parser liest eine Zeichenfolge von links nach rechts, übersetzt das jeweils gelesene Teilwort in ein Objekt eines Typs a und gibt daneben das jeweilige Restwort aus. Betrachtet man ihn als Automaten, dann bilden die Eingabewörter seine Zustände und die Objekte vom Typ a seine Ausgaben. Demzufolge kann er als Objekt der Instanz type Parser = TransM String der Huckepack-Monade implementiert werden. Ein vollständiger Parser wird aufgebaut aus den o.g. Monadenkonstanten bzw. -kombinatoren wie return, mzero, >>=, mplus, mapM, sat und many sowie den folgenden, die typische Scannerfunktionen realisieren. Alle diese Komponenten sind polymorph in der (Ausgabe-)Monade m, d.h. erst die spätere Instanziierung durch Maybe, Either oder [ ] legt fest, ob der Parser deterministisch oder nichtdeterministisch arbeitet, also höchstens ein korrektes Ergebnis berechnet oder alle (s.o). 215 Read-Instanzen einbinden Wir formulieren den Parser readTree :: Read a => ReadS (Bintree a) für binäre Bäume aus dem Abschnitt Typklassen monadisch: bintree :: Read a => Parser [ ] (Bintree a) bintree = do a <- TM reads msum [do "(" <- TM lex; left <- bintree msum [do "," <- TM lex; right <- bintree ")" <- TM lex return $ Branch left a right, do ")" <- TM lex return $ Branch left a Empty], do return $ leaf a] instance Read a => Read (Bintree a) where readsPrec _ = transM bintree rb1 = read "5(7(3, 8),6( 2) ) " :: Bintree Int ; 5(7(3,8),6(2)) rb2 = reads "5(7(3, 8),6( 2) ) " :: [(Bintree Int,String)] ; [(5(7(3,8),6(2))," "),(5,"(7(3, 8),6( 2) ) ")] rb3 = read "5(7(3,8),6(2))hh" :: Bintree Int ; Exception: ...: no parse rb4 = reads "5(7(3,8),6(2))hh" :: [(Bintree Int,String)] ; [(5(7(3,8),6(2)),"hh"),(5,"(7(3,8),6(2))hh")] 216 Monadische Scanner Standard-Parse-Funktionen wie reads und lex sind auf nichtdeterministische Parser zugeschnitten. Im Folgenden definieren und verwenden wir nur generische Parser, deren Ausgabemonade – wie oben erwähnt – beliebig instanziiert werden kann. Die ersten hier angegebenen Parser sind insofern Scanner, als sie Symbole oder Elemente von Basistypen erkennen und ggf. weiterreichen, aber daraus keine komplexeren Zielobjekte konstruieren. getChr liest das erste Zeichen eines Strings und gibt es als Ergebnis aus: getChr :: MonadPlus m => Parser m Char getChr = TM $ \str -> do c:str <- return str; return (c,str) Ein Matchfehler bei der Zuweisung führt zum Wert von fail, der von der Instanziierung von m abhängt (s.o.). char chr und string str erwarten das Zeichen chr bzw. den String str: char :: MonadPlus m => Char -> Parser m Char char = sat getChr . (==) string :: MonadPlus m => String -> Parser m String string = mapM char 217 token p erlaubt vor und hinter des von p erkannten Strings Leerzeichen, Zeilenumbrüche oder Tabulatoren: MonadPlus m => Parser m a -> Parser m a token p = do space; a <- p; space; return a where space = many $ sat getChr isDelim tchar :: MonadPlus m => Char -> Parser m Char tchar = token . char isDelim isSpecial isDigit isLetter = = = = (`elem` (`elem` (`elem` (`elem` " \n\t") "();=!>+-*^") ['0'..'9']) ['a'..'z']++['A'..'Z']) 218 Es folgen vier Scanner, die Elemente von Basistypen (Wahrheitswerte, natürliche Zahlen, ganze Zahlen bzw. Identifier) erkennen und in entsprechende Haskell-Typen übersetzen. bool :: MonadPlus m => Parser m Bool bool = msum [do token $ string "True"; return True, do token $ string "False"; return False] nat,int :: MonadPlus m => Parser m Int nat = do ds <- some $ sat getChr isDigit; return $ read ds int = nat `mplus` do char '-'; n <- nat; return $ -n identifier :: MonadPlus m => Parser m String identifier = do first <- sat getChr isLetter rest <- many $ sat getChr $ not . (isSpecial ||| isDelim) let x = first:rest guard $ x `notElem` words "true false if then else while" return x 219 Ein monadischer Parser für reguläre Ausdrücke Aus Monaden-Kombinatoren und o.a. Scannerfunktionen lässt sich sehr leicht ein monadischer Parser für reguläre Ausdrücke - z.B. über Char - zusammensetzen. Wir erweitern den im Abschnitt Haskell: Datentypen im dortigen Parser verwendeten Datentyp Reg um einen Konstruktor Sat, dessen Parser wie Chr ein Zeichen liest, dieses aber nur dann akzeptiert, wenn es die Bedingung im Argument von Sat erfüllt: data RegM = Eps | Chr Char | Sat (Char -> Bool) | Reg :+ Reg | Reg :* Reg | Plus Reg | Star Reg | Refl Reg reg reg reg reg reg reg reg :: MonadPlus m => RegM -> Parser m String Eps = return [] (Chr c) = do char c; return [c] (Sat f) = do c <- sat getChr f; return [c] (e :+ e') = reg e `mplus` reg e' (e :* e') = do str <- reg e; str' <- reg e'; return $ str++str' (Plus e) = do str <- reg e msum [do str' <- reg $ Star e; return $ str++str', return str] reg (Star e) = reg (Plus e) `mplus` return [] reg (Refl e) = reg e `mplus` return [] 220 Expr-Parser parseExpr str übersetzt, falls möglich, den String str in das durch ihn bezeichnete Expr-Objekt: parseExpr :: MonadPlus m => Parser m Expr parseExpr = do a <- summand msum [do tchar '-'; b <- summand; return $ a :- b, do as <- many $ do tchar '+'; summand return $ if null as then a else Sum $ a:as] summand :: MonadPlus m => Parser m Expr summand = do a <- factor msum [do tchar '^'; i <- int; return $ a :^ i, do as <- many $ do tchar '*'; factor return $ if null as then a else Prod $ a:as] factor :: MonadPlus m => Parser m Expr factor = msum [do x <- token identifier; return $ Var x, do i <- token int; scalar i, do char '('; a <- parseExpr; tchar ')'; return a] 221 scalar :: MonadPlus m => Int -> Parser m Expr scalar i = msum [do tchar '*'; a <- summand; return $ i :* a, return $ Con i] Die vom Parser implizit vorgenommene Auszeichnung von Summanden bzw. Faktoren innerhalb der Menge aller Expr-Objekte dient nicht nur der Berücksichtigung von Operatorprioritäten (+ und - vor * und ^), sondern auch der Vermeidung linksrekursiver Aufrufe des Parsers: Zwischen je zwei aufeinanderfolgenden Aufrufen muss mindestens ein Zeichen gelesen werden, damit der zweite Aufruf ein kürzeres Argument hat als der erste und so die Termination des Parsers gesichert ist. 222 Testumgebung für Expr import PainterSVG (drawTerm,readFileAndDo) evalFromFile :: Int -> IO () evalFromFile n = readFileAndDo "source" $ eval n . read eval :: Int -> Expr -> IO () eval n exp = case n of 1 -> do writeFile "term" $ show exp drawTerm "term" 2 -> do writeFile "norm" $ show $ reduce exp drawTerm "norm" 3 -> loop1 exp _ -> do let code = compileE exp writeFile "code" $ showCode code loop2 code compile :: Int -> IO () compile n = readFileAndDo "source" act where act str = case transM parseE str of Just (exp,rest) -> do unparsedSuffix rest eval n exp _ -> putStrLn "syntax error" 223 input :: IO ([String],Store,Bool) input = do putStrLn "Enter variables!"; str <- getLine let vars = words str putStrLn "Enter values!"; str <- getLine let vals = map read $ words str vals' = vals++replicate (length vars-length vals) 0 return (vars, listsToFun 0 vars vals', nonempty vars) loop1 :: Expr -> IO () loop1 exp = do (_,store,proceed) <- input let result = evalE exp store when proceed $ do putStrLn $ "result = "++show result loop1 exp loop2 :: [StackCom] -> IO () loop2 code = do (_,store,proceed) <- input let (result:_,_) = executeE code ([],store) when proceed $ do putStrLn $ "result = "++show result loop2 code 224 unparsedSuffix :: String -> IO () unparsedSuffix rest = when (nonempty rest) $ putStrLn $ "unparsed suffix: "++rest showCode :: [StackCom] -> String showCode cs = fold2 f "" [0..length cs-1] cs where f str n c = str++'\n':replicate (5-length lab) ' ' ++lab++": "++show c where lab = show n evalFromFile n erwartet ein Expr-Objekt exp in der Datei source und führt eval n darauf aus. eval 1 zeichnet exp und die Normalform von exp in ein Fenster mit dem Titel term bzw. norm. eval 2 und eval 3 starten eine Schleife, die in jedem Durchlauf eine Variablenbelegung einliest und den entsprechenden Wert von exp ausgibt. eval 2 ruft dazu den Expr-Interpreter evalE auf, eval 3 übersetzt exp mit compileE in Zielcode und führt diesen mit executeE aus. compile n erwartet Quellcode für ein Expr-Objekt exp in der Datei source, parsiert ihn und führt eval n darauf aus. readFileAndDo wurde im Abschnitt Transitionsmonaden definiert. 225 ` Monadische Compiler Mit Hilfe monadischer Operationen lässt sich das Schema eines LL-Compilers ein weiteres Mal vereinfachen und sogar noch insofern verallgemeinern, als durch die Wahl der Zielmonade gesteuert werden kann, ob er deterministisch oder nichtdeterministisch arbeitet, ob zu jeder Eingabe höchstens ein Übersetzungsresultat oder alle möglichen Zielobjekte ausgegeben werden sollen, ob Fehlermeldungen hinzugefügt werden oder nicht. Monadischer LL-Compiler mit Backtracking Sei G = (N, Z, P, BΣ, BA) eine bewachte CFG, P = { p11 = (A1 → v11), . . . , p1n1 = (A1 → v1n1 ), ... pk1 = (Ak → vk1), . . . , pknk = (Ak → vknk )} und alg eine G-Algebra. Wir implementieren die Funktion compStep (siehe Vom LLParser zum LL-Compiler) unter Verwendung der Parser-Monade: 226 compStep_Ai :: MonadPlus m => Alg a1 ... ak -> Parser m ai 1 ≤ i ≤ k compStep_A :: MonadPlus m => Parser m A A ∈ BS Compiler, der die Elemente von BA_A erkennt und übersetzt compStep_x :: MonadPlus m => Parser m String x ∈ Z \BA compStep_Ai alg = msum [try_pi1,...,try_pin_i] 1 ≤i ≤k where try_pij = return f_pij alg 1 ≤ j ≤ ni mit vij = try_pij = do a1 <- compStep_x1 1 ≤ j ≤ ni mit vij 6= a2 <- compStep_x2 x1 . . .xn = vij . . . an <- compStep_xn {i1, . . . , im} = {i|ai ∈ N \Z} return f_pij alg ai1 ... aim i1 < . . . < im compStep_x = token $ string x x ∈ Z \BA Wird außerhalb dieser Definitionen nur der Compiler für das Startsymbol von G, sagen wir: S, aufgerufen, dann können die Compiler für die anderen Nichtterminale zu lokalen Funktionen von compStep_S gemacht werden, womit sich der alg-Parameter dieser Funktionen vermeiden lässt. 227 Beispiel SAB Die Regeln p1 = S → aB, p2 = S → bA, p3 = S → , p4 = A → aS, p5 = A → bAA, p6 = B → bS, p7 = B → aBB von SAB liefern nach obigem Schema den folgenden LL-Compiler von L(SAB) nach alg: compStep_S alg = msum where try_p1 try_p2 try_p3 compStep_A alg = msum where try_p4 try_p5 [try_p1, try_p2, try_p3}] = do compStep_a; c <- compStep_B alg; return f_p1 alg = do compStep_b; c <- compStep_A alg; return f_p2 alg = return f_p3 alg [try_p4, try_p5] = do compStep_a; c <- compStep_S alg; return f_p4 alg = do compStep_b; c <- compStep_A alg; d <- compStep_A return f_p5 alg c d compStep_B alg = msum [try_p6, try_p7] where try_p6 = do compStep_b; c <- compStep_S alg; return f_p6 alg try_p7 = do compStep_a; c <- compStep_B alg; d <- compStep_B return f_p7 alg c d compStep_a = token $ string a; compStep_b = token $ string b c c c alg c alg Dieser Compiler steht auch am Ende von Java.hs, zusammen mit einer Testalgebra sabCounter, deren Trägermengen vom Typ (Int,Int) sind. Die erste Komponente eines Paares ist die Anzahl der a’s, die zweite die der b’s im jeweiligen Eingabewort. 228 Beispiel JavaLight-Parser Wir wollen den JavaLight-Parser nicht nur mit verschiedenen Zielsprachen (Algebren) instanziieren, sondern auch mit unterschiedlichen Eingabetypen parametrisieren. Deshalb gehen wir von Parser = TransM String zurück zur allgemeinen Huckepackmonade TransM state m und definieren eine Unterklasse StateParser von MonadPlus mit einer Funktion toState, die String-Parser in Parser mit einem anderen Eingabetyp (= state-Instanz) übersetzt: class MonadPlus m => StateParser state m where toState :: Parser Maybe a -> TransM state m a StateParser hat zwei Typvariablen: state und m. Die Instanzen von m sind auf solche der Typklasse MonadPlus beschränkt. Die Verwendung von Typklassen mit mehreren Typvariablen erfordert die Option -fglasgow-exts beim Aufruf des ghci. Die folgende Funktion wendet toState auf Parser von Zeichen an, die von einem anderen Parser erkannte Objekte einschließen. enclosed :: StateParser state m => String -> TransM state m a -> TransM state m a enclosed [open,close] p = do toState $ char open; a <- p toState $ char close; return a 229 Die StateParser-Instanz für (deterministische) String-Parser ist durch den Parserkombinator token (s.o.) gegeben: instance Token String Maybe where toState = token Die zweite StateParser-Instanz, mit der wir den JavaLight-Parser aufrufen wollen, lautet wie folgt: type Pos = (Int,Int) instance StateParser (Pos,String) (Either Pos) where toState p = TM token where token (_,"") = mzero token ((i,_),'\n':cs) = line ((i+1,1),cs) token (pos,c:cs) | isDelim c = token (pos,cs) token ps = line ps line (pos,c:cs) | isDelim c = line (pos,cs) line (pos@(i,j),cs) = case transM p cs of Just (x,ds) -> Right (x,((i,j+length cs-length ds),ds)) _ -> Left pos Eine Eingabe vom Typ (Pos,String) besteht aus einem String cs und dessen Anfangsposition (i, j) innerhalb der gesamten Eingabe: cs beginnt in der j-ten Spalte der i-ten Zeile der Eingabe. Anstelle eines Zielobjektes gibt ein (Pos,String)-Parser im Fall der scheiternden Parsierung von cs den Ausdruck Left (i,j) zurück. 230 Die (Either Pos)-Instanz von MonadPlus m (siehe Monaden) bewirkt, dass im Fall des Scheiterns mehrerer - mit msum verknüpfter - Parser nur die Fehlermeldung desjenigen Parsers weitergegeben wird, der das längste Eingabepräfix verarbeitet hat. Damit auch im Erfolgsfall nur dieser Parser sein Ergebnis weitergibt, müssen alle mit msum verknüpfte Parserlisten absteigend bzgl. der Länge der jeweils erkannten Eingabepräfixe sortiert werden! Bis auf die Verallgemeinerung des Eingabetyps folgt die Definition des JavaLight-Parsers der Version 3 des LL-Compiler-Schemas: compJava :: StateParser state m => JavaAlg a b c d e f g h i k -> state -> m (a,state) compJava alg = transM commands where commands = do c <- command msum [do cs <- commands return $ seq_ alg c cs, return $ embedC alg c] 231 command = msum [do toState $ char ';'; return $ skip alg, do x <- toState $ identifier e <- enclosed "=;" intE return $ assign alg x e, do toState $ string "if"; be <- boolE cs <- enclosed "" commands msum [do toState $ string "else" ds <- enclosed "" commands return $ cond alg be cs ds, return $ cond1 alg be cs], do toState $ string "while"; be <- boolE cs <- enclosed "" commands return $ loop alg be cs] intE = do e <- product; e' <- sumSect; return $ sum_ alg e e' sumSect = msum [do toState $ char '+'; e <- intE return $ addSect alg e, do toState $ char '-'; e <- intE return $ subSect alg e, return $ emptyS alg] product = do e <- factor; e' <- prodSect; return $ prod alg e e' prodSect = msum [do toState $ char '*'; e <- product return $ mulSect alg e, do toState $ char '/'; e <- product return $ divSect alg e, return $ emptyP alg] 232 factor = msum [do i <- toState int; return $ embedI alg i, do x <- toState identifier; return $ var alg x, do e <- enclosed "()" intE return $ encloseI alg e] boolE = do be <- conjunct msum [do toState $ string "||"; be' <- boolE return $ or_ alg be be', return $ embedA alg be] conjunct = do be <- literal msum [do toState $ string "&&"; be' <- conjunct return $ and_ alg be be', return $ embedL alg be] literal = msum [do b <- toState bool; return $ embedB alg b, do e <- intE; x <- toState relation e' <- intE; return $ rel alg x e e', do toState $ char '!'; be <- literal return $ not_ alg be, do be <- enclosed "()" boolE return $ encloseB alg be] 233 Beginnt die Ausführung des Zielcodes einer Kommandofolge cs im Zustand ([],store,0), dann endet sie im Zustand ([],store’,length cs), wobei store’ die von compJava javaState aus der initialen Variablenbelegung store berechnete finale Variablenbelegung ist: execute (compJava javaStack cs) ([],store,0) = ([],compJava javaState cs store,length cs). Wählt man execute als Zielinterpreter, dann beschreibt die Gleichung im Sinne des Abschnitts Compiler-Korrektheit die Korrektheit von compJava. Für ihren induktiven Beweis müssten compJava durch den Interpreter evalCS und cs durch den Syntaxbaum compJava javaTerm cs ersetzt werden. Da compJava dem im Abschnitt Vom LL-Parser zum LL-Compiler angebenen Schema eines LL-Compilers folgt, ist die Korrektheit des Parseranteils von compJava ohne weitere Überprüfung gewährleistet. 234 Testumgebung für JavaLight import PainterSVG (Pos,Tree(F),drawTermC,readFileAndDo) compile :: Int -> IO () compile n = readFileAndDo "source" $ act where act str = case n of 1 -> case compJava javaTerm str of Just (t,rest) -> do unparsedSuffix rest writeFile "term" $ show t; drawTermC "term" _ -> putStrLn "syntax error" 2 -> case compJava javaWord str of Just (str,_) -> writeFile "word" str _ -> putStrLn "syntax error" 3 -> case compJava javaDeri str of Just (t,_) -> do writeFile "deri" $ show t drawTermC "deri" _ -> putStrLn "syntax error" 4 -> case compJava javaList str of Just (f,_) -> writeFile "list" $ f True 0 _ -> putStrLn "syntax error" 5 -> case compJava javaState str of Just (trans,_) -> loop3 trans _ -> putStrLn "syntax error" 6 -> case compJava javaStack str of 235 Just (f,_) -> do let (code,_) = f 0 writeFile "code" $ showCode code loop2 code _ -> putStrLn "syntax error" _ -> case compJava javaTerm ((1,1),str) :: Either Pos (Commands,(Pos,String)) of Right (t,(_,rest)) -> do unparsedSuffix rest writeFile "term" $ show t; drawTermC "term" Left (-2147483648,-2147483648) minBound -> putStrLn "symbol missing at final position" Left pos -> putStrLn $ "wrong symbol at position " ++show pos loop1 :: (Store -> Store) -> IO () loop1 trans = do (vars,initial,proceed) <- input let f x = putStrLn $ x++" = "++show (trans initial x) when proceed $ do mapM_ f vars; loop1 trans loop2 :: [StackCom] -> IO () loop2 code = do (vars,initial,proceed) <let (_,final,_) = execute f x = putStrLn $ x++" when proceed $ do mapM_ f input code ([],initial,0) = "++show (final x) vars; loop2 code 236 compile n erwartet ein Quellprogramm vom Typ Commands in der Datei source und übersetzt ihn in den Fällen n = 1 und n = 7 in einen Syntaxbaum. In den Fällen n = 2 bis n = 6 übersetzt compile n den Quellcode in die Algebra javaWord, javaDeri, javaList, javaState bzw. javaStack. Zusätzlich starten compile 5 und compile 6 eine Schleife, welche in jedem Durchlauf eine Variablenbelegung einliest und die durch Interpretation des Quellprogramms in javaState bzw. durch Ausführung des Zielcodes berechnete Variablenbelegung ausgibt. In den Fällen n = 1 bis n = 6 wird der Parser mit der ersten, im Fall n = 6 mit der zweiten StateParser-Instanz aufgerufen (s.o.) Beispiele von compile 7 erzeugter Fehlermeldungen: "while true {x=5;};y=;" "while true {x=5+;}" "x==5;" ; ; ; unparsed suffix: y=; wrong symbol at position (1,16) wrong symbol at position (1,3) readFileAndDo wurde im Abschnitt Transitionsmonaden definiert. Die Definitionen von unparsedSuffix, showCode und input stehen in der Testumgebung für Expr. 237 Beispiel Alignments Auch im folgenden Beispiel ist die Eingabe mehrdimensional. Sie besteht hier aus einem Paar von Stringlisten xs und ys, das in die Menge alis(xs, ys) der Alignments von xs und ys übersetzt werden soll. Vorausgesetzt wird eine Funktion compl : String → String → Bool, die angibt, welche Strings zueinander “komplementär” sind und deshalb aneinander “andocken” können. Sei A = String ] {Nothing} und h : A∗ → String ∗ die Funktion, die aus einem Wort alle Vorkommen von Nothing streicht. Dann ist für alle xs, ys ∈ String ∗ S alis(xs, ys) =def n∈N{(a1 . . . an, b1 . . . bn, c1 . . . cn) ∈ An × An × {0, 1, 2}n | h(a1 . . . an) = xs ∧ h(b1 . . . bn) = ys ∧ ∀1≤i≤n: (compl(ai, bi) ∧ ci = 2) ∨ (ai = bi ∧ ci = 1) ∨ ci = 0}. Die Elemente von alis(xs, ys) lassen sich eindeutig als Terme beschreiben: data Align = Compl String String Align | Equal String Align | Ins String Align | Del String Align | End Die Programme dieses Beispiels stehen auch in der Datei Align.hs. 238 Align ist die Termalgebra folgender Algebrenklasse: data AlignAlg ali = AlignAlg {compl :: String -> String -> ali -> ali, equal,ins,del :: String -> ali -> ali, end :: ali} Die Tripeldarstellung von Alignments in obiger Definition lässt sich durch folgende Algebra implementieren: type Triple = ([Maybe String],[Maybe String],[Int]) tripAli :: AlignAlg Triple tripAli = AlignAlg compl equal ins del end where compl a b (s1,s2,s3) = (Just a:s1,Just b:s2,2:s3) equal a (s1,s2,s3) = (Just a:s1,Just a:s2,1:s3) ins a (s1,s2,s3) = (Nothing:s1,Just a:s2,0:s3) del a (s1,s2,s3) = (Just a:s1,Nothing:s2,0:s3) end = ([],[],[]) 239 Der Parser von Stringlisten soll nicht alle Alignments ausgeben, sondern nur die mit maximalen Matches, das sind die Tripel (as, bs, cs) mit den längsten aus Einsen und Zweien bestehenden Teilwörtern von cs. Um sie herauszufiltern, benutzen wir die Tripeldarstellung von Alignments. maxmatch t liefert die Länge des längsten Matches von t: maxmatch :: Triple -> Int maxmatch = f False 0 0 where f False _ n (_:s1,_:s2,i:s3) | i /= 0 = f True 1 n (s1,s2,s3) f _ m n (_:s1,_:s2,i:s3) | i /= 0 = f True (m+1) n (s1,s2,s3) f _ m n (_:s1,_:s2,0:s3) = f False 0 (max m n) (s1,s2,s3) f _ m n _ = max m n matchcount :: Triple -> Int matchcount (_,_,s) = length $ filter (/= 0) s maxima :: Ord b => (a -> b) -> [a] -> [a] maxima f s = [x | x <- s, f x == maximum (map f s)] matchcount t zählt die Matches der Länge 1 von t. maxima f s liefert die Teilliste aller Elemente von s, die unter f den größten Wert haben. 240 Für jede Liste ts von Alignments kann der Parser den Wert von maxima matchcount ts induktiv während der parallelen Erzeugung der einzelnen Elemente von ts berechnen. Den korrekten Wert von maxima maxmatch ts kann er hingegen erst ermitteln, wenn alle Elemente von ts vollständig vorliegen. Der Parser berechnet die Alignments durch Aufruf der Funktion align : N2 → T riple∗ an der Stelle (0, 0). align erzeugt sie in wechselseitiger Rekursion mit drei weiteren Funktionen desselben Typs. Jede der vier Funktionen liest die beiden Stringlisten xs und ys parallel und gibt an der Stelle (i, j) ∈ N2 Alignments des (|xs| − i)-elementigen Suffixes von xs und des (|ys| − j)-elementigen Suffixes von ys zurück, also an der Stelle (0, 0) Alignments von xs bzw. ys selbst. Um Mehrfachberechnungen von Funktionswerten zu vermeiden, werden die vier Funktionen als Felder dargestellt. Ist A eine Instanz der Typklasse Ix für Indexmengen, dann wird die Einschränkung einer Funktion f : A → B auf das Intervall [m . . . n] mit mkArray in ein Feld umgewandelt. Die Applikation f (i) wird zum Feldzugriff mkArray f!i. mkArray :: Ix a => (a,a) -> (a -> b) -> Array a b mkArray bounds@(m,n) f = array bounds [(x,f x) | x <- [m..n]] 241 Hier ist der vollständige Alignment-Parser: mkAlign :: [String] -> [String] -> (String -> String -> Bool) -> [Triple] mkAlign xs ys c = maxima maxmatch $ align!(0,0) where alg = tripAli align' = maxima matchcount . (align!) lg1 = length xs; lg2 = length ys bds = ((0,0),(lg1,lg2)) align = mkArray bds f where f ij@(i,j) = if i == lg1 then if j == lg2 then [end alg] else ts else if j == lg2 then us else match!ij++ts++us where ts = insert!ij; us = delete!ij match = mkArray bds f where f (i,j) = if x == y then map (equal alg x) ts else if c x y || c y x then map (compl alg x y) ts else [] where x = xs!!i; y = ys!!j; ts = align' (i+1,j+1) insert = mkArray bds f where f (i,j) = map (ins alg $ ys!!j) $ align' (i,j+1) delete = mkArray bds f where f (i,j) = map (del alg $ xs!!i) $ align' (i+1,j) 242 Testumgebung für Alignments import PainterSVG (drawLGraph,red,green,blue,light,readFileAndDo) drawAlis :: String -> IO () drawAlis file = readFileAndDo file f where f str = do writeFile "alignments" $ show $ concat $ zipWithIndices mkPaths alis drawLGraph "alignments" where (str1,str2) = break (== '\n') str (xs,ys) = (words str1,words str2) compl x y = (x == "a" && y == "t") || genetischer Code (x == "c" && y == "g") alis = mkAlign xs ys compl type LPath = ([(String,Int,Int)],RGB) mkPaths :: Int -> Triple -> [LPath] mkPaths j (s1,s2,s3) = concat (zipWithIndices f s3) ++ [(fold2 (g 0) [] is s1,light blue), (fold2 (g 1) [] is s2,light blue)] where is = [0..length s1-1] f _ 0 = []; f i 1 = [(arc i,green)]; f i _ = [(arc i,red)] arc i = [("",i,j+j),("",i,j+j+1)] g n path i (Just a) = path++[(a,i,j+j+n)] g _ path _ _ = path zipWithIndices f s = zipWith f [0..length s-1] s 243 drawAlis "file" liest zwei (untereinander geschriebene) Stringlisten aus der Datei file und schreibt ihre Alignments als Objekte vom Typ [LPath] in die Datei alignments, wo sie drawLGraph liest und zeichnet. Die zwei Alignments von a c t a c t g c t und a g a t a g mit maximalen Matches Das Alignment von a d f a a a a a a und a a a a a a d f a mit maximalem Match 244 ` Mehrpässige Compiler Zurück zum Abschnitt Attributierte Grammatiken. Die Trägermengen der dort vorgestellten Algebren haben folgende Form: Inh1 × . . . × Inhm → Der1 × . . . × Dern. Inh1, . . . , Inhm und Der1, . . . , Dern bezeichnen die Wertebereiche der jeweiligen vererbten (inherited) bzw. abgeleiteten (derived) Attribute. Für eine Signatur Σ und eine Σ-Algebra A können wir o.B.d.A. dieselbe Trägermenge für alle Sorten von Σ voraussetzen. Sei c : s1 × . . . × sk → s ein Konstruktor von Σ. Schaut man sich die Beispiele im Abschnitt Attributierte Grammatiken an, dann lautet das Schema einer Interpretation von cA : Ak → A von c in A offenbar wie folgt: cA(f1, . . . , fk )(x1, . . . , xm) = (e1, . . . , en) where (x11, . . . , x1n) = f1(e11, . . . , e1m) ... (1) (xk1, . . . , xkn) = fk (ek1, . . . , ekm) 245 Ist s → w0s1w1 . . . sk wk die Grammatikregel, aus der c hervorgeht, dann wird (1) üblicherweise als Liste von Attributzuweisungen an die Regel geschrieben: s → w0s1w1 . . . sk wk s.der1 := e1 . . . s.dern := en s1.inh1 := e11 . . . s1.inhm := e1m ... sk .inh1 := ek1 . . . sk .inhm := ekm Für jede Belegung der Variablen f1, . . . , fk , x1, . . . , xm bilden die lokalen Definitionen hinter der where-Klausel von (1) ein Gleichungssystem. cA ist genau dann wohldefiniert, wenn es in den Variablen xij , 1 ≤ i ≤ k, 1 ≤ j ≤ n, eindeutig lösbar ist. Nach dem Fixpunktsatz von Knaster und Tarski ist das der Fall, wenn alle Funktionen h, aus denen die Ausdrücke eij , 1 ≤ i ≤ k, 1 ≤ j ≤ m, zusammengesetzt sind, monoton bezüglich der flachen Verbände Inh1 ∪ {⊥}, . . . , Inhm ∪ {⊥}, Der1 ∪ {⊥}, . . . , Dern ∪ {⊥} sind, wenn also x = ⊥ aus h(x) = ⊥ folgt. 246 ⊥ stellt hier den Wert jedes Ausdrucks dar, dessen Auswertung nicht terminiert. Die Monotonie von h hängt also von der Strategie ab, nach der ein Ausdruck h(e) ausgewertet wird, insbesondere im Fall, dass die Auswertung von (Teilen von) e nicht terminiert. Die Strategie muss aber nicht nur Monotonie gewährleisten, sie muss vor allem eine Fixpunktstrategie sein, d.h. genau die Lösung von (1) berechnen, die der Fixpunktsatz liefert. Die von Haskell verwendete lazy-evaluation-Strategie hat diese Eigenschaft, eine innermost-Strategie, die eine Auswertung von h(e) mit der Auswertung von e beginnt, hat sie nicht! Wir wollen die theoretischen Grundlagen dieser Ergebnisse hier nicht weiter vertiefen, aber noch anmerken, dass die Betrachtung flacher Verbände nicht ausreicht, obwohl die Lösungen von (1) in solchen Verbänden liegen. Kommen nämlich in den Ausdrücken eij nicht nur Standardfunktionen, sondern auch rekursiv definierte vor, dann gehen deren Rekursionsgleichungen in die Strategie und die Monotonieforderung ein. Die Lösungen dieser Gleichungen liegen jedoch in (nichtflachen) Funktionsverbänden! Lazy evaluation ist allerdings auch dann eine Fixpunktstrategie. 247 Es gibt also immer eine Lösung von (1). Da sie aus Elementen flacher Verbände besteht, ist sie sogar - fast - eindeutig: Gibt es mehrere Lösungen, dann sind alle bis auf eine gleich ⊥. Das wäre jedoch schlecht, denn ist ⊥ eine Lösung, dann liefert jede Fixpunktstrategie genau diese, was bedeutet, dass die Lösungssuche nicht terminiert! Wir brauchen also Bedingungen an (1), deren Gültigkeit ausschließt, dass ⊥ eine Lösung von (1) ist. Eine solche Bedingung ist die Zyklenfreiheit der Benutzt-Relation der Variablen von (1). Um die Benutzt-Relation zu ermitteln, zerlegen wir (1) in ein System regulärer Gleichungen, deren linke Seiten Variablen der Menge X = {xij | 1 ≤ i ≤ k, 1 ≤ j ≤ n} sind: x11 = π1(f1(e11, . . . , e1m)) ... x1n = πn(f1(e11, . . . , e1m)) ... xij = πj (fi(ei1, . . . , eim)) ... xk1 = π1(fk (ek1, . . . , ekm)) ... xkn = πn(fk (ek1, . . . , ekm)) πi liefert die i-te Komponente eines m-Tupels. 248 (2) xij ∈ X benutzt x ∈ X, wenn x in πj (fi(ei1, . . . , eim)) vorkommt. Ist die BenutztRelation azyklisch, dann kann man die Gleichungen von (2) so anordnen, dass die Ausdrücke auf ihren rechten Seiten in der Reihenfolge ihres Auftretens im (umgeordneten) Gleichungssystem ausgewertet werden können, die Lösungsberechnung also terminiert und damit einen Wert ungleich ⊥ liefert. Enthält die Benutzt-Relation einen Zyklus, dann kann man versuchen, die parallele Berechnung und Verwendung von Werten aller abgeleiteten bzw. vererbten Attributmenge so zu sequentialisieren, dass in jedem Schritt zwar nur eine Teilmenge berechnet bzw. verwendet wird, die Komposition der Schritte jedoch eine äquivalente Definition von cA liefert. Wir wollen die einzelnen Attribute über Indizes identifizieren und stellen daher die Trägermenge von A folgendermaßen dar: Inh1 × . . . × Inhm → Derm+1 × . . . × Dern. Es gibt also insgesamt n Attribute. Die ersten m sind vererbt, die restlichen n − m sind abgeleitet. Der Wertebereich eines Attributs i ≤ m ist Inhi, der eines Attributs i > m ist Deri. 249 Wir passen (1) an diese Notation an: cA(f1, . . . , fk )(x01, . . . , x0m) = (e(k+1)(m+1), . . . , e(k+1)n) where (x1(m+1), . . . , x1n) = f1(e11, . . . , e1m) ... (xk(m+1), . . . , xkn) = fk (ek1, . . . , ekm) (3) Der Index i einer Variablen xai oder eines Ausdrucks edi bezeichnet also das Attribut, in dessen Wertebereich xai bzw. edi liegt (a für Anwendungsstelle, d für Definitionsstelle). Der Attributabhängigkeitsgraph für c, depgraph(c)(i, j) ⊆ {0, . . . , k} × . . . × {1, . . . , k + 1}, liefert für je zwei Attribute i und j die Abhängigkeiten bei ihrer Berechnung bzw. Verwendung in (3): depgraph(c)(i, j) =def {(a, d) | xa,i kommt in ed,j vor}. Man sieht sofort, dass (3) eine eindeutige Lösung hat, die durch sequentielle Auswertung der Ausdrücke in (3) berechnet werden kann, wenn für alle Attribute i, j und (a, d) ∈ depgraph(c)(i, j) a < d gilt. 250 (4) Ist (4) verletzt, dann versuchen wir, die gesamte Attributmenge At = {1, . . . , n} so in Teilmengen At1, . . . , Atr zu zerlegen, dass für alle Attribute i, j und Pässe 1 ≤ p, q ≤ r (4) nur dann nicht gilt, wenn i in einem früheren Pass als j berechnet wird: i ∈ Atp ∧ j ∈ Atq =⇒ p < q ∨ (p = q ∧ ∀ (a, d) ∈ depgraph(c)(i, j) : a < d). (5) Gilt (5), dann lassen sich die Attributberechnungen und -verwendungen in (3) so umordnen, dass (3) auch in diesem Fall eine eindeutige Lösung hat, die man durch sequentielle Auswertung der Ausdrücke in (3) erhält. Für alle Pässe 1 ≤ p ≤ r und f : Inh1 × . . . × Inhm → Derm+1 × . . . × Dern bezeichne π p die Projektion eines Attributwertetupels auf das Teiltupel der Komponenten, deren Indizes zu Atp gehören, und f p : π p(Inh1 × . . . × Inhm) → π p(Derm+1 × . . . × Dern) die entsprechende Einschränkung von f , d.h. f 1 × . . . × f r = f . 251 Das obige Schema einer Konstruktorinterpretation cA(f1, . . . , fk )(x01, . . . , x0m) = (e(k+1)(m+1), . . . , e(k+1)n) where (x1(m+1), . . . , x1n) = f1(e11, . . . , e1m) ... (xk(m+1), . . . , xkn) = fk (ek1, . . . , ekm) (3) wird zu: cA(f1, . . . , fk )(x01, . . . , x0m) = (e(k+1)(m+1), . . . , e(k+1)n) where π 1(x1(m+1), . . . , x1n) = f11(π 1(e11, . . . , e1m)) ... π 1(xk(m+1), . . . , xkn) = fk1(π 1(ek1, . . . , ekm)) ... π r (x1(m+1), . . . , x1n) = f1r (π r (e11, . . . , e1m)) ... π r (xk(m+1), . . . , xkn) = fkr (π r (ek1, . . . , ekm)) 252 Der LAG-Algorithmus (Left-to-right-Attributed-Grammar) berechnet die kleinste Zerlegung der Attributmenge At, die (5) erfüllt, sofern eine solche existiert. Sei constrs die Menge aller Konstruktoren und ats die Menge aller Attribute der gegebenen Grammatik. Ausgehend von der einelementigen Zerlegung [ats] verändert check_partition die ersten beiden Elemente der jeweils aktuellen Zerlegung (next und curr), bis entweder next leer ist und damit eine Zerlegung gefunden wurde, die (5) erfüllt, oder curr leer ist, was bedeutet, dass keine solche Zerlegung existiert. Bei depgraph c wird der Attributabhängigkeitsgraphen für den Konstruktor c berechnet. Beachte auch die drei Aufrufe von foldl, die drei ineinandergeschachtelten Schleifen entsprechen! 253 least_partition ats = reverse . check_partition [ats] [] check_partition next (curr:partition) = if changed then check_partition next' (curr':partition) else case (next',curr') of ([],_) -> curr':partition Zerlegung gefunden (_,[]) -> [] keine Zerlegung möglich _ -> check_partition [] (next':curr':partition) Zerlegung erweitern where (next',curr',changed) = foldl check_constr (next,curr,False) constrs check_constr state c = foldl (check_atpair dep) state [(a,d) | a <- ats, d <- ats, not (null dep (a,d))] where dep = depgraph c check_atpair dep state atp = foldl (check_dep atpair) state (dep atpair) check_dep (a,d) (next,curr,changed) (i,j) = if (a `elem` next || (a `elem` curr && i>=j)) && d `elem` curr Die aktuelle Zerlegung next:curr:... verletzt (5). then (d:next,curr`minus`[d],True) else (next,curr,changed) d wird vom vorletzten zum letzten Zerlegungselement next verschoben. 254 ` Korrektheit des JavaLight-Compilers* In den Abschnitten Rund um Expr bzw. Transitionsmonaden haben wir die Korrektheit des Expr- bzw. JavaLight-Compilers durch direkten Bezug auf die Kommutativität des Diagramms TΣ evalSem evalZiel - (1) execute ? ? Sem Ziel - encode M ach aus dem Abschnitt Compiler-Korrektheit plausibel gemacht. In diesem Abschnitt wurde aber auch ein Kriterium für die Kommutativität genannt, das sich aus der Initialität der Termalgebra TΣ ergibt: Sind alle vier Abbildungen von (1) Σ-homomorph, dann kommutiert (1). Gilt das für (die Signatur von) JavaLight? 255 Für die Korrektheitsskizze im Abschnitt Transitionsmonaden haben wir javaState (siehe Ein Haskell-Datentyp für Algebren) als Semantik A der Quellsprache, javaStack als Zielsprache, State −◦→ State mit State = Int ∗ × Store × Int (Keller, Speicher, Befehlszähler) als Semantik B der Zielsprache und execute als Zielinterpreter gewählt. Für zwei Mengen A und B bezeichnet A −◦→ B die Menge der partiellen Funktionen von A nach B. Bekanntermaßen ist die funktionale Interpretation eines Programms an den Stellen, an denen es nicht terminiert, undefiniert. Deshalb müssen die Assemblerprogramme von javaStack als partielle Funktionen interpretiert werden. Das Gleiche gilt für die (abstrakten) Quellprogramme von JavaLight. Sie können den Konstruktor loop enthalten, für den wir in der Definition von javaState nur die folgende Gleichung angegeben haben: loop be c st = if be st then loop be c $ c st else st (3) Mit dem in der Definition von javaState verwendeten Haskelltyp Store -> Store ist also Store−◦→ Store gemeint. Ist (3) überhaupt – in der Variablen loop(be) – lösbar? Die Lösung wäre auf jeden Fall eine Funktion des Typs (Store −◦→ Store) → (Store −◦→ Store). 256 Allgemein gilt: Für zwei Mengen A und B bilden die partiellen Funktionen von A nach B einen ω-vollständiger t-Halbverband (semi-lattice), d.h. A −◦→ B besitzt eine Halbordnung ≤, ein kleinstes Element ⊥ und Suprema ti∈Nai aller aufsteigenden Ketten a1 ≤ a2 ≤ . . . Das kleinste Element von A−◦→ B ist die nirgends definierte Funktion. Die Halbordnung ist wie folgt definiert: f ≤g ⇐⇒def ∀ a ∈ A : f (a) definiert ⇒ f (a) = g(a). Eine Funktion h : A → B zwischen zwei aufwärtsvollständigen Verbänden heißt aufwärtsstetig, wenn für alle aufsteigenden Ketten a1 ≤ a2 ≤ . . . von A h(ti∈Nai) = ti∈Nh(ai) gilt. Ist A = B, dann ist nach dem Fixpunktsatz von Kleene lfp(h) =def ti∈N hi(⊥) der kleinste Fixpunkt von h, m.a.W. die kleinste Lösung der Gleichung x = h(x) in x. 257 Damit erhalten wir die im Abschnitt Ein Haskell-Datentyp für Algebren angekündigte Definition von loopjavaState, die Gleichung (3) erfüllt: Für alle be : Store → Bool und c : Store −◦→ Store, loopjavaState(be)(c) =def ti∈N hibe,c(⊥), wobei hbe,c : (Store −◦→ Store) → Store −◦→ Store für alle f : Store −◦→ Store und st ∈ Store definiert ist durch: f (c(st)) falls be(st) = True, hbe,c(f )(st) = st sonst. Damit der Zielinterpreter execute JavaLight-homomorph wird, vereinfachen wir seinen Definitions- und Wertebereich so, dass er - analog zu evalSem - einer kanonische Abbildung entspricht, die automatisch homomorph ist. Da die Zielsprache und ihre Semantik nichts mit JavaLight zu tun haben, werden ihnen zunächst andere Signaturen zugrundegelegt und sie dann so zu JavaLight-Algebren erweitert, dass JavaLight-homomorph wird: 258 Wir bilden execute aus einer Auswertungs- und einer Entfaltungsfunktion bzgl. einer Konstruktor- bzw. Destruktorsignatur CΣ = (S, CF, BΣ, BA) bzw. DΣ = (S, DF, BΣ, BA Die Assemblerprogramme von javaStack werden als CΣ-Gleichungselemente (s.u.) einer ComE genannten DΣ-Algebra formuliert, während die Semantik der Zielsprache als CΣ-Algebra PF dargestellt wird, die State −◦ → State auf State0 −◦ → State0 mit State0 = Int ∗ × Store reduziert. Die Sorten von CΣ und DΣ sind dieselben: S = {com, Int, String}. Int und String sind Basissorten und werden immer durch Z bzw. eine Menge von Zeichenfolgen interpretiert. Die uniformen Erweiterungen von ComE und PF zu JavaLight-Algebren nennen wir javaComE bzw. javaPF . Beide sind - genauso wie javaStack - Instanzen desselben Typs von JavaLight-Algebren: type ICI com = Int -> (com,Int) type javaExt com = JavaAlg (ICI com) (ICI com) (ICI com) (ICI com) [ICI com] [ICI com] (ICI com) javaStack :: javaExt [StackCom] javaComE :: javaExt ComE javaPF :: javaExt (State' -> State') 259 Wir müssen javaExt von der oben definierten Abbildung einer Menge (com) auf eine JavaLight-Algebra erweitern zum Funktor von der Kategorie der Mengen und Abbildungen zur Kategorie der JavaLight-Algebren und -Homomorphismen, d.h. wir brauchen noch eine Funktion javaFun :: (a -> b) -> javaExt a -> javaExt b Die zu finden ist nicht schwierig, genausowenig wie eine Abbildung mkEqs : [StackCom] → ComE (s.u.) und der JavaLight-Homomorphismus encode : javaState → javaPF zwischen Quell- und Zielsprachensemantik. Am Ende sind tatsächlich alle Abbildungen von (1) JavaLight-homomorph und wir können die Korrektheit des JavaLight-Compilers aus der Initialität der JavaLight-Termalgebra folgern: TΣ(JavaLight) evaljavaState evaljavaStack- javaStack javaF un(mkEqs)- javaComE (4) javaF un(I) ? javaState ? - encode 260 javaPF execute ist - wie oben bemerkt - die Komposition zweier Abbildungen: execute • - • 6 ComE unfold ComE - CTCΣ evalPF- ? PF Die Schnittstelle ist CTCΣ, die Algebra der endlichen oder unendlichen (!) CΣ-Bäume. Hier sind zunächst einmal die Konstruktoren und Destruktoren von CΣ bzw. DΣ: CF = { sub, pow, gr, inv, jump :: com → com, push, add, mul :: com Int → com, load, save :: com String → com, jumpC :: com com → com, halt :: com } Bis auf halt und die Kleinschreibung entsprechen die Namen der Konstruktoren denen von StackCom. Sie sind aber hier keine Konstanten, die zu Listen zusammengefasst werden, die dann die Programme bilden. Stattdessen hat - bis auf halt - jeder com-Konstruktor f mindestens ein Argument der Sorte com, das einer auf das f entsprechende StackComKommando folgenden Befehlsliste entspricht. 261 DF besteht aus genau einem Destruktor, nämlich ({sub, pow, gr, inv, jump} × com) + ({push, add, mul} × com × Int) + δ :: com → ({load, save} × com × String) + ({jumpC} × com × com) + {halt} Mit CF werden Zielprogramme zusammengesetzt, mit δ werden sie zerlegt. Der Range von δ verwendet - abweichend von unserer obigen Definition einer Destruktorsignatur - die Typkonstruktoren + und ×. Im Abschnitt über Coalgebren wurde bereits auf die Notwendigkeit der Verwendung von + hingewiesen. Für eine endliche Menge C stehen auch die Copotenzen (copowers) wie z.B. C × s × s0 für Summen, z.B. C × s × s0 für die Summen von |C| vielen Kopien von s × s0. Die verbleibenden (inneren) Produkte wie s × s0 werden als eigene Sorten betrachtet. Ihre Destruktoren sind die jeweiligen Projektionen, z.B. π1 : s × s0 → s und π2 : s × s0 → s0. 262 Die CΣ-Algebra PF PF com = State0 −◦→ State0 subPF , powPF , grPF , inv PF , , jumpPF :: PF com → PF com subPF (f ) = f ◦ g where g(a : b : stack, store) = ((b − a) : stack, store) powPF (f ) = f ◦ g where g(a : b : stack, store) =(ba : stack, store) (1 : stack, store) falls b > a grPF (f ) = f ◦ g where g(a : b : stack, store) = (0 : stack, store) sonst g(0 : stack, store) = (1 : stack, store) inv PF (f ) = f ◦ g where g(1 : stack, store) = (0 : stack, store) jumpPF (f ) = f pushPF , addPF , mulPF : PF com × Int → PF com pushPF (f, a) = f ◦ g where g(stack, store) = (a : stack, store) addPF (f, n) = f ◦ g where g(a1 : ... : an : stack, store) = ((an + ... + a1) : stack, store) mulPF (f, n) = f ◦ g where g(a1 : ... : an : stack, store) = ((an ∗ ... ∗ a1) : stack, store) loadPF , savePF : PF com × String → PF com loadPF (f, x) = f ◦ g where g(stack, store) = (store(x) : stack, store) savePF (f, x) = f ◦ g where g(a : stack, store) = (stack, update(store)(x)(a)) 263 In allen nicht aufgeführten Fällen ist g und damit auch f ◦ g undefiniert! f ist, intuitiv gesagt, die Semantik des Zielprogramms, das auf den sub-, pow-, gr- bzw. inv-Befehl folgt. jumpC PF : PF com × PF com → PF com jumpC PF (f, g)(1 : stack, store) = f jumpC PF (f, g)(0 : stack, store) = g Die DΣ-Algebra ComE Sei Σ eine Konstruktorsignatur mit Sortenmenge S, Basissortenmenge BS und Basisalgebra BA. Für alle n > 0 sei Xn eine n-elementige Menge (S \ BS)-sortierter Variablen, s ∈ S \ BS und x ∈ Xn,s. Eine (S \ BS)-sortierte Funktion E : Xn → (TΣ(Xn) \ Xn) heißt ideales Σ-Gleichungssystem und das Paar (x, E) Σ-Flussdiagramm der Sorte s. Die (S \ BS)-sortierte Menge der Σ-Flussdiagramm bezeichnen wir mit F lowΣ. Jedes Element (x, E) von F lowΣ beschreibt einen Graphen, dessen Knotenmarkierungen Konstruktoren oder Elemente von BA sind. Er setzt sich zusammen aus den Bildern von E - die Terme, also endliche Bäume sind -, wobei jede dort auftretende Variable z ∈ Xn mit der Wurzel von E(z) gleichgesetzt wird. x stelle man sich als “Eingangsknoten” vor. 264 ComEcom = TCΣ(F lowCΣ)com ({sub, pow, gr, inv, jump × ComEcom}) ∪ ({push, add, mul} × ComEcom × Int) ∪ δ ComE : ComEcom → ({load, save} × ComEcom × String) ∪ ({jumpC} × ComE × ComE ) ∪ {halt} com com ∀ (x, E) ∈ F lowCΣ,com : E(x) = f t ⇒ δ ComE (x, E) = (f, t) ∀ f :: w → com ∈ CF, t ∈ TCΣ(F lowCΣ)w : δ ComE (f t) = (f, t) Demnach muss die oben verlangte Funktion mkEqs : [StackCom] → ComE ein Assemblerprogramm in sein Flussdiagramm übersetzen. Sie könnte also in Haskell wie folgt definiert werden: type Eqs = Int -> (Label,[Int]) data Label = Sub_ | Pow_ | Gr_ | Inv_ | Jump_ | Push_ | Add_ | Mul_ | Load_ | Save_ | JumpC | Halt mkEqs :: [StackCom] -> Eqs mkEqs coms = listsToFun (Halt,[]) [0..length coms-1] $ map f coms where lcoms = zip labs coms labs = [0..length coms] 265 targets = [k | (_,Jump(k)) <- coms]++ concat[[k,i+1] | (i,JumpT(k)) <- coms]++ concat[[k,i+1] | (i,JumpF(k)) <- coms] ******** 266 Definition von CTΣ für eine beliebige Konstruktorsignatur Σ = (S, F, BΣ, BA) Σ-Bäume sind partielle Funktionen von N∗ nach F : Für alle s ∈ S ist CTΣ,s die Menge aller t : N∗ −◦→ F mit folgenden Eigenschaften: t = ⊥s ∨ ran(t()) = s, ∀ w ∈ N∗, i ∈ N : ran(t(wi)) = si ⇒ ∃ n > i, s0, . . . , si−1, si+1, . . . , sn−1 ∈ S : dom(t(w)) = s0 . . . si . . . sn−1. Die Funktionssymbole von Σ werden in CTΣ wie folgt interpretiert: Sei f :: s0 . . . sn−1 → s ∈ F und für alle 0 ≤ i < n sei ti ∈ CTΣ,si . f CTΣ (t0, . . . , tn−1)() = f, ∀ i ∈ N, w ∈ N∗ : f CTΣ (t0, . . . , tn−1)(iw) = 267 ti(w) falls i < n, undefiniert sonst. Da die Trägermengen von CTΣ aus partiellen Funktionen bestehen, bilden sie einen ωvollständigen t-Halbverband, dessen Halbordnung, kleinstes Element und Suprema wie oben für eine beliebige Menge A −◦→ B definiert sind. Mehr noch: die Interpretationen der Funktionssymbole von Σ sind aufwärtsstetig. Damit gehört CTΣ einer Unterkategorie der Klasse aller Σ-Algebren an, deren Objekte wir aufwärtsstetige Σ-Algebren nennen. Im Abschnitt Initiale Algebren haben wir die Termalgebra TΣ als initiale Σ-Algebra charakterisiert, d.h. für jede Σ-Algebra A gibt es genau einen Σ-Homomorphismus von TΣ nach A. CTΣ hat die gleiche Eigenschaft, sofern wir vor die Substantive Algebra und Homomorphismus das Adjektiv aufwärtsstetig setzen. Man bezeichnet CTΣ deshalb als initiale aufwärtsstetige CΣ-Algebra. 268 ` Deklarationen und ihre Adressierung* Erweiterung von JavaLiight konkrete Syntax exp → Int | Bool | String | fun String | String(actList) | exp∧ | exp[exp] | exp. String actList → exp∗ abstrakte Ci: Cb: Id: Fid: Apply: Deref Af: Rf: Actuals: 269 Syntax Int → exp Bool → exp String → exp String → exp String actList → exp exp → exp exp exp → exp exp String → exp exp∗ → actList konkrete Syntax abstrakte Syntax MkProg: stat → prog prog → stat Skip: stat stat → Assign: exp exp → stat | exp := exp Read: String → stat | read String Write: exp → stat | write exp Var: String type → stat | var String: type New: String → stat | new String Main: stat → stat | begin stat end String parList type stat → stat | fun String(parList):type is stat end Fun: For: String type → par par → String:type Funfor: String type → par | fun String:type ∗ Formals: par∗ → parList parList → par INT: type type → Int BOOL: type | Bool POINTER: type → type | ^type ARRAY: Int Int type → type | array [Int..Int] type RECORD: stat → type | record stat Name: String → type | String 270 Attributbereiche type type type type type data Symtab = Id -> (TypeDesc,Int,Int) transient Depth = Int vererbt Label = Int vererbt Code = [Command] abgeleitet Offset = Int abgeleitet TypeDesc = INT | BOOL | Pointer TypeDesc | Func Int TypeDesc | Formalfunc TypeDesc | Array Int Int TypeDesc | Record (String -> (TypeDesc,Int,Int)) | Name String abgeleitet Symtab liefert die jeweils aktuelle Symboltabelle, Depth die Schachtelungstiefe des aktuellen Scopes, also des innersten Blocks, in dem der gerade übersetzte Ausdruck auftritt, Label die nächste freie Befehlsnummer, Code den Zielcode und Offset den Platzbedarf für eine Liste aktueller Parameter. 271 data ImpAlg exp acts prog stat par parList type = ImpAlg {ci :: Int -> exp, cb :: Bool -> exp, id :: String -> exp, fid :: String -> exp, apply :: String -> actList -> exp, deref :: exp -> exp, af :: exp -> exp -> exp, rf :: exp -> String -> exp, actuals :: [exp] -> acts, mkProg: stat -> prog, skip: stat, assign: exp -> exp -> stat, read: String -> stat, write: exp -> stat, var: String -> type -> stat, new: String -> stat, main: stat -> stat, fun: String -> parList -> type -> stat -> stat, for, funfor: String -> type -> par, formals: [par] -> parList, int, bool: type, pointer: type -> type, array: Int -> Int -> type, record: stat -> type, name: String -> type} 272 assemblyAlg :: ImpAlg (Symtab -> Depth -> Label -> (Code,TypeDesc)) Trägermenge für (Symtab -> Depth -> Label -> (Code,Offset) Trägermenge für Code Trägermenge für (Symtab -> Depth -> Reladr -> Label -> (Code,Symtab,Reladr)) Trägermenge für (Symtab -> Depth -> Reladr -> (Symtab,Reladr)) Trägermenge für (Symtab -> Depth -> Reladr -> Symtab) Trägermenge für parList TypeDesc Trägermenge für exp acts prog stat par type assemblyAlg = ImpAlg ci cb id fid apply deref af rf not_ leq add sub mul actuals mkProg skip assign read write seq cond loop typedef var new main fun for funfor int bool pointer array record name where mkProg stat = code where (code,_,_) = stat (const (INT,0,0)) 0 0 0 ... siehe mein Übersetzerbau-Skript, Kapitel 5 273 Literatur [1] R. Giegerich, C. Meyer, Algebraic Dynamic Programming, Proc. AMAST 2002, Springer LNCS 2422 (2002) 249-364 [2] J.A. Goguen, J.W. Thatcher, E.G. Wagner, J.B. Wright, Initial Algebra Semantics and Continuous Algebras, J. ACM 24 (1977) 68-95 [3] D. Knuth, Semantics of Context-Free Languages, Mathematical Systems Theory 2 (1968) 127-145; Corrections: Math. Systems Theory 5 (1971) 95-96 [4] E. Moggi, Notions of Computation and Monads, Information and Computation 93 (1991) 55-92 [5] E.G. Manes, M.A. Arbib, Algebraic Approaches to Program Semantics, Springer (1986) [6] R.N. Moll, M.A. Arbib, A.J. Kfoury, Introduction to Formal Language Theory, Springer (1988) [7] P. Padawitz, Formale Methoden des Systementwurfs, http://fldit-www.cs.uni-dortmund.de/∼peter/TdP96.pdf [8] W.C. Rounds, Mappings and Grammars on Trees, Mathematical Systems Theory 4 (1970) 256-287 274 [9] J.J.M.M. Rutten, Automata and coinduction (an exercise in coalgebra), Proc. CONCUR ’98, Springer LNCS 1466 (1998) 194–218 [10] K. Sen, G. Rosu, Generating Optimal Monitors for Extended Regular Expressions, Proc. Runtime Verification ’03, Elsevier ENTCS 89 (2003) [11] J.W. Thatcher, E.G. Wagner, J.B. Wright, More on Advice on Structuring Compilers and Proving Them Right, Theoretical Computer Science 15 (1981) 223-249 [12] J.W. Thatcher, J.B. Wright, Generalized Finite Automata Theory with an Application to a Decision Problem of Second-Order Logic, Theory of Computing Systems 2 (1968) 57-81 275 Index BΣ, 10 TΣ(X), 12 BA, 10 TΣ, 12, 16 Σ(FPF), 29 Σ(JavaLight), 30 Σ(G), 27 λ-Abstraktion, 36 λ-Applikation, 36 ω-vollständiger t-Halbverband, 257 ∼ParseE , 68 state(ϕ), 99 abgeleitetes Attribut, 174 Abl(G), 41 Ableitungsalgebra, 41 abstrakte Syntax, 27 Abstraktion, 75 Accept, 58 Aktionstabelle, 101 Algebra, 13, 32 Alignments, 238 Allrelation, 61 aufwärtsstetige Funktion, 257 Basisalgebra, 10 Basissignatur, 10 Baumautomat, 14 Beh, 53 Beobachter, 53 bewachte CFG, 48 Bildautomat, 57 bottom-up-Parser, 93 CFG, 24 Coalgebra, 54 Coinduktion, 61 compile, 85 compileE, 156 276 Compiler, 6 Compiler-Korrektheit, 19, 35 compStep, 85 Datentypen, 132, 159 Destruktor, 10 dom, 10 Domain, 10 Fixpunktsatz von Knaster und Tarski, 43 flacher Verband, 246 Flow, 264 Flussdiagramm, 264 follow, 95 FPF, 25 fst, 95 Funktionssymbol, 10 ECFG, 83 eindeutig, 33 erweiterte kontextfreie Grammatik, 83 eval, 16 evalRel, 172 execute, 188 executeE, 156 Expr, 132 Gleichungssystem, 18 Gleichungssystem einer CFG, 43 GLS(G), 43 goto-Tabelle, 103 Grammatikregel, 24 Grundterm, 12 Faktor, 18 Felder, 241 finale Algebra, 54 Fixpunktsatz für CFGs, 49 Fixpunktsatz von Kleene, 44, 257 ideales Gleichungssystem, 264 Induktion, 61 initiale Algebra, 16 initiale aufwärtsstetige Algebra, 268 Interpreter, 6 Homomorphismus, 15, 32 277 isomorph, 15 Korrektheit monadischer Parser, 35 JavaAlg, 163 javaDeri, 168 JavaLight, 26 JavaLight-Compiler, 186 JavaLight-Parser, 229 javaList, 184 javaStack, 189 javaState, 171 javaTerm, 164 javaWord, 167 Lösung eines Gleichungssystems, 18 Lang, 22 Linksrekursion, 80 Listen, 121 Listenfaltungen, 127 Listenkomprehension, 131 LL-Compiler, 85, 226 LL-Parser, 82 LR(k)-Grammatik, 96 LR-Automat für G, 99 LR-Compiler, 105 LR-Parser, 93 Kellermaschine, 154, 186 Kern, 71 kleinste Unteralgebra, 33 Konfluenzkriterien, 69 Kongruenz, 61 Konstante, 10 Konstruktor, 10 kontextfreie Grammatik, 24 Korrektheit des JavaLight-Compilers, 255 Monaden, 191 monomorph, 120 NF, 69 Nichtterminal, 24 Normalform, 69 Normalformalgebra, 69 278 Obs, 53 Parse, 66 Parse(N), 77 ParseE, 67 ParseE(G), 77 ParseE-Äquivalenz, 68 Parser, 5, 38 Plusmonade, 35 polymorph, 120 ran, 10 Range, 10 recursive descent, 88 Redukt, 14 Reg, 21 Reg(N), 77 Reguläre Ausdrücke, 21 reguläres Gleichungssystem, 44 Restriktion, 75 SAB, 28 Scanner, 5 Signatur, 10 sortierte Funktion, 15 sortierte Menge, 11 Sprache einer CFG, 33 spracherkennender Automat, 58 StackCom, 154 State, 186 StateE, 154 Store, 134 Syntaxbaum, 27, 33 Term, 12 Termalgebra, 16 Termauswertung, 17 Termersetzung, 66 Termidentität, 61 Terminal, 24 Testumgebung für Alignments, 243 Testumgebung für JavaLight, 235 Testumgebung für Expr, 223 top-down-Parser, 82 279 Trägermenge, 13 transientes Attribut, 174 Typklassen, 140 unfold, 54 Unteralgebra, 33 Variablenbelegung, 17 vererbtes Attribut, 174 Verhaltensgleichheit, 61 Wächter, 37 Word(G), 32 Wortalgebra, 32 Wortmengenalgebra, 22 Zustandssorte, 52 280