Übersetzerbau Algebraic Compiler Construction Peter Padawitz, TU Dortmund, Germany Webseite zur LV: fldit-www.cs.uni-dortmund.de/ueb.html Thursday 24th March, 2016 00:33 1 Inhalt Mit ∗ markierte Abschnitte bzw. Kapitel werden zur Zeit in der LV nicht behandelt. Zur Navigation auf Titel, nicht auf Seitenzahlen klicken! 1 Einleitung 2 Algebraische Modellierung - Mengen und Typen - Signaturen - 2.9 Konstruktive Signaturen - 2.10 Destruktive Signaturen - 2.11 Algebren - 2.12 Terme und Coterme - 2.13 Beispiele - 2.14 Die Algebren Bool und Regword (BS) - 2.15 Die Algebren Beh(X, Y ), Pow (X), Lang(X) und Bro(BS) - 2.16 Die Erreichbarkeitsfunktion - 2.17 Termfaltung und Zustandsentfaltung - 2.21 * Compiler für reguläre Ausdrücke - 2.22 * Minimale Automaten 7 14 14 30 31 34 38 44 49 56 57 61 62 74 77 2 - 2.25 * Baumautomaten 3 * Rechnen mit Algebren - Unteralgebren - Kongruenzen und Quotienten - Automatenminimierung durch Quotientenbildung - Transitionsmonoid und syntaktische Kongruenz - 3.6 Termsubstitution - Termäquivalenz und Normalformen - 3.10 Normalformen regulärer Ausdrücke - 3.11 Die Brzozowski-Gleichungen - 3.12 Optimierter Brzozowski-Automat - 3.13 Erkenner regulärer Sprachen sind endlich 4 Kontextfreie Grammatiken (CFGs) - Nicht-linksrekursive CFGs - Abstrakte Syntax - Wort- und Ableitungsbaumalgebra - Modelle der Ausdrücke von JavaLight 5 Parser und Compiler für CFGs - 5.1 Funktoren und Monaden - Compilermonaden - Monadenbasierte Parser und Compiler 80 82 83 87 91 92 94 96 101 102 104 106 109 114 117 126 131 136 138 150 152 3 6 LL-Compiler 7 LR-Compiler 8 Haskell: Typen und Funktionen 9 Haskell: Listen 10 Haskell: Datentypen und Typklassen 11 Algebren in Haskell - 11.1 Beispiele - 11.2 Datentyp der JavaLight-Algebren - 11.3 Die Termalgebra von JavaLight - 11.4 Die Wortalgebra von JavaLight - 11.5 Das Zustandsmodell von JavaLight - 11.6 * Die Ableitungsbaumalgebra von JavaLight 12 Attributierte Übersetzung - 12.1 * Binärdarstellung rationaler Zahlen - 12.2 * Strings mit Hoch- und Tiefstellungen - 12.3 * Übersetzung regulärer Ausdrücke in erkennende Automaten - 12.4 * Darstellung von Termen als hierarchische Listen - 12.5 Eine kellerbasierte Zielsprache für JavaLight 13 JavaLight+ = JavaLight + I/O + Deklarationen + Prozeduren - 13.1 Assemblersprache mit I/O und Kelleradressierung - 13.2 Grammatik und abstrakte Syntax von JavaLight+ 157 177 205 215 226 240 241 246 247 248 250 253 256 257 259 261 267 270 276 276 283 4 14 15 16 17 18 19 - 13.3 JavaLight+-Algebra javaStackP * Mehrpässige Compiler Funktoren und Monaden in Haskell Monadische Compiler - 16.1 Compilerkombinatoren - 16.2 Monadische Scanner - 16.3 Monadische LL-Compiler - 16.4 Generischer JavaLight-Compiler - 16.5 Korrektheit des JavaLight-Compilers - 16.6 Generischer XMLstore-Compiler * Induktion, Coinduktion und rekursive Gleichungen - Induktion - Induktive Lösungen - Coinduktion - Coinduktive Lösungen - 17.11 Cotermbasierte Erkenner regulärer Sprachen * Iterative Gleichungen - 18.2 Das iterative Gleichungssystem einer CFG - 18.5 Von Erkennern regulärer Sprachen zu Erkennern kontextfreier Sprachen * Interpretation in stetigen Algebren - 19.1 Schleifensemantik 287 310 318 327 331 333 335 338 340 344 346 346 348 353 360 376 378 379 384 394 399 5 - 19.2 Semantik der Assemblersprache StackCom ∗ - Erweiterte Σ-Bäume - Cosignaturen und ihre Algebren Literatur Index 401 408 415 6 1 Einleitung Die Folien dienen dem Vor- (!) und Nacharbeiten der Vorlesung, können und sollen aber deren regelmäßigen Besuch nicht ersetzen! Interne Links (einschließlich der Seitenzahlen im Index) sind an ihrer dunkelroten Färbung, externe Links (u.a. zu Wikipedia) an ihrer magenta-Färbung erkennbar. Dunkelrot gefärbt ist auch das jeweils erste Vorkommen einer Notation. Fettgedruckt ist ein Begriff in der Regel nur an der Stelle, an der er definiert wird. Jede Kapitelüberschrift und jede Seitenzahl in der rechten unteren Ecke einer Folie ist mit dem Inhaltsverzeichnis verlinkt. Namen von Haskell-Modulen wie Java.hs sind mit den jeweiligen Programmdateien verknüpft. 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 wie 5 oder True, die der Scanner einer Basismenge (Int, Float, Bool, Identifier, etc.) zuordnet. In der Grammatik kommt ein solches Symbol nicht vor, jedoch der Namen der Basismenge, der es angehört. 7 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. Ein Compiler (Programm zur semantischen Analyse und Codeerzeugung) übersetzt ein Programm einer Quellsprache in ein semantisch äquivalentes Programm einer Zielsprache. Ein Interpreter • wertet einen Ausdruck in Abhängigkeit von einer Belegung seiner Variablen aus bzw. • führt eine Prozedur in Abhängigkeit von einer Belegung ihrer Parameter aus. Letzteres ist ein Spezialfall von Ersterem. Deshalb werden wir einen Interpreter stets als die Faltung von Termen (Ausdrücken, die aus Funktionssymbolen einer Signatur bestehen) in einer Algebra (Interpretation der Signatur) modellieren. 8 Auch Scanner, Parser und Interpreter sind Compiler, insofern sie Objekte von einer Repräsentation in eine andere übersetzen. Die Zielrepräsentation eines Interpreters ist i.d.R. eine Funktion, die Eingabedaten verarbeitet. Daher liegt es nahe, alle o.g. Programme nach denjenigen 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 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. 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. 9 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 wirklich 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 Symbolund 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. Die beiden folgenden Grafiken skizzieren die Struktur des algebraischen Ansatzes und seine Auswirkung auf die Generizität der Programme. Auf die Details wird in den darauffolgenden Kapiteln eingegangen. Dabei wird die jeweilige Funktionalität der o.g. Programme in mathematisch präzise Definitionen gefasst, auf denen Entwurfsregeln aufbauen, deren Befolgung automatisch zu korrekten Compilern, Interpretern, etc. führen. 10 Erkenner Programmeigenschaften Σ(G)-Formeln Bool Σ(G)-Algebren Quellsprache L(G) Verifizierer Unparser konkrete Syntax Grammatik G abstrakte Syntax Signatur Σ(G) Syntaxbäume Termalgebra TΣ(G) Optimierer allgemein: Termfaltung Parser Quellsprache L(G) Unparser Compiler Ableitungsbäume Abl(G) optimierte Syntaxbäume Zielsprache Von konkreter über abstrakte Syntax zu Algebren 11 Menge Faltung kontextfreie Sprache Parser Termalgebra Faltung Menge Faltung Menge Algebra kontextfreie Sprache Parser Termalgebra generische Faltung Algebra Algebra Algebra kontextfreie Sprache generischer Compiler Algebra Algebra Der Weg zum generischen Compiler 12 fact =1; while x > 1 {fact = fact*x; x = x-1;} Eingabewort (Element der JavaLight-Algebra javaWord) parse fold Seq Assign fact Embed Sum Prod EmbedI Loop NilS NilP 1 EmbedC Block EmbedL Seq Atom Sum Prod Var > Sum NilS NilP x Assign Prod EmbedI fact NilS NilP 1 Sum Prod Var fact Embed Assign NilS x Prodsect * Var x NilP Sum Prod Var x Syntaxbaum (Element der JavaLight-Algebra javaTerm) NilP Sumsect - Prod EmbedI NilS NilP 1 fold fold Zustandstransitionsfunktion (Element der JavaLight-Algebra javaState) Assemblerprogramm (Element der JavaLight-Algebra javaStack) 13 2 Algebraische Modellierung Mengen und Typen ∅ bezeichnet die leere Menge, 1 die Menge {} und 2 die Menge {0, 1}. 0 und 1 stehen hier in der Regel für die Wahrheitswerte false und true. Für alle n > 1, [n] =def {1, . . . , n}. Wir setzen voraus, dass die Definitionen einer Potenzmenge, binären Relation bzw. (partiellen oder totalen) Funktion sowie der Vereinigung, des Durchschnitts, des Komplements, der Disjunktheit und der Mächtigkeit (Kardinalität) von Mengen bekannt sind. λ-Abstraktionen zur anonymen Definition und die (sequentielle) Komposition mehrerer Funktionen sollten ebenfalls bekannt sein. Für jede Menge A bezeichnet |A| die Kardinalität von A, P(A) die Potenzmenge von A und idA : A → A die durch idA(a) = a für alle a ∈ A definierte Identität auf A. Für jede Teilmenge B von A bezeichnet incB : B → A die durch incB (b) = b für alle b ∈ B definierte Inklusion. Für alle Mengen A, B bezeichnen A → B und B A die Menge der totalen und A (→ B die Menge der partiellen Funktionen von A nach B. def (f ) bezeichnet den Definitionsbereich einer partiellen Funktion f : A (→ B. 14 Mehrsortige Mengen und Funktionen Sei S eine Menge. Eine S-sortige oder S-indizierte Menge ist ein Tupel A = (As)s∈S von Mengen. Manchmal schreiben wir A auch für die Vereinigung der Mengen As über alle s ∈ S. Seien A1, . . . , An S-sortige Mengen. Eine S-sortige Teilmenge von A1 × · · · × An ist eine S-sorted Menge R = (Rs)s∈S mit Rs ⊆ A1,s × . . . × An,s für alle s ∈ S. Seien A = (As)s∈S und B = (Bs)s∈S S-sortige Mengen. Eine S-sortige Funktion f : A → B ist eine S-sortige Menge (fs)s∈S derart, dass für alle s ∈ S, fs eine Funktion von As nach Bs ist. Sei I eine Menge. Das kartesische Produkt (der Komponenten) einer I-sortigen Menge A = (Ai)i∈I ist wie folgt definiert: [ Xi∈I Ai = {f : I → Ai | ∀ i ∈ I : f (i) ∈ Ai}. i∈I Xi∈I Ai aus der einzigen Funktion von ∅ nach i∈I Ai. Gibt es eine Menge A mit Ai = A für alle i ∈ I, dann stimmt Xi∈I Ai offenbar mit AI Im Fall I = ∅ besteht S überein. Im Fall I = [n] für ein n > 1 schreibt man auch A1 × · · · × An anstelle von An anstelle von AI . Xi∈I Ai und 15 Sei I eine Menge. Die disjunkte Vereinigung (der Komponenten) einer I-sortigen Menge A = (Ai)i∈I ist wie folgt definiert: ] [ Ai = (Ai × {i}). i∈I i∈I Die disjunkte Vereinigung des leeren Mengentupels wird definiert als die leere Menge. U Gibt es eine Menge A mit Ai = A für alle i ∈ I, dann stimmt i∈I Ai offenbar mit A × I überein. U Im Fall I = [n] für ein n > 1 schreibt man auch A1 + · · · + An anstelle von i∈I Ai. Der Plusoperator + und der Sternoperator ∗ bilden jede Menge A, die das leere Wort nicht enthält, auf die Menge der (nichtleeren) Wörter oder Listen über A ab: S n A+ =def n>0 A , 1 =def {}, A∗ =def 1 ∪ A+. Die Folge der Komponenten eines Elements von A+ wird manchmal (z.B. in Haskell) nicht wie oben mit runden, sondern mit eckigen Klammern eingeschlossen oder (wie in der Theorie formaler Sprachen) zusammen mit den Kommas ganz weggelassen. Teilmengen von A∗ werden auch Sprachen über A genannt. 16 Die Konkatenationen · : A∗ × A∗ → A∗ und · : P(A∗) × P(A∗) → P(A∗) sind wie folgt induktiv definiert: Für alle s, v = (a1, . . . , am), w = (b1, . . . , bn) ∈ A∗ und B, C ⊆ A∗, · w = w, v · = v, v · w = (a1, . . . , am, b1, . . . , bn), B · C = {v · w | v ∈ B, w ∈ C}. Für alle w ∈ A+ bezeichnet head(w) das erste Element von w und tail(w) die Restliste, d.h. es gilt w = head(w) · tail(w). |w| bezeichnet die Länge eines Wortes w, d.h. die Anzahl seiner Elemente. 17 L ⊆ A∗ heißt präfixabgeschlossen, wenn für alle w ∈ A∗ und a ∈ A aus wa ∈ L w ∈ L folgt. Eine partielle Funktion f : A∗ (→ B mit präfixabgeschlossenem Definitionsbereich heißt deterministischer Baum. f ist wohlfundiert (a: e), wenn es n ∈ N gibt mit |w| ≤ n für alle w ∈ def (t). Anschaulich gesprochen ist f wohlfundiert, wenn f endliche Tiefe hat, wenn also alle von der Wurzel ausgehenden Pfade endlich sind. Tatsächlich kann man sich f als (möglicherweise unendlichen) Baum tf mit Kantenmarkierungen aus A und Knotenmarkierungen aus B vorstellen, der keine zwei Kanten mit derselben Quelle, aber verschiedener Markierung besitzt. f () ∈ B ist die Markierung der Wurzel von tf und jeder von der Wurzel ausgehende Pfad p endet in einem Knoten, der mit f (w) markiert ist, wobei w ∈ A+ die Folge der Markierungen der Kanten von p ist. Demnach kann ein deterministischer Baum als eine Art Record notiert werden: tf = f (){x → tλw.f (xw) | x ∈ def (t) ∩ A}. Die Menge aller deterministischer Bäume von A∗ nach B wird mit dtr(A, B) bezeichnet, die Menge der wohlfundierten darunter mit wdtr(A, B). 18 Für alle Mengen A, B, Funktionen f : A → B, C ⊆ A, D ⊆ B und n > 0, ∆nA =def {(a1, . . . , an) ∈ An | ∀ 1 ≤ i < n : ai = ai+1} Diagonale von A f (C) =def {f (a) | a ∈ C} Bild von C unter f = P(f )(C) (siehe Mengenfunktor) img(f ) =def f (A) f −1(D) =def {a ∈ A | f (a) ∈ D} Urbild von D unter f ker(f ) =def {(a, a0) ∈ A × A | f (a) = f (a0)} Kern von f graph(f ) =def {(a, f (a)) ∈ A × B | a ∈ A} f |C : C → B c 7→ f (c) f [b/a] : A → B Graph von f Einschränkung von f auf C Update von f c 7→ if c = a then b else f (c) χ : P(A) → χ(C) C 7→ λa.if a ∈ C then 1 else 0 charakteristische Funktion von C 19 f ist surjektiv ⇔def img(f ) = B f ist injektiv ⇔def ker(f ) = ∆2A f ist bijektiv ⇔def ∃ g : B → A : g ◦ f = idA ∧ f ◦ g = idB A und B heißen isomorph, geschrieben: A ∼ = B, wenn es eine bijektive Funktion von A nach B gibt. Aufgabe Zeigen Sie: P(A) ∼ = 2A . Aufgabe Zeigen Sie: P(A × B) ∼ = P(B)A. o Aufgabe Zeigen Sie: f : A → B ist bijektiv ⇔ f ist injektiv und surjektiv. o o Aufgabe Zeigen Sie: Ist f : A → B bijektiv, dann gibt es genau eine (üblicherweise mit f −1 bezeichnete) Funktion g : B → A mit g ◦ f = idA und f ◦ g = idB . o 20 Produkte und Summen als universelle Konstruktionen Sei I eine nichtleere Menge und A eine I-sortige Menge. Das kartesische Produkt und die disjunkte Vereinigung eines Mengentupels A haben bestimmte universelle Eigenschaften, d.h. erstens, dass alle Mengen, die diese Eigenschaften haben, isomorph sind und zweitens, dass jede Menge, die zu einer anderen Menge, die diese Eigenschaften hat, isomorph ist, selbst diese Eigenschaften hat. Jede Menge, die die universellen Eigenschaften des kartesischen Produkts bzw. der disjunkten Vereinigung von A hat, nennt man Produkt bzw. Summe von A: Sei A = (Ai)i∈I ein Mengentupel, P eine Menge und π = (πi : P → Ai)i∈I ein Funktionstupel. Das Paar (P, π) heißt Produkt von A, wenn es für alle Tupel f = (fi : B → Ai)i∈I genau eine Funktion g : B → P gibt derart, dass für alle i ∈ I Folgendes gilt: πi ◦ g = f i (1) πi heißt i-te Projektion von P und g Produktextension oder Range-Tuplung von f . g wird mit hfiii∈I und im Fall I = [n], n > 1, auch mit hf1, . . . , fni bezeichnet. Demnach ist im Fall I = ∅ eine Menge P genau dann ein Produkt, wenn es zu jeder Menge B genau eine Funktion von B nach P gibt, wenn also P einelementig ist. 21 Wegen der Eindeutigkeit von Produktextensionen sind zwei Funktionen f, g : B → P genau dann gleich, wenn ihre Produktkomponenten paarweise miteinander übereinstimmen: (∀ i ∈ I : πi ◦ f = πi ◦ g) ⇒ f = g. (2) Mit f = λx.a : 1 → P und g = λx.b : 1 → P folgt aus (2), dass zwei Elemente a, b ∈ P genau dann gleich sind, wenn für alle i ∈ I πi(a) = πi(b) gilt. Beweis. (2) ∀ i ∈ I : πi(a) = πi(b) ⇒ ∀ i ∈ I : πi ◦ f = πi ◦ g ⇒ f = g ⇒ a = f () = g() = b. o Aufgabe 2.1 Sei P = Xi∈I Ai (s.o.) und πi(f ) =def f (i) für alle i ∈ I und f ∈ P . Zeigen Sie, dass (P, (πi)i∈I ) ein Produkt von A ist, wenn die Extension von (fi : B → Ai)i∈I wie folgt definiert wird: • Für alle b ∈ B und i ∈ I, hfiii∈I (b)(i) =def fi(b). o Satz 2.2 (Produkte sind bis auf Isomorphie eindeutig) (i) Seien (P, π) und (P 0, π 0) Produkte von A. Dann sind P und P 0 isomorph. (ii) Sei (P, π) ein Produkt von A, P 0 eine Menge und h : P 0 → P eine bijektive Abbildung. 22 Dann ist (P 0, π 0) mit π 0 = (πi ◦ h)i∈I ebenfalls ein Produkt von A. Beweis von (i). Um die Extensionen auf P ersten von denen auf P 0 zu unterscheiden, wird die schließende Klammer letzterer gestrichelt. Sei h =def hπii0i∈I : P → P 0 und h0 =def hπi0 ii∈I : P 0 → P . Dann gilt (1) (1) (1) (1) πi ◦ h0 ◦ h = πi ◦ hπi0 ii∈I ◦ hπii0i∈I = πi0 ◦ hπii0i∈I = πi, πi0 ◦ h ◦ h0 = πi0 ◦ hπii0i∈I ◦ hπi0 ii∈I = πi ◦ hπi0 ii∈I = πi0 für alle i ∈ I, also h0 ◦ h = idP und h ◦ h0 = idP 0 wegen (2). Beweis von (ii). Sei f = (fi : B → Ai)i∈I und g =def h−1 ◦ hfiii∈I : B → P 0. g erfüllt (1) mit π 0 anstelle von π: Für alle i ∈ I, (1) πi0 ◦ g = πi ◦ h ◦ g = πi ◦ h ◦ h−1 ◦ hfiii∈I = πi ◦ hfiii∈I = fi. g ist eindeutig: Sei g 0 : B → P 0 eine weitere Funktion, die (1) erfüllt, für die also πi0 ◦ g 0 = fi für alle i ∈ I gilt. Daraus folgt πi ◦ h ◦ g = πi0 ◦ g = fi = πi0 ◦ g 0 = πi ◦ h ◦ g 0, also h ◦ g = h ◦ g 0 wegen (2) und damit g = h−1 ◦ h ◦ g = h−1 ◦ h ◦ g 0 = g 0. o 23 Sei (P, π) ein Produkt von (Ai)i∈I , (P 0, π 0) ein Produkt von (Bi)i∈I und f = (fi : Ai → Bi)i∈I . Die Funktion Y fi =def hfi ◦ πii : P → P 0 i∈I heißt Produkt von f . Daraus folgt πk ◦ ( Y f i ) = f k ◦ πk i∈I für alle k ∈ I. Sei I eine nichtleere Menge und n > 0. f I =def Q i∈I [n] f, f1 × · · · × fn =def f . Aufgabe 2.3 Zeigen Sie, dass im Fall P = Xi∈I Ai und P 0 = Xi∈I Bi (siehe Aufgabe 2.1) für Q alle f ∈ P ( i∈I fi)(f ) mit λi.fi ◦ f übereinstimmt. o 24 Vom Produkt kommt man zur Summe, indem man alle Funktionspfeile umdreht: Sei A = (Ai)i∈I ein Mengentupel, S eine Menge und ι = (ιi : Ai → S)i∈I ein Funktionstupel. Das Paar (S, ι) heißt Summe oder Coprodukt von A, wenn es für alle Tupel f = (fi : Ai → B)i∈I genau eine Funktion g : S → B gibt mit g ◦ ιi = fi (3) für alle i ∈ I. ιi heißt i-te Injektion von S und g Summenextension oder Domain-Tuplung von f . g wird mit [fi]i∈I und im Fall I = [n], n > 1, auch mit [f1, . . . , fn] bezeichnet. Demnach ist im Fall I = ∅ eine Menge S genau dann eine Summe, wenn es zu jeder Menge B genau eine Funktion von S nach B gibt, wenn also S die leere Menge ist. Wegen der Eindeutigkeit von Summenextensionen sind zwei Funktionen f, g : S → B genau dann gleich, wenn ihre Summenkomponenten paarweise miteinander übereinstimmen: (∀ i ∈ I : f ◦ ιi = g ◦ ιi) ⇒ f = g. (4) Aus (4) folgt, dass für alle a ∈ S genau ein Paar (b, i) mit ιi(b) = a existiert. 25 Beweis. Gäbe es a ∈ S \ S i∈I ιi (Ai ), dann würden f = λx.0 : S → 2 und g = (λx.if x ∈ S \ {a} then 0 else 1) : S → 2 (4) verletzen, weil für alle i ∈ I und b ∈ Ai Folgendes gilt: (f ◦ ιi)(b) = f (ιi(b)) = 0 = g(ιi(b)) = (g ◦ ιi)(b). Für alle i ∈ I und a ∈ Ai sei fi(a) = (a, i). Dann gilt für alle i, j ∈ I, a ∈ Ai und b ∈ Aj mit ιi(a) = ιj (b): (3) (a, i) = fi(a) = ([fi]i∈I ◦ ιi)(a) = [fi]i∈I (ιi(a)) = [fi]i∈I (ιj (b)) = ([fi]i∈I ◦ ιj )(b) (3) = fj (b) = (b, j). o Aufgabe 2.4 U Sei S = i∈I Ai (s.o.) und ιi(a) =def (a, i) für alle i ∈ I und a ∈ Ai. Zeigen Sie, dass (S, (ιi)i∈I ) eine Summe von A ist, wenn die Extension von (fi : Ai → B)i∈I wie folgt definiert wird: Für alle i ∈ I und a ∈ Ai, [fi]i∈I (a) =def fi(a). o Aufgabe 2.5 S Sei S = i∈I Ai und ιi(a) =def a für alle i ∈ I und a ∈ Ai. Zeigen Sie, dass (S, (ιi)i∈I ) eine Summe von A ist, falls A aus paarweise disjunkten Mengen besteht. o 26 Aufgabe 2.6 Sei A eine Menge, S = A∗ (s.o.) und ιn(a) =def a für alle n ∈ N und a = (a1, . . . , an) ∈ An. Zeigen Sie, dass (S, (ιn)n∈N) eine Summe von (An)n∈N ist. o Satz 2.7 (Summen sind bis auf Isomorphie eindeutig) (i) Seien (S, ι) und (S 0, ι0) Summen von A. Dann sind P und P 0 isomorph. (ii) Sei (S, ι) eine Summe von A, S 0 eine Menge und h : S → S 0 eine bijektive Abbildung. Dann ist (S 0, ι0) mit ι0 = (h ◦ ιi)i∈I ebenfalls eine Summe von A. Beweis. Analog zu Satz 2.2. Es müssen nur alle Funktionspfeile umgedreht werden. o Sei (S, ι) eine Summe von (Ai)i∈I , (S 0, ι0) eine Summe von (Bi)i∈I und f = (fi : Ai → Bi)i∈I . Die Funktion a fi =def [ι0i ◦ fi] : S → S 0 i∈I heißt Summe von f . 27 Daraus folgt ( a fi) ◦ ιk = ιk ◦ fk i∈I für alle k ∈ I. Sei n > 0. f + =def f ∗ =def f1 + · · · + fn =def [n] n∈N f , id1 + f +, ` ` i∈[n] fi . U U Aufgabe 2.8 Zeigen Sie, dass im Fall S = i∈I Ai und S 0 = i∈I Bi (siehe Aufgabe 2.4) ` für alle i ∈ I und a ∈ Ai ( i∈I fi)(a, i) mit (fi(a), i) übereinstimmt. o 28 Typen Sei S eine endliche Menge von – Sorten genannten – Symbolen und I eine Menge nichtleerer Indexmengen, die implizit die Mengen 1, 2 und [n], n > 1, enthält. Die Menge T (S, I) der I-polynomialen Typen über S ist die kleinste Menge von Ausdrücken mit folgenden Eigenschaften: • S ∪ I ⊆ T (S, I). ` Q • Für alle I ∈ I und {ei}i∈I ⊆ T (S, I), i∈I ei, i∈I ei ∈ T (S, I). Q ` Ein Typ der Form i∈I ei bzw. i∈I ei heißt Produkttyp bzw. Summentyp. Für alle I ∈ I, n > 1 und e, e1, . . . , en ∈ T (S, I) verwenden wir folgende Kurzschreibweisen: Q e1 × · · · × en =def i∈[n] ei , ` e1 + · · · + en =def i∈[n] ei , Q eI =def i∈I e, en =def e[n], e+ =def e + ` n>1 e + n , e∗ =def 1 + e . 29 Signaturen Eine Signatur Σ = (S, I, F ) besteht aus Mengen S und I wie oben sowie einer endlichen Menge F typisierter Funktionssymbole f : e → e0 mit e, e0 ∈ T (S, I), die üblicherweise Operationen genannt werden. dom(f ) = e heißt Domain, ran(f ) = e0 Range von f . f : e → e0 ∈ F heißt Konstruktor bzw. Destruktor, wenn e bzw. e0 zu S gehört. Σ heißt konstruktive bzw. destruktive Signatur, wenn F aus Konstruktoren bzw. Destruktoren besteht und für alle s ∈ S die Menge aller f ∈ F mit ran(f ) = s bzw. dom(f ) = s implizit zu I gehört. Die komponentenweise Vereinigung zweier Signaturen Σ und Σ0 wird mit Σ ∪ Σ0 bezeichnet. Konstruktoren dienen der Synthese von Elementen einer S-sortigen Menge, Destruktoren liefern Werkzeuge zu ihrer Analyse. Die abstrakte Syntax einer kontextfreien Grammatik (siehe Kapitel 4) ist eine konstruktive Signatur, während Parser, Interpreter und Compiler auf Automatenmodellen beruhen, die destruktive Signaturen interpretieren. 30 Die syntaktische Beschreibung jedes mathematischen Modells, das auf induktiv definierten Mengen basiert, kann als konstruktive Signatur formuliert werden. Solchen Modellen stehen die zustandsorientierten gegenüber, die Objekte nicht anhand ihres Aufbaus, sondern ausschließlich durch Beobachtung ihres Verhaltens voneinander unterscheiden. Dementsprechend werden aus den Operationssymbolen der jeweils zugrundeliegenden destruktiven Signatur keine Objekte, sondern Beobachtungsinstrumente zusammengesetzt. In den folgenden Beispielen steht hinter 1 die Trägermenge der initialen bzw. finalen Algebra der jeweiligen Signatur (siehe 2.11 und 2.17). Unter I listen wir nur die nicht implizit in I enthaltenen (1, 2 und [n]; s.o.) auf. Seien X, Y, X1, . . . , Xn, Y1, . . . , Yn, E1, . . . , En beliebige Mengen und BS eine endliche Menge von Basismengen, die ∅ und 1 enthält. 2.9 Konstruktive Signaturen • Mon 1 Unmarkierte binäre Bäume (Monoide sind Mon-Algebren; siehe 2.11.) S = {mon}, I = ∅, F = { one : 1 → mon, mul : mon × mon → mon }. 31 • Nat 1 N S = {nat}, I = ∅, F = { zero : 1 → nat, succ : nat → nat }. • Lists(X, Y ) 1 X ∗ × I S = {list}, I = {X, Y }, F = { nil : Y → list, cons : X × list → list }. • List(X) =def Lists(X, 1) 1 X ∗, alternativ: S = {list}, I = {X, N>1}, F = {[. . . ] : X ∗ → list}. • Bintree(X) 1 binäre Bäume endlicher Tiefe mit Knotenmarkierungen aus X S = {btree}, I = {X}, F = { empty : 1 → btree, bjoin : btree × X × btree → btree }. 32 • Tree(X) 1 Bäume endlicher Tiefe und endlichen Knotenausgrads mit Knotenmarkierungen aus X S = {tree, trees}, I = {X}, F = { join : X × trees → tree, nil : 1 → trees, cons : tree × trees → trees }. • Reg(BS) 1 reguläre Ausdrücke über BS S = { reg }, I = {BS}, F = { par : reg × reg → reg, seq : reg × reg → reg, iter : reg → reg, base : BS → reg }. (parallele Komposition) (sequentielle Komposition) (Iteration) (Einbettung der Basismengen) • CCS(Act) 1 Calculus of Communicating Systems (kommunizierende Prozesse) S = { proc }, I = {Act}, F = { pre : Act → proc, cho : proc × proc → proc, par : proc × proc → proc, res : proc × Act → proc, rel : proc × ActAct → proc }. (prefixing by an action) (choice) (parallelism) (restriction) (relabelling) 33 2.10 Destruktive Signaturen ∗ • DAut(X, Y ) 1 Y X = Verhalten deterministischer Moore-Automaten mit Eingabemenge X und Ausgabemenge Y S = { state }, (Zustandsmenge) I = { X, Y }, (Eingabemenge X und Ausgabemenge Y ) F = { δ : state → stateX , (Transitionsfunktion) β : state → Y }. (Ausgabefunktion) ∗ • Acc(X) =def DAut(X, 2) 1 P(X) ∼ = 2X = Verhalten deterministischer Akzeptoren von Sprachen über X • Stream(X ) =def DAut(1, X) 1 X N S = {stream}, I = {X}, F = { head : stream → X, tail : stream → stream }, alternativ: S = {stream}, I = {X, N}, F = {get : stream → X N}. • coList(X) 1 X ∗ ∪ X N S = {list}, I = {X}, F = {split : list → 1 + X × list}. 34 • Infbintree(X) 1 binäre Bäume unendlicher Tiefe mit Knotenmarkierungen aus X S = {btree}, I = {X}, F = { root : btree → X, left, right : btree → btree }. • coBintree(X) 1 binäre Bäume beliebiger Tiefe mit Knotenmarkierungen aus X S = {btree}, I = {X}, F = {split : btree → 1 + btree × X × btree}. • Inftree(X) 1 endlich verzweigende Bäume unendlicher Tiefe mit Knotenmarkierungen aus X S = {tree}, I = {X, N>1}, F = { root : tree → X, subtrees : tree → tree+ }. • FBtree(X) 1 endlich verzweigende Bäume beliebiger Tiefe mit Knotenmarkierungen aus X S = {tree}, I = {X, N>1}, F = { root : tree → X, subtrees : tree → tree∗ }. 35 • coTree(X) 1 beliebig verzweigende Bäume beliebiger Tiefe mit Knotenmarkierungen aus X S = { tree }, I = {X}, F = { root : tree → X, subtrees : tree → trees, split : trees → 1 + tree × trees }. ∗ • PAut(X, Y ) 1 (1 + Y )X = Verhalten partieller Moore-Automaten S = {state}, I = {X, Y }, F = { δ : state → (1 + state)X , β : state → Y }. ∗ • NAut(X, Y ) 1 (Y ∗)X = Verhalten nichtdeterministischer (bildendlicher) Moore-Automaten S = { state }, I = {X, Y, N>1}, F = { δ : state → (state∗)X , β : state → Y }. • SAut(X) 1 ([0, 1] × Y )∗ = Verhalten stochastischer Automaten ohne Eingabe S = { state }, I = {Y, [0, 1], N>1}, F = { δ : state → ([0, 1] × state)∗, β : state → Y }. 36 • Proctree(Act) 1 Prozessbäume, deren Kanten mit Aktionen markiert sind S = { state }, I = {Act, N>1}, F = { δ : tree → (Act × tree)∗ }. + • Mealy(X, Y ) 1 Y X = Verhalten von Mealy-Automaten S = {state}, I = {X, Y }, F = { δ : state → stateX , β : state → Y X }. + Y X ist isomorph zur Menge der kausalen Stromfunktionen f : X ∗ → Y ∗. f heißt kausal, wenn für alle s, s0 ∈ AN and n ∈ N Folgendes gilt: take(n)(s) = take(n)(s0) ⇒ f (s)(n) = f (s0)(n). • Class(I) 1 Verhalten von n Methoden einer Objektklasse [17] S = { state }, I = { X1, . . . , Xn, Y1, . . . , Yn, E1, . . . , En }, F = { mi : state → ((state × Yi) + Ei)Xi | 1 ≤ i ≤ n }. 37 Algebren Seien S und I wie oben. Eine T (S, I)-sortige Menge A heißt typverträglich, wenn für alle I ∈ I, AI mit I übereinstimmt und es für alle {ei}i∈I ⊆ T (S, I) Funktionstupel π = (πi : AQi∈I ei → Aei )i∈I und ι = (ιi : Aei → A`i∈I ei )i∈I gibt derart, dass (AQi∈I ei , π) ein Produkt und (A`i∈I ei , ι) eine Summe von (Aei )i∈I ist. Für alle e ∈ T (S, I) und a ∈ Ae nennen wir e den Typ von a. Das Typlifting einer S-sortigen Menge A ist Erweiterung von A zu einer typverträglichen T (S, I)-sortigen Menge, die entsteht, wenn man Indexmengen, Produkte und Summen wie folgt interpretiert: Für alle I ∈ I und {ei}i∈I ⊆ T (S, I), AI = I, AQi∈I ei = Xi∈I Aei , U A`i∈I ei = i∈I Aei . Aufgabe Sei A typverträglich. Zeigen Sie, dass für alle I ∈ I, n > 1 und e, e1, . . . , en ∈ T (S, I) Folgendes gilt: 38 Ae1×···×en Ae1+···+en AeI Aen Ae+ Ae∗ ∼ = ∼ = ∼ = ∼ = ∼ = ∼ = Ae1 × . . . × Aen , Ae1 + · · · + Aen , AIe , Ane, A+ e, A∗e . o Seien A, B typverträgliche T (S, I)-sortige Mengen. Eine T (S, I)-sortige Funktion h : A → B heißt typverträglich, wenn für alle I ∈ I und {ei}i∈I ⊆ T (S, I) Folgendes gilt: • hI = idI , Q ` • hQi∈I ei = i∈I hei und h`i∈I ei = i∈I hei . (1) (2) Das Typlifting einer S-sortigen Funktion h : A → B ist die – ebenfalls mit h bezeichnete – Erweiterung von h zu einer typverträglichen T (S, I)-sortigen Funktion. Aufgabe Sei h typverträglich. Zeigen Sie, dass für alle I ∈ I, n > 0 und e, e1, . . . , en ∈ T (S, I) Folgendes gilt: 39 he1×···×en he1+···+en heI hen he+ he∗ = = = = = = he1 × . . . × hen , he1 + · · · + hen , hIe , [n] he , h+ e, h∗e . o Sei Σ = (S, I, F ) eine Signatur. Eine Σ-Algebra A = (A, Op) besteht aus einer typverträglichen T (S, I)-sortigen Menge A und einer F -sortigen Menge Op = (f A : Ae → Ae0 )f :e→e0∈F von Funktionen, den Operationen von A. Für alle e ∈ T (S, I) heißt Ae Trägermenge (carrier set) oder Interpretation von e in A. Für alle f : e → e0 ∈ F heißt f A : Ae → Ae0 Interpretation von f in A. Im Fall |Ae| = 1 schreibt man f A anstelle von f A(a). Häufig wird auch die Vereinigung aller Trägermengen von A mit A bezeichnet! AlgΣ bezeichnet die Klasse aller Σ-Algebren. Algebren destruktiver Signaturen werden auch Coalgebren genannt. 40 Seien A, B Σ-Algebren. Das Typlifting einer S-sortigen Funktion h : A → B heißt ΣHomomorphismus, wenn für alle f : e → e0 ∈ F he0 ◦ f A = f B ◦ he gilt. Ist h bijektiv, dann heißt h Σ-Isomorphismus und A und B sind Σ-isomorph. h induziert die Bildalgebra h(A): • Für alle e ∈ T (S, I), h(A)e =def he(Ae). • Für alle f : e → e0 ∈ F und a ∈ Ae, f h(A)(h(a)) =def f B (h(a)). Algebra-Beispiele Die Menge der natürlichen Zahlen ist Trägermenge der Nat-Algebra NN = (A, Op = {zeroNN : 1 → N, succNN : N → N}) und auch der List(1)-Algebra NL = (B, Op = {nilNL : 1 → N, consNL : 1 × N → N}), die wie folgt definiert sind: 41 Anat = Blist = N und für alle n ∈ N, zeroNN succNN (n) nilNL consNL(n) = = = = 0, n + 1, 0, n + 1. Die Menge der Wörter über einer Menge X ist Trägermenge der List(X)-Algebra SL(X ) = (A, Op = {nilSL : 1 → X ∗, consSL : X × X ∗ → X ∗}) und auch der Mon-Algebra SM (X ) = (B, Op = {oneSM (X ) : 1 → X ∗, mulSM (X ) : X ∗ × X ∗ → X ∗}), die wie folgt definiert sind: Alist = Bmon = X ∗ und für alle x ∈ X und v, w ∈ X ∗, nilSL(X ) consSL(X )(x, w) oneSM (X ) mulSM (X )(v, w) = = = = , xw, , v · w. Die Menge X X der Funktionen von X nach X ist Trägermenge der Mon-Algebra FM (X ) = (A, Op = {oneFM (X ) : 1 → X X , mulFM (X ) : X X × X X → X X }), 42 die wie folgt definiert ist: Amon = X X und für alle f, g : X → X, oneFM (X ) = idX , mulFM (X )(f, g) = g ◦ f. Eine Mon-Algebra A heißt Monoid, wenn mulA assoziativ und oneA ein links- und rechtsneutrales Element bzgl. mulA ist. Demnach sind SM(X) und FM(X) Monoide. Die folgende Stream(Z)-Algebra zo repräsentiert die Ströme 0, 1, 0, 1, . . . und 1, 0, 1, 0, . . . : zostream headzo(Blink ) tailzo(Blink ) headzo(Blink 0) tailzo(Blink 0) = = = = = {Blink , Blink 0}, 0, Blink 0, 1, Blink . Die folgende Acc(Z)-Algebra eo dient der Erkennung der Parität der Summe von Zahlenfolgen (siehe Beispiel 2.23) und ist wie folgt definiert: eostate δ eo(Esum) δ eo(Osum) β eo = = = = {Esum, Osum}, λx.if even(x) then Esum else Osum, λx.if odd(x) then Esum else Osum, λst.if st = esum then 1 else 0. o 43 2.12 Terme und Coterme Sei Σ = (S, I, F ) eine Signatur, V eine S-sortige Menge, [ [ XΣ =def I ∪ {sel} und YΣ,V =def I ∪ V ∪ {tup}. Im Folgenden verwenden wir die im Abschnitt Mengen und Typen eingeführte Notation für deterministische Bäume. Sei Σ konstruktiv. CTΣ(V ) bezeichnet die größte T (S, I)-sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften: • Für alle I ∈ I, MI = I. (1) • Für alle s ∈ S und t ∈ Ms gehört t zu Vs (2) oder gibt es c : e → s ∈ F und t0 ∈ Me mit t = c{sel → t0}. (3) • Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), t ∈ MQi∈I ei und i ∈ I gibt es ti ∈ Mei mit t = tup{i → ti | i ∈ I}. (4) • Für alle I ∈ I, {ei}i∈I ⊆ T (S, I) und t ∈ M`i∈I ei gibt es i ∈ I und t0 ∈ Mei mit t = i{sel → t0}. (5) 44 (1) (2/6) (3/7) (4) (5) Knoten und Kanten eines Σ-Terms. Neben jedem Knoten n steht der Typ von Termen mit Wurzel n. CTΣ(V ) ist typverträglich. Beweis. Für all I ∈ I und {ei}i∈I ⊆ T (S, I) ist (CTΣ(V )Qi∈I , π) mit πi({tup → ti | i ∈ I}) =def ti und ti ∈ CTΣ(V )ei ein Produkt und (CTΣ(V )`i∈I , ι) mit ιi(t) =def i{sel → t} und t ∈ CTΣ(V )ei eine Summe von (CTΣ(V )i)i∈I . o Die Elemente von CTΣ(V ) heißen Σ-Terme über V . 45 Die Elemente von CTΣ =def CTΣ(∅) heißen Σ-Grundterme. Aufgabe Zeigen Sie, dass TΣ(V ) =def CTΣ(V ) ∩ wdtr(XΣ, YΣ,V ) die kleinste T (S, I)sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit (1) und folgenden Eigenschaften ist: • Für alle s ∈ S, Vs ⊆ Ms. (6) • Für alle c : e → s ∈ F und t ∈ Me gehört c{sel → t} zu Ms. (7) • Für alle I ∈ I, {ei}i∈I ⊆ T (S, I) und ti ∈ Mei , i ∈ I, gehört tup{i → ti | i ∈ I} zu (8) MQi∈I ei . • Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), i ∈ I und t ∈ Mei gehört i{sel → t} zu M`i∈I ei . (9) TΣ(V ) und CTΣ(V ) sind die Trägermengen von Σ-Algebren (s.u.). Darüberhinaus sind wohlfundierte Σ-Terme Bestandteile von Formeln wie z.B. den in Kapitel 18 iterativen Gleichungen wie (1) in Abschnitt 2.13. 46 Sei Σ destruktiv. DTΣ(V )bezeichnet die größte T (S, I)-sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit (1), (4), (5) und folgender Eigenschaft: • Für alle s ∈ S und t ∈ Ms gibt es x ∈ Vs und für alle d : s → e ∈ F gibt es td ∈ Me mit t = x{d → td | d : s → e ∈ F }. (10) (1) (10/11) (4) (5) Knoten und Kanten eines Σ-Coterms. Neben jedem Knoten n steht der Typ von Cotermen mit Wurzel n. Die Elemente von DTΣ(V ) heißen Σ-Coterme über V . 47 Die Elemente von DTΣ =def DTΣ(1) heißen Σ-Grundcoterme. Aufgabe Zeigen Sie, dass coTΣ(V ) =def DTΣ(V ) ∩ wdtr(XΣ, YΣ,V ) die kleinste T (S, I)sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit (1), (8), (9) und folgender Eigenschaft ist: • Für alle s ∈ S, x ∈ Vs, d : s → e ∈ F und td ∈ Me gehört x{d → td | d : s → e ∈ F } zu Ms. (11) Demgegenüber bezeichnet TΣ(V ) die kleinste T (S, I)-sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit (1), (8), (9) und folgender Eigenschaft: • Für alle s ∈ S, d : s → e ∈ F und td ∈ Me gehört {d → td | d : s → e ∈ F } zu Ms. (12) Wie im Fall einer konstruktiven Signatur werden die Elemente von TΣ(V ) (wohlfundierte) Σ-Terme über V genannt. coTΣ(V ) und DTΣ(V ) sind die Trägermengen von Σ-Algebren (s.u.). Im Gegensatz zu Σ-Termen kommen Σ-Coterme in Formeln nicht vor. Stattdessen werden auch im Fall einer destruktiven Signatur nur wohlfundierte Σ-Terme in Formeln verwendet wie z.B. den Gleichungen (2) und (3) von Abschnitt 2.13. 48 Zur Vereinfachung der Wortdarstellung deterministischer Bäume verwenden wir die folgenden Abkürzungen: Sei c ∈ YΣ,V und n > 1. c steht für c{sel → }, c(t) steht für c{sel → t}, c(t1, . . . , tn) steht für c{sel → tup{1 → t1, . . . , n → tn}}, c{. . .} steht für c{sel → tup{. . .}}, {. . .} steht für {. . .}. 2.13 Beispiele Sei Σ = Nat (siehe 2.9). CTΣ,nat ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft: • Für alle t ∈ M gilt t = zero oder gibt es u ∈ M mit t = succ(u). TΣ,nat ist die kleinste Teilmenge M von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften: • zero ∈ M . • Für alle t ∈ M , succ(t) ∈ M . 49 Sei Σ = List(X) (siehe 2.9). CTΣ,list ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft: • Für alle t ∈ M gilt t = nil oder gibt es x ∈ X und u ∈ M mit t = cons(x, u). TΣ,list ist die kleinste Teilmenge M von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften: • nil ∈ M . • Für alle x ∈ X und t ∈ M , cons(x, t) ∈ M . Sei V = {blink, blink 0}. Die folgenden iterativen Gleichungen zwischen List(Z)-Termen über V haben eine eindeutige Lösung in CTList(Z) (siehe Kapitel 18): blink = cons(0, blink 0), blink 0 = cons(1, blink). (1) Deshalb definiert (1) blink und blink 0 als zwei Elemente von CTList(Z). Als eindeutige Lösungen iterativer Gleichungen repräsentierbare unendliche Terme heißen rational. Sei Σ = Reg(BS) (siehe 2.9). TΣ,reg ist die kleinste Teilmenge M von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften: • Für alle t, u ∈ M , par(t, u), seq(t, u) ∈ M . • Für alle t ∈ M , iter(t) ∈ M . 50 • Für alle B ∈ BS, base(B) ∈ M . Sei Σ = Stream(X) (siehe 2.10). DTΣ,stream ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft: • Für alle t ∈ M gibt es x ∈ X und t0 ∈ M mit t = {head → x, tail → t0} ∈ M . ε head tail 0 ε tail head 1 ε head 2 tail ε Stream(N)-Coterm, der den Strom aller natürlichen Zahlen darstellt Sei V = {blink, blink 0}. Da zo eine Stream(Z)-Algebra (siehe 2.11) und DTStream(Z) eine finale Stream(Z)-Algebra ist (s.u.), lösen die Coterme unfold zo(Blink ) und unfold zo(Blink 0) die folgenden Gleichungen zwischen Stream(Z)-Cotermen über V eindeutig in blink bzw. blink 0: 51 blink = {head → 0, tail → blink 0}, blink 0 = {head → 1, tail → blink} (2) (siehe Kapitel 18). Deshalb definiert (2) blink und blink 0 als zwei Elemente von DTStream(Z). Als eindeutige Lösungen iterativer Gleichungen repräsentierbare unendliche Coterme heißen rational. Sei Σ = Colist(X) (siehe 2.10). DTΣ,list ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft: • Für alle t ∈ M gilt t = {split → 1} oder gibt es x ∈ X und u ∈ M mit t = {split → 2{1 → x, 2 → u}}. Sei Σ = DAut(X, Y ) (siehe 2.10). DTΣ,state ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft: • Für alle t ∈ M und x ∈ X gibt es tx ∈ M und y ∈ Y mit t = {δ → tup{x → tx | x ∈ X}, β → y}. 52 ε β 0 δ 1 β ε x y ε 0 tup x z ε y δ ε tup β z δ tup ε δ ε ε x 1 β tup x z ε ε z ε y ε y ε Acc({x, y, z})-Coterm, der einen Akzeptor der Sprache {w ∈ {x, y, z}∗ | w enthält x oder z} darstellt Sei V = {esum, osum}. Da eo eine Acc(Z)-Algebra (siehe 2.11) und DTAcc(Z) eine finale Acc(Z)-Algebra ist (s.u.), lösen die Coterme unfold eo(Esum) und unfold eo(Osum) die folgenden iterativen Gleichungen zwischen Acc(Z)-Cotermen über V eindeutig in esum bzw. esum: 53 esum = {δ → tup({x → esum | x ∈ even} ∪ {x → osum | x ∈ odd}), β → 1}, osum = {δ → tup{x → osum | x ∈ even} ∪ {x → esum | x ∈ odd}), β → 0}. even und odd bezeichnen die Mengen der geraden bzw. ungeraden ganzen Zahlen. (3) o CTΣ(V ) und DTΣ(V ) sind Σ-Algebren Sei Σ = (S, I, C) eine konstruktive Signatur. CTΣ(V ) wird wie folgt zur Σ-Algebra erweitert: • Für alle c : e → s ∈ C, t ∈ CTΣ(V )e, cCTΣ(V )(t) =def c{sel → t}. • Für alle I ∈ I, ti ∈ CTΣ(V )ei , i ∈ I, and k ∈ I, πk (tup{i → ti | i ∈ I}) =def tk . • Für alle I ∈ I, i ∈ I and t ∈ CTΣ(V )ei , ιi(t) =def i{sel → t}. Sei Σ = (S, I, D) eine destruktive Signatur. DTΣ(V ) wird wie folgt zur Σ-Algebra erweitert: • Für alle s ∈ S, x ∈ Vs and td ∈ DTΣ(V )e, d : s → e ∈ D, and d0 : s → e0, d0DTΣ(V )(x{d → td | d : s → e ∈ D}) =def td0 . 54 • Für alle I ∈ I, ti ∈ DTΣ(V )ei , i ∈ I, and k ∈ I, πk (tup{i → ti | i ∈ I}) =def tk . • Für alle I ∈ I, i ∈ I and t ∈ DTΣ(V )ei , ιi(t) =def i{sel → t}. Die Interpretation von Destruktoren in DTΣ(V ) machen Coterme zu eier Art analytischer Funktionen: Zwei Coterme t, t0 ∈ DTΣ(V )s sind genau dann gleich, wenn die “Anfangswerte” t() und t0() und für alle d : s → e die “Ableitungen” dDTΣ(V )(t) und dDTΣ(V )(t0) miteinander übereinstimmen. Die atomaren Formeln der Prädikatenlogik sind aus Σ-Termen und Prädikaten (= Relationssymbolen) zusammengesetzt. Prinzipiell kommt man dort mit einer einzigen Sorte aus, muss dann aber die Trennung zwischen mehreren Datenbereichen durch die Einführung eines einstelligen Prädikats für jeden Datenbereich wiedergeben. So entspricht z.B. die Sorte nat der Signatur Nat (siehe 2.9) das Prädikat isNat mit den Axiomen isNat(zero), isNat(x) ⇒ isNat(succ(x)). (1) (2) Im Rahmen einer Nat umfassenden Signatur Σ wäre ein Σ-Term t genau dann ein NatGrundterm des Typs nat, wenn isNat(t) mit Inferenzregeln der Prädikatenlogik aus (1) und (2) ableitbar ist. 55 2.14 Die Algebren Bool und Regword (BS) (siehe 2.9) Die Menge 2 = {0, 1} ist Trägermenge der Reg(BS)-Algebra Bool : Für alle x, y ∈ 2 und B ∈ BS \ 1, parBool (x, y) seq Bool (x, y) iterBool (x) baseBool (1) baseBool (B) = = = = = max{x, y}, x ∗ y, 1, 1, 0. Die üblichen Wortdarstellungen regulärer Ausdrücke e wie z.B. aab∗ + c(aN)∗ bilden die Reg(BS)-Algebra Regword (BS): Sei syms(BS) = {+,∗ , (, )} ∪ {B | B ∈ BS}. Im Fall |B| = 1 setzen wir B mit dem einen Element von B gleich. Ob ein Teilausdruck e geklammert werden muss oder nicht, hängt von der Priorität des Operationssymbols f ab, zu dessen Domain e gehört. Die Trägermenge von Regword (BS) besteht deshalb aus Funktionen, die, abhängig von der Priorität von f , die Wortdarstellung von e mit oder ohne Klammern zurückgibt: Regword (BS)reg = N → syms(BS)∗. 56 Aus den Prioritäten 0,1,2 von par, seq bzw. iter ergeben sich folgende Interpretation der Operationssymbole von Reg(BS): Für alle f, g : N → syms(BS)∗, B ∈ BS und w ∈ syms(BS)∗, parRegword (BS)(f, g) seq Regword (BS)(f, g) iterRegword (BS)(f ) baseRegword (BS)(B) enclose(True)(w) enclose(False)(w) = = = = = = λn.enclose(n > 0)(f (0) + g(0)), λn.enclose(n > 1)(f (1) g(1)), λn.enclose(n > 2)(f (2)∗), λn.B, (w), w. Ist die Funktion f : N → syms(BS)∗ das Ergebnis der Faltung eines Reg(BS)-Terms t in Regword (BS) (s.u.), dann liefert f (0) eine Wortdarstellung von t, die keine überflüssigen Klammern enthält. Regword(String) ist im Haskell-Modul Compiler.hs durch regWord implementiert. o 2.15 Die Algebren Beh(X, Y ), Pow (X), Lang(X) und Bro(BS) (siehe 2.10) Seien X und Y Mengen. 57 Funktionen von X ∗ nach Y nennen wir Verhaltensfunktionen (behavior functions). Sie bilden die Trägermenge folgender DAut(X, Y )-Algebra Beh(X, Y ): ∗ Beh(X, Y )state = Y X . Für alle f : X ∗ → Y , x ∈ X und w ∈ X ∗, δ Beh(X,Y )(f )(x)(w) = f (xw) und β Beh(X,Y )(f ) = f (). Beh(X, 2) ist im Haskell-Modul Compiler.hs durch behFun implementiert. ∗ Eine zu Beh(X, 2)state = 2X bijektive Menge ist die Potenzmenge P(X ∗) (siehe Mengen und Typen). Daraus ergibt sich die Acc(X)-Algebra Pow (X) mit Pow (X)state = P(X ∗). Für alle L ⊆ X ∗ und x ∈ X, ( δ Pow (X)(L)(x) = {w ∈ X ∗ | xw ∈ L} und β Pow (X)(L) = 1 falls ∈ L, 0 sonst. 58 ∗ Aufgabe Zeigen Sie, dass die Funktion χ : P(X ∗) → 2X , die jeder Teilmenge von X ∗ ihre charakteristische Funktion zuordnet (siehe Mengen und Typen), ein Acc(X)-Homomorphismus von Pow (X) nach Beh(X, 2) ist. o Sei BS eine endliche Menge von Basismengen, die ∅ und 1 enthält, und X = S BS \ 1. P(X ∗) ist nicht nur die Trägermenge der Acc(X)-Algebra Pow (X), sondern auch der folgendermaßen definierten Reg(BS)-Algebra Lang(X) der Sprachen über X: Lang(X)reg = P(X ∗). Für alle B ∈ BS und L, L0 ⊆ X ∗, parLang(X)(L, L0) seq Lang(X)(L, L0) iterLang(X)(L) baseLang(X)(B) = = = = L ∪ L0 , L · L0 , L∗, B. Anstelle von Lang(X) wird in Compiler.hs die Reg(BS)-Algebra regB mit der Trägermen∗ ge 2X implementiert. Sie macht χ zum Reg(BS)-Homomorphismus. Aufgabe Zeigen Sie, dass χ ein Reg(BS)-Homomorphismus von Lang(X) nach regB ist. o 59 Die Menge der Reg(BS)-Grundterme ist nicht nur eine Trägermenge der Reg(BS)-Algebra TReg(BS), sondern auch der wie folgt definierten Acc(X)-Algebra Bro(BS) (accT in Compiler.hs; siehe [2, 15]), die Brzozowski-Automat genannt wird: Bro(BS)state = TReg(BS),reg . Die Interpretationen von δ und β in Bro(BS) werden induktiv über dem Aufbau von Reg(BS)-Grundtermen definiert: Für alle t, u ∈ TReg(BS) und B ∈ BS \ 1, δ Bro(BS)(par(t, u)) = λx.par(δ Bro(BS)(t)(x), δ Bro(BS)(u)(x)), δ Bro(BS)(seq(t, u)) = λx.par(seq(δ Bro(BS)(t)(x), u), if β Bro(BS)(t) = 1 then δ Bro(BS)(u)(x) else reg(∅)), δ Bro(BS)(iter(t)) = λx.seq(δ Bro(BS)(t)(x), iter(t)), δ Bro(BS)(base(1)) = base(∅), δ Bro(BS)(base(B)) = λx.if x ∈ B then base(1) else base(∅), β Bro(BS)(par(t, u)) = max{β Bro(BS)(t), β Bro(BS)(u)}, β Bro(BS)(seq(t, u)) = β Bro(BS)(t) ∗ β Bro(BS)(u), β Bro(BS)(iter(t)) = 1, β Bro(BS)(base(1)) = 1, β Bro(BS)(base(B)) = 0. 60 2.16 Die Erreichbarkeitsfunktion Sei A = (A, Op) eine DAut(X, Y )-Algebra und Z = Astate. Die Erreichbarkeitsfunktion reachA : X ∗ → Z Z von A ist wie folgt induktiv definiert: Für alle x ∈ X und w ∈ X ∗, reachA() = idA, reachA(xw) = reachA(w) ◦ λa.δ A(a)(x). Der Definitionsbereich X ∗ von reachA ist Trägermenge der Mon-Algebra SM(X), der Wertebereich Z Z ist Trägermenge der Mon-Algebra FM(Z) (siehe 2.11). reachA ist Mon-homomorph: reachA(oneSM (X )) = reachA() = idA = oneFM (Z ). Für alle x ∈ X und v, w ∈ X ∗, reachA(mulSM (X )(, w)) = reachA(w) = reachA(w) = reachA(w) ◦ idA = reachA(w) ◦ reachA() = mulFM (Z )(reachA(), reachA(w)), reachA(mulSM (X )(xv, w)) = reachA(xvw) = reachA(x(mulSM (X )(v, w))) = reachA(mulSM (X )(v, w)) ◦ λa.δ A(a)(x) ind. hyp. = (mulFM (Z )(reachA(v), reachA(w)) ◦ λa.δ A(a)(x) 61 = (reachA(w) ◦ reachA(v)) ◦ λa.δ A(a)(x) = reachA(w) ◦ (reachA(v) ◦ λa.δ A(a)(x)) = reachA(w) ◦ reachA(xv) = mulFM (Z )(reachA(xv), reachA(w)). o 2.17 Termfaltung und Zustandsentfaltung Sei Σ = (S, I, C) eine konstruktive Signatur, A = (A, Op) eine Σ-Algebra, V eine S-sortige Menge von “Variablen” und g : V → A eine – Variablenbelegung (valuation) genannte – S-sortige Funktion. In Abhängigkeit von g wertet die wie folgt induktiv definierte T (S, I)-sortige Funktion g ∗ : TΣ(V ) → A, die Extension von g, wohlfundierte Σ-Terme über V in A aus: • Für alle • Für alle • Für alle • Für alle ∗ πk (gQ i∈I I ∈ I, gI∗ = idI . s ∈ S und x ∈ Vs, gs∗(x) = gs(x). c : e → s ∈ F und t ∈ TΣ(V )e, gs∗(c{sel → t}) = cA(ge∗(t)). I ∈ I und {ei}i∈I ⊆ T (S, I), ti ∈ TΣ(V )ei , i ∈ I, und k ∈ I, ∗ ei ({tup → ti | i ∈ I})) = gek (tk ). • Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), k ∈ I und t ∈ TΣ(V )ek , ∗ ∗ g` ei (k{sel → t}) = ιk (gek (t)). i∈I (1) (2) (3) (4) (5) 62 Satz 2.18 ([32], Theorem FREE) g ∗ ist Σ-homomorph und der einzige Σ-Homomorphismus von TΣ(V ) nach A ist, der (2) erfüllt. Offenbar hängt die Einschränkung von g ∗ auf Grundterme nicht von g ab. Sie wird Termfaltung genannt und mit fold A bezeichnet. Umgekehrt stimmt g ∗ mit der Termfaltung fold A(g) : TΣ0 → A(g) überein, wobei Σ0 =def (S, I ∪ {Vs | s ∈ S}, C ∪ {vars : Vs → s | s ∈ S}) A(g) und A(g) ∈ AlgΣ0 definiert ist durch A(g)|Σ = A und vars = g für alle s ∈ S. Aus Satz 2.18 folgt sofort: fold A ist der einzige Σ-Homomorphismus von TΣ nach A. (6) Wegen (6) nennt man TΣ eine initiale Σ-Algebra. Wie die Produkt- oder Summeneigenschaft ist auch Initialität eine universelle Eigenschaft (vgl. Satz 2.2 bzw. 2.7): Alle initialen Σ-Algebren sind Σ-isomorph und jede zu einer initialen Σ-Algebra isomorphe Σ-Algebra ist initial. TΣ wird deshalb auch die initiale Σ-Algebra genannt. 63 Z.B. ist neben TNat auch NN eine initiale Nat-Algebra und neben TList(X) auch SL(X ) eine initiale List(X)-Algebra (siehe 2.11). Aufgabe Wie lauten die Isomorphismen von TNat nach NN bzw. von TList(X) nach SL? o Die Faltung fold Lang(X)(t) eines Reg(BS)-Grundterms – also eines regulären Ausdrucks – t in Lang(X) heißt Sprache von t (siehe 2.15). Umgekehrt nennt man L ⊆ X ∗ eine reguläre Sprache, wenn L zum Bild von fold Lang(X) gehört. Die Haskell-Funktion foldReg von Compiler.hs implementiert die Faltung fold A : TReg(BS) → A in einer beliebigen Reg(BS)-Algebra A. Aufgabe Zeigen Sie fold Bool = β Bro(BS). o Aufgabe Zeigen Sie durch Induktion über den Aufbau von Reg(BS)-Grundtermen, dass für alle t ∈ TReg(BS) die folgende Äquivalenz gilt: ∈ fold Lang(X)(t) ⇔ fold Bool (t) = 1. o 64 Sei Σ = (S, I, D) eine destruktive Signatur, A = (A, Op) eine Σ-Algebra, V eine S-sortige Menge von “Farben” und g : A → V eine – Färbung (coloring) genannte – S-sortige Funktion. In Abhängigkeit von g berechnet die wie folgt definierte T (S, I)-sortige Funktion g # : A → DTΣ(V ), die Coextension von g, zu jedem Zustand a ∈ A das Verhalten von a in Gestalt eines Σ-Coterms über V : • Für alle I ∈ I, gI# = idI . • Für alle s ∈ S und a ∈ As, gs#(a) = gs(a){d → ge#(dA(a)) | d : s → e ∈ D}. • Für alle I ∈ I, (ei)i∈I ∈ T (S, I) und a ∈ AQi∈I ei , # # gQ ei (a) = tup{i → gei (πi (a)) | i ∈ I}. i∈I • Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), k ∈ I und a ∈ Aek , # # g` ei (ιk (a)) = k{sel → gek (a)}. i∈I (1) (2) (3) (4) Nicht g # selbst, wohl aber jedes einzelne Bild g #(a) : XΣ∗ (→ YΣ,V von g # ist induktiv definiert. So ist z.B. (2) eine Kurzform für: 65 falls w = , gs(a) gs#(a)(w) = ge#(dA(a))(w0) falls ∃ d : s → e ∈ D, w0 ∈ XΣ∗ : w = dw0, undefiniert sonst. Satz 2.19 ([32], Theorem COFREE) g # ist Σ-homomorph und der einzige Σ-Homomorphismus von A nach DTΣ(V ) ist, der für alle a ∈ A g #(a)() = g(a) erfüllt. Offenbar hängt die Einschränkung von g # auf Grundcoterme nicht von g ab. Sie wird Zustandsentfaltung genannt und mit unfold A bezeichnet. Umgekehrt stimmt g # mit der Zustandsentfaltung unfold A(g) : A(g) → DTΣ0 überein, wobei Σ0 =def (S, I ∪ {Vs | s ∈ S}, D ∪ {vars : s → Vs | s ∈ S}) A(g) und A(g) ∈ AlgΣ0 definiert ist durch A(g)|Σ = A und vars = g für alle s ∈ S. Aus Satz 2.19 folgt sofort: unfold A ist der einzige Σ-Homomorphismus von A nach DTΣ. (5) Wegen (5) nennt man DTΣ eine finale oder terminale Σ-Algebra. 66 Wie Initialität ist auch Finalität eine universelle Eigenschaft: Alle finalen Σ-Algebren sind Σ-isomorph und jede zu einer finalen Σ-Algebra isomorphe Σ-Algebra ist final. DTΣ wird deshalb auch die finale Σ-Algebra genannt. Z.B. ist neben DTStream(X) auch StreamFun(X ) eine finale Stream(X)-Algebra (siehe 17.5), neben DTDAut(X,Y ) auch Beh(X, Y ) eine finale DAut(X, Y )-Algebra und neben DTAcc(X) auch Pow (X) eine finale Acc(X)-Algebra (siehe 2.15). Beispiel 2.20 Sei Σ = DAut(X, Y ). Die Trägermengen von DTΣ und Beh(X, Y ) sind isomorph: Sei L = {(δ, x) | x ∈ X}. DTΣ besteht aus allen Funktionen von L∗ + L∗β nach 1 + Y , die für alle w ∈ L∗ w auf und wβ auf ein Element von Y abbilden, m.a.W.: DTΣ ∼ = 1 L∗ ×Y L∗ β ∼ = Y L∗ β L∗ β ∼ =X ∗ ∼ = ∗ YX . ξ : Beh(X, Y ) → DTΣ bezeichne den entsprechenden DAut(X, Y )-Isomorphismus. o Aufgabe Wie lautet der Isomorphismus zwischen DTAcc(X) und Pow (X)? o 67 Die Acc(X)-Algebra ist DTAcc(X) ist im Haskell-Modul Compiler.hs durch accC implementiert. Coterme kann man als verallgemeinerte Verhaltensfunktionen betrachten. Auch der Realisierungsbegriff von Abschnitt 2.11 lässt sich von Beh(X, Y ) auf beliebige destruktive Signaturen übertragen: Für alle Σ-Algebren, s ∈ S und a ∈ As, (A, a) realisiert t ∈ DTΣ,s ⇔def unfold A s (a) = t. Aufgabe Zeigen Sie, dass χ : P(X) → 2X ein Acc(X)-Homomorphismus von Pow (X) nach Beh(X, 2) ist. o Sei A = (A, Op) eine Acc(X)-Algebra. Da Pow (X) final ist, gibt es genau einen Acc(X)Homomorphismus unfoldP A : A → Pow (X). 68 Daraus ergeben sich folgende Äquivalenzen: fold Lang(X) ist Acc(X)-homomorph ⇔ fold Lang(X) = unfoldP Bro(BS) : TReg(BS) → P(X ∗) (1) (2) ⇔ unfoldP Bro(BS) ist Reg(BS)-homomorph. (3) Aufgabe Zeigen Sie (2), indem Sie (1) oder (3) beweisen. o (2) folgt auch aus der Form der Gleichungen, die Bro(BS) definieren (siehe Beispiel 17.10). Sei A = (A, Op) eine DAut(X, Y )-Algebra. Da Beh(X, Y ) final ist, gibt es genau einen DAut(X, Y )-Homomorphismus unfoldB A : A → Beh(X, Y ). unfoldB A ordnet jedem Zustand a ∈ Astate eine Verhaltensfunktion zu, die für jedes Eingabewort w ∈ X ∗ die Ausgabe desjenigen Zustandes zurückgibt, den A nach Verarbeitung von w erreicht hat: runA (a) βA unfoldB A(a) = X ∗ −→ Astate −→ Y. ∗ A X runA : Astate → AX state setzt die Transitionsfunktion δ : Astate → Astate auf Wörter fort: 69 Für alle a ∈ Astate, x ∈ X und w ∈ X ∗, runA(a)() =def a, runA(a)(xw) =def runA(δ A(a)(x))(w). (4) (5) runA ist die Erreichbarkeitsfunktion von 2.16 mit vertauschten Argumenten: runA(a)(w) = reachA(w)(a). Außerdem gilt für alle v, w ∈ X ∗: runA(a)(vw) = runA(runA(a)(v))(w). (6) (4) und (6) machen (die dekaskadierte Version von) runA zur Aktion des Monoids X ∗: Für eine beliebige Zustandsmenge Q und ein beliebiges Monoid M mit Multiplikation ∗ und neutralem Element e heißt eine Funktion (·) : Q × M → Q Aktion von M , wenn für alle q ∈ Q und m, m0 ∈ M Folgendes gilt: q · e = q, q · (m ∗ m0) = (q · m) · m0. (7) (8) Aufgabe Sei h der Isomorphismus von DTDAut(X,Y ) nach Beh(X, Y ). Zeigen Sie: runA = h ◦ id# A. o (9) 70 Initiale Automaten Sei a ∈ Astate. Man nennt das Paar (A, a) einen initialen Automaten, der die Verhaltensfunktion unfoldB A(a) : X ∗ → Y realisiert (siehe 2.15). Sei L ⊆ X ∗ und Y = 2. (A, a) erkennt oder akzeptiert L, wenn (A, a) die charakteristische Funktion χ(L) : X ∗ → 2 von L realisiert. ∗ Da χ : P(X ∗) → 2X Acc(X)-homomorph ist (s.o.) und Beh(X, 2) eine finale Acc(X)Algebra, stimmt unfoldB A mit χ ◦ unfoldP A überein. Folglich wird L ⊆ X ∗ genau dann von (A, a) erkannt, wenn L mit unfoldP A(a) übereinstimmt: unfoldP A(a) = L ⇔ χ(unfoldP A(a)) = χ(L) ⇔ unfoldB A(a) = χ(L) ⇔ (A, a) erkennt L. (6) Insbesondere gilt für jeden regulären Ausdruck t ∈ TReg(BS), dass der initiale Automat (Bro(BS), t) die Sprache von t erkennt: (Bro(BS), t) erkennt fold Lang(X)(t) (6) (2) ⇔ unfoldP Bro (BS)(t) = fold Lang(X)(t) ⇔ fold Lang(X)(t) = fold Lang(X)(t). 71 Damit erfüllt (Bro(BS), t) dieselbe Aufgabe wie der klassische Potenzautomat zur Erkennung der Sprache von t, der über den Umweg eines nichtdeterministischen Automaten mit -Übergängen aus t gebildet wird (siehe Beispiel 12.3). Übersicht über die oben definierten Reg(BS)- bzw. DAut(X, Y )-Algebren und ihre jeweiligen Implementierungen in Compiler.hs: 72 Signatur Reg(BS) Acc(X) TReg(BS) RegT TReg(BS) regT initial Bro(BS) accT P(X ∗) Lang(X) Trägermenge 2X ∗ YX 2 Algebra χ(Lang(X)) regB Pow (X) final χ(Pow (X)) Beh(X, 2) behFun final Beh(X, Y ) final ∗ N → syms(BS)∗ DAut(X, Y ) Regword (BS) regWord Bool 73 2.21 Compiler für reguläre Ausdrücke Im Folgenden werden wir einen Parser für die Wortdarstellung eines regulären Ausdrucks t mit dessen Faltung in einer beliebigen Reg(BS)-Algebra A verknüpfen und damit ein erstes Beispiel für einen Compiler erhalten, der die Elemente einer Wortmenge ohne den Umweg über Reg(BS)-Terme nach A – übersetzt. Werden reguläre Ausdrücke als Wörter syms(BS) (siehe 2.14) eingelesen, dann muss dem Erkenner fold χ(Lang(X))(t) eine Funktion parseREG : syms(BS)∗ → M (TReg(BS)) vorgeschaltet werden, die jedes korrekte Eingabewort w in einen oder mehrere Reg(BS)Terme t mit fold Regword (BS)(t)(0) = w überführt. M ist ein monadischer Funktor (siehe 5.1), der die Ausgabe des Parsers steuert und insbesondere Fehlermeldungen erzeugt, falls w keinem Reg(BS)-Term entspricht. REG steht hier für eine konkrete Syntax, d.h. eine kontextfreie Grammatik, deren Sprache mit dem Bild von TReg(BS) unter flip(fold Regword (BS))(0) übereinstimmt (siehe Beispiel 4.1). Sei X eine Menge, G eine kontextfreie Grammatik mit abstrakter Syntax Σ und Sprache L ⊆ X ∗. In Kapitel 5 werden wir einen generischen Compiler für L, der jedes Wort von L in Elemente einer Σ-Algebra A übersetzt, als natürliche Transformation compileG des konstanten Funktors const(X ∗) nach M M formulieren. 74 parseG ist diejenige Instanz von compileG, die Wörter von L in Syntaxbäume (= ΣT Grundterme) übersetzt: parseG = compileGΣ . Im Fall G = REG, Σ = Reg(BS), X = syms(BS) und M (A) = A+1 für alle Σ-Algebren A stimmt compileA G (w) mit der Faltung des durch w dargestellten regulären Ausdrucks t in der Reg(BS)-Algebra A überein: compile ist nat. Transformation T G compileA = M (fold A)(compileGReg(BS) (w)) G (w) = M (fold A)(parseG(w)) = (fold A + id1)(parseG(w)) = (fold A + id1)(parseG(fold Regword (BS)(t)(0))) = (fold A + id1)(t) = fold A(t). Im Haskell-Modul Compiler.hs ist compileREG durch die Funktion regToAlg implementiert, deren Zahlparameter eine von 8 Zielalgebren auswählt. 75 Im folgenden Diagramm sind die wichtigsten Beziehungen zwischen den behandelten Algebren und Homomorphismen zusammengefasst: parseREG 1 + fold Lang(X) 1+χ 1 + Lang(X) 1 + χ(Lang(X)) syms(BS) 1 + TReg(BS) ≺ f f f = = inc = inc inc Regword (BS) π1 ◦ fold + ∪ ∪ Bool≺≺ fold Bool = β Bro(BS) TReg(BS) w w w w w w w w w fold Lang(X) = Bro(BS) unfold δ Bro(BS) g Bro(BS)X Bro(BS) Lang(X) w w w w w w w w w Pow (X) δ Pow (X) g Pow (X)X ∪ χ(Lang(X)) w w w w w w = w w w χ χ Beh(X, 2) δ Beh(X,2) g Beh(X, 2)X 76 2.22 Minimale Automaten Wie zeigt man, dass initiale Automaten bestimmte Verhaltensfunktionen realisieren bzw. Sprachen erkennen? Sei A = (A, Op) eine Acc(X)-Algebra, und f : A → P(X ∗). Da Pow (X) eine finale Acc(X)-Algebra ist (s.o.), erkennt für alle a ∈ Astate der initiale Automat (A, a) genau dann die Sprache f (a), wenn f Acc(X)-homomorph ist. Beispiel 2.23 Sei Pn L = {(x1, . . . , xn) ∈ Z | i=1 xi ist gerade}, P n L0 = {(x1, . . . , xn) ∈ Z∗ | i=1 xi ist ungerade}. ∗ Die Funktion h : eo → Pow (X) (siehe 2.11) mit h(Esum) = L und h(Osum) = L0 ist Acc(Z)-homomorph. Also wird L vom initialen Automaten (eo, Esum) und L0 vom initialen Automaten (eo, Osum) erkannt. o Sei (A, a) ein initialer Automat. Die Elemente der Menge hai =def runA(a)(X ∗) heißen Folgezustände von a in A. 77 Satz 2.24 Sei A = (A, Op) eine DAut(X, Y )-Algebra. (i) Für alle a ∈ A ist hai die (Trägermenge der) kleinste(n) DAut(X, Y )-Unteralgebra von A (siehe Kapitel 3), die a enthält. (ii) Für alle Σ-Homomorphismen h : A → B gilt h(hai) = hh(a)i. (iii) Für alle f : X ∗ → Y ist (hf i, f ) eine minimale Realisierung von f . Beweis. (i): Wir zeigen zunächst durch Induktion über |w|, dass für alle a ∈ A, x ∈ X und w ∈ X ∗ Folgendes gilt: runA(a)(wx) = δ A(runA(a)(w))(x). (5) (7) (4) runA(a)(x) = runA(a)(x) = runA(δ A(a)(x))() = δ A(a)(x). Für alle y ∈ X, (5) runA(a)(ywx) = runA(δ A(a)(y))(wx) ind. hyp. = δ A(runA(δ A(a)(y))(w))(x) (5) = δ A(runA(yw))(x) Wegen (7) gilt δ A(b)(x) ∈ hai für alle b ∈ hai und x ∈ X. Also ist hai eine Unteralgebra von A. Sei B eine Unteralgebra von A, die a enthält. Durch Induktion über |w| erhält man aus (4) und (5), dass runA(a)(w) für alle w ∈ X ∗ zu B gehört. Also ist hai die kleinste Unteralgebra von A, die a enthält. 78 (ii): Da h Σ-homomorph ist, ist h auch mit run verträglich (s.o.), d.h. für alle w ∈ X ∗ gilt h(runA(a)(w)) = runB (h(a))(w). Daraus folgt sowohl h(hai) ⊆ hh(a)i als auch hh(a)i ⊆ h(hai). ∗ (iii): Da Beh(X, Y ) final ist, stimmt unfold B Beh(X,Y ) (s.o.) mit der Identität auf Y X überein. Also ist unfold B Beh(X,Y )(f ) = f und damit (hf i, f ) wegen (i) ein initialer Automat, der f realisiert. Sei (A = (A, Op), a) ein initialer Automat, der f realisiert. Wegen (ii) ist h : hai → hunfoldB A(a)i mit h(b) =def unfoldB A(b) für alle b ∈ hai wohldefiniert und surjektiv. Daraus folgt |hf i| = |hunfoldB A(a)i| ≤ |hai| ≤ |A|. o 79 2.25 Baumautomaten [3, 33, 34, 42] Sei Σ eine konstruktive Signatur und Y eine Menge. Ein (Σ, Y )-Baumautomat (A, out) enthält neben einer Σ-Algebra A = (A, Op) eine S-sortige Funktion out : A → Y . Die Funktion out ◦ fold A : TΣ → Y nennen wir das von (A, out) realisierte Baumverhalten. Wie man leicht sieht, ist die Funktion h : TList(X) → X ∗ nil 7→ cons(x, t) 7→ x · h(t) bijektiv. Folglich verallgemeinert der Begriff eines Baumverhaltens f : TΣ → Y den einer Verhaltensfunktion f : X ∗ → Y . Dementsprechend nennen wir im Fall Y = 2 (S-sortige) Teilmengen von TΣ Baumsprachen und nennen L(A, out) =def χ−1(out ◦ fold A) die vom (Σ, Y )-Baumautomaten (A, out) erkannte Baumsprache und L ⊆ TΣ regulär, wenn es einen endlichen (!) (Σ, 2)-Baumautomaten (A, out) gibt, der L erkennt, d.h. die Gleichung out ◦ fold A = χ(L) erfüllt. Ein initialer Automat (A = (A, Op), a) liefert den (List(X), Y )-Baumautomaten (B, β A) 80 mit Blist = Areg , nilB = a und consB = λ(x, a).δ A(a)(x), der unfoldB A(a) ◦ h realisiert. Umgekehrt liefert ein (List(X), Y )-Baumautomat (B, f ) den initialen Automaten (A, nilB ) mit Areg = Blist, δ A = λa.λx.consB (x, a) und β A = f, der f ◦ fold A ◦ h−1 (im Sinne von 2.17) realisiert. Eine Baumsprache L ⊆ TList(X) ist also genau dann regulär, wenn es einen endlichen initialen Automaten (A, a) gibt, der h(L) erkennt, was wiederum – wegen 3.13 oder 12.3 – genau dann gilt, wenn h(L) – im Sinne von 2.17 – regulär ist. Baumautomaten werden zum Beispiel bei der formalen Behandlung von XML-Dokumenten eingesetzt (siehe Beispiel 4.3). 81 3 Rechnen mit Algebren In diesem Abschnitt werden die beiden wichtigsten einstelligen Algebratransformationen: die Bildung von Unteralgebren bzw. Quotienten, und ihr Zusammenhang zu Homomorphismen vorgestellt. Unteralgebren und Quotienten modellieren Restriktionen bzw. Abstraktionen eines gegebenen Modells. Beide Konstrukte sind aus der Mengenlehre bekannt. Sie werden hier auf S-sortige Mengen fortgesetzt. Sei Σ = (S, I, F ) eine Signatur, A = (Ae)e∈T (S,I) eine typverträgliche T (S, I)-sortige Menge, n > 0 und Rs ⊆ Ans für alle s ∈ S. R = (Rs)s∈S wird wie folgt zur T (S, I)-sortigen Relation geliftet: • For all I ∈ I, RI =def ∆nI. • For all I ∈ I and {ei}i∈I ⊆ T (S, I), RQi∈I ei =def {(a1, . . . , an) ∈ AnQ i∈I ei | ∀ i ∈ I : (πi(a1), . . . , πi(an)) ∈ Rei }, R`i∈I ei =def {(ιi(a1), . . . , ιi(an)) | (a1, . . . , an) ∈ Rei , i ∈ I} ⊆ An` i∈I ei . 82 Lemma LIFT Seien g, h : A → B S-sortige Funktionen und R = (Rs)s∈S und R0 = (Rs0 )s∈S die S-sortigen Relationen mit Rs = {a ∈ As | g(a) = h(a)} und Rs0 = {(g(a), h(a)) | a ∈ As} für alle s ∈ S. Dann gilt Re = {a ∈ Ae | ge(a) = he(a)} und Re0 = {(ge(a), he(a)) | a ∈ Ae} für alle e ∈ T (S, I). o Unteralgebren Sei Σ = (S, I, F ) eine Signatur und A = (A, Op) eine Σ-Algebra. Eine S-sortige Teilmenge R = (Rs)s∈S von A heißt Σ-Invariante, wenn für alle f : e → e0 ∈ F und a ∈ Re f A(a) ∈ Re0 gilt. Man nennt R Σ-Invariante, weil die Zugehörigkeit von Elementen von A zu R invariant gegenüber der Anwendung von Operationenvon Σ ist. R induziert die Σ-Unteralgebra A|R von A: • Für alle e ∈ T (S, I), (A|R )e =def Re. • Für alle f : e → e0 ∈ F und a ∈ Re, f A|R (a) =def f A(a). 83 Beispiel 3.1 S Sei X = BS \ 1. Die Menge fold Lang(X)(TReg(BS)) der regulären Sprachen über X ist eine Reg(BS)-Invariante von Lang(X). Das folgt allein aus der Verträglichkeit von fold Lang(X) mit den Operationen von Reg(BS). Für jede Signatur Σ, jede Σ-Algebra A = (A, Op) und jeden Σ-Homomorphismus h : A → B ist (h(As))s∈S eine Σ-Invariante. Die von ihr induzierte Unteralgebra von A stimmt mit der weiter oben definierten Bildalgebra h(A) überein. Für jede konstruktive Signatur Σ ist TΣ(V ) eine Σ-Unteralgebra von CTΣ(V ). Für jede destruktive Signatur Σ ist coTΣ(V ) eine Σ-Unteralgebra von DTΣ(V ). o Satz 3.2 (1) Sei B = (B, Op0) eine Unteralgebra von A = (A, Op). Die T (S, I)-sortige Funktion incB = (incBe : Be → Ae)e∈T (S,I) ist Σ-homomorph. (2) (Homomorphiesatz) Sei h : C → A ein Σ-Homomorphismus. h(C) ist eine Unteralgebra von A. h0 : C → h(C) c 7→ h(c) ist ein surjektiver Σ-Homomorphismus. 84 Ist h injektiv, dann ist h0 bijektiv. Alle Σ-Homomorphismen h00 : C → h(C) mit inch(C) ◦ h00 = h stimmen mit h0 überein. h A C h0 inch(A) h(A) (3) Sei Σ eine konstruktive Signatur und A eine Σ-Algebra. fold A(TΣ) ist die kleinste ΣUnteralgebra von A und TΣ ist die einzige Σ-Unteralgebra von TΣ. Demnach erfüllen alle zu TΣ isomorphen Σ-Algebren A das Induktionsprinzip, d.h. eine prädikatenlogische Formel ϕ gilt für alle Elemente von A, wenn ϕ von allen Elementen einer Σ-Unteralgebra von A erfüllt wird. (4) Sei A eine Σ-Algebra und die A die einzige Σ-Unteralgebra von A. Dann gibt es für alle Σ-Algebren B höchstens einen Σ-Homomorphismus h : A → B. Beweis von (3). Sei B eine Unteralgebra von A. Da TΣ initial und incB Σ-homomorph ist, kommutiert das folgende Diagramm: 85 fold A A TΣ fold = B incB B Daraus folgt für alle t ∈ TΣ, fold A(t) = incB (fold B (t)) = fold B (t) ∈ B. Also ist das Bild von fold A in B enthalten. Sei B eine Unteralgebra von TΣ. Da TΣ initial ist, folgt incB ◦fold B = idTΣ aus (1). Da idTΣ surjektiv ist, ist auch incB surjektiv. Da incB auch injektiv ist, sind B und TΣ isomorph. Also kann B nur mit TΣ übereinstimmen. Beweis von (4). Seien g, h : A → B Σ-Homomorphismen. Dann ist C = {a ∈ A | g(a) = h(a)} eine Σ-Unteralgebra von A: Sei f : e → e0 ∈ F und a ∈ Ae mit ge(a) = he(a). Da g und h Σ-homomorph sind, gilt ge0 (f A(a)) = f B (ge(a)) = f B (he(a)) = he0 (f A(a)). Da g und h S-sortig sind, folgt f A(a) ∈ Ce aus Lemma LIFT. 86 Da A die einzige Σ-Unteralgebra von A ist, stimmt C mit A überein, d.h. für alle a ∈ A gilt g(a) = h(a). Also ist g = h. o Beispiel 3.3 (siehe Beispiel 3.1) Nach Satz 3.2 (3) ist fold Lang(X)(TReg(BS)) die kleinste Unteralgebra von Lang(X). o Kongruenzen und Quotienten Sei Σ = (S, I, F ) eine Signatur und A = (A, Op) eine Σ-Algebra. Eine S-sortige Teilmenge R = (Rs)s∈S von A2 heißt Σ-Kongruenz, wenn für alle s ∈ S Rs eine Äquivalenzrelation ist und für alle f : e → e0 ∈ F und (a, b) ∈ Re (f A(a), f B (b)) ∈ Re0 gilt. R induziert die Σ-Quotientenalgebra A/R von A: • Für alle e ∈ T (S, I), (A/R)e =def {[a]R | a ∈ Ae}, wobei [a]R = {b ∈ Ae | (a, b) ∈ Re}. • Für alle f : e → e0 ∈ F und a ∈ Ae, f A/R ([a]R ) =def [f A(a)]R . 87 Satz 3.4 (ist dual zu Satz 3.2) (1) Sei A eine Σ-Algebra und R eine Σ-Kongruenz auf A. Die natürliche Abbildung natR : A → A/R, die jedes Element a ∈ A auf seine Äquivalenzklasse [a]R abbildet, ist Σ-homomorph. (2) (Homomorphiesatz) Sei h : A → B ein Σ-Homomorphismus und ker(h) ⊆ A2 der Kern von h, d.h. ker(h) = {(a, b) | h(a) = h(b)}. ker(h) ist eine Σ-Kongruenz. h0 : A/ker(h) → B [a]ker(h) 7→ h(a) ist ein injektiver Σ-Homomorphismus. Ist h surjektiv, dann ist h0 bijektiv. Alle Σ-Homomorphismen h00 : A/ker(h) → B mit h00 ◦ nat = h stimmen mit h0 überein. h B A h0 natker(h) A/ker(h) 88 (3) Sei Σ eine destruktive Signatur, A eine Σ-Algebra und Fin eine finale Σ-Algebra (s.o). Der – Verhaltenskongruenz von A genannte – Kern des eindeutigen Σ-Homomorphismus unfold A : A → Fin ist die größte Σ-Kongruenz auf A und die Diagonale von Fin 2 die einzige Σ-Kongruenz R auf Fin. Demnach erfüllen alle zu Fin isomorphen Σ-Algebren A das Coinduktionsprinzip: Sei E eine Menge von Σ-Gleichungen t = u zwischen Σ-Termen über V , die denselben Typ haben. E gilt in A, wenn es eine Σ-Kongruenz R auf A gibt, die für alle t = u ∈ E und g : V → A das Paar (g ∗(t), g ∗(u)) enthält. (4) Sei B eine Σ-Algebra und die Diagonale von B 2 die einzige Σ-Kongruenz R auf B. Dann gibt es für alle Σ-Algebren A höchstens einen Σ-Homomorphismus h : A → B. Beweis von (3). Sei R eine Kongruenz auf A. Da Fin final und natR Σ-homomorph ist, kommutiert das folgende Diagramm: unfold A Fin A unfold A/R natR A/R 89 Daraus folgt für alle a, b ∈ A, (a, b) ∈ R ⇒ [a]R = [b]R ⇒ unfold A(a) = unfold A/R ([a]R ) = unfold A/R ([b]R ) = unfold A(b). Also ist R im Kern von unfold A enthalten. Sei R eine Kongruenz auf Fin. Da Fin final ist, folgt unfold Fin/R ◦natR = idFin aus (1). Da idFin injektiv ist, ist auch natR injektiv. Da natR auch surjektiv ist, sind Fin und Fin/R isomorph. Also kann R nur die Diagonale von Fin 2 sein. Beweis von (4). Seien g, h : A → B Σ-Homomorphismen. Dann ist R = {(g(a), h(a)) | a ∈ A} eine Σ-Kongruenz auf B: Sei f : e → e0 ∈ F , a ∈ Ae und (ge(a), he(a)) ∈ Re. Da g und h Σ-homomorph sind, gelten f B (ge(a)) = ge0 (f A(a)) und f B (he(a)) = he0 (f A(a)). Da g und h S-sortig sind, folgt (f B (ge(a)), f B (he(a))) = (ge0 (f A(a)), he0 (f A(a))) ∈ Re0 aus Lemma LIFT. Da ∆2B die einzige Σ-Kongruenz auf B ist, stimmt R mit ∆2B überein, d.h. für alle a ∈ A gilt g(a) = h(a). Also ist g = h. o 90 Automatenminimierung durch Quotientenbildung Eine minimale Realisierung einer Verhaltensfunktion f : X ∗ → Y erhält man auch als Quotienten eines gegebenen initialen Automaten A: Im Beweis von Satz 2.24 (iii) wurde der DAut(X, Y )-Homomorphismus h : hai → hf i definiert. Wegen der Surjektivität von h ist hai/ker(h) nach Satz 3.4 (2) DAut(X, Y )isomorph zu hf i. Nach Definition von h ist ker(h) = ker(unfold A) ∩ hai2. Nach Satz 3.4 (3) ist ker(unfold A) die größte Σ-Kongruenz auf A, also die größte binäre Relation R auf A, die für alle a, b ∈ Astate folgende Bedingung erfüllt: (a, b) ∈ R ⇒ β A(a) = β A(b) ∧ ∀ w ∈ X ∗ : (δ A(a)(w), δ A(b)(w)) ∈ R. (1) Ist Astate endlich, dann lässt sich ker(unfold A) schnell und elegant mit dem in Abschnitt 2.3 von [27] und in [31] beschriebene (und in Haskell implementierte) Paull-UngerVerfahren berechnen. Es startet mit R0 = A2state und benutzt die Kontraposition (a, b) 6∈ R ⇐ β A(a) 6= β A(b) ∨ ∀ w ∈ X ∗ : (δ A(a)(w), δ A(b)(w)) 6∈ R (2) von (1), um R0 schrittweise auf den Kern von unfold A zu reduzieren. 91 Transitionsmonoid und syntaktische Kongruenz Sei A = (A, Op) eine DAut(X, Y )-Algebra. Das Bild der Mon-homomorphen Erreichbarkeitsfunktion reachA : X ∗ → (Astate → Astate) von A heißt Transitionsmonoid von A. Satz 3.5 (Transitionsmonoide und endliche Automaten) Sei a ∈ Astate und R der Kern von reachhai : X ∗ → (hai → hai). hai ist genau dann endlich, wenn R endlich viele Äquivalenzklassen hat oder das Transitionsmonoid von hai endlich ist. Beweis. Nach Satz 3.4 (2) ist das Transitionsmonoid von hai Mon-isomorph zum Quotienten X ∗/R. Außerdem ist die Abbildung h : X ∗/R → hai [w]R 7→ δA∗(a)(w) wohldefiniert: Sei (v, w) ∈ R. Dann gilt reachhai(v) = reachhai(w), also insbesondere h([v]R ) = runA(a)(v) = δ hai∗(a)(v) = reachhai(v)(a) = reachhai(w)(a) = δ hai∗(a)(w) = runA(a)(w) = h([w]R ). 92 Da h surjektiv ist, liefert die Komposition h ◦ g mit dem Isomorphismus g : reachhai(X ∗) → X ∗/R eine surjektive Abbildung von reachhai(X ∗) nach hai. Also überträgt sich die Endlichkeit des Transitionsmonoids von hai auf hai selbst. Umgekehrt ist das Transitionsmonoid jedes endlichen Automaten A endlich, weil es eine Teilmenge von Astate → Astate ist. o Sei L ⊆ X ∗. Das Transitionsmonoid von hLi heißt syntaktisches Monoid und der Kern von reachhLi syntaktische Kongruenz von L. Letztere lässt sich als Menge aller Paare (v, w) ∈ X ∗ × X ∗ mit uvu0 ∈ L ⇔ uwu0 ∈ L für alle u, u0 ∈ X ∗ charakterisieren. Satz 3.5 impliziert, dass sie genau dann endlich viele Äquivalenzklassen hat, wenn L von einem endlichen initialen Automaten (A, a) erkannt wird, was wiederum zur Regularität von L äquivalent ist (siehe 3.13 für einen direkten Beweis oder Beispiel 12.3 für den klassischen über die Konstruktion eines nichtdeterministischen Akzeptors von L). Folglich kann die Nichtregularität einer Sprache L oft gezeigt werden, indem aus der Endlichkeit der Zustandsmenge eines Akzeptors von L ein Widerspruch hergeleitet wird. Wäre z.B. L = {xny n | n ∈ N} regulär, dann gäbe es einen endlichen Automaten (A, a) mit unfold A(a) = L. 93 Demnach müsste für alle n ∈ N ein Zustand bn ∈ Areg existieren mit runA(a)(xn) = bn und β A(runA(bn)(y n)) = 1. Da A endlich ist, gäbe es i, j ∈ N mit i 6= j und bi = bj . Daraus würde jedoch unfold A(xiy j ) = β A(runA(a)(xiy j )) = β A(runA(runA(a)(xi))(y j )) = β A(runA(bi)(y j )) = β A(runA(bj )(y j )) = 1 folgen, im Widerspruch dazu, dass xiy j nicht zu L gehört. Also ist L nicht regulär. 3.6 Termsubstitution Sei Σ = (S, I, F ) eine Signatur. Belegungen von Variablen durch Σ-Terme heißen Substitutionen und werden üblicherweise mit kleinen griechischen Buchstaben bezeichnet. Im Gegensatz zu den Belegungen von Kapitel 2 beschränken wir den Wertebereich von Substitutionen σ : V → TΣ(V ) nicht auf die Trägermenge der Algebra TΣ(V ), also auf Terme ohne λ und ite. Um ungewollte Bindungen von Variablen zu vermeiden, muss die Fortsetzung σ ∗ : TΣ(V ) → TΣ(V ) von σ auf Terme anders als die Auswertung g ∗ : TΣ(V ) → A einer Belegung g : V → A von Kapitel 2 definiert werden: 94 • Für alle X ∈ ( BS, e ∈ T (S, I), x ∈ VX und t ∈ TΣ(V )e, λx0.σ[x0/x]∗(t) falls x ∈ var(σ(free(t) \ {x})), ∗ σ (λx.t) = λx.σ[x/x]∗(t) sonst. • Für alle e ∈ T (S, I) t ∈ TΣ(V )2 und u, v ∈ TΣ(V )e, σ ∗(ite(t, u, v)) = ite(σ ∗(t), σ ∗(u), σ ∗(v)). In den restlichen Fällen ist σ ∗ genauso definiert wie g ∗: • Für alle x ∈ V , σ ∗(x) = σ(x). • Für alle x ∈ X ∈ BS, σ ∗(x) = x. • Für alle n > 1 und t1, . . . , tn ∈ TΣ(V ), σ ∗(t1, . . . , tn) = (σ ∗(t1), . . . , σ ∗(tn)). • Für alle f : e → e0 ∈ F und t ∈ TΣ(V )e, σ ∗(f t) = f σ ∗(t). • Für alle X ∈ BS, e ∈ T (S, I), t ∈ TΣ(V )eX und u ∈ TΣ(V )X , σ ∗(t(u)) = σ ∗(t)(σ ∗(u)). σ ∗(t) heißt σ-Instanz von t und Grundinstanz, falls σ alle Variablen von t auf Grundterme abbildet. Man schreibt häufig tσ anstelle von σ ∗(t) sowie {t1/x1, . . . , tn/xn} für die Substitution σ mit σ(xi) = ti für alle 1 ≤ i ≤ n und σ(x) = x für alle x ∈ V \ {x1, . . . , xn}. 95 Satz 3.7 (Auswertung und Substitution) (1) Für alle Belegungen g : V → A und Σ-Homomorphismen h : A → B gilt: (h ◦ g)∗ = h ◦ g ∗. (2) Für alle Substitutionen σ, τ : V → TΣ(V ) gilt: (σ ∗ ◦ τ )∗ = σ ∗ ◦ τ ∗. Beweis. (1) Induktion über den Aufbau von Σ-Termen. (2) folgt aus (1), weil σ ∗ ein ΣHomomorphismus ist. o Termäquivalenz und Normalformen Sei Σ = (S, I, F ) eine konstruktive Signatur. In diesem Abschnitt geht es um Σ-Algebren A, die eine gegebene Menge E von Σ-Gleichungen (s.o.) erfüllen, d.h. für die Folgendes gilt: • für alle t = t0 ∈ E und g : V → A gilt g ∗(t) = g ∗(t0). AlgΣ,E bezeichnet die Klasse aller Σ-Algebren, die E erfüllen. 96 Aus Satz 3.7 (1) folgt sofort, dass AlgΣ,E unter Σ-homomorphen Bildern abgeschlossen, d.h. für alle Σ-Homomorphismen h : A → B gilt: A ∈ AlgΣ,E ⇒ h(A) ∈ AlgΣ,E . Die Elemente der Menge Inst(E ) =def {(tσ, t0σ) | (t, t0) ∈ E, σ : V → TΣ(V )} (6) heißen Instanzen von E. Die E-Äquivalenz ≡E ist definiert als kleinste Σ-Kongruenz, die Inst(E) enthält. Aufgabe Zeigen Sie, dass der Quotient TΣ(V )/≡E E erfüllt. o Satz 3.8 Für alle Belegungen g : V → A in eine Σ-Algebra A, die E erfüllt, faktorisiert g ∗ : TΣ(V ) → A durch TΣ(V )/≡E , d.h. es gibt einen Σ-Homomorphismus h : TΣ(V )/≡E → A mit h ◦ nat≡E = g ∗. 97 V incV g TΣ(V ) g ∗ nat≡E TΣ(V )/≡E (3) h g≺ A Beweis. Da der Kern von g ∗ eine Σ-Kongruenz ist, die Inst(E ) enthält (siehe [29], Satz GLK), ≡E aber die kleinste derartige Relation auf TΣ(V ) ist, ist letztere im Kern von g ∗ enthalten. Folglich ist h : TΣ(V )/≡E → A mit h([t]≡E ) = h(t) für alle t ∈ TΣ(V ) wohldefiniert. (3) kommutiert, was zusammen mit der Surjektivität und Σ-Homomorphie von nat≡E impliziert, dass auch h Σ-homomorph ist. o Die durch den gestrichelten Pfeil angedeutete Eigenschaft von h, der einzige Σ-Homomorphismus zu sein, der (3) kommutativ macht, folgt ebenfalls aus der Surjektivität und Σ-Homomorphie von nat≡E . Zusammen mit TΣ/≡E ∈ AlgΣ,E impliziert Satz 3.8, dass TΣ/≡E initial in AlgΣ,E ist, dass es also für alle A ∈ AlgΣ,E genau einen Σ-Homomorphismus h : TΣ/≡E → A gibt. 98 Da g ∗ im Fall V = ∅ mit fold A übereinstimmt, reduziert sich das obige Diagramm zu folgendem: nat≡E A TΣ fold A (3) h TΣ/≡E Beispiel 3.9 Sei Σ = Mon, x, y, z ∈ V und E = {mul(one, x) = x, mul(x, one) = x, mul(mul(x, y), z) = mul(x, mul(y, z))}. Die freie Mon-Algebra TMon (V ) ist kein Monoid, wohl aber ihr Quotient TMon (V )/ ≡E . Man nennt ihn das freie Monoid (über V ). Aufgabe Zeigen Sie, dass das freie Monoid tatsächlich ein Monoid ist, also E erfüllt, und Mon-isomorph zu V ∗ ist. o Bei der Implementierung von Quotienten werden isomorphe Darstellungen bevorzugt, deren Elemente keine Äquivalenzklassen sind, diese aber eindeutig repräsentieren. Z.B. sind die Wörter über V eindeutige Repräsentanten der Äquivalenzklassen von TMon (V )/≡E . 99 Bei der Berechnung äquivalenter Normalformen beschränkt man sich meist auf die folgende Teilrelation von ≡E , die nur “orientierte” Anwendungen der Gleichungen von E zulässt: Die E-Reduktionsrelation →E besteht aus allen Paaren (u{t/x}, u{t0/x}) mit t = t0 ∈ E, u ∈ TΣ(V ) und σ : V → TΣ(V ). + Die kleinste transitive Relation auf TΣ(V ), die →E enthält, wird mit →E bezeichnet. + Aufgabe Zeigen Sie, dass →E die kleinste transitive und mit Σ verträgliche Relation auf + TΣ(V ) ist, die Inst(E ) enthält. Folgern Sie daraus, dass →E eine Teilmenge von ≡E ist.o + Sei t ∈ TΣ(V ). u ∈ TΣ(V ) heißt E-Normalform von t, wenn t →E u gilt und u zu einer vorgegebenen Teilmenge von TΣ(V ) gehört. Eine Funktion reduce : TΣ(V ) → TΣ(V ) heißt E-Reduktionsfunktion, wenn für alle t ∈ TΣ(V ), reduce(t) eine E-Normalform von t ist. Sei A ∈ AlgΣ,E und g : V → A. Wegen + →E ⊆ ≡E ⊆ ker(g ∗) (siehe obige Aufgabe und den Beweis von Satz 3.8) gilt g ∗(t) = g ∗(reduce(t)) für alle 100 t ∈ TΣ(V ), also kurz: g ∗ ◦ reduce = g ∗. (3) Allgemeine Methoden zur Berechnung von Normalformen werden in [29], §5.2 behandelt. Beispiel 3.10 Normalformen regulärer Ausdrücke E bestehe aus folgenden Reg(BS)-Gleichungen: f (f (x, y), z) = f (x, f (y, z)) par(x, y) = par(y, x) seq(x, par(y, z)) = par(seq(x, y), seq(x, z)) seq(par(x, y), z) = par(seq(x, z), seq(y, z)) par(x, x) = x par(mt, x) = x par(x, mt) = x seq(eps, x) = x seq(x, eps) = x seq(mt, x) = mt seq(x, mt) = mt f (ite(x, y, z), z 0) = ite(x, f (y, z 0), f (z, z 0)) f (z 0, ite(x, y, z)) = ite(x, f (z 0, y), f (z 0, z)) (Assoziativität von f ∈ {par, seq}) (Kommutativität von par) (Linksdistributivität von seq über par) (Rechtsdistributivität von seq über par) (Idempotenz von par) (Neutralität von mt bzgl. par) (Neutralität von eps bzgl. seq) (Annihilation) (f ∈ {par, seq}) (f ∈ {par, seq}) Demnach entfernt eine E-Reduktionsfunktion mt und Mehrfachkopien von Summanden aus Summen sowie eps aus Produkten, ersetzt alle Produkte, die mt enthalten, durch mt, 101 distribuiert seq über par und linearisiert geschachtelte Summen und Produkte rechtsassoziativ. Angewendet auf Reg(BS)-Grundterme entspricht sie deren Faltung in der Reg(BS)Algebra regNorm (siehe Compiler.hs). S Sei X = BS. Da Lang(X) E erfüllt, gilt (3) für A = Lang(X). Algebren, die E erfüllen, heißen idempotente Semiringe. Da E weder den Sternoperator iter : reg → reg noch die Konstanten B : 1 → reg enthält, brauchen diese in einem Semiring nicht definiert zu sein. Auch die Idempotenz von par ist keine Anforderung an Semiringe. Kleene-Algebren sind Semiringe, auf denen ein Sternoperator definiert ist und die neben E weitere (den Sternoperator betreffende) Gleichungen erfüllen. Die Elemente von Beh(X, Y ) (siehe 2.7) heißen formale Potenzreihen, wenn Y ein Semiring ist (siehe [37], Kapitel 9). 3.11 Die Brzozowski-Gleichungen Die folgende Menge BRE von – Brzozowski-Gleichungen genannten – Reg(BS)Gleichungen hat in TReg(BS) genau eine Lösung, d.h. es gibt genau eine Erweiterung von TReg(BS) zur Acc(X)-Algebra, die BRE erfüllt (siehe Beispiel 17.4). In Abschnitt 2.7 wurde diese Lösung unter dem Namen Bro(BS) eingeführt. Existenz und Eindeutigkeit folgen aus Satz 17.1 und werden dort aus dem in Satz 3.2 (3) eingeführten 102 Induktionsprinzip abgeleitet. δ(eps) δ(mt) δ(B) δ(par(t, u)) δ(seq(t, u)) δ(iter(t)) β(eps) β(mt) β(B) β(par(t, u)) β(seq(t, u)) β(iter(t)) = = = = = = = = = = = = λx.mt λx.mt λx.ite(x ∈ B, eps, mt) λx.par(δ(t)(x), δ(u)(x)) λx.par(seq(δ(t)(x), u), ite(β(t), δ(u)(x), mt)), λx.seq(δ(t)(x), iter(t)) 1 0 0 max{β(t), β(u)} β(t) ∗ β(u) 1 t und u sind hier Variablen der Sorte reg. Nach obiger Lesart definiert BRE Destruktoren (δ und β) auf der Basis von Konstruktoren von Reg(BS). Umgekehrt lässt sich BRE auch als Definition der Konstruktoren auf der Basis der Destruktoren δ und β auffassen: Nach Satz 17.7 hat BRE nämlich in der finalen Acc(X)-Algebra Pow (X) genau eine coinduktive Lösung, d.h. es gibt genau eine Erweiterung von Lang(X) zur Reg(BS)-Algebra, die BRE erfüllt (siehe Beispiel 17.10). o 103 3.12 Optimierter Brzozowski-Automat Der Erkenner Bro(BS) regulärer Sprachen (siehe Abschnitt 2.11) benötigt viel Platz, weil die wiederholten Aufrufe von δ Bro(BS) aus t immer größere Ausdrücke erzeugen. Um das zu vermeiden, ersetzen wir Bro(BS) durch die Acc(X)-Algebra Norm(BS) (norm in Compiler.hs), die bis auf die Interpretation von δ mit Bro(BS) übereinstimmt. δ Norm(BS) normalisiert die von δ Bro(BS) berechneten Folgezustände mit der in Beispiel 3.10 beschriebenen Reduktionsfunktion reduce : TReg(BS)(V ) → TReg(BS)(V ). Für alle t ∈ TReg(BS), δ Norm(BS)(t) =def reduce ◦ δ Bro(BS)(t). (1) fold Lang(X) ◦ reduce = fold Lang(X). (2) Gemäß Beispiel 3.10 gilt 104 Es bleibt zu zeigen, dass für alle t ∈ TReg(BS) die initialen Automaten (Bro(BS), t) und (Norm(BS), t) dieselben Sprachen erkennen, d.h. unfold Norm(BS)(t) = unfold Bro(BS)(t). (3) Wir beweisen (3) durch Induktion über die Länge der Wörter über X. β Norm(BS)(runNorm(BS)(t)()) = β Norm(BS)(t) = β Bro(BS)(t) = β Bro(BS)(runBro(BS)(t)()). Für alle x ∈ X und w ∈ X ∗, β Norm(BS)(runNorm(BS)(t)(xw)) = β Norm(BS)(runNorm(BS)(δ Norm(BS)(t)(x))(w)) = unfold Norm(BS)(δ Norm(BS)(t)(x))(w) ind. hyp. = unfold Bro(BS)(δ Norm(BS)(t)(x))(w) (1) = unfold Bro(BS)(reduce(δ Bro(BS)(t))(x))(w) = fold Lang(X)(reduce(δ Bro(BS)(t))(x))(w) (2) = fold Lang(X)(δ Bro(BS)(t)(x))(w) = unfold Bro(BS)(δ Bro(BS)(t)(x))(w) = β Bro(BS)(runBro(BS)(δ Bro(BS)(t)(x))(w)) = β Bro(BS)(runBro(BS)(t)(xw)). Also gilt (3): unfold Norm(BS)(t) = {w ∈ X ∗ | β Norm(BS)(runNorm(BS)(t)(w)) = 1} = {w ∈ X ∗ | β Bro(BS)(runBro(BS)(t)(w)) = 1} = unfold Bro(BS)(t). Im Haskell-Modul Compiler.hs entspricht der Erkenner unfold Norm(BS)(t) dem Aufruf regToAl "" w 4, wobei w die Wortdarstellung von t ist. o 105 3.13 Erkenner regulärer Sprachen sind endlich Aus der Gültigkeit von BRE in Pow (X) lassen sich die folgenden Gleichungen für runPow (X) : P(X ∗) → P(X ∗)X ∗ ableiten (siehe 2.9): Für alle w ∈ X ∗, B ∈ BS und L, L0 ⊆ P(X ∗), ( 1 falls w = , runPow (X)(epsLang(X))(w) = ∅ sonst, runPow (X)(mtLang(X))(w) = ∅, C falls w = , Lang(X) runPow (X)(B )(w) = 1 falls w ∈ C, ∅ sonst, runPow (X)(parLang(X)(L, L0))(w) = runPow (X)(L)(w) ∪ runPow (X)(L0)(w), runPow (X)(seq Lang(X)(L, L0))(w) = {uv | u ∈ runPow (X)(L)(w), v ∈ L0} S ∪ uv=w (if ∈ runPow (X)(L)(u) then runPow (X)(L0)(v) else ∅), (4) (5) (6) (7) (8) 106 runPow (X)(iterLang(X)(L))(w) = {uv | u ∈ runPow (X)(L)(w), v ∈ iterPow (X)(L)} S T ∪ u1...unv=w (if ∈ ni=1 runPow (X)(L)(ui) then {uv 0 | u ∈ runPow (X)(L)(v), v 0 ∈ iterPow (X)(L)} else ∅) (9) (siehe z.B. [37], Theorem 10.1). Wir erinnern an Satz 2.24 (iii), aus dem folgt, dass für alle L ⊆ X ∗ der Unterautomat (hLi, L) von (Pow (X), L) ein minimaler Erkenner von L ist. Ist L die Sprache eines regulären Ausdrucks t, ist also L = fold Lang(X)(t), dann ist hLi = {runPow (X)(fold Lang(X)(t))(w) | w ∈ X ∗}. Daraus folgt durch Induktion über den Aufbau von t, dass hLi endlich ist: Im Fall t ∈ {eps, mt} ∪ {B | B ∈ BS} besteht hLi wegen (4), (5) und (6) aus zwei, einem bzw. drei Zuständen. Im Fall t = par(t0, t00) folgt |hLi| ≤ |hfold Lang(X)(t0)i| ∗ |hfold Lang(X)(t00)i| aus (7). Im Fall t = seq(t0, t00) folgt |hLi| ≤ |hfold Lang(X)(t0)i| ∗ 2|hfold Im Fall t = iter(t0) folgt |hLi| ≤ 2|hfold Lang(X) (t0 )i| Lang(X) (t00 )i| aus (8). aus (9). 107 Damit ist ohne den üblichen Umweg über Potenzautomaten (siehe Beispiel 12.3) gezeigt, dass reguläre Sprachen von endlichen Automaten erkannt werden. 108 4 Kontextfreie Grammatiken (CFGs) Sie ordnen einer konstruktiven Signatur Σ eine konkrete Syntax und damit eine vom Compiler verstehbare Quellsprache zu, so dass er diese in eine als Σ-Algebra formulierte Zielsprache übersetzen kann. Auch wenn das zu die Quellsprache bereits als konstruktive Signatur Σ und die Zielsprache als Σ-Algebra gegeben sind, benötigt der Compiler eine kontextfreie Grammatik, um Zeichenfolgen in Elemente der Algebra zu übersetzen. Eine kontextfreie Grammatik (CFG) G = (S, BS, R) besteht aus • einer endlichen Menge S von Sorten, die auch Nichtterminale oder Variablen genannt werden, • einer endlichen Menge BS nichtleerer Basismengen, deren Singletons Terminale genannt und mit ihrem jeweiligen einzigen Element gleichgesetzt werden. • einer endlichen Menge R von Regeln s → w mit s ∈ S und w ∈ (S ∪ BS)∗, die auch Produktionen genannt werden. n > 0 Regeln s → w1, . . . , s → wn mit derselben linken Seite s werden oft zu der einen Regel s → w1| . . . |wn zusammengefasst. 109 Beispiel 4.1 Sei BS eine endliche Menge von Basismengen, die ∅ und 1 enthält, syms(BS) wie in 2.14 definiert und R = {reg → reg + reg, reg → reg reg, reg → reg ∗, reg → (reg)} ∪ {reg → B | B ∈ BS}. REG =def ({reg}, syms(BS), R) ist eine CFG. o Oft genügt es, bei der Definition einer CFG nur deren Regeln und Basismengen anzugeben. Automatisch bilden dann die Symbole, die auf der linken Seite einer Regel vorkommen, die Menge S der Sorten, während alle anderen Wörter und Symbole (außer dem senkrechten Strich |; s.o.) auf der rechten Seite einer Regel Terminale oder Namen mehrelementiger Basismengen sind. Beispiel 4.2 Die Regeln der CFG JavaLight für imperative Programme mit Konditionalen und Schleifen lauten wie folgt: 110 Commands → Command → Sum Sumsect Prod Prodsect Factor Disjunct Conjunct Literal → → → → → → → → Command Commands | Command {Commands} | String = Sum; | if Disjunct Command else Command | if Disjunct Command | while Disjunct Command Prod Sumsect {+, −} Prod Sumsect | Factor Prodsect {∗, /} Factor Prodsect | Z | String | (Sum) Conjunct || Disjunct | Conjunct Literal && Conjunct | Literal !Literal | Sum Rel Sum | 2 | (Disjunct) String, {+, −}, {∗, /}, Z, Rel und 2 sind die mehrelementigen Basismengen von JavaLight. String bezeichnet die Menge aller Zeichenfolgen außer den in Regeln von JavaLight vorkommenden Symbolen und Wörtern außer String. Rel bezeichnet eine Menge nicht näher spezifizierter binärer Relationen auf Z. Ein aus der Sorte Commands ableitbares JavaLight-Programm ist z.B. fact = 1; while x > 1 {fact = fact*x; x = x-1;} o 111 Beispiel 4.3 XMLstore (siehe [19], Abschnitt 2) Store → Orders Order P erson Emails Email Items Item Stock ItemS Suppliers Id → → → → → → → → → → → hstorei hstocki Stock h/stocki h/storei | hstorei Orders hstocki Stock h/stocki h/storei Order Orders | Order horderi hcustomeri P erson h/customeri Items h/orderi hnamei String h/namei | hnamei String h/namei Emails Email Emails | hemaili String h/emaili Item Items | Item hitemi Id hpricei String h/pricei h/itemi ItemS Stock | ItemS hitemi Id hquantityi Z h/quantityi Suppliers h/itemi hsupplieri P erson h/supplieri | Stock hidi String h/idi 112 Die Sprache von XMLstore beschreibt XML-Dokumente wie z.B. das folgende: <store> <order> <customer> <name> John Mitchell </name> <email> [email protected] </email> </customer> <item> <id> I18F </id> <price> 100 </price> </item> </order> <stock> <item> <id> IG8 </id> <quantity> 10 </quantity> <supplier> <name> Al Jones </name> <email> [email protected] </email> <email> [email protected] </email> </supplier> </item> <item> <id> J38H </id> <quantity> 30 </quantity> <item> <id> J38H1 </id> <quantity> 10 </quantity> <supplier> <name> Richard Bird </name> </supplier> </item> <item> <id> J38H2 </id> <quantity> 20 </quantity> <supplier> <name> Mick Taylor </name> </supplier> </item> </item> </stock> </store> 113 Nicht-linksrekursive CFGs Sei G = (S, BS, R) eine CFG und X = S BS. X ist die Menge der Eingabesymbole, die Compiler für G verarbeiten. Demgegenüber tauchen auf den rechten Seiten der Regeln von G nur (Namen für) komplette Basismengen auf! Die klassische Definition einer linksrekursiven Grammatik verwendet die (Links-)Ableitungsrelation →G = {(vsw, vαw) | s → α ∈ R, v, w ∈ (S ∪ BS)∗}. + ∗ →G und →G bezeichnen den transitiven bzw. reflexiv-transitiven Abschluss von →G. + G heißt linksrekursiv, falls es s ∈ S und w ∈ (S ∪ BS)∗ mit s →G sw gibt. Z.B. ist REG linksrekursiv, JavaLight und XMLstore jedoch nicht (siehe Beispiele 4.1-4.3). 114 Linksassoziative Auswertung erzwingt keine Linksrekursion Sind alle Regeln, deren abstrakte Syntax binäre Operationen darstellen, dann werden aus diesen Operationen bestehende Syntaxbäume rechtsassoziativ ausgewertet. Bei ungeklammerten Ausdrücke wie x+y−z−z 0 oder x/y∗z/z 0 führt aber nur die linkassoziative Faltung zum gewünschten Ergebnis. Die gegebene Grammatik muss deshalb um Sorten und Regeln erweitert werden, die bewirken, dass aus der linkassoziativen eine semantisch äquivalente rechtsassoziative Faltung wird. Im Fall von JavaLight bewirken das die Sorten Sumsect und Prodsect und deren Regeln. Die Namen der Sorten weisen auf deren Interpretation als Mengen von Sektionen hin, also von Funktionen wie z.B. (∗5) : Z → Z. Angewendet auf eine Zahl x, liefert (∗5) das Fünffache von x. Die linkassoziative Faltung ((x+y)−z)−z 0 bzw. ((x/y)∗z)/z 0 der obigen Ausdrücke entspricht daher der – von den Regeln für Sumsect und Prodsect bewirkten – rechtsassoziativen Faltung ((−z 0) ◦ ((−z) ◦ (+y)))(x) bzw. ((/z 0) ◦ ((∗z) ◦ (/y)))(x). Die Sektionssorten von JavaLight würden automatisch eingeführt werden, wenn man die folgende Entrekursivierung auf eine linksrekursive Variante der Grammatik anwendet. 115 Verfahren zur Eliminierung von Linksrekursion Sei G = (S, BS, R) eine CFG und S = {s1, . . . , sn}. Führe für alle 1 ≤ i ≤ n die beiden folgenden Schritte in der angegebenen Reihenfolge durch: • Für alle 1 ≤ j < i und Regelpaare (si → sj v, sj → w) ersetze die Regel si → sj v durch die neue Regel si → wv. • Falls vorhanden, streiche die Regel si → si. • Für alle Regelpaare (si → siw, si → ev) mit e ∈ (S ∪ BS) \ {si} ersetze die Regel o si → siw durch die drei neuen Regeln si → evs0i, s0i → ws0i und s0i → w. Die Verwendung von jeweils drei Sorten für arithmetische bzw. Boolesche Ausdrücke (Sum, Prod und Factor bzw. Disjunct, Conjunct und Literal ) bewirkt ebenfalls eine bestimmte Auswertungsreihenfolge und zwar diejenige, die sich aus den üblichen Prioritäten arithmetischer bzw. Boolescher Operationen ergibt. 116 Abstrakte Syntax Sei G = (S, BS, R) eine CFG und Z = {B ∈ BS | |B| = 1} (Terminale von G). Die konstruktive Signatur Σ(G) = (S, BS, F (G)) mit F (G) = {fr : 1 → s | r = (s → w) ∈ R, w ∈ Z ∗} ∪ {fr : e1 × · · · × en → s | r = (s → w0e1w1 . . . enwn) ∈ R, n > 0, w0 . . . wn ∈ Z ∗, e1, . . . , en ∈ S ∪ BS \ Z} heißt abstrakte Syntax von G. Beispiel 4.4 Die Signatur Reg(BS) ist eine Teilsignatur der abstrakten Syntax der CFG REG von Beispiel 4.1. Aufgabe Zu welcher Regel von REG fehlt in Reg(BS) der entsprechende Konstruktor? o 117 Die Konstruktoren von Σ(G) lassen sich i.d.R. direkt aus den Terminalen von G basteln. So wird manchmal für den aus der Regel r = (s → w0e1w1 . . . enwn) mit wi ∈ Z ∗ entstandenen Konstruktor fr die (Mixfix-)Darstellung w0 w1 . . . wn gewählt. So könnte beispielsweise der Konstruktor f : Disjunct × Command × Command für die JavaLight-Regel Commands → if Disjunct Command else Command (siehe Beispiel 4.2) if else genannt werden. Um Verwechslungen zwischen Terminalen und Konstruktoren vorzubeugen, werden wir hier jedoch keine Terminale als Namen für Konstruktoren verwenden. Umgekehrt kann jede endlich verzweigende konstruktive Signatur Σ = (S, I, F ) in eine CFG G(Σ) = (S, BS ∪ {(, ), , }, R) mit Σ(G(Σ)) = Σ überführt werden, wobei R = {s → f (e1, . . . , en) | f : e1 × · · · × en → s ∈ F }. Σ(G)-Grundterme werden Syntaxbäume von G genannt. Alle ihre Knoten sind mit Operationssymbolen markiert. 118 Beispiel 4.5 SAB Die Grammatik SAB besteht aus den Sorten S, A, B, den Terminalen a, b und den Regeln r1 = S → aB, r2 = S → bA, r3 = S → , r4 = A → aS, r5 = A → bAA, r6 = B → bS, r7 = B → aBB. Demnach lauten die Konstruktoren der abstrakten Syntax von SAB wie folgt: f1 : B → S, f2 : A → S, f3 : 1 → S, f4 : S → A, f5 : A × A → A, f6 : S → B, f7 : B × B → B. f1 f7 f6 f6 f1 f3 f6 ε f3 ε Syntaxbaum des Eingabewortes aababb Mit markierte Blätter eines Syntaxbaums werden künftig weglassen. 119 Die folgende SAB-Algebra SABcount berechnet die Anzahl #a(w) bzw. #b(w) der Vorkommen von a bzw. b im Eingabewort w: SABcount S = SABcount A = SABcount B = N2, f1SABcount = f4SABcount = λ(i, j).(i + 1, j), f2SABcount = f6SABcount = λ(i, j).(i, j + 1), f3SABcount = (0, 0), f5SABcount = λ((i, j), (k, l)).(i + k, j + l + 1), f7SABcount = λ((i, j), (k, l)).(i + k + 1, j + l). o 120 Beispiel 4.6 Σ(JavaLight) = (S, I, F ) S = { Commands, Command , Sum, Sumsect, Prod , Prodsect, Factor , Disjunct, Conjunct, Literal } I = { 1, 2, {+, −}, {∗, /}, Z, String, Rel } F = { seq : Command × Commands → Commands, embed : Command → Commands, block : Commands → Command , assign : String × Sum → Command , cond : Disjunct × Command × Command → Command , cond1, loop : Disjunct × Command → Command , sum : Prod × Sumsect → Sum, sumsect : {+, −} × Prod × Sumsect → Sumsect, nilS : 1 → Sumsect, prod : Factor × Prodsect → Prod , prodsect : {∗, /} × Factor × Prodsect → Prodsect, nilP : 1 → Prodsect, embedI : Z → Factor , var : String → Factor , encloseS : Sum → Factor , 121 disjunct : Conjunct × Disjunct → Disjunct, embedC : Conjunct → Disjunct, conjunct : Literal × Conjunct → Conjunct, embedL : Literal → Conjunct, not : Literal → Literal , atom : Sum × Rel × Sum → Literal , embedB : 2 → Literal , encloseD : Disjunct → Literal } Z.B. hat das JavaLight-Programm fact = 1; while x > 1 {fact = fact ∗ x; x = x − 1; } folgenden Syntaxbaum: 122 Seq Assign fact Embed Sum Prod EmbedI Loop NilS NilP 1 EmbedC Block EmbedL Seq Atom Sum Prod Var x > Sum NilS NilP Assign Prod EmbedI 1 fact NilS NilP Sum Prod Var fact Embed Assign NilS x Prodsect * Var x NilP Sum Prod Var x NilP Sumsect - Prod EmbedI NilS NilP 1 javaToAlg "prog" 1 (siehe Java.hs) übersetzt das JavaLight-Programm in der Datei prog in den zugehörigen Syntaxbaum und schreibt diesen in die Datei Pix/javaterm.svg. Mit https://cloudconvert.org kann diese schnell in ein anderes Format konvertiert werden. 123 Beispiel 4.7 Σ(XMLstore) = (S , I, ∅, F ) S = = I = F = { { { { Store, Orders, Order, P erson, Emails, Email, Items, Item, Stock, ItemS, Suppliers, Id } 1, String, Id , Z } store : Stock → Store, storeO : Orders × Stock → Store, orders : P erson × Items × Orders → Orders, embedP : P erson × Items → Orders, person : String → P erson, personE : String × Emails → P erson, emails : Email × Emails → Emails, none : 1 → Emails, email : String → Email, items : Id × String × Items → Items, embedI : Id × String → Items, stock : Id × Z × Supplier × Stock → Stock, embedS : Id × Z × Supplier → Stock, supplier : P erson → Suppliers, parts : Stock → Suppliers, id : String → Id } 124 StoreO EmbedP PersonE Stock EmbedI John Mitchell Emails Id 10 Supplier Id 100 IG8 Email None I18F [email protected] EmbedS PersonE Id 30 Parts Al Jones Emails Email J38H Emails Id 10 Supplier [email protected] Email None J38H1 [email protected] Stock PersonE EmbedS Id 20 Supplier Richard Bird None J38H2 PersonE Mick Taylor None Syntaxbaum des XML-Dokumentes von Beispiel 4.3 xmlToAlg "xmldoc" 1 (siehe Compiler.hs) übersetzt das XMLstore-Dokument in der Datei xmldoc in den zugehörigen Syntaxbaum übersetzt und schreibt diesen in die Datei Pix/xmlterm.svg. xmlToAlg "xmldoc" 2 (siehe Compiler.hs) übersetzt xmldoc in eine Listen-ProdukteSummen-Darstellung und schreibt diese in die Datei Pix/xmllist.svg. Für Beispiel 4.3 sieht sie folgendermaßen aus: 125 () [] [] () () John Mitchell [] [email protected] [] () () IG8 10 One J38H 30 Two () Al Jones [] [] I18F 100 [email protected] [email protected] () J38H1 10 One () J38H2 20 One Richard Bird [] Sei G = (S, BS, R) eine CFG, X = S Mick Taylor [] BS und Z = {B ∈ BS | |B| = 1}. Wort- und Ableitungsbaumalgebra Neben TΣ(G) lassen sich auch die Menge der Wörter über X und die Menge der Ableitungsbäume von G zu Σ(G)-Algebren erweitern. 126 Word (G), die Wortalgebra von G • Für alle s ∈ S, Word (G)s =def X ∗. Word (G) • Für alle w ∈ Z ∗ und r = (s → w) ∈ R, fr () =def w. • Für alle n > 0, w0 . . . wn ∈ Z ∗, e1, . . . , en ∈ S ∪ BS \ Z, r = (s → w0e1w1 . . . enwn) ∈ R und (v1, . . . , vn) ∈ (X ∗)n, frWord (G)(v1, . . . , vn) =def w0v1w1 . . . vnwn. Beispiel 4.8 Nochmal die Regeln von SAB (siehe Beispiel 4.5): r1 = S → aB, r2 = S → bA, r3 = S → , r4 = A → aS r5 = A → bAA, r6 = B → bS, r7 = B → aBB. Alle drei Trägermengen der Wortalgebra Word (SAB) sind durch {a, b}∗ gegeben. Die Konstruktoren von Σ(SAB) werden in Word (SAB) wie folgt interpretiert: Für alle v, w ∈ {a, b}∗, Word (SAB) Word (SAB) (w) = f4 (w) = aw, f1 Word (SAB) Word (SAB) f2 (w) = f6 (w) = bw, Word (SAB) f3 = , Word (SAB) f5 (v, w) = bvw, Word (SAB) f7 (v, w) = avw. o 127 t ∈ TΣ(G) heißt G-Syntaxbaum für w ∈ X ∗, falls fold Word (G)(t) = w gilt. G ist eindeutig, wenn fold Word (G) injektiv ist. Die Sprache L(G) von G ist die Menge der Wörter über X, die sich aus der Faltung eines Syntaxbaums in Word (G) ergeben: L(G) =def fold Word (G)(TΣ(G)). Z.B. ist L(REG) (siehe 4.1) die Menge aller Wörter über syms(BS) (siehe 2.14), die reguläre Ausdrücke über BS darstellen, formal: L(REG) = {w ∈ syms(BS)∗ | ∃ t ∈ TReg(BS) : fold Regword (BS)(t)(0) = w}. Zwei Grammatiken G und G0 heißen äquivalent, wenn ihre Sprachen übereinstimmen. Die Theorie formaler Sprachen liefert eine äquivalente Definition von L(G), die die oben definierte Ableitungsrelation →G verwendet: Für alle s ∈ S, + L(G)s = {w ∈ BS ∗ | s →G w}. 128 javaToAlg "prog" 2 (siehe Java.hs) übersetzt das JavaLight-Programm in der Datei prog in die Interpretation des zugehörigen Syntaxbaums in Word (JavaLight) und schreibt diese in die Datei javasource. Die Inhalte von prog und javasource stimmen also miteinander überein! Abl(G), die Ableitungsbaumalgebra von G Für alle s ∈ S ist Abl(G) ist das kleinste (S ∪BS)-Tupel von Bäumen, deren innere Knoten mit Sorten und deren Blätter mit Basismengen markiert sind, und die folgende Eigenschaft haben: • Für alle B ∈ BS, Abl(G)B = {B}. • Für alle r = (s → (e1, . . . , en)) ∈ R und (t1, . . . , tn) ∈ Abl(G)e1 × . . . × Abl(G)en , s(t1, . . . , tn) ∈ Abl(G)s. s(t1, . . . , tn) repräsentiert den Baum mit Wurzelmarkierung s und maximalen Unterbäumen t1, . . . , tn. Abl(G) interpretiert die Konstruktoren von Σ(G) wie folgt: Abl(G) • Für alle w ∈ Z ∗ und r = (s → w) ∈ R, fr () =def s(w). 129 • Für alle n > 0, w0 . . . wn ∈ Z ∗, e1, . . . , en ∈ S ∪ BS \ Z, r = (s → w0e1w1 . . . enwn) ∈ R und (t1, . . . , tn) ∈ Abl(G)e1 × . . . × Abl(G)en , frAbl(G)(t1, . . . , tn) =def s(w0, t1, w1, . . . , tn, wn). Commands Command Commands fact = Sum ; Command Prod Sumsect while Disjunct Factor Prodsect Command Conjunct 1 Commands Literal Sum > Sum Prod Sumsect Factor Prodsect x Command fact = Sum ; Prod Sumsect Factor Prodsect 1 Commands Command Prod Sumsect Factor fact x = Sum ; Prodsect Prod * Factor Prodsect Factor Prodsect x x Sumsect - Prod Sumsect Factor Prodsect 1 Ableitungsbaum von fact = 1; while x > 1 {fact = fact*x; x = x-1;} 130 javaToAlg "prog" 3 (siehe Java.hs) übersetzt das JavaLight-Programm in der Datei prog in den zugehörigen Ableitungsbaum und schreibt diesen in die Datei Pix/javaderi.svg. Beispiel 4.9 Zwei Modelle der Ausdrücke von JavaLight Wir interpretieren hier zunächst nur die Sorten und Operationen von Σ(JavaLight), die mit Ausdrücken zu tun haben, also alle außer Command und Commands. Der erste Modell A = (A, Op) ignoriert darüberhinaus Programmvariablen (Strings), d.h. der Konstruktor var : String → Factor bleibt uninterpretiert. ASum = AProd = AFactor = Z ASumsect = AProdsect = Z → Z ADisjunct = AConjunct = ALiteral = 2 embedDA : AConjunct embedLA : ALiteral encloseS A : ASum encloseDA : ADisjunct embedI A : Z embedB A : 2 x → → → → → → 7→ ADisjunct AConjunct AFactor ALiteral AFactor ALiteral x 131 sumA : AProd × ASumsect → ASum prodA : AFactor × AProdsect → AProd (x, f ) 7→ f (x) sumsectA : {+, −} × AProd × ASumsect → ASumsect prodsectA : {∗, /} × AFactor × AProdsect → AProdsect (op, x, f ) 7→ λy.f (y op x) nilS A : 1 → ASumsect nilP A : 1 → AProdsect 7→ λx.x disjunctA : AConjunct × ADisjunct → ADisjunct (x, y) 7→ x ∨ y conjunctA : ALiteral × AConjunct → AConjunct (x, y) 7→ x ∧ y notA : ALiteral → ALiteral x 7→ ¬x 132 atomA : ASum × Rel × ASum → ALiteral (x, rel, y) 7→ x rel y Sei Store = String → Z (Menge der Belegungen von Programmvariablen durch ganze Zahlen). Das zweite Modell B = (B, Op) liftet jede Trägermenge M ∈ {Z, Z → Z, 2} von A zur Funktionsmenge Store → M : BSum = BProd = BFactor = Store → Z BSumsect = BProdsect = Store → (Z → Z) BDisjunct = BConjunct = BLiteral = Store → 2 B interpretiert embedD, embedL, encloseS und encloseD wie A als Identitäten. varB : String → BFactor x 7→ λstore.store(x) Die restlichen Operationen von Σ(JavaLight) werden zustandsabhängig gemacht: embedI B : Z → BFactor embedB B : 2 → BLiteral a 7→ λstore.a 133 sumB : BProd × BSumsect → BSum prodB : BFactor × BProdsect → BProd (f, g) 7→ λstore.g(store)(f (store)) sumsectB : {+, −} × BProd × BSumsect → BSumsect prodsectB : {∗, /} × BFactor × AProdsect → BProdsect (op, f, g) 7→ λstore.λx.g(store)(x op f (store)) nilS B : 1 → BSumsect nilP B : 1 → BProdsect 7→ λstore.λx.x disjunctB : BConjunct × BDisjunct → BDisjunct (f, g) 7→ λstore.(f (store) ∨ g(store)) conjunctB : BLiteral × BConjunct → BConjunct (f, g) 7→ λstore.(f (store) ∧ g(store)) notB : BLiteral → BLiteral f 7→ λstore.¬f (store) 134 atomB : BSum × Rel × BSum → BLiteral (f, rel, g) 7→ λstore.(f (store) rel g(store)) In Abschnitt 11.5 wird B als Teil der JavaLight-Algebra javaState in Haskell implementiert. 135 5 Parser und Compiler für CFGs Sei G = (S, BS, R) eine CFG, X = X übersetzt werden sollen. S BS und A eine Σ(G)-Algebra, in die Wörter über In Anlehnung an den klassischen Begriff eines semantisch korrekten Compilers [22, 41] verlangen wir, dass die Semantiken Sem und Mach seiner Quell- bzw. Zielsprache in der durch das folgende Funktionsdiagramm ausgedrückten Weise miteinander zusammenhängen: TΣ(G) fold Sem g Sem fold A (1) encode A evaluate g Mach Hierbei sind • Sem die ebenfalls als Σ(G)-Algebra gegebene Semantik der Quellsprache L(G), • Mach ein in der Regel unabhängig von Σ(G) definiertes Modell der Zielsprache, meist in Form einer abstrakten Maschine, • evaluate ein Interpreter, der Zielprogramme in der abstrakten Maschine Mach ausführt, 136 • encode eine Funktion, die Sem auf Mach abbildet und die gewünschte Arbeitsweise des Compilers auf semantischer Ebene reflektiert. Die Initialität der Termalgebra TΣ(G) erlaubt es uns, den Beweis der Kommutativität von (1) auf die Erweiterung von encode und evaluate zu Σ(G)-Homomorphismen zu reduzieren. Dazu muss zunächst Mach zu einer Σ(G)-Algebra gemacht werden, was z.B. bedeuten kann, die elementaren Funktionen der Zielsprache so in einer Signatur Σ0 zusammenzufassen, dass TΣ0 mit A übereinstimmt, jeder Konstruktor von Σ(G) einem Σ0-Term entspricht, Sem eine Σ0-Algebra ist und evaluate Σ0-Terme in Sem faltet. Die Darstellung von Σ(G)Konstruktoren durch Σ0-Terme bestimmt dann möglicherweise eine Definition von encode, die sowohl encode als auch evaluate Σ(G)-homomorph macht. So wurde z.B. in [41] die Korrektheit eines Compilers gezeigt, der imperative Programme in Datenflussgraphen übersetzt. Damit sind alle vier Abbildungen in Diagramm (1), also auch und die beiden Kompositionen evaluate ◦ fold A und encode ◦ fold Mach Σ(G)-homomorph. Da TΣ(G) eine initiale Σ(G)Algebra ist, sind beide Kompositionen gleich. Also kommutiert (1). Während fold A Syntaxbäume in der Zielsprache auswertet, ordnet fold Word (G) (siehe Kapitel 4) einem Syntaxbaum t das Wort der Quellsprache zu, aus dem ein Parser für G t berechnen soll. 137 In der Theorie formaler Sprachen ist der Begriff Parser auf Entscheidungsalgorithmen beschränkt, die anstelle von Syntaxbäumen lediglich einen Booleschen Wert liefern, der angibt, ob ein Eingabewort zur Sprache der jeweiligen Grammatik gehört oder nicht. Demgegenüber definieren wir einen Parser für G als eine S-sortige Funktion parseG : X ∗ → M (TΣ(G)), die entweder Syntaxbäume oder, falls das Eingabewort nicht zur Sprache von G gehört, Fehlermeldungen erzeugt. Welche Syntaxbäume bzw. Fehlermeldungen ausgegeben werden sollen, wird durch eine Monade M festgelegt. 5.1 Funktoren und Monaden Funktoren, natürlichen Transformationen und Monaden sind kategorientheoretische Grundbegriffe, die heutzutage jeder Softwaredesigner kennen sollte, da sie sich in den Konstruktionsund Transformationsmustern jedes denkbaren statischen, dynamischen oder hybriden Systemmodells wiederfinden. Die hier benötigten kategorientheoretischen Definitionen lauten wie folgt: Eine Kategorie K besteht aus • einer – ebenfalls mit K bezeichneten – Klasse von K-Objekten, • für alle A, B ∈ K einer Menge K(A, B) von K-Morphismen, 138 • einer assoziativen Komposition ◦ : K(A, B) × K(B, C) → K(A, C) (f, g) 7−→ g ◦ f, • einer Identität idA ∈ K(A, A), die bzgl. ◦ neutral ist, d.h. für alle B ∈ K und f ∈ K(A, B) gilt f ◦ idA = f = idB ◦ f . Im Kontext einer festen Kategorie K schreibt man meist f : A → B anstelle von f ∈ K(A, B). Wir haben hier mit vier Kategorien zu tun: Set, Set2 und SetS , deren Objekte alle Mengen, alle Mengenpaare bzw. alle S-sortigen Mengen und deren Morphismen alle Funktionen, alle Funktionspaare bzw. alle S-sortigen Mengen sind, sowie die Unterkategorie AlgΣ von SetS , deren Objekte alle Σ-Algebren und deren Morphismen alle Σ-Homomorphismen sind. Seien K, L Kategorien. Ein Funktor F : K → L ist eine Funktion, die jedem K-Objekt ein L-Objekt und jedem K-Morphismus f : A → B einen L-Morphismus F (f ) : F (A) → F (B) zuordnet sowie folgende Gleichungen erfüllt: • Für alle K-Objekte A, F (idA) = idF (A), (2) • Für alle K-Morphismen f : A → B and g : B → C, F (g ◦ f ) = F (g) ◦ F (f ). (3) 139 Zwei Funktoren F : K → L und G : L → M kann man wie andere Funktionen sequentiell zu weiteren Funktoren komponieren: Für alle K-Objekte und -Morphismen A, (GF )(A) =def G(F (A)). Beispiele Sei B ∈ L. Der konstante Funktor const(B) : K → L ordnet jedem K-Objekt das L-Objekt B zu und jedem K-Morphismus die Identität auf B. Der Identitätsfunktor Id K : K → K ordnet jedem K-Objekt und jedem K-Morphismus sich selbst zu. Der Diagonalfunktor ∆K : K → K2 ordnet jedem K-Objekt A das Objektpaar (A, A) und jedem K-Morphismus f das Morphismenpaar (f, f ) zu. Der Produktfunktor × : Set2 → Set ordnet jedem Mengenpaar (A, B) die Menge A × B und jedem Funktionspaar (f : A → B, g : C → D) die Funktion f × g =def λ(a, c).(f (a), g(c)) zu. 140 Der Listenfunktor Star : Set → Set ordnet jeder Menge A die Menge A∗ der Wörter über A zu und jeder Funktion f : A → B die Funktion Star (f ) : A∗ → B ∗ 7→ (a1, . . . , an) 7→ (f (a1), . . . , f (an)) Der Mengenfunktor P : Set → Set ordnen jeder Menge A die Potenzmenge P(A) zu und jeder Funktion f : A → B die Funktion P(f ) : P(A) → P(B) C 7→ {f (c) | c ∈ C} Sei E eine Menge von Fehlermeldungen, Ausnahmewerten o.ä. Der Ausnahmefunktor + E : Set → Set ordnet jeder Menge A die Menge A + E zu (siehe Kapitel 2) und jeder Funktion f : A → B die Funktion f +E :A+E → B+E (a, 1) 7→ (f (a), 1) (e, 2) 7→ (e, 2) Sei S eine Menge. 141 Der Potenz- oder Leserfunktor S : Set → Set über S ordnet jeder Menge A die Menge S → A zu und jeder Funktion f : A → B die Funktion f S : AS → B S g 7→ f ◦ g Der Copotenz- oder Schreiberfunktor × S : Set → Set über S ordnet jeder Menge A die Menge A × S zu und jeder Funktion f : A → B die Funktion f ×S :A×S → B×S (a, s) 7→ (f (a), s) Der Transitionsfunktor ( × S)S : Set → Set komponiert den Leser- mit dem Schreiberfunktor über S: Er ordnet also jeder Menge A die Menge (A×S)S zu und jeder Funktion f : A → B die Funktion (f × S)S : (A × S)S → (B × S)S g 7→ λs.(f (π1(g(s))), π2(g(s))) Aufgabe Zeigen Sie, dass die hier definierten Funktionen tatsächlich Funktoren sind, also (2) und (3) erfüllen, und dass sie sich von Set auf SetS fortsetzen lassen. o 142 Seien F, G : K → L Funktoren. Eine natürliche Transformation τ : F → G ordnet jedem K-Objekt A einen L-Morphismus τA : F (A) → G(A) derart, dass für alle KMorphismen f : A → B folgendes Diagramm kommutiert: τA F (A) G(A) F (f ) g F (B) G(f ) τB g G(B) Ein Funktor M : K → K heißt Monade, wenn es zwei natürliche Transformationen η : Id K → M (Einheit) und µ : M ◦ M → M (Multiplikation) gibt, die für alle A ∈ K das folgende Diagramm kommutativ machen: M (A) ηM (A) M (ηA) M (M (A)) ≺ M (A) (4) M (idA) µA g ≺ M (A) (5) M (idA) M (M (M (A))) M (µA) g M (M (A)) µM (A) M (M (A)) (6) µA µA g M (A) 143 Beispiele Viele der o.g. Funktoren sind Monaden. Einheit bzw. Multiplikation sind wie folgt definiert: Seien A, E, S Mengen. • Listenfunktor: ηA : A → A∗ a 7→ (a) µA : (A∗)∗ → A∗ (w1, . . . , wn) 7→ w1 · . . . · wn • Mengenfunktor: ηA : A → P(A) a 7→ {a} µA : P(P(A)) → P(A) S S 7→ S • Ausnahmefunktor: ηA : A → A + E a 7→ (a, 1) µA : (A + E) + E → A + E ((a, 1), 1) 7→ (a, 1) ((e, 2), 1) 7→ (e, 2) (e, 2) 7→ (e, 2) 144 • Leserfunktor: ηA : A → A S µA : (AS )S → AS a 7→ λs.a f 7→ λs.f (s)(s) • Schreiberfunktor: Hier muss S die Trägermenge eines Monoids M = (S, ∗, e) sein. ηA : A → A × S µA : (A × S) × S → A × S a 7→ (a, e) ((a, s), s0) 7→ (a, s ∗ s0) • Transitionsfunktor: ηA : A → (A × S)S µA : ((A × S)S × S)S → (A × S)S a 7→ λs.(a, s) f 7→ λs.π1(f (s))(π2(f (s))) Aufgabe Zeigen Sie, dass für diese Beispiele (4)-(6) gilt. o Seien A und B Mengen. Der bind-Operator = : M (A) × (A → M (B)) → M (B) wird wie folgt aus M und der Multiplikation von M abgeleitet: 145 Für alle A ∈ K und f : A → M (B), (= f ) = µB ◦ M (f ). (7) Intuitiv stellt man sich ein monadisches Objekt m ∈ M (A) als Berechnung vor, die eine – evtl. leere – Menge von Werten in A erzeugt. Ein Ausdruck der Form m = f wird dann wie folgt ausgewertet: Die von m berechneten Werte a ∈ A werden als Eingabe an die Berechnung f übergeben und von f (a) verarbeitet. Die Betrachtung der Elemente von M (A) als Berechnungen, die eine Ausgabe in A produzieren, lässt sich besonders gut am Beispiel der Transitionsmonade begründen. Aus der Multiplikation der Transitionsmonade und der allgemeinen Definition des bindOperators ergibt sich nämlich die folgende Charakterisierung des bind-Operators der Transitionsmonade: Für alle g : S → A × S und f : A → (S → B × S), g = f = µB (M (f )(g)) = µB ((f × S)S (g)) = µB (λs.(f (π1(g(s))), π2(g(s)))) = λs.π1((λs.(f (π1(g(s))), π2(g(s))))(s))(π2((λs.(f (π1(g(s))), π2(g(s))))(s))) = λs.π1(f (π1(g(s))), π2(g(s)))(π2(f (π1(g(s))), π2(g(s)))) = λs.f (π1(g(s)))(π2(g(s))). 146 Die Anwendung der Funktion (g = f ) : S → B ×S auf den Zustand s besteht demnach • in der Anwendung von g auf s, die die Ausgabe a = π1(g(s)) und den Folgezustand s0 = π2(g(s)) liefert, • und der darauffolgenden Anwendung von f (a) auf s0. Sei A, B, C Mengen, a ∈ A, m ∈ M (A), m0 ∈ M (M (A)), f : A → M (B), g : B → M (C) und h : A → B. Aus (4)-(7) erhält man die folgenden Eigenschaften von =: m = ηA ηA(a) = f (m = f ) = g = = = m, f (a), m = λa.f (a) = g. (8) (9) (10) (2)-(4) und (7) implizieren die folgenden Charakterisierungen von M (h) bzw. µA: M (h)(m) = m = ηB ◦ h, (11) µA(m0) = m0 = idM (A). Mit dem bind-Operator werden monadische Objekte sequentiell verknüpft. Plusmonaden haben zusätzlich eine parallele Komposition, die – analog zu η und µ – für Monaden M : Set → Set als natürliche Transformation ⊕ von M × M = × ◦ ∆ ◦ M nach M definiert wird. Mit ⊕ lässt sich z.B. das Backtracking bzw. der Nichtdeterminismus eines monadischen Compilers realisieren (siehe Kapitel 6). 147 Die Komposition von Monaden führt zu weiteren Monaden: Sei M eine Monade mit Einheit η, Multiplikation µ und paralleler Komposition ⊕ und S eine Menge. • Lesermonade über (M, ⊕): M 0 =def λA.M (A)S Die Morphismenabbildung von M , η, µ und ⊕ werden wie folgt auf M 0 fortgesetzt: Sei h : A → B. M 0(h) : M 0(A) → M 0(B) f 7→ M (h) ◦ f ηA : A → M 0(A) µA : M 0(M 0(A)) → M 0(A) f 7→ λs.f (s) = λg.g(s) ⊕ : M 0(A) × M 0(A) → M 0(A) (f, g) 7→ λs.f (s) ⊕ g(s) 148 • Transitionsmonade über (M, ⊕): M 0 =def λA.M (A × S)S Die Morphismenabbildung von M , η, µ und ⊕ werden wie folgt auf M 0 fortgesetzt: Sei h : A → B. M 0(h) : M 0(A) → M 0(B) f 7→ λs.f (s) = λ(a, s).η(h(a), s) ηA : A → M 0(A) a 7→ λs.η(a, s) µA : M 0(M 0(A)) → M 0(A) f 7→ λs.f (s) = λ(g, s).g(s) ⊕ : M 0(A) × M 0(A) → M 0(A) (f, g) 7→ λs.f (s) ⊕ g(s) Die Implementierung von Monaden in Haskell wird in Kapitel 15 behandelt, monadische Compiler in Kapitel 16. Letztere sind Transitionsmonaden über Ausnahmemonaden, wobei S die Menge X ∗ der zu verarbeitenden Wörter ist. Weitere Monadenbeispiele finden sich z.B. in [28], Kapitel 7. 149 Compilermonaden Sei S eine Menge, M : SetS → SetS eine Monade mit Einheit η, bind-Operator =, natürliche Transformationen ⊕ : M × M → M und set : M → P (wobei P hier den Mengenfunktor für S-sortige Mengen bezeichnet) und E = {m ∈ M (A) | setA(m) = ∅, A ∈ SetS } (“Menge der Ausnahmewerte”). M heißt Compilermonade, wenn für alle Mengen A, B, m, m0, m00 ∈ M (A), e ∈ E, f : A → M (B), h : A → B und a ∈ A Folgendes gilt: (m ⊕ m0) ⊕ m00 = m ⊕ (m0 ⊕ m00), M (h)(e) = e, M (h)(m ⊕ m0) = M (h)(m) ⊕ M (h)(m0), setA(m ⊕ m0) = setA(m) ∪ setA(m0), (CM 1) (CM 2) (CM 3) setA(ηA(a)) = {a}, S setB (m = f ) = {setB (f (a)) | a ∈ setA(m)}. 150 Nach Definition der Einheit η P und des bind-Operators =P der Mengenmonade P machen die letzten beiden Gleichungen set zum Monadenmorphismus. Aus ihnen folgt nämlich: setA ◦ ηA = ηAP , setB (m = f ) = setA(m) =P setB ◦ f. Außerdem erhält man für alle S-sortigen Mengen A, S-sortigen Funktionen h : A → B und m ∈ M (A): S setB (M (h)(m)) = setB (m = ηB ◦ h) = {setB (ηB (h(a)) | a ∈ setA(m)} S = {{h(a)} | a ∈ setA(m)} = h(setA(m)). Satz 5.2 Listen-, Mengen- und Ausnahmefunktoren sind Compilermonaden. Beweis. Sei A eine Menge, ⊕ = · und setA : A∗ → P(A) definiert durch setA() = ∅ und setA(s) = {a1, . . . , an} für alle s = (a1, . . . , an) ∈ A+. Damit ist Star eine Compilermonade. Sei A eine Menge, ⊕ = ∪ und setA : P(A) → P(A) definiert durch idP(A). Damit ist P eine Compilermonade. 151 Seien A und E disjunkte Mengen, ⊕ = λ(m, m0).m und setA : A + E → P(A) definiert durch setA(e) = ∅ für alle e ∈ E und setA(a) = {a} für alle a ∈ A. Damit ist λA.A + E eine Compilermonade. o Monadenbasierte Parser und Compiler S Sei G = (S, BS, R) eine CFG, X = BS, Σ(G) = (S, I, F ) und M : SetS → SetS eine Compilermonade. Wie bereits in Kapitel 1 informell beschrieben wurde, soll ein Compiler für G in die als Σ(G)-Algebra A formulierten Zielsprache einen Parser für G mit der Faltung in A der vom Parser erzeugten Syntaxbäume komponieren: compileA G ∗ parseG M (fold A ) = X −→ M (TΣ(G)) −→ M (A). (12) 152 Gilt (12), dann überträgt sich die Kommutativität von Diagramm (1) wie folgt auf die Kommutativität des folgenden Diagramms: X compileA G ∗ compileSem G M (A) M (evaluate) (13) g M (Sem) M (encode) g M (Mach) (12) A M (evaluate) ◦ compileA G = M (evaluate) ◦ M (fold ) ◦ parseG (1) M ist Funktor = M (evaluate ◦ fold A) ◦ parseG = M (encode ◦ fold Sem ) ◦ parseG M ist Funktor M (encode) ◦ M (fold Sem ) ◦ parseG = M (encode) ◦ compileSem G . = (12) Da TΣ(G) initial ist, stimmt fold TΣ(G) mit der Identität auf TΣ(G) überein. Also gilt: parseG = idM (TΣ(G)) ◦ parseG = M (idTΣ(G) ) ◦ parseG = M (fold TΣ(G) ) ◦ parseG (12) T = compileGΣ(G) . 153 Da TΣ(G) eine Σ(G)-Algebra und fold A Σ(G)-homomorph ist, ist (12) ein Spezialfall folgender Bedingung: Für alle Σ(G)-Homomorphismen h : A → B, B M (h) ◦ compileA G = compileG . (14) T Sei parseG = compileGΣ(G) und unparseG =def fold Word (G). Die Korrektheit von parseG lässt sich wie folgt formalisieren: Für alle w ∈ X ∗, P(unparseG)(setTΣ(G) (parseG(w))) ⊆ {w}, (15) in Worten: Die Faltung jedes von parseG(w) berechneten Syntaxbaums in der Algebra Word (G) stimmt mit w überein. Wegen P(unparseG) ◦ setTΣ(G) ◦ parseG = setWord (G) ◦ M (unparseG) ◦ parseG Word (G) = setWord (G) ◦ compileG ist (15) äquivalent zu Word (G) setWord (G)(compileG (w)) ⊆ {w}. (16) (15) erlaubt die Leerheit von setTΣ(G) (parseG(w)). Der Parser parseG ist vollständig, wenn er jedes Wort w ∈ L(G) in mindestens einen Syntaxbaum für w übersetzt, d.h. er erfüllt folgende Bedingung: Word (G) ∀ w ∈ L(G) : setWord (G)(compileG (w)) 6= ∅. (17) 154 Zusammenfassend ergibt sich folgende Definition: Ein generischer Compiler für G ist eine AlgΣ(G)-sortige Menge ∗ compileG = (compileA G : X → M (A))A∈AlgΣ(G) , die (14), (16) und (17) erfüllt. Sei W = Word (G). (16) und (17) machen setW ◦ compileW G zu der Summenextension (siehe Kapitel 2) [λw.{w}, λw.∅] : L(G) + X ∗ \ L(G) → P(X ∗). L(G) incL(G) X∗ ≺ incX ∗\L(G) X ∗ \ L(G) setW ◦ compileW G λw.{w} λw.∅ g ≺ P(X ∗) U.a. ist dann die Einschränkung von parseG auf L(G) rechtsinvers zu unparseG: setW ◦ M (unparseG) ◦ parseG ◦ incL(G) = setW ◦ M (fold W ) ◦ parseG ◦ incL(G) (12) = setW ◦ compileW G ◦ incL(G) = λw.{w}. 155 Schließlich gilt (14) genau dann, wenn compileG : const(X ∗) → M ◦ U eine natürliche Transformation ist, wobei const(X ∗), U : AlgΣ(G) → SetS die Funktoren bezeichnen, die • jeder Σ(G)-Algebra A die S-sortige Menge (X ∗)s∈S bzw. die Trägermenge von A und • jedem Σ(G)-Homorphismus h die S-sortige Funktion (idX ∗ )s∈S bzw. die h zugrundeliegende S-sortige Funktion zuordnen. 156 6 LL-Compiler Der in diesem Kapitel definierte generische Compiler überträgt die Arbeitsweise eines klassischen LL-Parsers auf Compiler mit Backtracking. Das erste L steht für seine Verarbeitung des Eingabewortes von links nach rechts, das zweite L für seine – implizite – Konstruktion einer Linksableitung. Da diese Arbeitsweise dem mit der Wurzel beginnenden schrittweisen Aufbau eines Syntaxbaums entspricht, werden LL-Parser auch top-down-Parser genannt. S Sei G = (S, BS, R) eine nicht-linksrekursive CFG, X = BS, Z = {B ∈ BS | |B| = 1}, M eine Compilermonade, errmsg : X ∗ → E, A eine Σ(G)-Algebra und s ∈ S. compileG heißt LL-Compiler, wenn ∗ compileA G,s : X → M (As ) wie folgt definiert ist: Für alle w ∈ X ∗, A (1) compileA G,s (w) = transs (w) = λ(a, w).if w = then ηA (a) else errmsg(w), wobei für alle für alle s ∈ S ∪ BS ∗ ∗ transA s : X → M (As × X ) wie folgt definiert ist: 157 Fall 1: s ∈ BS. Für alle x ∈ X und w ∈ X ∗, transA s (xw) = if x ∈ s then ηA×X ∗ (x, w) else errmsg(xw) transA s () = errmsg() Fall 2: s ∈ S. Für alle w ∈ X ∗, transA s (w) = M tryrA(w). (2) r=(s→e)∈R Für alle r = (s → (e1, . . . , en)) ∈ R und w ∈ X ∗, transA e1 (w) = λ(a1 , w1 ). transA (w ) = λ(a , w ). 1 2 2 e2 tryrA(w) = ... transA (w ) = λ(a , w ).η A n−1 n n A×X ∗ (fr (ai1 , . . . , aik ), wn ), en (3) wobei {i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS \ Z}. Während alle Summanden von (2) auf das gesamte Eingabewort w angewendet werden, wird es von tryrA Symbol für Symbol verarbeitet: Zunächst wird transA e1 auf w angewendet, A A dann transA e2 auf das von transe1 nicht verarbeitete Suffix w1 von w, transe3 auf das von transA e2 nicht verarbeitete Suffix w2 von w1 , usw. 158 Gibt es 1 ≤ j ≤ n derart, dass transA ej (w) scheitert, d.h. existiert kein aus ej ableitbares Präfix von w, dann übergibt tryrA das gesamte Eingabewort zur Parsierung an den nächsten Teilcompiler tryrA0 der Argumentliste von ⊕ in (2) (Backtracking). A Ist transA ei (w) jedoch für alle 1 ≤ i ≤ n erfolgreich, dann schließt der Aufruf von tryr mit der Anwendung von frA auf die Zwischenergebnisse ai1 , . . . , aik ∈ A. Klassische LL-Parser sind deterministisch, d.h. sie erlauben kein Backtracking, sondern setzen voraus, dass die ersten k Symbole des Eingabewortes w bestimmen, welcher der A Teilcompiler trys→e aufgerufen werden muss, um zu erkennen, dass w zu L(G)s gehört. CFGs, die diese Voraussetzung erfüllen, heißen LL(k)-Grammatiken. Im Gegensatz zur Nicht-Linksrekursivität lässt sich die LL(k)-Eigenschaft nicht immer durch eine Transformation der Grammatik erzwingen, auch dann nicht, wenn sie eine von einem deterministischen Kellerautomaten erkennbare Sprache erzeugt! Monadische Ausdrücke werden in Haskell oft in der do-Notation wiedergegeben. Sie verdeutlicht die Korrespondenz zwischen monadischen Berechnungen einerseits und imperativen Programmen andererseits. Die Rückübersetzung der do-Notation in die ursprünglichen monadischen Ausdrücke ist induktiv wie folgt definiert: 159 Sei m ∈ M (A), a ∈ A und m0 ∈ M (B). do m; a ← m0 = m = λa. do m0 do m; m0 = m do m0 Gleichung (10) von Kapitel 5 impliziert die Assoziativität des Sequentialisierungsoperators (;), d.h. do m; m0; m00 ist gleichbedeutend mit do (do m; m0); m00 und do m; (do m0; m00). Beispiel 6.1 SAB (siehe Beispiel 4.5) Die Regeln r1 = S → aB, r2 = S → bA, r3 = S → , r4 = A → aS, r5 = A → bAA, r6 = B → bS, r7 = B → aBB von SAB liefern nach obigem Schema die folgende Transitionsfunktion des LL-Compilers in eine beliebige SAB-Algebra alg: Für alle x, z ∈ {a, b} und w ∈ {a, b}∗, trans_z(xw) = if x = z then η(z,w) else errmsg(xw) trans_z() = errmsg() 160 trans_S(w) = try_r1(w) ⊕ try_r2(w) ⊕ try_r3(w) trans_A(w) = try_r4(w) ⊕ try_r5(w) trans_B(w) = try_r6(w) ⊕ try_r7(w) try_r1(w) try_r2(w) try_r3(w) try_r4(w) try_r5(w) try_r6(w) try_r7(w) = do (x,w) <- trans_a(w); (c,w) <- trans_B(w); = do (x,w) <- trans_b(w); (c,w) <- trans_A(w); = η(f_r3^alg,w) = do (x,w) <- trans_a(w); (c,w) <- trans_S(w); = do (x,w) <- trans_b(w); (c,w) <- trans_A(w); (d,w) <- trans_A(w); = do (x,w) <- trans_b(w); (c,w) <- trans_S(w); = do (x,w) <- trans_a(w); (c,w) <- trans_B(w); (d,w) <- trans_B(w); η(f_r1^alg(c),w) η(f_r2^alg(c),w) η(f_r4^alg(c),w) η(f_r5^alg(c,d),w) η(f_r6^alg(c),w) η(f_r7^alg(c,d),w) o 161 Sei W = Word (G). Um später zeigen zu können, dass der LL-Compiler vollständig ist, setzen wir voraus, dass • für alle s ∈ S, v ∈ L(G)s und w ∈ X ∗ r = (s → (e1, . . . , en)) ∈ R und für alle 1 ≤ i ≤ n vi ∈ L(G)ei mit v1 . . . vn = v und setW 2 (tryrW (w)) ⊆ setW 2 (transW s (w)) existieren. (LLC ) Gibt es keine Anordnung der Regeln in (2), die diese Bedingung erfüllt, dann müssen ggf. neue Sorten für die Vorkommen einer Sorte in bestimmten Kontexten eingeführt und die Regeln von R entsprechend modifiziert werden. Beispiel 6.2 Ein solcher Fall würde z.B. in JavaLight+ auftreten (siehe Kapitel 13), wenn wir dort anstelle der drei Sorten ExpSemi , ExpBrac und ExpComm die zunächst naheliegende eine Sorte Exp und die folgenden Regeln verwenden würden: 162 Command Exp Sum Sumsect Prod Prodsect Factor Disjunct Conjunct Literal Actuals Actuals0 → → → → → → → → → → → → String = Exp; | write Exp; | . . . Sum | Disjunct Prod Sumsect ... | Factor Prodsect ... | String | String Actuals | . . . Conjunct | . . . Literal | . . . String | String Actuals | . . . () | (Actuals0 Exp) | Exp, Actuals0 (A) (B) (C) Ein LL-Compiler für Command würde z.B. die Eingabe z = x<=11; als syntaktisch inkorrekt betrachten, weil der Teilcompiler für Exp zunächst nach dem längsten aus Sum ableitbaren Präfix von x<=11 sucht, x als solches erkennt und deshalb das Wort x<=11 nicht bis zum Ende liest. Die Ersetzung von Regel (B) durch Exp → Disjunct|Sum löst das Problem nicht, denn nun würde der Compiler z.B. die Eingabe z = x+11; als syntaktisch inkorrekt betrachten, weil der Teilcompiler für Exp zunächst nach dem längsten aus Disjunct ableitbaren Präfix 163 von x+11 sucht, x als solches erkennt und deshalb das Wort x+11 nicht bis zum Ende liest. Ersetzt man hingegen (A)-(C) durch Command ExpSemi ExpBrac ExpComm Actuals0 → → → → → String = ExpSemi | write ExpSemi | . . . Sum; | Disjunct; Sum) | Disjunct) Sum, | Disjunct, ExpBrac | ExpComm Actuals0 dann wird alles gut: Jetzt erzwingt das auf Sum oder Disjunct folgende Terminal (Semikolon, schließende Klammer bzw. Komma), dass die Compiler für ExpSemi , ExpBrac und ExpComm die jeweilige Resteingabe stets bis zu diesem Terminal verarbeiten. Natürlich genügt ein Compiler für alle drei Sorten. Man muss ihn lediglich mit dem jeweiligen Terminal parametrisieren (siehe Java2.hs). o Satz 6.4 transA ist wohldefiniert. Beweis durch Noethersche Induktion. Da S endlich und G nicht linksrekursiv ist, ist die folgende Ordnung > auf S ∪ BS Noethersch, d.h., es gibt keine unendlichen Ketten s1 > s2 > s3 > · · · : s > s0 ⇔def + ∃ w ∈ (S ∪ BS)∗ : s →G s0w. 164 Wir erweitern > zu einer ebenfalls Noetherschen Ordnung auf X ∗ × (S ∪ BS): (v, s) (w, s0) ⇔def |v| > |w| oder (|v| = |w| und s > s0). Nach Definition von transs(w) gibt es r = (s → e1 . . . en) ∈ R mit A A A transA s (w) = . . . transe1 (w) . . . transe2 (w1 ) . . . transen (wn−1 ) . . . Da w1, . . . , wn echte Suffixe von w sind, gilt s > e1 und |w| > |wi| für alle 1 ≤ i < n, also (w, s) (w, e1) und (w, s) (wi−1, ei) für alle 1 < i ≤ n. Die rekursiven Aufrufe von transA haben also bzgl. kleinere Argumente. Folglich terminiert jeder Aufruf von transA, m.a.W.: transA ist wohldefiniert. o Der Beweis, dass compileG Gleichung (15) von Kapitel 5 erfüllt, verwendet die folgenden Zusammenhänge zwischen einer Monade M und ihrem bind-Operator: Lemma 6.5 Sei M : SetS → SetS eine Monade und h : A → B eine S-sortige Funktion. Für alle m ∈ M (A), f : A → M (A) und g : B → M (B), M (h)(m = f ) = m = M (h) ◦ f, M (h)(m) = g = m = g ◦ h. (4) (5) 165 Sei Σ eine Signatur, m1, . . . , mn ∈ M (A), h : A → B Σ-homomorph, für alle 1 ≤ i ≤ n, fi : A → · · · → A → M (A), gi : B → · · · → B → M (B), und t ∈ TΣ(V ) mit var(t) = {x1, . . . , xn}, fi(a1) . . . (ai−1) = mi fi+1(a1) . . . (ai−1), fn+1(a1) . . . (an) = ηA(fa∗(t)) für alle a = (a1, . . . , an) ∈ A und gi(b1) . . . (bi−1) = M (h)(mi) gi+1(b1) . . . (bi−1), gn+1(b1) . . . (bn) = ηB (gb∗(t)) für alle b = (b1, . . . , bn) ∈ B, wobei fa : V → A und gb : V → B durch fa(xi) = ai und gb(xi) = bi für alle 1 ≤ i ≤ n definiert sind. Dann gilt M (h)(f1) = g1. (6) Beweis von (4). M (h)(m = f ) (10) in Kap. 5 = = (m = f ) = ηB ◦ h m = λa.f (a) = ηB ◦ h (11) in Kap. 5 = (11) in Kap. 5 m = λa.M (h)(f (a)) = m = M (h) ◦ f. 166 Beweis von (5). M (h)(m) = g = (m = ηB ◦ h) = g (9) in Kap. 5 = (10) in Kap. 5 = m = λa.ηB (h(a)) = g m = λa.g(h(a)) = m = g ◦ h. Beweis von (6). (4) M (h)(f1) = M (h)(m1 f2) = m1 = M (h) ◦ f2 = m1 = λa1.M (h)(f2(a1)) (4) = m1 = λa1.M (h)(m2 = f3(a1)) = m1 = λa1.m2 = M (h) ◦ f3(a1) = m1 = λa1.m2 = λa2.M (h)(f3(a1)(a2)) = · · · = m1 = λa1.m2 = · · · = λan.M (h)(fn+1(a1) . . . (an)) = m1 = λa1.m2 = · · · = λan.M (h)(ηA(fa∗(t))) = m1 = λa1.m2 = · · · = λan.ηB (h(fa∗(t))) = m1 = λa1.m2 = · · · = λan.ηB ((h ◦ fa)∗(t)) h◦fa =gh(a) = ∗ m1 = λa1.m2 = · · · = λan.ηB (gh(a) (t)) (7) 167 (5) g1 = M (h)(m1) = g2 = m1 = g2 ◦ h = m1 = λa1.g2(h(a1)) = m1 = λa1.M (h)(m2) = g3(h(a1)) = m1 = λa1.m2 = g3(h(a1)) ◦ h = m1 = λa1.m2 = λa2.g3(h(a1))(h(a2)) = · · · = m1 = λa1.m2 = · · · = λan.gn+1(h(a1)) . . . , (h(an)) ∗ = m1 = λa1.m2 = · · · = λan.ηB (gh(a) (t)) (8) o Aus (7) und (8) folgt (6). Satz 6.6 Sei h : A → B ein Σ(G)-Homomorphismus. Der oben definierte LL-Compiler erfüllt Gleichung (14) aus Kapitel 5, also A compileB G = M (h) ◦ compileG . (9) Beweis. Sei transB = M (h × X ∗) ◦ transA. (10) Dann gilt (9): Für alle w ∈ X ∗, (1) B compileB G (w) = trans (w) = λ(b, w).if w = then ηB (b) else errmsg(w) 168 (10) = M (h × X ∗)(transA(w)) = λ(b, w).if w = then ηB (b) else errmsg(w) (11) in Kap. 5 = (transA(w) = (ηB×X ∗ ◦ (h × X ∗))) = λ(b, w).if w = then ηB (b) else errmsg(w) = (transA(w) = λ(a, w).ηB×X ∗ (h(a), w)) = λ(b, w).if w = then ηB (b) else errmsg(w) (10) in Kap. 5 = transA(w) = λ(a, w).ηB×X ∗ (h(a), w) = λ(b, w).if w = then ηB (b) else errmsg(w) (9) in Kap. 5 = transA(w) = λ(a, w).if w = then ηB (h(a)) else errmsg(w) (CM 1) = transA(w) = λ(a, w).if w = then ηB (h(a)) else M (h)(errmsg(w)) (9),(11) in Kap. 5 = transA(w) = λ(a, w).if w = then ηA(a) = ηB ◦ h else errmsg(w) = ηB ◦ h = transA(w) = λ(a, w).(if w = then ηA(a) else errmsg(w)) = ηB ◦ h (10) in Kap. 5 = (transA(w) = λ(a, w).if w = then ηA(a) else errmsg(w)) = ηB ◦ h = compileA G (w) = ηB ◦ h (11) in Kap. 5 = M (h)(compileA G (w)). 169 Beweis von (10) durch Noethersche Induktion bzgl. der im Beweis von Satz 6.4 definierten Ordnung : Fall 1: s ∈ BS. Für alle x ∈ X und w ∈ X ∗, ∗ M (h × X ∗)(transA s (xw)) = if x ∈ s then M (h × X )(ηA×X ∗ (x, w)) else M (h × X ∗)(errmsg(xw)) η ist nat. Transformation, (CM 1 ) = (h×X ∗ )(x,w)=(h(x),w)=(x,w) = if x ∈ s then ηB×X ∗ ((h × X ∗)(x, w)) else errmsg(xw) if x ∈ s then ηB×X ∗ (x, w) else errmsg(xw) = transB s (xw), (CM 1) ∗ B M (h × X ∗)(transA s ()) = M (h × X )(errmsg()) = errmsg() = transs (). Fall 2: s ∈ S. Sei r = (s → w0) ∈ R. Für alle w ∈ X ∗, M (h × X ∗)(tryrA(w)) A transe1 (w) = λ(a1, w1). (3) . ) = M (h × X ∗)( .. transA (w ) = λ(a , w ).η A ∗ (f (a , . . . , a ), w ) n−1 n n A×X j j n en r 1 k A transe1 (w) = λx1. . ∗ = M (h × X )( .. ) transA (w ) = λx .η A ∗ (f (π (x ), . . . , π (x )), π (x )) en n−1 n A×X r 1 j1 1 jk 2 n 170 M (h × X ∗)(transA e1 (w)) = λx1 . . .. 6.5 (6) = ) ∗ A M (h × X )(trans (w )) n−1 en B ∗ = λxn.ηB×X (fr (π1(xj1 ), . . . , π1(xjk )), π2(xn)) ∗ A M (h × X )(transe1 (w)) = λ(b1, w1). . = .. M (h × X ∗)(transA (w )) = λ(b , w ).η B ∗ (f (b , . . . , b ), w ) n−1 n n B×X j1 jk n r en B transe1 (w) = λ(b1, w1). ind. hyp. . .. = transB (w ) = λ(b , w ).η B ∗ (f (b , . . . , b ), w ) en n−1 n n B×X r j1 jk (3) = tryrB (w). n (11) Daraus folgt (10): (2) ∗ A M (h × X ∗)(transA s (w)) = M (h × X )(⊕r=(s→e)∈R tryr (w)) (CM 2) (11) (2) = ⊕r=(s→e)∈R M (h × X ∗)(tryrA(w)) = ⊕r=(s→e)∈R tryrB (w) = transB s (w). o 171 Lemma 6.7 Sei M : SetS → SetS eine Compilermonade, f : AN → A und m1, . . . , mn ∈ M (A). setA(m1 = λa1. . . . mn = λan.ηA(f (a1, . . . , an))) V = {f (a1, . . . , an) | ni=1 ai ∈ setA(mi)}. Beweis. setA(m1 = λa1. . . . mn = λan.ηA(f (a1, . . . , an))) S = {setA(m2 = λa2. . . . mn = λan.ηA(f (a1, . . . , an))) | a1 ∈ setA(m1)} S S = { {setA(m3 = λa3. . . . mn = λan.ηA(f (a1, . . . , an))) | a2 ∈ setA(m2)} | a1 ∈ setA(m1)} = S {setA(m3 = λa3. . . . mn = λan.ηA(f (a1, . . . , an))) | a2 ∈ setA(m2), a1 ∈ setA(m1)} = ... S V = {setA(ηA(f (a1, . . . , an))) | ni=1 ai ∈ setA(mi)} V S = {{f (a1, . . . , an)} | ni=1 ai ∈ setA(mi)} V = {f (a1, . . . , an) | ni=1 ai ∈ setA(mi)}. o 172 Satz 6.8 Der oben definierte LL-Compiler ist korrekt, d.h. für alle w ∈ X ∗ gilt: Word (G) setWord (G)(compileG (w)) ⊆ {w} (12) (siehe (16) in Kapitel 5). Beweis. Sei W = Word (G). Für alle w ∈ X ∗ und s ∈ S ∪ BS gelte folgende Bedingung: 0 ∀ (v, v 0) ∈ setW 2 (transW s (w)) : vv = w. (13) Dann gilt auch (14): setW (compileW G,s (w)) (1) 0 0 0 = setW (transW s (w) = λ(v, v ).if v = then ηW (v) else errmsg(v )) S = {setW (if v 0 = then ηW (v) else errmsg(v 0)) | (v, v 0) ∈ setW 2 (transW s (w))} (13) S ⊆ {setW (if v 0 = then ηW (v) else errmsg(v 0)) | vv 0 = w} S = {if v 0 = then setW (ηW (v)) else setW (errmsg(v 0)) | vv 0 = w} S = {if v 0 = then {v} else ∅ | vv 0 = w} = {w}. Beweis von (15) durch Noethersche Induktion bzgl. der im Beweis von Satz 6.4 definierten Ordnung . 173 Fall 1: s ∈ BS. Sei x ∈ X, w ∈ X ∗ und (v, v 0) ∈ setW 2 (transW s (xw)). Dann gilt (v, v 0) ∈ setW 2 (if x ∈ s then ηW 2 (x, w) else errmsg(xw)) = if x ∈ s then setW 2 (ηW 2 (x, w)) else setW 2 (errmsg(xw)) = if x ∈ s then {(x, w)} else ∅, also vv 0 = xw. Fall 2: s ∈ S. Sei w ∈ X ∗ und (v, v 0) ∈ setW 2 (transW s (w)). Dann gilt L (2) W (v, v 0) ∈ setW 2 (transW (w))) = set ( 2 W s r=(s→e)∈R tryr (w)) (CM 3) S W ⊆ r=(s→e)∈R setW 2 (tryr (w)). Also gibt es r = (s → e1 . . . en) ∈ R mit (v, v 0) ∈ setW 2 (tryrW (w)). Sei w0 = w. Dann gilt (v, v 0) ∈ setW 2 (tryrW (w0)) (3) = setW 2 (transW e1 (w0 ) = λ(v1 , w1 ). ... W transW en (wn−1 ) = λ(vn , wn ).ηW 2 (fr (vj1 , . . . , vjk ), wn )) V Lemma 6 .7 = {(frW (vj1 , . . . , vjk ), wn) | ni=1(vi, wi) ∈ setW 2 (transW ei (wi−1 ))} V = {(v1 . . . vn, wn) | ni=1(vi, wi) ∈ setW 2 (transW (14) ei (wi−1 ))}. 174 V Also gibt es v1, . . . , vn, w1, . . . , wn ∈ X ∗ mit ni=1(vi, wi) ∈ setW 2 (transW ei (wi−1 )), V n v = v1 . . . vn und v 0 = wn. Nach Induktionsvoraussetzung folgt i=1 viwi = wi−1, also vv 0 = v1 . . . vnwn = w0 = w. o Satz 6.9 Der oben definierte LL-Compiler ist vollständig, d.h. für alle w ∈ L(G) gilt: Word (G) setW (compileG (w)) 6= ∅. (15) Beweis. Sei W = Word (G). Für alle s ∈ S ∪ BS, v ∈ L(G)s und w ∈ X ∗ gelte: (v, w) ∈ setW 2 (transW s (vw)). (16) Dann gilt auch (15): Sei w ∈ L(G). Word (G) setW (compileG,s (w)) (1) 0 0 0 = setW (transW s (w) = λ(v, v ).if v = then ηW (v) else errmsg(v )) S = {setW (if v 0 = then ηW (v) else errmsg(v 0)) | (v, v 0) ∈ setW 2 (transW s (w))} S = {setW (ηW (v)) | (v, ) ∈ setW 2 (transW s (w))} ∪ S 0 {setW (errmsg(v 0)) | (v, v 0) ∈ setW 2 (transW s (w)), v 6= } S S 0 = {{v} | (v, ) ∈ setW 2 (transW (w))} ∪ {∅ | (v, v 0) ∈ setW 2 (transW s s (w)), v 6= } = {v | (v, ) ∈ setW 2 (transW s (w))} (16) 6= ∅. 175 Beweis von (16) durch Noethersche Induktion bzgl. der im Beweis von Satz 6.4 definierten Ordnung . Fall 1: s ∈ BS. Dann ist v ∈ X und damit setW 2 (transW s (vw)) = setW 2 (ηW 2 (v, w)) = {(v, w)}. Fall 2: s ∈ S. Wegen (LLC ) gibt es r = (s → (e1, . . . , en)) ∈ R und für alle 1 ≤ i ≤ n vi ∈ L(G)ei mit v1 . . . vn = v und setW 2 (tryrW (w)) ⊆ setW 2 (transW s (w)). (17) Nach Induktionsvoraussetzung folgt n ^ (vi, vi+1 . . . vnw) ∈ setW 2 (transW ei (vi . . . vn w)). (18) i=1 Mit wi =def vi+1 . . . vnw für alle 0 ≤ i ≤ n ist (18) äquivalent zu: n ^ (vi, wi) ∈ setW 2 (transW ei (wi−1 )). (19) i=1 Aus (14), (17) und (19) folgt schließlich (v, w) = (v1 . . . vn, wn) ∈ setW 2 (tryrW (w0)) = setW 2 (tryrW (w)) ⊆ setW 2 (transW s (w)). o 176 7 LR-Compiler LR-Compiler lesen ein Wort w wie LL-Compiler von links nach rechts, konstruieren dabei jedoch eine Rechtsreduktion von w, d.h. die Umkehrung einer Rechtsableitung. Da dies dem schrittweisen, mit den Blättern beginnenden Aufbau eines Syntaxbaums entspricht, werden LR-Compiler auch bottom-up-Compiler genannt. Im Gegensatz zum LL-Compiler ist die entsprechende Übersetzungsfunktion eines LRCompilers iterativ. Die Umkehrung der Ableitungsrichtung (Reduktion statt Ableitung) macht es möglich, die Compilation durch einen Automaten, also eine iterativ definierte Funktion, zu steuern. Während LL-Compiler keine linksrekursiven CFGs verarbeiten können, sind LR-Compiler auf LR(k)-Grammatiken beschränkt (s.u.). Sei G = (S, BS, R) eine CFG und X = S BS. Wir setzen jetzt voraus, dass S das Symbol start enthält und dieses nicht auf der rechten Seite einer Regel von R auftritt. 177 Ablauf der LR-Übersetzung auf Ableitungsbäumen start φ ε vw Ableitungsbäume Eingabe v w Ableitungsbäume Eingabe vw ε Ableitungsbaum Eingabe G heißt LR(k)-Grammatik, falls das Vorauslesen von k noch nicht verarbeiteten Eingabesymbolen genügt, um zu entscheiden, ob ein weiteres Zeichen gelesen oder eine Reduktion durchgeführt werden muss, und, wenn ja, welche. Außerdem müssen die Basismengen von BS paarweise disjunkt sein. Beispiel 7.1 Die CFG G = ({S, A}, {∗, b}, {S → A, A → A ∗ A, A → b}) ist für kein k eine LR(k)-Grammatik. 178 Nach der Reduktion des Präfixes b ∗ b des Eingabewortes b ∗ b ∗ b zu A ∗ A liegt ein shiftreduce-Konflikt vor. Soll man A ∗ A mit der Regel A → A ∗ A zu A reduzieren oder erst die Resteingabe ∗b lesen und dann b mit A → b zu A reduzieren? Die Resteingabe determiniert liefert keine Antwort. o first- und follow-Wortmengen Sei CS = BS ∪ Z, k > 0, α ∈ (S ∪ BS)∗ und s ∈ S. ∗ ∗ first k (α) = {β ∈ CS k | ∃ γ ∈ CS ∗ : α →G βγ} ∪ {β ∈ CS <k | α →G β} ∗ follow k (s) = {β ∈ CS k | ∃ α, γ ∈ CS ∗ : start →G αsβγ} ∪ ∗ {β ∈ CS <k | ∃ α ∈ CS ∗ : start →G αsβ} first(α) = first 1(α) follow (s) = follow 1(s) Simultane induktive Definition der first-Mengen first() = {} C ∈ CS ∧ α ∈ (S ∪ BS)∗ ⇒ first(Cα) = {C} (s → α) ∈ R ∧ β ∈ (S ∪ BS)∗ ∧ C ∈ first(αβ) ⇒ C ∈ first(sβ) (1) (2) (3) 179 Sei G eine CFG. Wir definieren den LR(1)-Compiler für G in mehreren Schritten. Zunächst definieren wir einen Erkenner für die Sprache L(G)start. Während Erkenner für reguläre Sprachen reguläre Ausdrücke auf Funktionen des Typs X ∗ → 2 abbilden (siehe Kapitel 2 und 3), bildet der folgende Erkenner (recognizer) Wörter über den Sorten und Basismengen von G auf jene Funktionen ab: recog1 : (S ∪ BS)∗ → 2X ∗ formuliert. Sei γ, α, ϕ ∈ (S ∪ BS)∗, x ∈ X und w ∈ X ∗. recog1(γα)(xw) = recog1(γαx)(w) falls ∃ s → αβ ∈ R, v ∈ (S ∪ BS)∗ : ∗ start →G γsv, β 6= , x ∈ first(β) ∩ Z shift (read) recog1(γα)(xw) = recog1(γαB)(w) falls ∃ s → αβ ∈ R, B ∈ BS, v ∈ (S ∪ BS)∗ : ∗ start →G γsv, β 6= , x ∈ B ∈ first(β) shift (read) 180 recog1(γα)(w) = recog1(γs)(w) falls ∃ s → α ∈ R, B ∈ BS, v ∈ (S ∪ BS)∗ : ∗ start →G γsv, s 6= start, w = ∈ follow (s) ∨ head(w) ∈ follow (s) ∩ Z ∨ head(w) ∈ B ∈ follow (s) reduce recog1(ϕ)() = 1 falls start → ϕ ∈ R accept recog1(ϕ)(w) = 0 sonst reject recog1 ist genau dann wohldefiniert, wenn G eine LR(1)-Grammatik ist. recog1 ist korrekt bzgl. L(G)start, d.h. für alle w ∈ X ∗ gilt: recog1() = χ(L(G)start). (1) Die Funktion recog1 ist iterativ definiert. Um (1) benötigt man daher eine Invariante. Sie lautet wie folgt: Für alle w ∈ X ∗, ∗ recog1(ϕ)(w) = 1 ⇐⇒ start →G ϕw. (2) (2) zeigt man durch Induktion über |w|. Aus (2) folgt sofort (1). 181 Im zweiten Schritt wird das erste Argument von recog1 durch den Inhalt eines Zustandskellers ersetzt. Die zugrundeliegende endliche (!) Zustandsmenge Q ist die Bildmenge der Funktion state : (S ∪ BS)∗ → P(Quad) ∗ ϕ 7→ {(s, α, β, u) ∈ Quad | ∃ γ, v : start →G γsv, ϕ = γα, u = v = ∨ u = head(v) ∈ BS ∪ Z} wobei Quad die Menge aller Quadrupel (s, α, β, u) ∈ S × (S ∪ BS)∗ × (S ∪ BS)∗ × (BS ∪ Z ∪ 1) mit s → αβ ∈ R ist. Die Bedingungen in der Definition von recog1 werden durch Abfragen der Form (s, α, β, x) ∈ state(ϕ) ersetzt: recog1(γα)(xw) = recog1(γαx)(w) falls ∃ (s, α, β, ) ∈ state(γα) : β 6= , x ∈ first(β) ∩ Z recog1(γα)(xw) = recog1(γαB)(w) falls ∃ (s, α, β, ) ∈ state(γα), B ∈ BS : β 6= , x ∈ B ∈ first(β) recog1(γα)(w) = recog1(γs)(w) falls ∃ (s, α, , u) ∈ state(γα) : s 6= start, w = = u ∨ head(w) = u ∈ Z ∨ head(w) ∈ u ∈ BS 182 recog1(ϕ)() = 1 falls (start, ϕ, , ) ∈ state(ϕ) recog1(ϕ)(w) = 0 sonst Der LR-Automat für G hat die Eingabemenge S ∪ BS, die Zustandsmenge QG = {state(ϕ) | ϕ ∈ (S ∪ BS)∗} und die (goto-Tabelle genannte) partielle (!) Transitionsfunktion δG : QG (→ QS∪BS G state(ϕ) 7→ λs.state(ϕs) Simultane induktive Definition von QG und δG start → α ∈ R ⇒ (start, , α, ) ∈ q0 (s, α, s0β, u) ∈ q ∧ s0 → γ ∈ R ∧ v ∈ first(βu) ⇒ (s0, , γ, v) ∈ q (s, α, s0β, u) ∈ q, s0 ∈ S ∪ BS ⇒ (s, αs0, β, u) ∈ δG(q)(s0) (3) (4) (5) In der Definition von recog1 wird jedes Wort von (S ∪ BS)∗ durch eine gleichlange Liste von Zuständen des LR-Automaten ersetzt: 183 h : (S ∪ BS)∗ → Q∗G 7→ q0 =def state() (s1, . . . , sn) 7→ (state(s1 . . . sn), state(s1 . . . sn−1), . . . , state(s1), state()) Im Folgenden benutzen wir gelegentlich die Haskell-Funktionen (:) und (++), um auf Wörtern zu operieren (siehe Kapitel 9). ∗ Aus recog1 wird recog2 : Q∗G → 2X : Für alle q, qi ∈ QG, qs ∈ Q∗G, x ∈ X und w ∈ X ∗, recog2(q : qs)(xw) = recog2(δG(q)(x) : q : qs, w) falls ∃ (s, α, β, ) ∈ q : β 6= , x ∈ first(β) ∩ Z recog2(q : qs)(xw) = recog2(δG(q)(B) : q : qs, w) falls ∃ (s, α, β, ) ∈ q, B ∈ BS : β 6= , x ∈ B ∈ first(β) 184 recog2(q1 : · · · : q|α| : q : qs)(w) = recog2(δG(q)(s) : q : qs)(w) falls ∃ (s, α, , u) ∈ q1 : s 6= start, w = = u ∨ head(w) = u ∈ Z ∨ head(w) ∈ u ∈ BS recog2(q : qs)() = 1 falls ∃ ϕ : (start, ϕ, , ) ∈ q recog2(qs)(w) = 0 sonst Offenbar gilt recog1 = recog2 ◦ h, also insbesondere recog2(q0) = χ(L(G)start) (6) wegen (1). Um die Suche nach bestimmten Elementen eines Zustands in den Iterationsschritten von recog2 zu vermeiden, verwenden wir die – Aktionstabelle für G genannte – Funktion actG : QG × (BS ∪ Z ∪ 1) → R ∪ {shift, error }, die wie folgt definiert ist: 185 Für alle u ∈ BS ∪ Z ∪ 1, shift falls ∃ (s, α, β, ) ∈ q : β 6= , u ∈ first(β), actG(q, u) = s → α falls ∃ (s, α, , u) ∈ q, error sonst. Unter Verwendung von δG und actG erhält man eine kompakte Definition von recog2: Für alle q, qi ∈ QG, qs ∈ Q∗G, x ∈ X und w ∈ X ∗, recog2(q : qs)(xw) = recog2(δG(q)(x) : q : qs)(w) falls x ∈ Z, actG(q, x) = shift recog2(q : qs)(xw) = recog2(δG(q)(B) : q : qs)(w) falls ∃ B ∈ BS : x ∈ B, actG(q, B) = shift recog2(q1 : · · · : q|α| : q : qs)(w) = recog2(δG(q)(s) : q : qs)(w) falls ∃ u ∈ BS ∪ Z ∪ 1 : actG(q1, u) = s → α, w = = u ∨ head(w) = u ∈ Z ∨ head(w) ∈ u ∈ BS 186 recog2(q : qs)() = 1 falls actG(q, ) = start → α recog2(qs)(w) = 0 sonst Beispiel 7.2 SAB2 G = ({S, A, B}, {c, d, ∗}, R) R = {S → A, A → A ∗ B, A → B, B → c, B → d} LR-Automat für G q0 = {(S, , A, ), (A, , A ∗ B, ), (A, , B, ), (A, , A ∗ B, ∗), (A, , B, ∗), (B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)} q1 = δG(q0)(A) = {(S, A, , ), (A, A, ∗B, ), (A, A, ∗B, ∗)} q2 = δG(q0)(B) = {(A, B, , ), (A, B, , ∗)} q3 = δG(q0)(c) = {(B, c, , ), (B, c, , ∗)} = δG(q5)(c) q4 = δG(q0)(d) = {(B, d, , ), (B, d, , ∗)} = δG(q5)(d) q5 = δG(q1)(∗) = {(A, A∗, B, ), (A, A∗, B, ∗), (B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)} q6 = δG(q5)(B) = {(A, A ∗ B, , ), (A, A ∗ B, , ∗)} 187 goto-Tabelle für G A B c d ∗ q0 q1 q2 q3 q4 q1 q5 q5 q6 q3 q4 Aktionstabelle für G q0 q1 q2 q3 q4 q5 q6 c shift error error error error shift error d shift error error error error shift error ∗ error shift A → B B → c B → d error A → A ∗ B error S → A A → B B → c B → d error A → A ∗ B 188 Ein Erkennerlauf recog2(q0)(c ∗ d) = recog2(q3q0)(∗d) wegen actG(q0, c) = shift und δG(q0)(c) = q3 = recog2(q2q0)(∗d) wegen actG(q3, ∗) = B → c und δG(q0)(B) = q2 = recog2(q1q0)(∗d) wegen actG(q2, ∗) = A → B und δG(q0)(A) = q1 = recog2(q5q1q0)(d) wegen actG(q1, ∗) = shift und δG(q1)(∗) = q5 = recog2(q4q5q1q0)() wegen actG(q5, d) = shift und δG(q5)(d) = q4 = recog2(q6q5q1q0)() wegen actG(q4, ) = B → d und δG(q5)(B) = q6 = recog2(q1q0)() wegen actG(q6, ) = A → A ∗ B und δG(q0)(A) = q1 = 1 wegen actG(q1, ) = S → A o Für den dritten und letzten Schritt zum LR(1)-Compiler benötigen wir die abstrakte Syntax Σ(G) von G (siehe Kapitel 4). Sei A eine Σ(G)-Algebra. Aus recog2 wird der generische Compiler ∗ ∗ ∗ X compileA G : QG × A → M (A) . 189 Er startet mit q0 = state() im ansonsten leeren Zustandskeller. Die Bedeutung der Liste von A-Elementen im Definitionsbereich von compileA G entnimmt man am besten der folgenden Formalisierung der Korrektheit von compileG: • Für alle w ∈ X ∗ und t ∈ TΣ(G),start, A Word (G) (t) = w, compileA G (q0 , )(w) = η(fold (t)) ⇒ fold compileA G (q0 , )(w) = error(w) ⇒ w 6∈ L(G)start . (1) (2) Die Invariante von compileG, aus der (1) folgt, ist komplizierter die des Erkenners recog1 (s.o.): • Für alle α = w0s1w1 . . . snwn mit wi ∈ Z ∗ und si ∈ S ∪ BS, qs ∈ Q∗G, ai ∈ A, w ∈ X ∗, und t ∈ TΣ(G)({x1, . . . , xn}), ∗ ∗ compileA G (state(α) : qs, [a1 , . . . , an ])(w) = η(gA (t)) ⇒ gW (t) = αw, (3) wobei gA : {x1, . . . , xn} → A und gW : {x1, . . . , xn} → Word (G) xi auf ai bzw. si abbilden. Aus (3) ergibt sich die folgende iterative Definition von compileA G: 190 Für alle n ∈ N, q, qi ∈ QG, qs ∈ Q∗G, ai ∈ A, as ∈ A∗, x ∈ X und w ∈ X ∗, A compileA G (q : qs, as)(xw) = compileG (δG (q)(x) : q : qs, as)(w) falls x ∈ Z, actG(q, x) = shift A compileA G (q : qs, as)(xw) = compileG (δG (q)(B) : q : qs, as ++[x])(w) falls ∃ B ∈ BS : x ∈ B, actG(q, B) = shift compileA G (q1 : · · · : qn : q : qs, as ++[ai1 , . . . , aik ])(w) = A compileA G (δG (q)(s) : q : qs, as ++[fr (ai1 , . . . , aik )])(w) falls ∃ u ∈ BS ∪ Z ∪ 1 : actG(q1, u) = r = (s → e1 . . . en), {i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS}, w = = u ∨ head(w) = u ∈ Z ∨ head(w) ∈ u ∈ BS A compileA G (q : qs, [ai1 , . . . , aik ])() = η(fr (ai1 , . . . , aik )) falls actG(q, ) = r = (start → e1 . . . en), {i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS} compileA G (qs, as)(w) = errmsg(w) sonst 191 Im Fall A = TΣ(G) bestehen die Listen as, [ai1 , . . . , aik ] und [frA(ai1 , . . . , aik )] aus Syntaxbäumen. Die zweite Gleichung zeigt ihren schrittweisen Aufbau: ai ai 1 k as fr ai 1 ai k as 192 Beispiel 7.3 SAB2 Hier ist der um die schrittweise Konstruktion des zugehörigen Syntaxbaums erweiterte Erkennerlauf von Beispiel 7.2: T (G) ([0], [])(c ∗ d) T (G) ([3, 0], [])(∗d) T (G) ([2, 0], [fB→c])(∗d) T (G) ([1, 0], [fA→B (fB→c)])(∗d) T (G) ([5, 1, 0], [fA→B (fB→c)])(d) T (G) ([4, 5, 1, 0], [fA→B (fB→c)])() T (G) ([6, 5, 1, 0], [fA→B (fB→c), fB→d])() T (G) ([1, 0], [fA→A∗B (fA→B (fB→c), fB→d)])() compileGΣ = compileGΣ = compileGΣ = compileGΣ = compileGΣ = compileGΣ = compileGΣ = compileGΣ = η(fS→A(fA→A∗B (fA→B (fB→c), fB→d))) In der graphischen Darstellung dieses Parserlaufs sind die Knoten der Syntaxbäume nicht mit Konstruktoren, sondern mit den Regeln markiert, aus denen sie hervorgehen, wobei der Pfeil zwischen der linken und rechten Seite einer Regel durch den Unterstrich ersetzt wurde. o 193 Beispiel 7.4 Auf den folgenden Seiten steht die vom C-Compiler-Generator yacc (“yet another compiler-compiler”) aus der folgenden LR(1)-Grammatik G erzeugte Zustandsmenge – deren Elemente hier nur aus den ersten drei Komponenten der o.g. Quadrupel bestehen – zusammen mit der auf die einzelnen Zustände verteilten Einträge der goto- und Aktionstabelle: $accept prog statems assign exp -> -> -> -> -> prog $end statems exp . statems assign ; | ID : = exp exp + exp | exp * exp | (exp) | NUMBER | ID So besteht z.B. der Zustand 0 aus den beiden Tripeln ($accept, , $end) und (statems, , ). Die beim Lesen eines Punktes im Zustand 0 ausgeführte Aktion ist eine Reduktion mit Regel 3, also mit statems → . Der Folgezustand von 0 ist bei Eingabe von prog bzw. statems der Zustand 1 bzw. 2. Link zur graphischen Darstellung des Parserlaufs auf dem Eingabewort ID : = NUM ; ID : = ID + NUM ; NUM * ID + ID . Den Sonderzeichen entsprechen in der input-Spalte des Parserlaufs und den u.a. Tabellen passende Wortsymbole. Die Knoten der Syntaxbäume sind wie in Beispiel 7.3 mit Regeln markiert. 194 195 196 Im Folgenden sind die goto- bzw. Aktionstabelle noch einmal getrennt wiedergegeben. op (open) und cl (close) bezeichnen eine öffnende bzw. schließende Klammer. end steht für das leere Wort . Wieder wird auf den Pfeil zwischen linker und rechter Seite einer Regel verzichtet. 197 198 199 Im Folgenden werden die Regeln von G um C-Code erweitert, der eine Σ(G)-Algebra implementiert, die dem Zustandsmodell von JavaLight (siehe 11.5) ähnelt. 200 o 201 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) Hierarchie der CFG-Klassen Zur Definition von LL(k)-Grammatiken verweisen wir auf die einschlägige Literatur. Kurz gesagt, sind das diejenigen nicht-linksrekursiven CFGs, deren LL-Parser ohne Backtracking auskommen. 202 eindeutige kontextfreie det. kf. Sprachen =LR(1)-Spr. =SLR(1)-Spr. =LALR(1)-Spr. LL(k)Spr. Sprachen Hierarchie der entsprechenden Sprachklassen Für jeden Grammatiktyp T bedeutet die Formulierung L ist eine T -Sprache lediglich, dass eine T -Grammatik existiert, die L erzeugt. 203 Während der LL-Compiler von Kapitel 6 – nach Beseitigung von Linksrekursion – jede kontextfreie Grammatik verarbeitet, selbst dann, wenn sie mehrdeutig ist, zeigt die obige Grafik, dass die Forderung, dabei ohne Backtracking auszukommen, die Klasse der kompilierbaren Sprachen erheblich einschränkt: Unter dieser Bedingung ist die bottom-up-Übersetzung offenbar mächtiger als die top-down-Compilation. Umgekehrt wäre es den Versuch wert (z.B. in Form einer Bachelorarbeit), in Anlehnung an den obigen Compiler für LR(1)-Grammatiken einen bottom-up-Compiler mit Backtracking zu entwickeln. Da die Determinismusforderung wegfiele, bräuchten wir keinen Lookahead beim Verarbeiten der Eingabe, womit die Zustände generell nur aus Tripeln bestünden – wie im Beispiel 7.4. 204 8 Haskell: Typen und Funktionen Die Menge 1 wird in Haskell unit-Typ genannt und mit () bezeichnet. () steht auch für das einzige Element von 1. Die Menge 2 entspricht dem Haskell-Typ Bool . Ihre beiden Elemente 0 und 1 werden mit False bzw. True bezeichnet. Alle Typen von Haskell sind aus Standardtypen wie Bool , Int oder F loat, Typvariablen sowie Typkonstruktoren wie × (Produkt), + (Summe oder disjunkte Vereinigung) oder → (Funktionsmenge) aufgebaut. Jeder Typ bezeichnet eine Menge, jeder Typkonstruktor eine Funktion auf Mengen von Mengen. Typnamen beginnen stets mit einem Großbuchstaben, Typvariablen mit einem Kleinbuchstaben. Typvariablen stehen für Mengen, Individuenvariablen für Elemente einer Menge. Beide müssen mit einem Kleinbuchstaben beginnen. Das (kartesische) Produkt A1 × · · · × An von Mengen A1, . . . , An wird in Haskell wie seine Elemente, die n-Tupel (a1, . . . , an), mit runden Klammern und Kommas notiert: (A1, . . . , An). 205 Die Summe oder disjunkte Vereinigung SumAn von Mengen A1, . . . , An wird durch einen (Haskell-)Datentyp implementiert (siehe Kapitel 10). Funktionen werden benannt und durch rekursive Gleichungen definiert (s.u.) oder als λAbstraktion λp.e (Haskell-Notation: \p -> e) dargestellt, wobei p ein Muster für die möglichen Argumente (Parameter) von λp.e ist, z.B. p = (x, (y, z)). Muster bestehen aus Individuenvariablen und (Individuen-)Konstruktoren. Jede Variable kommt in einem Muster höchstens einmal vor. (Die λ-Abstraktionen von Kapitel 2 haben nur Variablen als Muster.) Der “Rumpf” e der λ-Abstraktion λp.e ist ein aus beliebigen Funktionen und Variablen zusammengesetzter Ausdruck. Ein Ausdruck der Form f (e) heißt Funktionsapplikation (auch: Funktionsanwendung oder -aufruf). Ist f eine λ-Abstraktion, dann nennt man f (e) eine λ-Applikation. Die λ-Applikation (λp.e)(e0) is auswertbar, wenn der Ausdruck e0 das Muster p matcht. Beim Matching werden die Variablen von p in e durch Teilausdrücke von e0 ersetzt. Die Auswertung einer Applikation von e wie z.B. (λ(x, y).x ∗ y + 5 + x)(7, 8), besteht in der Bildung der Instanz 7 ∗ 8 + 5 + 7 der λ-Abstraktion λ(x, y).x ∗ y + 5 + x und deren anschließender Auswertung: (λ(x, y).x ∗ y + 5 + x)(7, 8) ; 7 ∗ 8 + 5 + 7 ; 56 + 5 + 7 ; 68 206 Die ghci-Befehle :type \(x,y)->x*y+5+x bzw. :type (\(x,y)->x*y+5+x)(7,8) liefern die Typen Num a => (a,a) -> a bzw. Num a => a der λ-Abstraktion λ(x, y).x ∗ y + 5 + x bzw. λ-Applikation (λ(x, y).x ∗ y + 5 + x)(7, 8). Backslash (\) und Pfeil (->) sind offenbar die Haskell-Notationen des Symbols λ bzw. des Punktes einer λ-Abstraktion. Num ist die Typklasse für numerische Typen. Allgemein werden Typklassen in Kapitel 5 behandelt. Link zur schrittweisen Auswertung der λ-Applikation (λ(x, y).x ∗ y + 5 + x)(7, 8) Der Redex, d.i. der Teilausdruck, der im jeweils nächsten Schritt ersetzt wird, ist rot gefärbt. Das Redukt, d.i. der Teilausdruck, durch den der Redex ersetzt wird, ist grün gefärbt. Link zur schrittweisen Auswertung der λ-Applikation (λx.x(true, 4, 77) + x(false, 4, 77))(λ(x, y, z).if x then y + 5 else z ∗ 6) 207 Eine geschachelte λ-Abstraktion λp1.λp2. . . . .λpn.e kann durch λp1 p2 . . . pn.e abgekürzt werden. Sie hat einen Typ der Form t1 → (t2 → . . . (tn → t) . . . ), wobei auf diese Klammerung verzichtet werden kann, weil Haskell Funktionstypen automatisch rechtsassoziativ klammert. Anstelle der Angabe eines λ-Ausdrucks kann eine Funktion benannt und dann mit f mit Hilfe von Gleichungen definiert werden: f = \p -> e ist äquivalent zu f p = e (applikative Definition) Funktionen, die andere Funktionen als Argumente oder Werte haben, heißen Funktionen höherer Ordnung. Der Typkonstruktor → ist rechtsassoziativ. Also ist die Deklaration (+) :: Int -> (Int -> Int) ist äquivalent zu (+) :: Int -> Int -> Int Die Applikation einer Funktion ist linksassoziativ. Also ist ((+) 5) 6 ist äquivalent zu (+) 5 6 (+) ist zwar als Präfixfunktion deklariert, kann aber auch infix verwendet werden. Dann entfallen die runden Klammern um den Infixoperator: 5 + 6 ist äquivalent zu (+) 5 6 208 Das Gleiche gilt für jede Funktion f eines Typs der Form A → B → C. Besteht f aus Sonderzeichen, dann wird f bei der Präfixverwendung in runde Klammern gesetzt, bei der Infixverwendung nicht. Beginnt f mit einem Kleinbuchstaben und enthält f keine Sonderzeichen, dann wird f bei der Infixverwendung in Akzentzeichen gesetzt, bei der Präfixverwendung nicht: mod :: Int -> Int -> Int mod 9 5 ist äquivalent zu 9 `mod` 5 Die Infixnotation wird auch verwendet, um die in f enthaltenen Sektionen (Teilfunktionen) des Typs A → C bzw. B → C zu benennen. Z.B. sind die folgenden Sektionen Funktionen des Typs Int -> Int, während (+) und mod den Typ Int -> Int -> Int haben. (+5) (9`mod`) ist äquivalent zu ist äquivalent zu (+) 5 mod 9 ist äquivalent zu ist äquivalent zu \x -> x+5 \x -> 9`mod`x Eine Sektion wird stets in runde Klammern eingeschlossen. Die Klammern gehören zum Namen der jeweiligen Funktion. 209 Der Applikationsoperator ($) :: (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 ; 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. (f1 . f2 . ... . fn) a ; f1 (f2 (...(fn a)...))) 210 U.a. benutzt man den Kompositionsoperator, um in einer applikativen Definition Argumentvariablen einzusparen. So sind die folgenden drei Definitionen einer Funktion f : a → b → c äquivalent zueinander: f a b = g (h a) b f a = g (h a) f = g . h Welchen Typ hat hier g bzw. h? Auch die folgenden drei Definitionen einer Funktion f : a → b sind äquivalent zueinander: f a = g . h a f a = (g.) (h a) f = (g.) . h Welchen Typ hat hier g bzw. h? 211 Monomorphe und polymorphe Typen Ein Typ ohne Typvariablen heißt monomorph. Ein Typ mit Typvariablen wie z.B. a -> Int -> b heißt polymorph. Eine Funktion mit monomorphem oder polymorphem Typ heißt monomorphe bzw. polymorphe Funktion. Eine Funktion heißt mono- bzw. polymorph, wenn ihr Typ mono- bzw. polymorph ist. Ein Typ u heißt Instanz eines Typs t, wenn u durch Ersetzung der Typvariablen von t aus t entsteht. Typvariablen stehen für Mengen, Individuenvariablen stehen für Elemente einer Menge. Sie müssen ebenfalls mit einem Kleinbuchstaben beginnen. Eine besondere Individuenvariable ist der Unterstrich (auch Wildcard genannt). Er darf nur auf der linken Seite einer Funktionsdefinition vorkommen, was zur Folge hat, dass er für einen Teil eines Argumentmusters der gerade definierten Funktion steht, der ihre Werte an Stellen, die das Muster matchen, nicht beeinflusst (siehe Beispiele im nächsten Kapitel). Die oben definierten Funktionen ($) und (.) sind demnach polymorph. 212 Weitere polymorphe Funktionen höherer Ordnung Identität id :: a -> a id a = a id = \a -> a äquivalente Definition const :: a -> b -> a const a b = a const a = \b -> a const = \a -> \b -> a const = \a b -> a konstante Funktion äquivalente Definitionen update :: Eq a => (a -> b) -> a -> b -> a -> b update f a b a' = if a == a' then b else f a' update f :: a -> b -> a -> b update f a :: b -> a -> b Funktionsupdate (nicht im Prelude) äquivalente Schreibweisen update(f) update(f)(a) (update f) a 213 update f a b :: a -> b update(f)(a)(b) ((update f) a) b update f a b a' :: b update(f)(a)(b)(a') (((update f) a) b) a' flip :: (a -> b -> c) -> b -> a -> c flip f b a = f a b Vertauschung der Argumente flip mod 11 ist äquivalent zu (`mod` 11) (s.o.) 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 214 9 Haskell: Listen Sei A eine Menge. Die Menge A∗ (siehe Kapitel 2) wird in Haskell mit eckigen Klammern notiert, also durch [A] — ebenso wie ihre Elemente: Für (a1, . . . , an) ∈ A∗ schreibt man [a1, . . . , an]. Eine n-elementige Liste kann extensional oder als funktionaler Ausdruck dargestellt werden: [a1, . . . , an] ist äquivalent zu a1 : (a2 : (. . . (an : []) . . . )) Die Konstante [] (leere Liste) vom Typ [A] und die Funktion (:) (Anfügen eines Elementes von A ans linke Ende einer Liste) vom Typ A → [A] → [A] heißen Konstruktoren, weil sie nicht wie andere Funktionen Werte berechnen, sondern dazu dienen, die Elemente eines Typs aufzubauen. Auch werden sie benötigt, um die Zugehörigkeit eines Elementes eines Summentyps zu einem bestimmten Summanden wiederzugeben (siehe Kapitel 4). Die Klammern in a1 : (a2 : (. . . (an : []) . . . )) können weggelassen werden, weil der Typ von (:) keine andere Klammerung zulässt. Die durch mehrere Gleichungen ausgedrückten Fallunterscheidungen bei den folgenden Definitionen von Funktionen auf Listen ergeben sich aus verschiedenen Mustern der Funktionsargumente bzw. Bedingungen an die Argumente (Boolesche Ausdrücke hinter |). 215 Seien x, y, s Individuenvariablen. s ist ein Muster für alle Listen, [] das Muster für die leere Liste, [x] ein Muster für alle einelementigen Listen, x : s ein Muster für alle nichtleeren Listen, x : y : s ein Muster für alle mindestens zweielementigen Listen, usw. length :: [a] -> Int length (_:s) = length s+1 length _ = 0 length [3,44,-5,222,29] ; 5 Link zur schrittweisen Auswertung von length [3,44,-5,222,29] head :: [a] -> a head (a:_) = a head [3,2,8,4] ; 3 tail :: [a] -> [a] tail (_:s) = s tail [3,2,8,4] ; [2,8,4] (++) :: [a] -> [a] -> [a] (a:s)++s' = a:(s++s') _++s = s [3,2,4]++[8,4,5] ; [3,2,4,8,4,5] 216 (!!) :: [a] -> Int -> a (a:_)!!0 = a (_:s)!!n | n > 0 = s!!(n-1) [3,2,4]!!1 ; 2 init :: [a] -> [a] init [_] = [] init (a:s) = a:init s init [3,2,8,4] ; [3,2,8] last :: [a] -> a last [a] = a last (_:s) = last s last [3,2,8,4] ; 4 take take take take :: Int -> [a] -> [a] take 3 [3,2,4,8,4,5] ; [3,2,4] 0 _ = [] n (a:s) | n > 0 = a:take (n-1) s _ [] = [] drop drop drop drop :: Int -> [a] -> [a] drop 4 [3,2,4,8,4,5] ; [4,5] 0 s = s n (_:s) | n > 0 = drop (n-1) s _ [] = [] 217 Funktionslifting auf Listen map :: (a -> b) -> [a] -> [b] map f (a:s) = f a:map f s map _ _ = [] map (+1) [3,2,8,4] ; [4,3,9,5] map ($ 7) [(+1),(+2),(*5)] ; [8,9,35] map ($ a) [f1,f2,...,fn] ; [f1 a,f2 a,...,fn a] Link zur schrittweisen Auswertung von map(+3)[2..9] zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] zipWith f (a:s) (b:s') = f a b:zipWith f s s' zipWith _ _ _ = [] zipWith (+) [3,2,8,4] [8,9,35] ; [11,11,43] zip :: [a] -> [b] -> [(a,b)] zip = zipWith (,) zip [3,2,8,4] [8,9,35] ; [(3,8),(2,9),(8,35)] 218 Strings sind Listen von Zeichen Strings werden als Listen von Zeichen betrachtet, d.h. die Typen String und [Char] sind identisch. Z.B. haben die folgenden Booleschen Ausdrücke den Wert True: "" == [] "H" == ['H'] "Hallo" == ['H','a','l','l','o'] Also sind alle Listenfunktionen auf Strings anwendbar. words :: String -> [String] und unwords :: [String] -> String zerlegen bzw. konkatenieren Strings, wobei Leerzeichen, Zeilenumbrüche ('\n') und Tabulatoren ('\t') 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. 219 Listenfaltung Faltung einer Liste von links her f f foldl f state [a ,a ,a ,a ,a ] 1 2 3 4 5 Endzustand f f f state Anfangszustand a1 a2 a3 a4 a5 Eingaben foldl :: (a -> b -> a) -> a -> [b] -> a foldl f a (b:s) = foldl f (f a b) s foldl _ a _ = a a ist Zustand, b ist Eingabe f ist Zustandsüberführung Link zur schrittweisen Auswertung von foldl(+)(0)[1..5] foldl1 :: (a -> a -> a) -> [a] -> a foldl1 f (a:s) = foldl f a s sum = foldl (+) 0 and = foldl (&&) True minimum = foldl1 min product or maximum = foldl (*) 1 = foldl (||) False = foldl1 max 220 concat = foldl (++) [] concatMap :: (a -> [b]) -> [a] -> [b] concatMap f = concat . map f Parallele Faltung zweier Listen von links her fold2 f state [a1,a2,a3,a4,a5] [b1,b2,b3,b4,b5] f f f f f state a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 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 221 listsToFun :: Eq a => b -> [a] -> [b] -> a -> b listsToFun = fold2 update . const Beginnend mit const b, erzeugt listsToFun b schrittweise aus einer Argumentliste as und einer Werteliste bs die entsprechende Funktion: ( bs!!i falls i = max{k | as!!k = a, k < length(bs)}, listsToFun b as bs a = b sonst. Faltung einer Liste von rechts her entspricht der Auswertung ihrer Konstruktordarstellung in einer Algebra A, die die Konstruktoren (:) :: b -> [b] -> [b] und [] :: [b] als Funktion (:)A bzw. Konstante []A interpretiert (siehe Kapitel 2). (:)A : foldr f a [b1,b2,b3,b4,b5] f f (:)A : f f (:)A : (:)A : f b1 b2 b3 b4 b5 a b1 b2 b3 b4 b5 (:)A : [] []A 222 foldr :: (b -> a -> a) -> a -> [b] -> a foldr f a (b:s) = f b $ foldr f a s foldr _ a _ = a Der Applikationsoperator $ als Parameter von Listenfunktionen foldr ($) a [f1,f2,f3,f4] ; f1 $ f2 $ f3 $ f4 a foldl (flip ($)) a [f4,f3,f2,f1] ; f1 $ f2 $ f3 $ f4 a map f [a1,a2,a3,a4] map ($a) [f1,f2,f3,f4] ; ; [f a1,f a2,f a3,f a4] [f1 a,f2 a,f3 a,f4 a] zipWith ($) [f1,f2,f3,f4] [a1,a2,a3,a4] ; [f1 a1,f2 a2,f3 a3,f4 a4] 223 Listenlogik any :: (a -> Bool) -> [a] -> Bool any f = or . map f any (>4) [3,2,8,4] ; True all :: (a -> Bool) -> [a] -> Bool all f = and . map f all (>2) [3,2,8,4] ; False elem :: Eq a => a -> [a] -> Bool elem a = any (a ==) elem 2 [3,2,8,4] ; True notElem :: Eq a => a -> [a] -> Bool notElem a = all (a /=) notElem 9 [3,2,8,4] ; True filter :: (a -> Bool) -> [a] -> [a] filter (<8) [3,2,8,4] ; [3,2,4] filter f (a:s) = if f a then a:filter f s else filter f s filter f _ = [] 224 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' = [f a b | (a,b) <- zip s s'] filter f s = [a | a <- s, f a] zip(s)(s’) ist nicht das kartesische Produkt von s und s’. Dieses entspricht der Komprehension [(a,b) | a <- s, b <- s’]. Allgemeines Schema von Listenkomprehensionen: [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. Jede endlichstellige Relation lässt sich als Listenkomprehension implementieren, z.B. die Menge aller Tripel (a, b, c) ∈ A1 × A2 × A3, die ein Prädikat p : A1 × A2 × A3 → Bool erfüllen, durch [(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)]. 225 10 Haskell: Datentypen und Typklassen Zunächst das allgemeine Schema einer Datentypdefinition: data DT x_1 ... x_m = C_1 typ_11 ... typ_1n_1 | ... | C_k typ_k1 ... typ_kn_k typ11, . . . , typknk sind beliebige Typen, die außer x1, . . . , xm keine Typvariablen enthalten. DT heißt rekursiv, wenn DT in mindestens einem dieser Typen vorkommt. Die durch DT implementierte Menge besteht aus allen Ausdrücken der Form C_i e_1 ... e_n_i, wobei 1 ≤ i ≤ n und für alle 1 ≤ j ≤ ni ej ein Element des Typs typij ist. Als Funktion hat Ci den Typ typ_i1 -> ... -> typ_in_i -> DT a_1 ... a_m. Alle mit einem Großbuchstaben beginnenden Funktionssymbole und alle mit einem Doppelpunkt beginnenden Folgen von Sonderzeichen werden vom Haskell-Compiler als Konstruktoren eines Datentyps aufgefasst und müssen deshalb irgendwo im Programm in einer Datentypdefinition vorkommen. 226 Die Unterschiede in der Schreibweise von Konstruktoren bei Infix- bzw. Präfixverwendung sind dieselben wie bei anderen Funktionen. Beispiel 10.1 Der Haskell-Standardtyp für Listen data [a] = [] | a : [a] Für jede Menge A besteht [A] aus • allen endlichen Ausdrücken a1 : . . . : an : [] mit a1, . . . , an ∈ A und • allen unendlichen Ausdrücken a1 : a2 : a3 : . . . mit {ai | i ∈ N} ⊆ A. [A] ist die größte Lösung der Gleichung M = {[]} ∪ {a : s | a ∈ A, s ∈ M } (1) in der Mengenvariablen M und damit [A] eine Haskell-Implementierung der Menge CTList(A) aller List(A)-Grundterme. Als Elemente von [Int] lauten die List(Z)-Grundterme blink und blink’ wie folgt: blink,blink' :: [Int] blink = 0:blink' blink' = 1:blink 227 Die endlichen Ausdrücke von [A] bilden die kleinste Lösung von (1) und damit eine HaskellImplementierung der Menge TList(A) der List(A)-Grundterme endlicher Tiefe (siehe 2.13). o Beispiel 10.2 (siehe 2.13) TReg(BS) ist isomorph zur kleinsten und CTReg(BS) zur größten Lösung der Gleichung M = {par(t, u) | t, u ∈ M } ∪ {seq(t, u) | t, u ∈ M } ∪ {iter(t) | t ∈ M } ∪ {base(B) | B ∈ BS} (2) in der Mengenvariablen M . Beide Mengen werden deshalb durch folgenden Datentyp implementiert: data RegT bs = Par (RegT bs) (RegT bs) | Seq (RegT bs) (RegT bs) | Iter (RegT bs) | Base bs o Summentypen Die Summe oder disjunkte Vereinigung A1 + · · · + An =def {(a, 1) | a ∈ A1} ∪ · · · ∪ {(a, n) | a ∈ An} von n > 1 Mengen A1, . . . , An (siehe Kapitel 2) wird durch einen nicht-rekursiven Datentyp mit n Konstruktoren C1, . . . , Cn implementiert, welche die Summanden voneinander 228 trennen, m.a.W.: es gibt eine Bijektion zwischen A1 + · · · + An und der Menge {C1(a) | a ∈ A1} ∪ · · · ∪ {Cn(a) | a ∈ An}. Z.B. implementiert der Standardtyp data Maybe a = Nothing | Just a die binäre Summe 1 + A, die A um ein Element zur Darstellung “undefinierter” Werte erweitert. Damit lassen sich partielle Funktionen totalisieren wie die Funktion lookup, mit der der Graph einer Funktion von A nach B modifiziert wird (siehe Kapitel 2): lookup :: Eq a => a -> [(a,b)] -> Maybe b lookup a ((a',b):r) = if a == a' then Just b else lookup a r lookup _ _ = Nothing Der Standardtyp Either implementiert beliebige binäre Summen: data Either a b = Left a | Right b Die Menge aller ganzen Zahlen kann als dreistellige Summe implementiert werden: data INT = Zero | Plus PosNat | Minus PosNat data PosNat = One | Succ PosNat 229 PosNat implementiert die Menge aller positiven natürlichen Zahlen (und ist rekursiv). Datentypen mit Destruktoren Um auf die Argumente eines Konstruktors zugreifen zu können, ordnet man ihnen Namen zu, die field labels genannt werden und Destruktoren im Sinne von Kapitel 2 wiedergeben. In obiger Definition von DT wird C_i typ_i1 ... typ_in_i erweitert zu: C_i {d_i1 :: typ_i1, ..., d_in_i :: typ_in_i} Wie Ci , so ist auch dij eine Funktion. Als solche hat sie den Typ DT a1 ... am -> typ_ij Destruktoren sind invers zu Konstruktoren. Z.B. hat der folgende Ausdruck den Wert ej : d_ij (C_i e_1 ... e_ni) Destruktoren nennt man in OO-Sprachen Attribute, wenn ihre Wertebereiche aus Standardtypen zusammengesetzt ist, bzw. Methoden (Zustandstransformationen), wenn der 230 Wertebereich mindestens einen Datentyp mit Destruktoren enthält. Außerdem schreibt man dort in der Regel x.d ij anstelle von d ij(x). Mit Destruktoren lautet das allgemeine Schema einer Datentypdefinition also wie folgt: data DT a_1 ... a_m = C_1 {d_11 :: typ_11,..., d_1n_1 :: typ_1n_1} | ... | C_k {d_k1 :: typ_k1,..., d_kn_k :: typ_kn_k} Elemente von DT können mit oder ohne Destruktoren definiert werden: obj = C_i e_i1 ... e_in_i ist äquivalent zu obj = C_i {d_i1 = e_i1,..., d_in_i = e_in_i} Die Werte einzelner Destruktoren von obj können wie folgt verändert werden: obj' = obj {d_ij_1 = e_1',..., d_ij_m = e_m'} obj 0 unterscheidet sich von obj dadurch, dass den Destruktoren d ij 1,...,d ij m neue Werte, nämlich e 1’,...,e m’zugewiesen wurden. 231 Destruktoren dürfen nicht rekursiv definiert werden. Folglich deutet der Haskell-Compiler jedes Vorkommen von attrij auf der rechten Seite einer Definitionsgleichung als eine vom gleichnamigen Destruktor verschiedene Funktion und sucht nach deren Definition. Dies kann man nutzen, um dij doch rekursiv zu definieren, indem in der zweiten Definition von obj (s.o.) die Gleichung d ij = ej durch d ij = d ij ersetzt und d ij lokal definiert wird: obj = C_i {d_i1 = e_1,..., d ij = d_ij,..., d_in_i = en_i} where d_ij ... = ... Ein Konstruktor darf nicht zu mehreren Datentypen gehören. Ein Destruktor darf nicht zu mehreren Konstruktoren unterschiedlicher Datentypen gehören. Beispiel Listen mit Destruktoren könnten in Haskell durch folgenden Datentyp definiert werden: data ListD a = Nil | Cons {hd :: a, tl :: ListD a} Man beachte, dass die Destrukturen hd :: ListD a -> a und tl :: ListD a -> ListD a, 232 die den Kopf bzw. Rest einer nichtleeren Liste liefern, partielle Funktionen sind, weil sie nur auf mit dem Konstruktor Cons gebildete Elemente von ListD(A) anwendbar sind. o Die Destruktoren eines Datentyps DT sind immer dann totale Funktionen, wenn DT genau einen Konstruktor hat. Wie das folgende Beispiel nahelegt, ist das aus semantischer Sicht keine Einschränkung: Jeder Datentyp ohne Destruktoren ist isomorph zu einem Datentyp mit genau einem Konstruktor und einem Destruktor. Beispiel 10.3 Listen mit totalen Destruktoren (siehe 2.13) DTcoList(A) ist isomorph zur größten Lösung der Gleichung M = {coListC(Nothing)} ∪ {coListC(Just(a, s)) | a ∈ A, s ∈ M } (3) in der Mengenvariablen M und wird deshalb durch folgenden Datentyp implementiert: data ColistC a = ColistC {split :: Maybe (a,ColistC a)} Wie man leicht sieht, ist die größte Lösung von (3) isomorph zur größten Lösung von (1), also zu CTList(A). Die Einschränkung von CTList(A) auf unendliche Listen ist isomorph zu DTStream(A) und wird daher durch folgenden Datentyp implementiert: 233 data StreamC a = (:<) {hd :: a, tl :: StreamC a} Als Elemente von StreamC(Int) lauten die Stream(Z)-Coterme blink und blink’ wie folgt: blink,blink' :: StreamC Int blink = 0:<blink' blink' = 1:<blink o Beispiel 10.4 (siehe 2.13) DTDAut(X,Y ) ist isomorph zur größten Lösung der Gleichung M = {DA(f )(y) | f : X → M , y ∈ Y } in der Mengenvariablen M und wird deshalb durch folgenden Datentyp implementiert: data DAutC x y = DA {deltaC :: x -> DAutC x y, betaC :: y} Als Elemente von DAutC (Int) lauten die Acc(Z)-Coterme esum und osum wie folgt: esum,osum :: DAutC Int Bool esum = DA {deltaC = \x -> if even x then esum else osum, betaC = True} osum = DA {deltaC = \x -> if odd x then esum else osum, betaC = False} o 234 Typklassen stellen Bedingungen an die Instanzen einer Typvariablen. Die Bedingungen bestehen in der Existenz bestimmter Funktionen, z.B. class Eq a where (==), (/=) :: a -> a -> Bool (/=) = (not .) . (==) Eine Instanz einer Typklasse besteht aus den Instanzen ihrer Typvariablen sowie Definitionen der in ihr deklarierten 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! Der Typ jeder Funktion einer Typklasse muss die - in der Regel eine - Typvariable der Typklasse mindestens einmal enthalten. Sonst wäre die Funktion ja gar nicht von (der jeweiligen Instanz) der Typvariable abhängig. 235 10.5 Mengenoperationen auf Listen insert :: Eq a => a -> [a] -> [a] insert a s@(b:s') = if a == b then s else b:insert a s' insert a _ = [a] union :: Eq a => [a] -> [a] -> [a] union = foldl $ flip insert Mengenvereinigung unionMap :: Eq a => (a -> [b]) -> [a] -> [b] unionMap f = foldl union [] . map f concatMap für Mengen subset :: Eq a => [a] -> [a] -> Bool s `subset` s' = all (`elem` s') s Mengeninklusion Unterklassen Typklassen können wie Objektklassen andere Typklassen erben. Die jeweiligen Oberklassen werden vor dem Erben vor dem Pfeil => aufgelistet. class Eq a => Ord a where (<=), (<), (>=), (>) :: a -> a -> Bool 236 max, min :: a -> a -> a a < b = a <= b && a /= b a >= b = b <= a a > b = b < a max x y = if x >= y then x else y min x y = if x <= y then x else y Ausgeben Vor der Ausgabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion show aufgerufen, die zur Typklasse Show a gehört. class Show a where show :: a -> String show x = shows x "" shows :: a -> String -> String shows = showsPrec 0 showsPrec :: Int -> a -> String -> String 237 Das String-Argument von showsPrec wird an die Ausgabe des Argumentes vom Typ a angefügt. Steht deriving Show am Ende der Definition eines Datentyps, dann werden dessen Elemente in der Darstellung ausgegeben, in der sie im Programmen vorkommen. Für andere Ausgabeformate müssen entsprechende Instanzen von show oder showsPrec definiert werden. Einlesen Bei der Eingabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion read aufgerufen, die zur Typklasse Read a gehört: class Read a where readsPrec :: Int -> String -> [(a,String)] reads :: String -> [(a,String)] reads = readsPrec 0 read :: String -> a read s = case [x | (x,t) <- reads s, ("","") <- lex t] of [x] -> x 238 [] _ -> error "PreludeText.read: no parse" -> error "PreludeText.read: ambiguous parse" reads s liefert eine Liste von Paaren, bestehend aus dem als Element vom Typ a erkannten Präfix von s und der jeweiligen Resteingabe (= Suffix von s). lex :: String -> [(a,String)] ist eine Standardfunktion, die ein evtl. aus mehreren Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und sowohl das Symbol als auch die Resteingabe ausgibt. Der Generator ("","") <- lex t in der obigen Definition von read s bewirkt, dass nur die Paare (x,t) von reads s berücksichtigt werden, bei denen die Resteingabe t aus Leerzeichen, Zeilenumbrüchen und Tabulatoren besteht. Steht deriving Read am Ende der Definition eines Datentyps, dann werden dessen Elemente in der Darstellung erkannt, in der sie in Programmen vorkommen. Für andere Eingabeformate müssen entsprechende Instanzen von readsPrec definiert werden. Jede Instanz von readsPrec ist – im Sinne von Kapitel 5 – ein Parser in die Listenmonade. Da die Implementierung von Parsern und Compilern in Haskell erst in späteren Kapiteln behandelt wird, geben wir an dieser Stelle keine Beispiele für readsPrec an. 239 11 Algebren in Haskell Sei Σ = (S, I, F ) eine Signatur, I = {x1, . . . , xk }, S = {s1, . . . , sm} und F = {f1 : e1 → e01, . . . , fn : en → e0n}. Jede Σ-Algebra entspricht einem Element des folgenden polymorphen Datentyps: data Sigma x1 ... xk s1 ... sm = Sigma {f1 :: e1 -> e1',..., fn :: en -> en'} Die Sorten und Operationssymbole von Σ werden durch Typvariablen bzw. Destruktoren wiedergegeben und durch die Trägermengen bzw. (kaskadierten) Funktionen der jeweiligen Algebra instanziiert. Um eine Signatur Σ in Haskell zu implementieren, genügt es daher, den Datentyp ihrer Algebren nach obigem Schema zu formulieren. Der Datentyp Sigma(x1) . . . (xk ) repräsentiert im Gegensatz zu den Datentypen der Beispiele 10.1-10.4, die Trägermengen einzelner Algebren implementieren, die Klasse aller ΣAlgebren. 240 11.1 Erweiterung von Term- und anderen Trägermengen zu Algebren (siehe 2.13 und 2.14) data Nat nat = Nat {zero :: nat, succ :: nat -> nat} natT implementiert TN at, listT implementiert TList(X): natT :: Nat Int natT = Nat {zero = 0, succ = (+1)} data List x list = List {nil :: list, cons :: x -> list -> list} listT implementiert TList(X) (siehe 10.1): listT :: List x [x] listT = List {nil = [], cons = (:)} Die folgende Funktion implementiert die Faltung fold alg : TList(X) → alg von List(X)Grundtermen endlicher Tiefe in einer beliebigen List(X)-Algebra alg: foldList :: List x list -> [x] -> list foldList alg [] = nil alg foldList alg (x:s) = cons alg x $ foldList alg s 241 data Reg bs reg = Reg {par,seq :: reg -> reg -> reg, iter :: reg -> reg, base :: bs -> reg,} regT implementiert TReg(BS), regWord implementiert Regword (BS) (siehe 2.14): regT :: bs -> Reg bs (RegT bs) (siehe 10.2) regT = Reg {par = Par, seq = Seq, iter = Iter, base = Base} regWord :: Show bs => bs -> Reg bs (Int -> String) regWord bs = Reg {par = \f g n -> enclose (n > 0) $ f 0 ++ '+':g 0, seq = \f g n -> enclose (n > 1) $ f 1 ++ '.':g 1, iter = \f n -> enclose (n > 2) $ f 2 ++ "*", base = \b -> show . const b} where enclose b w = if b then '(':w++")" else w Die folgende Funktion implementiert die Faltung fold alg : TReg(BS) → alg von regulären Ausdrücken (= Reg(BS)-Grundtermen) in einer beliebigen Reg(BS)-Algebra alg: foldReg :: Reg bs reg -> RegT bs -> reg foldReg alg t = case t of Par t u -> par alg (f t) $ f u Seq t u -> seq alg (f t) $ f u 242 Iter t -> iter alg $ f t Base b -> base alg b where f = foldReg alg instance Show bs => Show (RegT bs) where showsPrec = flip $ foldReg regWord data Stream x s = Stream {head :: s -> x, tail :: s -> list} streamC :: Stream x (StreamC x) streamC = Stream {head = hd, tail = tl} (siehe 10.3) Die Stream(Z)-Algebra zo (siehe 2.11) kann wie folgt implementiert werden: data ZO = Blink | Blink' zo :: Stream Int Blinks zo = Stream {head = \case Blink -> 0; Blink' -> 1, tail = \case Blink -> Blink'; Blink' -> Blink} Die finale Stream(X)-Algebra StreamFun(X) (siehe 17.5) kann wie folgt implementiert werden: 243 streamFun :: Stream x (Int -> x) streamFun = Stream {head = ($0), tail = \s n -> s $ n+1} Die folgenden Funktionen implementieren die Entfaltungen von Elementen einer beliebigen Stream(X)-Algebra alg zu Elementen von StreamFun(X) bzw. DTStream(X): unfoldStreamF :: Stream x list -> list -> Int -> x unfoldStreamF alg s = \case 0 -> head_ alg s n -> unfoldStreamF alg (tail_ alg s) $ n-1 unfoldStream :: Stream x list -> list -> StreamC x unfoldStream alg s = head alg s :< unfoldStream alg $ tail alg s data DAut x y state = DAut {delta :: state -> x -> state, beta :: state -> y} Der Datentyp DAutC(X)(Y) von Beispiel 10.4 ist eine DAut(X,Y)-Algebra: dAutC :: DAut x y (DAutC x y) dAutC = DAut {delta = deltaC, beta = betaC} (siehe 10.4) Die Acc(Z)-Algebra eo (siehe 2.11) kann wie folgt implementiert werden: 244 data EO = Esum | Osum deriving Eq eo :: DAut Int Bool EO eo = DAut {delta = \case Esum -> f . even; Osum -> f . odd, beta = (== Esum)} where f b = if b then Esum else Osum Die finale DAut(X, Y )-Algebra Beh(X, Y ) (siehe 2.15) kann wie folgt implementiert werden: behFun :: DAut x y ([x] -> y) behFun = DAut {delta = \f x -> f . (x:), beta = ($ [])} Die folgenden Funktionen implementieren die Entfaltungen von Elementen einer beliebigen DAut(X, Y )-Algebra alg zu Elementen von Beh(X, Y ) bzw. DTDAut(X,Y ): unfoldDAutF :: DAut x y state -> state -> [x] -> y unfoldDAutF alg s = \case [] -> beta alg s x:w -> unfoldDAutF alg (delta alg s x) w unfoldDAut :: DAut x y state -> state -> StateC x y unfoldDAut alg s = DA {deltaC = unfoldDAut alg . delta alg s, betaC = beta alg s} 245 Nach Beispiel 2.23 realisieren die initialen Automaten (eo, Esum) und (eo, Osum) die Verhaltensfunktion even ◦ sum : Z∗ → 2 bzw. odd ◦ sum : Z∗ → 2. Da eo Acc(Z)-isomorph zur Unteralgebra von A = DAutC(Z)(Bool ) mit der Trägermenge {esum, osum} ist, werden die beiden Funktionen auch von den initialen Automaten (A, esum) und (A, osum) realisiert. Es gelten also folgende Gleichungen: unfoldDAutF eo Esum = even . sum = unfoldDAutF dAutC esum unfoldDAutF eo Osum = odd . sum = unfoldDAutF dAutC osum o (siehe Beispiel 4.2 und Java.hs) data JavaLight commands command sum_ sumsect prod prodsect factor disjunct conjunct literal = JavaLight {seq_ :: command -> commands -> commands, embed :: command -> commands, block :: commands -> command, assign :: String -> sum_ -> command, cond :: disjunct -> command -> command -> command, cond1,loop :: disjunct -> command -> command, sum_ :: prod -> sumsect -> sum_, sumsect :: String -> prod -> sumsect -> sumsect, nilS :: sumsect, 246 prod prodsect nilP embedI var encloseS disjunct embedC conjunct embedL not_ atom embedB encloseD :: :: :: :: :: :: :: :: :: :: :: :: :: :: factor -> prodsect -> prod, String -> factor -> prodsect -> prodsect, prodsect, Int -> factor, String -> factor, sum_ -> factor, conjunct -> disjunct -> disjunct, conjunct -> disjunct, literal -> conjunct -> conjunct, literal -> conjunct, literal -> literal, sum_ -> String -> sum_ -> literal, Bool -> literal, disjunct -> literal} 11.3 Die Termalgebra von JavaLight Zunächst wird die Menge der JavaLight-Terme analog zu den TList(X) und TReg(BS) (siehe 10.1 bzw. 10.2) durch Datentypen implementiert und zwar jeweils einen für jede Sorte von JavaLight: data Commands data Command = Seq (Command,Commands) | Embed Command deriving Show = Block Commands | Assign (String,Sum) | Cond (Disjunct,Command,Command) | Cond1 (Disjunct,Command) | 247 data data data data data data data data Sum Sumsect Prod Prodsect Factor Disjunct Conjunct Literal = = = = = = = = Loop (Disjunct,Command) deriving Show Sum (Prod,Sumsect) deriving Show Sumsect (String,Prod,Sumsect) | NilS deriving Show Prod (Factor,Prodsect) deriving Show Prodsect (String,Factor,Prodsect) | NilP deriving Show EmbedI Int | Var String | EncloseS Sum deriving Show Disjunct (Conjunct,Disjunct) | EmbedC Conjunct deriving Show Conjunct (Literal,Conjunct) | EmbedL Literal deriving Show Not Literal | Atom (Sum,String,Sum) | EmbedB Bool | EncloseD Disjunct deriving Show javaTerm :: JavaLight Commands Command Sum Sumsect Prod Prodsect Factor Disjunct Conjunct Literal javaTerm = JavaLight {seq_ = curry Seq, embed = Embed, block = Block, assign = curry Assign, cond = curry3 Cond, cond1 = curry Cond1, loop = curry Loop, sum_ = curry Sum, sumsect = curry3 Sumsect, nilS = NilS, prod = curry Prod, prodsect = curry3 Prodsect, nilP = NilP, embedI = EmbedI, var = Var, encloseS = EncloseS, disjunct = curry Disjunct, embedC = EmbedC, conjunct = curry Conjunct, embedL = EmbedL, not_ = Not, atom = curry3 Atom, embedB = EmbedB, encloseD = EncloseD} 11.4 Die Wortalgebra von JavaLight javaWord :: JavaLight String String String String String String String String String String 248 javaWord = JavaLight {seq_ embed block assign cond cond1 loop sum_ sumsect nilS prod prodsect nilP embedI var encloseS disjunct embedC conjunct embedL not_ atom embedB encloseD = = = = = = = = = = = = = = = = = = = = = = = = (++), id, \cs -> " {"++cs++"}", \x e -> x++" = "++e++"; ", \e c c' -> "if "++e++c++" else"++c', \e c -> "if " ++e++c, \e c -> "while "++e++c, (++), \op e f -> op++e++f, "", (++), \op e f -> op++e++f, "", show, id, \e -> '(':e++")", \e e' -> e++" || "++e', id, \e e' -> e++" && "++e', id, \be -> '!':be, \e rel e' -> e++rel++e', show, \e -> '(':e++")"} 249 11.5 Das Zustandsmodell von JavaLight (siehe Beispiel 4.9) type St a = Store -> a rel :: String -> Int -> Int -> Bool rel str = case str of "<" -> (<) ">" -> (>) "<=" -> (<=) ">=" -> (>=) "==" -> (==) "!=" -> (/=) javaState :: JavaLight (St Store) (St Store) (St Int) (St (Int -> Int)) (St Int) (St (Int -> Int)) (St Int) (St Bool) (St Bool) (St Bool) javaState = JavaLight {seq_ = flip (.), embed = id, block = id, assign = \x e st -> update st x $ e st, cond = cond, cond1 = \f g -> cond f g id, loop = loop, sum_ = \f g st -> g st $ f st, sumsect = \str f g st i -> g st $ op str i $ f st, nilS = const id, prod = \f g st -> g st $ f st, 250 prodsect nilP embedI var encloseS disjunct embedC conjunct embedL not_ atom embedB encloseD = = = = = = = = = = = = = \str f g st i -> g st $ op str i $ f st, const id, const, flip ($), id, liftM2 (||), id, liftM2 (&&), id, (not .), \f str -> liftM2 (rel str) f, const, id} where cond :: St Bool -> St Store -> St Store -> St Store cond f g h st = if f st then g st else h st loop :: St Bool -> St Store loop f g = cond f (loop f g op str = case str of "+" -> "-" -> "*" -> "/" -> -> St Store . g) id (+) (-) (*) div 251 Z.B. hat das JavaLight-Programm prog = fact = 1; while x > 1 {fact = fact*x; x = x-1;} die folgende Interpretation in A = javaState: compileA JavaLight (prog) : Store → Store store 7→ λz.if z = x then 0 else if z = fact then store(x)! else store(z) Ausdrücke werden hier so interpretiert wie im zweiten in Beispiel 4.9 definierten Modell von JavaLight. Da die Ausführung von Kommandos, zumindest von denen, die loop enthalten, manchmal nicht terminieren, müssen sie als partielle Funktionen interpretiert werden, was die Erweiterung der Trägermengen des Zustandsmodells zu ω-CPOs (halbgeordneten Mengen mit Kettensuprema und kleinstem Element) erfordert (siehe Kapitel 19). Tatsächlich implementiert die Haskell-Funktion loop von javaState das zu einem ω-CPO erweiterte Zustandsmodell, dessen Details in Abschnitt 19.1 zu finden sind. o 252 11.6 Die Ableitungsbaumalgebra von JavaLight type TS = Tree String javaDeri :: JavaLight TS TS TS TS TS TS TS TS TS TS javaDeri = JavaLight {seq_ = \c c' -> F "Commands" [c,c'], embed = \c -> F "Commands" [c], block = \c -> command [c], assign = \x e -> command [leaf x,leaf "=",e,leaf ";"], cond = \e c c' -> command [leaf "if",e,c,leaf "else",c'], cond1 = \e c -> command [leaf "if",e,c], loop = \e c -> command [leaf "while",e,c], sum_ = \e f -> F "Sum" [e,f], sumsect = \op e f -> F "Sumsect" [leaf op,e,f], nilS = leaf "Sumsect", prod = \e f -> F "Prod" [e,f], prodsect = \op e f -> F "Prodsect" [leaf op,e,f], nilP = leaf "Prodsect", embedI = \i -> factor [leaf $ show i], var = \x -> factor [leaf x], encloseS = \e -> factor [leaf "(",e,leaf ")"], disjunct = \e e'-> F "Disjunct" [e,leaf "||",e'], embedC = \e -> F "Disjunct" [e], conjunct = \e e'-> F "Conjunct" [e,leaf "&&",e'], embedL = \e -> F "Conjunct" [e], 253 not_ = \be -> literal [leaf "!",be], atom = \e rel e' -> literal [e,leaf rel,e'], embedB = \b -> literal [leaf $ show b], encloseD = \e -> literal [leaf "(",e,leaf ")"]} where command = F "Command" factor = F "Factor" literal = F "Literal" leaf = flip F [] 254 11.7 Datentyp der XMLstore-Algebren (siehe Beispiel 4.3 und Compiler.hs) data XMLstore store orders person emails email items stock suppliers id = XMLstore {store :: stock -> store, storeO :: orders -> stock -> store, orders :: person -> items -> orders -> orders, embedO :: person -> items -> orders, person :: String -> person, personE :: String -> emails -> person, emails :: email -> emails -> emails, none :: emails, email :: String -> email, items :: id -> String -> items -> items, embedI :: id -> String -> items, stock :: id -> Int -> suppliers -> stock -> stock, embedS :: id -> Int -> suppliers -> stock, supplier :: person -> suppliers, parts :: stock -> suppliers, id_ :: String -> id} 255 12 Attributierte Übersetzung Um die Operationen der Zielalgebra einer Übersetzung induktiv definieren zu können, müssen Trägermengen oft parametrisiert werden oder die Wertebereiche bereits parametrisierter Trägermengen um zusätzliche Komponenten erweitert werden, die zur Berechnung von Parametern rekursiver Aufrufe des Übersetzers benötigt werden. Die Zielalgebra A hat dann die Form Av1 × . . . × Avm → Aa1 × . . . × Aan . (1) Die Indizes v1, . . . , vm und a1, . . . , an heißen vererbte Attribute (inherited attributes) bzw. abgeleitete Attribute (derived, synthesized attributes) von s. Für alle 1 ≤ i ≤ m und 1 ≤ j ≤ n ist Avi bzw. Aaj die Menge der möglichen Werte des Attributs vi bzw. aj . Vererbte Attribut(wert)e sind z.B. Positionen, Adressen, Schachtelungstiefen, etc. Abgeleitete Attribut(wert)e sind das eigentliche Zielobjekt wie auch z.B. dessen Typ, Größe oder Platzbedarf, das selbst einen Wert eines abgeleiteten Attributs bildet. Gleichzeitig vererbte und abgeleitete Attribute heißen transient. Deren Werte bilden den Zustandsraum, der einen Compiler zur Transitionsfunktion eines Automaten macht, dessen Ein- und Ausgaben Quell- bzw. Zielprogramme sind. 256 Kurz gesagt, ergänzen Attribute einen Syntaxbaum um Zusatzinformation, die erforderlich ist, um ein bestimmtes Übersetzungsproblem zu lösen. In unserem generischen Ansatz sind sie aber nicht – wie beim klassischen Begriff einer Attributgrammatik – Dekorationen von Syntaxbäumen, sondern Komponenten der jeweiligen Zielalgebra. Nach der Übersetzung in eine Zielfunktion vom Typ (1) werden alle vererbten Attribute mit bestimmten Anfangswerten initialisiert. Dann werden die Zielfunktion auf die Anfangswerte angewendet und am Schluss das Ergebnistupel abgeleiteter Attributwerte mit π1 auf die erste Komponente, die das eigentliche Zielobjekt darstellt, projiziert. Beispiel 12.1 Binärdarstellung rationaler Zahlen (siehe auch Compiler.hs) konkrete Syntax G abstrakte Syntax Σ(G) rat → nat. | rat0 | rat1 mkRat : nat → rat app0, app1 : rat → rat nat → 0 | 1 | nat0 | nat1 0, 1 : 1 → nat app0, app1 : nat → nat 257 Die Zielalgebra A Arat enthält neben dem eigentlichen Zielobjekt ein weiteres abgeleitetes Attribut mit Wertebereich Q, 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. Anat = N Arat = Q × Q 0A : Anat 1A : Anat 0A = 0 1A = 1 app0A : Anat → Anat app1A : Anat → Anat app0A(n) = n ∗ 2 app1A(n) = n ∗ 2 + 1 mkRatA : Anat → Arat mkRatA(val) = (val, 1) app0A : Arat → Arat app1A : Arat → Arat app0A(val, inc) = (val, inc/2) app1A(val, inc)) = (val + inc/2, inc/2) 258 Beispiel 12.2 Strings mit Hoch- und Tiefstellungen konkrete Syntax G abstrakte Syntax Σ(G) string → string box | app : string × box → string string ↑ box | up : string × box → string string ↓ box | down : string × box → string box mkString : box → string box → (string) | Char mkBox : string → box embed : Char → box Die Zielalgebra A Astring 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 wird, • neben dem eigentlichen Zielobjekt ein weiteres abgeleitetes Attribut mit Wertebereich N3, das die Länge sowie - auf eine feste Grundlinie bezogen - Höhe und Tiefe des Rechtecks liefert. Astring = Abox = Strings mit Hoch- und Tiefstellungen × N2 → N3 259 appA : Astring × Abox → Astring appA(f, g)(x, y) = (str str0, l + l0, max h h0, max t t0) where (str, l, h, t) = f (x, y) (str0, l0, h0, t0) = g(x + l, y) upA : Astring × Abox → Astring 0 upA(f, g)(x, y) = (strstr , l + l0, h + h0 − 1, max t (t0 − h + 1)) where (str, l, h, t) = f (x, y) (str0, l0, h0, t0) = g(x + l, y + h − 1) downA : Astring × Abox → Astring downA(f, g)(x, y) = (strstr0 , l + l0, max h (h0 − t − 1), t + t0 − 1) where (str, l, h, t) = f (x, y) (str0, l0, h0, t0) = g(x + l, y − t + 1) mkString A : Abox → Astring mkString A(f ) = f mkBoxA : Astring → Abox embedA : Char → Abox mkBoxA(f ) = f emdedA(c)(x, y) = (c, 1, 2, 0) 260 Beispiel 12.3 Übersetzung regulärer Ausdrücke in erkennende Automaten Die folgenden Ersetzungsregeln beschreiben die schrittweise Übersetzung eines regulären Ausdrucks (= Reg(BS)-Grundterms) t in einen nichtdeterministischen Automaten, der die Sprache von t erkennt (siehe 2.17). Die Regeln basieren auf [9], Abschnitt 3.2.3). Zunächst wird die erste Regel auf t angewendet. Es entsteht ein Graph mit zwei Knoten und einer mit t markierten Kante. Dieser wird nun mit den anderen Regeln schrittweise verfeinert, bis an allen seinen Kanten nur noch Elemente von BS stehen. Dann stellt er den Transitionsgraphen eines Automaten dar, der t erkennt. 261 t 0 t 1 t s par(t,t') s' s s' t' s seq(t,t') s' s t' t s' ε s iter(t) s' s ε ε t s' ε s base(ø) s' s s base(B) s' s s' B s' B BS\{ø} 262 1 0 Mit obigen Regeln aus dem regulären Ausdruck t = (aa∗ + bc(bc)∗)∗ in Wortdarstellung (siehe 2.14) konstruierter Automat, der die Sprache von t erkennt 263 Die Zielalgebra regNDA Jede Regel außer der ersten entspricht der Interpretation einer Operation von Reg(BS) in folgender Reg(BS)-Algebra. regNDAreg = (NDA × N3 → NDA × N) Hierbei ist NDA = N → (BS → N∗) der Typ nichtdeterministischer erkennender Automaten mit ganzzahligen Zuständen und Eingaben aus BS. regNDAreg enthält • den Automaten als transientes Attribut, das mit der Funktion λn.λs. (Automat ohne Zustandsübergänge) 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. Die Operationen von Reg(BS) werden von regNDA wie folgt interpretiert: parregNDA : regNDAreg × regNDAreg → regNDAreg parregNDA(f, g)(δ, s, s0, next) = g(δ 0, s, s0, next0) where (δ 0, next0) = f (δ, s, s0, next) 264 seq regNDA : regNDAreg × regNDAreg → regNDAreg seq regNDA(f, g)(δ, s, s0, next) = g(δ 0, next, s0, next0) where (δ 0, next0) = f (δ, s, next, next + 1) iterregNDA : regNDAreg → regNDAreg iterregNDA(f )(δ, s, s0, next) = (addTo(δ4)(s)()(s0), next3) where next1 = next + 1 next2 = next1 + 1 (δ1, next3) = f (δ, next, next1, next2) δ2 = addTo(δ1)(s)()(next) δ3 = addTo(δ2)(next1)()(next) δ4 = addTo(δ3)(next1)()(s0) baseregNDA : BS → regNDAreg baseregNDA(∅)(δ, s, s0, next) = (δ, next) ∀ B ∈ BS \ {∅} : baseregNDA(B)(δ, s, s0, next) = (addTo(δ)(s)(B)(s0), next) x addTo(δ)(s)(x)(s0) fügt die Transition s → s0 zur Transitionsfunktion δ hinzu. 265 Der durch Auswertung eines regulären Ausdrucks (= Reg(BS)-Grundterms) in regNDA erzeugte Automat nda ∈ NDA ist nichtdeterministisch und hat -Übergänge. Er lässt sich wie üblich in einen deterministischen Potenzautomaten ohne -Übergänge transformieren. Da 0 der Anfangszustand, 1 der Endzustand und auch alle anderen Zustände von nda ganze Zahlen sind, lassen sich alle Zustände des Potenzautomaten pow(nda) als Listen ganzer Zahlen darstellen. pow(nda) ist, zusammen mit seinem Anfangszustand, der -Hülle des Anfangszustandes 0 von nda, eine DAut(BS, 2)-Algebra mit Zustandsmenge Z∗ (siehe 2.13): accNDA :: NDA -> (DAut BS Bool [Int], [Int]) accNDA nda = (DAut {delta = (epsHull .) . deltaP, beta = (1 `elem`)}, epsHull [0]) where deltaP :: [Int] -> BS -> [Int] deltaP qs x = unionMap (flip nda x) qs epsHull :: [Int] -> [Int] epsHull qs = if qs' `subset` qs then qs else epsHull $ qs `union` qs' where qs' = deltaP qs regNDA und accNDA sind im Haskell-Modul Compiler.hs implementiert. 266 12.4 Darstellung von Termen als hierarchische Listen Mit folgender JavaLight-Algebra javaList wird z.B. der Syntaxbaum des Programms fact = 1; while x > 1 {fact = fact*x; x = x-1;} in die Form einer hierarchischen Liste gebracht: 267 Die Trägermengen von javaList enthalten zwei vererbte Attribute vom Typ 2 bzw. Z. Der Boolesche Wert gibt an, ob der jeweilige Teilstring str hinter oder unter den vorangehenden zu schreiben ist. Im zweiten Fall wird str um den Wert des zweiten Attributs eingerückt. type BIS = Bool -> Int -> String javaList :: JavaLight BIS BIS BIS BIS BIS BIS BIS BIS BIS BIS javaList = JavaLight {seq_ = indent2 "commands", embed = indent1 "embed", block = indent1 "block", assign = \x e -> indent2 "assign" (indent0 x) e, cond = indent3 "cond", cond1 = indent2 "cond1", loop = indent2 "loop", sum_ = indent2 "sum", sumsect = \op e f -> indent3 "sumsect" (indent0 op) e f, nilS = indent0 "nilS", prod = indent2 "prod", prodsect = \op e f -> indent3 "prodsect" (indent0 op) e f, nilP = indent0 "nilP", embedI = \i -> indent1 "embedI" $ indent0 $ show i, var = \x -> indent1 "var" $ indent0 x, encloseS = indent1 "encloseS", 268 disjunct = indent2 "disjunct", embedC = indent1 "embedC", conjunct = indent2 "conjunct", embedL = indent1 "embedL", not_ = indent1 "not", atom = \e rel e'-> indent3 "atom" e (indent0 rel) e', embedB = \b -> indent1 "embedB" $ indent0 $ show b, encloseD = indent1 "encloseD"} where indent0 x = blanks x [] indent1 x f = blanks x [f] indent2 x f g = blanks x [f,g] indent3 x f g h = blanks x [f,g,h] blanks :: String -> [BIS] -> BIS blanks x fs b n = if b then str else '\n':replicate n ' '++str where str = case fs of f:fs -> x++' ':g True f++ concatMap (g False) fs _ -> x g b f = f b $ n+length x+1 269 12.5 Eine kellerbasierte Zielsprache für JavaLight Der folgende Datentyp liefert die Befehle einer Assemblersprache, die auf einem Keller vom Typ Z und einem Speicher vom Typ Store = String → Z operiert. Letzterer ist natürlich nur die Abstraktion eines realen Speichers, auf dessen Inhalt nicht über Strings, sondern über Adressen, z.B. Kellerpositionen, zugegriffen wird. Ein solche – realistischere – Assemblersprache wird erst im nächsten Kapitel behandelt. data StackCom = Push Int | Pop | Load String | Save String | Add | Sub | Mul | Div | Or_ | And_ | Inv | Cmp String | Jump Int | JumpF Int Diese Befehle bilden die möglichen Eingaben eines endlichen Automaten, dessen Zustände jeweils aus einem Kellerinhalt und einer Variablenbelegung bestehen: type State = ([Int],Store,Int) Die Bedeutung der Befehle ergibt sich aus ihrer Verarbeitung durch eine Transitionsfunktion executeCom: executeCom :: StackCom -> State -> State 270 executeCom com (stack,store,n) = case com of Push a -> (a:stack,store,n+1) Pop -> (tail stack,store,n+1) Load x -> (store x:stack,store,n+1) Save x -> (stack,update store x $ head stack,n+1) Add -> (a+b:s,store,n+1) where a:b:s = stack Sub -> (b-a:s,store,n+1) where a:b:s = stack Mul -> (a*b:s,store,n+1) where a:b:s = stack Div -> (b`div`a:s,store,n+1) where a:b:s = stack Or_ -> (max a b:s,store,n+1) where a:b:s = stack And_ -> (a*b:s,store,n+1) where a:b:s = stack Inv -> ((a+1)`mod`2:s,store,n+1) where a:s = stack Cmp str -> (c:s,store,n+1) where a:b:s = stack c = if rel str a b then 1 else 0 -- siehe 11.5 Jump k -> (stack,store,k) JumpF k -> (stack,store,if a == 0 then k else n+1) where a:_ = stack Sprungbefehle können nur im Kontext einer vollständigen Befehlsfolge cs ausgeführt werden. 271 n ist die Position des Befehls von cs, den der Interpreter als nächsten ausführt: execute :: [StackCom] -> State -> State execute cs state@(_,_,n) = if n >= length cs then state else execute cs $ executeCom (cs!!n) state Um Sprungziele, also die ganzzahligen Parameter der Sprungbefehle, berechnen zu können, muss der Compiler die Position jedes erzeugten Befehls innerhalb des gesamten Zielprogramms kennen. Dieses erzeugt er durch Interpretation des (abstrakten) Quellprogramms in der folgenden JavaLight-Algebra javaStack , deren Trägermengen neben dem jeweiligen Zielcode code ein (vererbtes) Attribut haben, das die Nummer des ersten Befehls von code wiedergibt. Dementsprechend interpretiert javaStack alle Sorten von JavaLight durch den Funktionstyp LCom = Int -> [StackCom] javaStack :: JavaLight LCom LCom LCom LCom LCom LCom LCom LCom LCom LCom javaStack = JavaLight {seq_ = seq_, embed = id, block = id, assign = \x e lab -> e lab++[Save x,Pop], cond = \e c c' lab 272 cond1 loop sum_ sumsect nilS prod prodsect nilP embedI var encloseS disjunct embedC conjunct embedL not_ atom embedB encloseD = = = = = = = = = = = = = = = = = = = -> let (code,exit) = fork e c 1 lab code' = c' exit in code++Jump (exit+length code'):code', \e c lab -> fst $ fork e c 0 lab, \e c lab -> fst (fork e c 1 lab)++[Jump lab], apply2 "", \op -> apply2 op . apply1 op, const [], apply2 "", \op -> apply2 op . apply1 op, const [], \i -> const [Push i], \x -> const [Load x], id, apply2 "|", id, apply2 "&", id, apply1 '!', \e rel -> apply2 rel e, \b -> const [Push $ if b then 1 else 0], id} 273 where apply1 :: String -> LCom -> LCom apply1 op e lab = e lab++[case op of "!" -> Inv "+" -> Add; "*" -> Mul; "-" -> Sub "/" -> Div] seq_ :: LCom -> LCom -> LCom seq_ e e' lab = code++e' (lab+length code) where code = e lab apply2 :: String -> LCom -> LCom -> LCom apply2 op e e' lab = code++e' (lab+length code)++ [case op of "|" -> Or_ "&" -> And_ _ -> Cmp op] where code = e lab fork :: LCom -> LCom -> Int -> Int -> ([StackCom],Int) fork e c n lab = (code++JumpF exit:code',exit) where code = e lab lab' = lab+length code+1 code' = c lab' exit = lab'+length code'+n 274 Beispiel 12.6 (Fakultätsfunktion) Steht das JavaLight-Programm fact = 1; while x > 1 {fact = fact*x; x = x-1;} in der Datei prog, dann übersetzt es javaToAlg "prog" 6 (siehe Java.hs) in ein Zielprogramm vom Typ [StackCom] und schreibt dieses in die Datei javatarget ab. Es lautet wie folgt: 0: 1: 2: 3: 4: 5: 6: 7: Push 1 Save "fact" Pop Load "x" Push 1 Cmp ">" JumpF 18 Load "fact" 8: 9: 10: 11: 12: 13: 14: 15: Load Mul Save Pop Load Push Sub Save "x" 16: Pop 17: Jump 3 "fact" "x" 1 "x" 275 13 JavaLight+ = JavaLight + I/O + Deklarationen + Prozeduren (siehe Java2.hs) 13.1 Assemblersprache mit I/O und Kelleradressierung Die Variablenbelegung store : String → Z im Zustandsmodell von Abschnitt Assemblerprogramme als JavaLight-Zielalgebra wird ersetzt durch den Keller stack ∈ Z∗, der jetzt nicht nur der schrittweisen Auswertung von Ausdrücken dient, sondern auch der Ablage von Variablenwerten unter vom Compiler berechneten Adressen. Weitere Zustandskomponenten sind: • der Inhalt des Registers BA für die jeweils aktuelle Basisadresse (s.u.), • der Inhalt des Registers STP für die Basisadresse des statischen Vorgängers des jeweils zu übersetzenden Blocks bzw. Funktionsaufrufs (s.u.), • der schon in Abschnitt 12.5 benutzte Befehlszähler pc (program counter), • der Ein/Ausgabestrom io, auf den Lese- bzw. Schreibbefehle zugreifen. Der entsprechende Datentyp lautet daher wie folgt: data State = State {stack,io :: [Int], ba,stp,pc :: Int} 276 Bei der Übersetzung eines Blocks b oder Prozeduraufrufs f (es) reserviert der Compiler Speicherplatz für die Werte aller lokalen Variablen des Blocks b bzw. Rumpfs (der Deklaration) von f . Dieser Speicherplatz ist Teil eines b bzw. f (es) zugeordneten Kellerabschnitts (stack frame, activation record). Dessen Anfangsadresse ist die Basisadresse ba der lokalen Variablen. Die absolute Adresse einer lokalen Variablen x ist die Kellerposition, an welcher der jeweils aktuelle Wert von x steht. Sie ist immer die Summe von ba und dem – Relativadresse von x genannten und vom Compiler in der Symboltabelle (s.u.) gespeicherten – Abstand zum Beginn des Kellerabschnitts. Wird zur Laufzeit der Block b bzw. – als Teil der Ausführung von f (es) – der Rumpf von f betreten, dann speichert ein vom Compiler erzeugter Befehl die nächste freie Kellerposition als aktuelle Basisadresse im Register BA. Der statische Vorgänger von b bzw. f (es) ist der innerste Block bzw. Prozedurrumpf, in dem b bzw. die Deklaration von f steht. Der b bzw. f (es) zugeordnete Kellerabschnitt beginnt stets mit dem Display, das aus den – nach aufsteigender Schachtelungstiefe geordneten – Basisadressen der Kellerabschnitte aller umfassenden Blöcke und Prozedurrümpfe besteht. 277 Der Compiler erzeugt und verwendet keine ganzzahligen, sondern nur symbolische Adressen, d.h. Registernamen (BA, STP, TOP) oder mit einer ganzen Zahl indizierte (inDexed) Adressen der Form Dex(adr)(i): data SymAdr = BA | STP | TOP | Dex SymAdr Int | Con Int Der Inhalt von TOP ist immer die Adresse der ersten freien Kellerposition. Ist s der Kellerinhalt, dann entspricht die absolute Adresse adr der Kellerposition |s| − 1 − adr. 278 Die folgende Funktion baseAdr berechnet die jeweils aktuelle (symbolische) Basisadresse: baseAdr :: Int -> Int -> SymAdr baseAdr declDep dep = if declDep == dep then BA else Dex BA declDep Der Compiler ruft baseAdr bei der Übersetzung jeder Verwendung einer Variable x auf. declDep und dep bezeichnen die Deklarations- bzw. Verwendungstiefe von x. Stimmen beide Werte überein, dann ist x eine lokale Variable und die Basisadresse von x steht (zur Laufzeit) im Register BA. Andernfalls ist x eine globale Variable, d.h. x wurde in einem Block bzw. Prozedurrumpf deklariert, der denjenigen, in dem x verwendet wird, umfasst (declDep<dep). Die Basisadresse von x ist in diesem Fall nicht im Register BA, sondern unter dem Inhalt der declDep-ten Position des dem Block bzw. Prozedurrumpf, in dem x verwendet wird, zugeordneten Kellerabschnitt zu finden (s.o.). Die folgenden Funktionen berechnen aus symbolischen Adressen absolute Adressen bzw. Kellerinhalte: absAdr,contents :: State -> SymAdr -> Int absAdr _ (Con i) = i absAdr state BA = ba state absAdr state STP = stp state 279 absAdr state TOP absAdr state (Dex BA i) absAdr state (Dex STP i) absAdr state (Dex TOP i) absAdr state (Dex adr i) contents state (Dex adr i) contents state adr = = = = = = length $ stack state ba state+i stp state+i length (stack state)+i contents state adr+i s!!(k-i) where (s,k) = stackPos state adr = absAdr state adr stackPos :: State -> SymAdr -> ([Int],Int) stackPos state adr = (s,length s-1-contents state adr) where s = stack state updState updState updState updState :: State -> SymAdr -> state BA x = state STP x = state (Dex adr i) x = Int -> State state {ba = x} state {stp = x} state {stack = updList s (k-i) x} where (s,k) = stackPos state adr Mit der Anwendung von absAdr oder contents auf eine – evtl. geschachtelte – indizierte Adresse wird eine Folge von Kelleradressen und -inhalten durchlaufen. 280 absAdr liefert die letzte Adresse der Folge, contents den Kellerinhalt an der durch diese Adresse beschriebenen Position. Die Zielprogramme von Javalight+ setzen sich aus folgenden Assemblerbefehlen mit symbolischen Adressen zusammen: data StackCom = PushA SymAdr | Push SymAdr | Pop | Save SymAdr | Move SymAdr SymAdr | Add | Sub | Mul | Div | Or_ | And_ | Inv | Cmp String | Jump SymAdr | JumpF Int | Read SymAdr | Write Analog zu Abschnitt 12.5 ist die Bedeutung der Befehle durch eine Transitionsfunktion executeCom gegeben – die jetzt auch die Sprungbefehle verarbeitet: executeCom :: StackCom -> State -> State executeCom com state = case com of PushA adr -> state' {stack = absAdr state adr:stack state} Push adr -> state' {stack = contents state adr: stack state} Pop -> state' {stack = tail $ stack state} Save adr -> updState state' adr $ head $ stack state 281 Move adr adr' -> updState state' adr' $ contents state adr Add -> applyOp state' (+) Sub -> applyOp state' (-) Mul -> applyOp state' (*) Div -> applyOp state' div Or_ -> applyOp state' max And_ -> applyOp state' (*) Inv -> state' {stack = (a+1)`mod`2:s} where a:s = stack state Cmp rel -> state' {stack = mkInt (evalRel rel b a):s} where a:b:s = stack state Jump adr -> state {pc = contents state adr} JumpF lab -> state {pc = if a == 0 then lab else pc state+1, stack = s} where a:s = stack state Read adr -> if null s then state' else (updState state' adr $ head s) {io = tail s} where s = io state Write -> if null s then state' else state' {io = io state++[head s]} where s = stack state 282 where state' = state {pc = pc state+1} applyOp :: State -> (Int -> Int -> Int) -> State applyOp state op = state {stack = op b a:s} where a:b:s = stack state execute wird ebenfalls an das neue Zustandsmodell angepasst: execute :: [StackCom] -> State -> State execute cs state = if curr >= length cs then state else execute cs $ executeCom (cs!!curr) state where curr = pc state 13.2 Grammatik und abstrakte Syntax von JavaLight+ JavaLight+ enthält neben den Sorten von JavaLight die Sorten Formals und Actuals für Listen formaler bzw. aktueller Parameter von Prozeduren. Auch die Basismengen von JavaLight werden übernommen. Hinzu kommt eine für formale Parameter. Sie besteht aus mit zwei Konstruktoren aus dem jeweiligen Parameternamen und einem Typdeskriptor gebildeten Ausdruck: 283 data TypeDesc = INT | BOOL | UNIT | Fun TypeDesc Int | ForFun TypeDesc data Formal = Par String TypeDesc | FunPar String [Formal] TypeDesc FunPar(x)(t) bezeichnet einen funktionalen formalen Parameter, also eine Prozedurvariable. Sie hat den Typ ForFun(t). t ist hier der Typ der Prozedurergebnisse. Demgegenüber bezeichnet Fun(t,lab) den Typ einer Prozedurkonstanten mit Ergebnistyp t und Codeadresse lab. Dementsprechend enthält JavaLight+ auch die Regeln von JavaLight. Hinzu kommen die folgenden Regeln für Ein/Ausgabebefehle, Deklarationen, Prozeduraufrufe und Parameterlisten. Außerdem sind jetzt auch Boolesche Variablen und Zuweisungen an diese zugelassen. Command → Formals Formals 0 ExpSemi ExpBrac ExpComm Factor → → → → → → read String; | write ExpSemi | {Commands} | TypeDesc String Formals {Commands} | TypeDesc String; | String = ExpSemi | String Formals {Commands} | String Actuals () | (Formals 0 Formal ) | Formal , Formals 0 Sum; | Disjunct; Sum) | Disjunct) Sum, | Disjunct, String Actuals 284 Literal → Actuals → Actuals0 → String | String Actuals () | (Actuals0 ExpBrac | ExpComm Actuals0 In Beispiel 6.2 wurde begründet, warum drei verschiedene Sorten für Ausdrücke (ExpSemi , ExpBrac und ExpComm) benötigt werden. Beim Übergang zur abstrakten Syntax können wir die Unterscheidung zwischen den drei Sorten wieder aufheben. Erlaubt man nur JavaLight+-Algebren A, die Formals durch Formal ∗ und Actuals durch (ASum + ADisjunct )∗ interpretieren, dann lautet der Datentyp der JavaLight+-Algebren wie folgt (siehe Java2.hs): data JavaLightP commands command exp sum_ sumsect prod prodsect factor disjunct conjunct literal formals actuals = JavaLightP {seq_ :: command -> commands -> commands, embed :: command -> commands, block :: commands -> command, assign :: String -> exp -> command, applyProc :: String -> actuals -> command, cond :: disjunct -> command -> command -> command, cond1,loop :: disjunct -> command -> command, read_ :: String -> command, write_ :: exp -> command, vardecl :: String -> TypeDesc -> command, 285 fundecl formals embedS sum_ sumsect nilS prod prodsect nilP embedI varInt applyInt encloseS embedD disjunct embedC conjunct embedL not_ atom embedB varBool applyBool encloseD actuals :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: String -> formals -> TypeDesc -> commands -> command, [Formal] -> formals, sum_ -> exp, prod -> sumsect -> sum_, String -> prod -> sumsect -> sumsect, sumsect, factor -> prodsect -> prod, String -> factor -> prodsect -> prodsect, prodsect, Int -> factor, String -> factor, String -> actuals -> factor, sum_ -> factor, disjunct -> exp, conjunct -> disjunct -> disjunct, conjunct -> disjunct, literal -> conjunct -> conjunct, literal -> conjunct, literal -> literal, sum_ -> String -> sum_ -> literal, Bool -> literal, String -> literal, String -> actuals -> literal, disjunct -> literal, [exp] -> actuals} 286 13.3 JavaLight+-Algebra javaStackP (siehe Java2.hs) javaStackP hat wie javaStack nur eine Trägermenge (ComStack) für alle Kommandosorten sowie eine Trägermenge (ExpStack) für alle Ausdruckssorten (außer denen für Sektionen): type ComStack = Int -> Symtab -> Int -> Int -> ([StackCom],Symtab,Int) type ExpStack = Int -> Symtab -> Int -> ([StackCom],TypeDesc) type Symtab = String -> (TypeDesc,Int,Int) Die Symboltabelle (vom Typ Symtab) ordnet jeder Variablen drei Werte zu: Typ, Schachtelungstiefe ihrer Deklaration, also die Zahl der die Deklaration umfassenden Blöcke und Prozedurrümpfe, sowie eine Relativadresse (s.o.). Transiente Attribute der Kommandosorten sind die Symboltabelle und die nächste freie Relativadresse (adr; s.u.). Weitere vererbte Attribute der Kommando- und Ausdruckssorten sind die nächste freie Befehlsnummer (lab; s.u.) und die Schachtelungstiefe (depth; s.u.) des jeweiligen Kommandos bzw. Ausdrucks. Weitere abgeleitete Attribute der Ausdruckssorten sind der Zielcode und der Typdeskriptor des jeweiligen Ausdrucks. Die Symboltabelle ist bei Ausdruckssorten nur vererbt. 287 Listen formaler bzw. aktueller Parameter werden von javaStackP als Elemente der folgenden Trägermengen interpretiert: type FormsStack = Symtab -> Int -> Int -> ([ComStack],Symtab,Int) type ActsStack = Int -> Symtab -> Int -> ([StackCom],Int) Formale Parameter sind Teile von Funktionsdeklarationen. Deshalb haben Listen formaler Parameter Attribute von Kommandosorten: Symboltabelle, Schachtelungstiefe, nächste freie Relativadresse und sogar zusätzlichen Quellcode (vom Typ [ComStack]), der aus jedem funktionalen Parameter eine lokale Funktionsdeklaration erzeugt (siehe Übersetzung formaler Parameter). Aktuelle Parameter sind Ausdrücke und haben deshalb (fast) die gleichen Attribute wie Ausdruckssorten. Anstelle eines Typdeskriptors wird die Länge der jeweiligen Parameter berechnet. Den Platz von TypeDesc nimmt daher Int ein. Aktuelle Parameter können im Gegensatz zu anderen Vorkommen von Ausdrücken Prozedurvariablen sein. Der Einfachheit halber überprüft unser Compiler nicht, ob diese nur an Parameterpositionen auftreten, so wie er auch Anwendungen arithmetischer Operationen auf Boolesche Variablen oder Boolescher Operationen auf Variablen vom Typ Z ignoriert. 288 Die Typverträglichkeit von Deklarationen mit den Verwendungen der deklarierten Variablen könnte aber mit Hilfe einer Variante von javaStackP überprüft werden, deren Trägermengen um Fehlermeldungen angereichert sind. Bis auf die Interpretation von Blöcken und die Einbindung der o.g. Attribute gleicht die Interpretation der Programmkonstrukte von JavaLight in javaStackP derjenigen in javaStack . An die Stelle der Lade- und Speicherbefehle, die javaStack erzeugt und die zur Laufzeit Daten von einer Variablenbelegung store : String → Z zum Keller bzw. vom Keller nach store transportieren, treten die oben definierten Lade- und Speicherbefehle, die Daten oder Kelleradressen zwischen Kellerplätzen hin- und herschieben. javaStackP :: JavaAlgP ComStack ComStack ExpStack ExpStack ExpStack ExpStack ExpStack ExpStack ExpStack ExpStack ExpStack FormsStack ActsStack javaStackP = JavaLightP {seq_ = seq_, embed = id, block = block, assign = assign, applyProc = applyProc, cond = cond, cond1 = \e c -> (((fst .) .) .) . fork e c 0 loop = loop, read_ = read_, write_ = write_, 289 vardecl fundecl formals embedS sum_ sumsect nilS prod prodsect nilP embedI varInt applyInt encloseS embedD disjunct embedC conjunct embedL not_ atom embedB varBool applyBool encloseD = = = = = = = = = = = = = = = = = = = = = = = = = vardecl, fundecl, formals, id, apply2 "", \op -> apply2 "" . apply1 op, _ _ -> ([],INT), apply2 "", \op -> apply2 "" . apply1 op, \_ _ _ -> ([],INT), \i _ _ _ -> ([Push $ Con i],INT), var, applyFun, id, id, apply2 "|", id, apply2 "&", id, apply1 "!", \e rel e' -> apply (Cmp rel) (e,e'), \b _ _ _ -> ([Push $ Con $ mkInt b],BOOL), var, applyFun, id, 290 actuals = actuals} where apply1 :: String -> ExpStack -> ExpStack apply1 op e lab st dep = (fst (e lab st dep)++ [case op of "!" -> Inv; "+" -> Add; "-" -> Sub "*" -> Mul; _ -> Div], INT) seq_ :: ComStack -> ComStack -> ComStack seq_ c c' lab st dep adr = (code++code',st2,adr2) where (code,st1,adr1) = c lab st dep adr (code',st2,adr2) = c' (lab+length code) st1 dep adr1 apply2 :: String -> ExpStack -> ExpStack -> ExpStack apply2 op e e' lab st dep = (code++code'++cs,td) where (code,td) = e lab st dep code' = fst $ e' (lab+length code) st dep cs = case op of "" -> []; "|" -> [Or_] "&" -> [And_]; _ -> [Cmp op] 291 Übersetzung eines Blocks block :: ComStack -> ComStack block c lab st dep adr = (code',st,adr) where bodylab = lab+dep+3; dep' = dep+1 (code,_,local) = c bodylab st dep' dep' code' = Move TOP STP:pushDisplay BA dep++Move STP BA: bodylab: code++replicate (local-dep') Pop++Save BA: replicate dep' Pop pushDisplay :: Int -> SymAdr -> [StackCom] pushDisplay dep reg = foldr push [Push reg] [0..dep-1] where push i code = Push (Dex reg i):code javaStackP umschließt im Gegensatz zu javaStack bei der Zusammenfassung einer Kommandofolge cs zu einem Block den Code von cs mit zusätzlichen Zielcode: Move TOP STP Speichern des Stacktops im Register STP pushDisplay dep BA Kellern des Displays des umfassenden Blocks und dessen Adresse Move STP BA Der Inhalt von STP wird zur neuen Basisadresse code Zielcode für die Kommandofolge des Blocks replicate (local-dep') Pop Entkellern der lokalen Variablen des Blocks Save BA Speichern der alten Basisadresse in BA replicate dep' Pop Entkellern des Displays 292 Übersetzung einer Zuweisung assign :: String -> ExpStack -> ComStack assign x e lab st dep adr = (fst (e lab st dep)++[Save $ Dex ba adrx,Pop], st,adr) where (_,declDep,adrx) = st x ba = baseAdr declDep dep Zielcodeerläuterung: Zielcode für den Ausdruck e, der mit einem Befehl zur Kellerung des aktuellen Wertes von e schließt Save $ Dex ba adrx Speichern des aktuellen Wertes von e unter der Kelleradresse Dex ba adrx von x, die sich aus der Basisadresse ba von x und der in der Symboltabelle gespeicherten Relativadresse adrx von x ergibt Pop Entkellern des aktuellen Wertes von e code Übersetzung von Konditionalen und Schleifen cond :: ExpStack -> ComStack -> ComStack -> ComStack cond e c c' lab st dep adr = (code++Jump (Con $ exit+length code'):code', st2,adr2) where ((code,st1,adr1),exit) = fork e c 1 lab st dep adr (code',st2,adr2) = c' exit st1 dep adr1 293 loop :: ExpStack -> ComStack -> ComStack loop e c lab st dep adr = (code++[Jump $ Con lab],st',adr') where (code,st',adr') = fst $ fork e c 1 lab st dep adr fork :: ExpStack -> ComStack -> Int -> Int -> Symtab -> Int -> Int -> (([StackCom],Int,Symtab,Int),Int) fork e c n lab st dep adr = ((code++JumpF exit:code',st',adr'),exit) where code = fst $ e lab st dep lab' = lab+length code+1 (code',st',adr') = c lab' st dep adr exit = lab'+length code'+n Übersetzung eines Lesebefehls read_ :: String -> ComStack read_ x _ st dep adr = ([Read $ Dex ba adrx],st,adr) where (_,declDep,adrx) = st x ba = baseAdr declDep dep Zielcodeerläuterung: Read $ Dex ba adrx Speichern des ersten Elementes c des Ein/Ausgabestroms io unter der Adresse Dex ba adrx, die sich aus der Basisadresse ba des Kellerbereichs, in dem x deklariert wurde, und der in der Symboltabelle gespeicherten Relativadresse adrx von x ergibt. c wird aus io entfernt. 294 Übersetzung eines Schreibbefehls write_ :: ExpStack -> ComStack write_ e lab st dep adr = (fst (e lab st dep)++[Write,Pop],st,adr) Zielcodeerläuterung: Zielcode für den Ausdruck e, der mit einem Befehl zur Kellerung des aktuellen Wertes von e schließt Write Anhängen des Wertes von e an den Ein/Ausgabestrom io Pop Entkellern des aktuellen Wertes von e code Übersetzung der Deklaration einer nichtfunktionalen Variable vardecl :: String -> TypeDesc -> ComStack vardecl x td _ st dep adr = ([Push $ Con 0],update st x (td,dep,adr),adr+1) Reservierung eines Kellerplatzes für Werte von x Außerdem trägt vardecl den zum Typ von x gehörigen Typdeskriptor sowie dep (aktuelle Schachtelungstiefe) und adr (nächste freie Relativadresse) unter x in die Symboltabelle ein. 295 Übersetzung einer Funktions- oder Prozedurdeklaration fundecl :: String -> FormsStack -> TypeDesc -> ComStack -> ComStack fundecl f pars td body lab st dep adr = (code',st1,adr+1) where codelab = lab+2 st1 = update st f (Fun td codelab,dep,adr) dep' = dep+1 (parcode,st2,_) = pars st1 dep' $ -2 coms = foldl1 seq_ $ parcode++[body] bodylab = codelab+dep+2 (code,_,local) = coms bodylab st2 dep' dep' retlab = Dex TOP $ -1 exit = bodylab+length code+local+1 code' = Push (Con 0):Jump (Con exit): codelab: Move TOP BA:pushDisplay dep STP++ bodylab: code++replicate local Pop++[Jump retlab] exit: Zielcodeerläuterung: Push $ Con 0 Jump $ Con exit Reservierung eines Kellerplatzes für den Wert eines Aufrufs von f Sprung hinter den Zielcode Der Code zwischen codelab und exit wird erst bei der Ausführung des Zielcodes eines Aufrufs von f abgearbeitet (siehe applyFun): Move TOP BA Die nächste freie Kellerposition wird zur neuen Basisadresse. 296 Dieser Befehl hat die von fundecl berechnete Nummer codelab und wird angesprungen, wenn der Befehl Jump codelab des Zielcodes eines Aufrufs von f ausgeführt wird (siehe applyProc). pushDisplay dep STP Kellern des Displays des umfassenden Prozedurrumpfs und dessen Adresse parcode++[body] erweiterter Prozedurrumpf (siehe formals) replicate local Pop Entkellern der lokalen Variablen und des Displays des Aufrufs von f Jump retlab Sprung zur Rücksprungadresse, die bei der Ausführung des Zielcodes des Aufruf von f vor dem Sprung zur Adresse des Codes von f gekellert wurde, so dass sie am Ende von dessen Ausführung wieder oben im Keller steht Übersetzung formaler Parameter formals :: [Formal] -> FormsStack formals pars st dep adr = foldl f ([],st,adr) pars where f (cs,st,adr) (Par x td) = (cs,update st x (td,dep,adr'),adr') (1) where adr' = adr-1 f (cs,st,adr) (FunPar x@('@':g) pars td) = (cs++[c],st',adr') (2) where adr' = adr-3 st' = update st x (ForFun td,dep,adr') c = fundecl g (formals pars) td $ assign g $ applyFun x $ actuals $ map act pars act (Par x _) = var x act (FunPar (_:g) _ _) = var g 297 Formale Parameter einer Prozedur g sind Variablen mit negativen Relativadressen, weil ihre aktuellen Werte beim Aufruf von g direkt vor dem für den Aufruf reservierten Kellerabschnitt abgelegt werden. Eine Variable vom Typ INT (Fall 1) benötigt einen Kellerplatz für ihre aktuelle Basisadresse, eine Prozedurvariable (Fall 2) hingegen drei: • einen für die aktuelle Basisadresse, die hier die Anfangsadresse des dem aktuellen Prozeduraufruf zugeordneten Kellerabschnitt ist, • einen für die aktuelle Codeadresse und • einen für die aktuelle Resultatadresse, das ist die Position des Kellerplatzes mit dem Wert des aktuellen Prozeduraufrufs. Der Compiler formal (siehe Java2.hs) erkennt einen formalen Funktions- oder Prozedurparameter g an dessen Parameterliste pars und speichert diesen unter dem Namen @g. formals erzeugt den attributierten Code c der Funktionsdeklaration g(pars){g = @g(pars)} und übergibt ihn als Teil von parcode an die Deklaration des statischen Vorgängers von g (s.u.). Damit werden g feste Basis-, Code- und Resultatadressen zugeordnet, so dass nicht nur die Aufrufe von g, sondern auch die Zugriffe auf g als Variable korrekt übersetzt werden können. 298 Übersetzung von Variablenzugriffen var :: String -> ExpStack var x _ st dep = case td of Fun _ codelab -> ([Push ba,Push $ Con codelab, PushA $ Dex ba adr],td) _ -> ([Push $ Dex ba adr],td) where (td,declDep,adr) = st x ba = baseAdr declDep dep (1) (2) Zielcodeerläuterung: Im Fall (1) ist x ein aktueller Parameter eines Aufrufs e = g(e1 , . . . , en ) einer Prozedur g, d.h. es gibt 1 ≤ i ≤ n mit ei = x, und x ist selbst der Name einer Prozedur! Im Quellprogramm geht e eine Deklaration von g voran, deren i-ter formaler Parameter eine Prozedurvariable f ist. f wird bei der Ausführung des Codes für e durch x aktualisiert, indem die drei von (siehe formals) für f reservierten Kellerplätze mit den o.g. drei Wertkomponenten von x belegt werden (siehe parcode in applyFun). Beim Aufruf von x werden die drei Komponenten gekellert: Push ba Push $ Con codelab PushA $ Dex ba adr Kellern der Basisadresse ba von x Kellern der Codeadresse codelab von x Kellern der Resultatadresse Dex ba adr von x, die sich aus der Basisadresse ba von x und der von fundecl bzw. formals in die Symboltabelle eingetragenen Relativadresse adr für das Resultat von e ergibt 299 Im Fall (2) genügt ein Befehl: Push $ Dex ba adr Kellern des Wertes von x, der unter der Kelleradresse Dex ba adr steht, die sich aus der Basisadresse ba von x und der in der von vardecl bzw. formals in die Symboltabelle eingetragenen Relativadresse adr von x ergibt Übersetzung von Prozeduraufrufen applyFun :: String -> ActsStack -> ExpStack applyFun f acts lab st dep = case td of Fun td codelab -> (code ba (Con codelab) $ Dex ba adr, (1) td) ForFun td -> (code (Dex ba adr) (Dex ba $ adr+1) (2) $ Dex (Dex ba $ adr+2) 0, td) where (td,declDep,adr) = st f (parcode,parLg) = acts lab st dep retlab = lab+length parcode+4 ba = baseAdr declDep dep code ba codelab result = parcode++Push BA:Move ba STP: Push (Con retlab):Jump codelab: retlab: Pop:Save BA:Pop:replicate parLg Pop++ [Push result] 300 Zielcodeerläuterung: parcode Push BA Move ba STP Push $ Con retlab Jump codelab Zielcode für die Aufrufparameter Kellern der aktuellen Basisadresse Speichern der Basisadresse von f im Register STP Kellern der Rücksprungadresse retlab Sprung zur Codeadresse codelab von f, die von fundecl berechnet wurde An dieser Stelle wird bei der Ausführung des Zielcodes zunächst code(f ) abgearbeitet (siehe fundecl). Dann geht es weiter mit: Pop Save BA Pop replicate parLg Pop Push result Entkellern der Rücksprungadresse retlab Speichern der alten Basisadresse in BA Entkellern der alten Basisadresse Entkellern der Aufrufparameter Kellern des Aufrufwertes 301 Im Fall (1) geht dem Aufruf von f im Quellprogramm eine Deklaration von f voran, deren Übersetzung die Symboltabelle um Einträge für f erweitert hat, aus denen applyFun die Adressen bzw. Befehlsnummern ba, codelab und result berechnet und in obigen Zielcode einsetzt. Im Fall (2) ist f eine Prozedurvariable, d.h. im Quellprogramm kommt der Aufruf von f im Rumpf der Deklaration einer Prozedur g vor, die f als formalen Parameter enthält. Der Zielcode wird erst bei einem Aufruf von g ausgeführt, d.h. nachdem f durch eine Prozedur h aktualisiert und die von formals für f reservierten Kellerplätze belegt wurden. Der Zielcode des Aufrufs von f ist also in Wirklichkeit Zielcode für einen Aufruf von h. Dementsprechend sind ba die Anfangsadresse des dem Aufruf zugeordneten Kellerabschnitts und damit Dex ba adr, Dex ba $ adr+1 und Dex ba $ adr+2 die Positionen der Kellerplätze mit der Basisadresse, der Codeadresse bzw. der Resultatadresse von h. Folglich kommt applyFun durch Zugriff auf diese Plätze an die aktuellen Werte von ba, codelab und result heran und kann sie wie im Fall (1) in den Zielcode des Aufrufs einsetzen. Damit Push result analog zum Fall (1) nicht die Resultatadresse von h, sondern den dort abgelegten Wert kellert, muss sie vorher derefenziert werden. Deshalb wird result im Fall (2) auf Dex (Dex ba $ adr+2) 0 gesetzt. 302 Display d. statischen Vorgängers Basisadr. d. stat. Vorgängers STP Display d. statischen Vorgängers Display d. statischen Vorgängers BA Parameter Parameter Parameter Basisadr. d. dyn. Vorgängers Basisadr. d. dyn. Vorgängers Basisadr. d. dyn. Vorgängers Rücksprungadresse Rücksprungadresse Rücksprungadresse TOP TOP BA BA Display d. statischen Vorgängers Basisadr. d. stat. Vorgängers lokale Adressen TOP Kellerzustand beim Sprung zur Codeadresse Kellerzustand während der Ausführung des Funktionsrumpfes Kellerzustand beim Rücksprung Der Zielcode der Parameterliste acts setzt sich aus dem Zielcode der einzelnen Ausdrücke von acts zusammen, wobei acts von hinten nach vorn abgearbeitet wird, weil in dieser Reihenfolge Speicherplatz für die entsprechenden formalen Parameter reserviert wurde (siehe formals). 303 Den Aufruf einer Prozedur p ohne Rückgabewert (genauer gesagt: mit Rückgabewert vom Typ UNIT) betten wir in eine Zuweisung an p ein: applyProc :: String -> ActsStack -> ComStack applyProc f p = assign p . applyFun p Übersetzung aktueller Parameter actuals :: [ExpStack] -> ActsStack actuals pars lab st dep = foldr f ([],0) pars where f e (code,parLg) = (code++code',parLg+ case td of Fun _ _ -> 3 _ -> 1) where (code',td) = e (lab+length code) st dep In [27], Kapitel 5, wird auch die Übersetzung von Feldern und Records behandelt. Grundlagen der Kompilation funktionaler Sprachen liefert [27], Kapitel 7. Die Übersetzung objektorientierter Sprachen ist Thema von [44], Kapitel5. 304 Beispiel 13.4 (Vier JavaLight+-Programme für die Fakultätsfunktion aus Java2.hs) Int x; read x; Int fact; fact=1; while x>1 fact=x*fact; x=x-1; write fact; Int f(Int x) if x<2 f=1; else f=x*f(x-1); Int x; read x; write f(x); f(Int x,Int fact) if x<2 write fact; else f(x-1,fact*x) Int x; read x; f(x,1) Int f(Int x,Int g(Int x,Int y)) if x<2 f=1; else f=g(x,f(x-1,g)); Int g(Int x,Int y) g=x*y; Int x; read x; write f(x,g); javaToStack "prog" [n] übersetzt jedes der obigen Programme in eine Befehlsfolge vom Typ [StackCom], legt diese in der Datei javacode ab und transformiert die Eingabeliste [n] in die Ausgabeliste [n!]. Der Zielcode des vierten Programms lautet wie folgt: 0: Push (Con 0) 1: Jump (Con 68) 305 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: Move Push Push Jump Move Push Push Push Push Push Move Push Jump Pop Save Pop Pop Pop Push Save Pop Pop Pop Jump Push TOP BA STP (Con 0) (Con 26) TOP BA (Dex STP 0) STP (Dex BA (-4)) (Dex BA (-3)) BA (Dex (Dex BA 1) (-6)) (Con 15) (Dex (Dex BA 1) (-5)) begin f begin g y x STP BA (Dex (Dex (Dex BA 1) (-4)) 0) (Dex (Dex BA 1) 1) g=@g(x,y) (Dex TOP (-1)) (Dex BA (-3)) end g x 306 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: Push (Con 2) Cmp "<" JumpF 34 Push (Con 1) Save (Dex (Dex BA 0) 0) Pop Jump (Con 65) Push BA Push (Con 6) PushA (Dex BA 1) Push (Dex BA (-3)) Push (Con 1) Sub Push BA Move (Dex BA 0) STP Push (Con 44) Jump (Con 2) Pop Save BA Pop Pop Pop Pop Pop Push (Dex (Dex BA 0) 0) x<2 f=1 g g g x x-1 jump to f f(x-1,g) 307 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: Push Push Move Push Jump Pop Save Pop Pop Pop Push Save Pop Pop Pop Jump Push Jump Move Push Push Push Mul Save Pop (Dex BA (-3)) BA BA STP (Con 57) (Con 6) x jump to g BA (Dex BA 1) (Dex (Dex BA 0) 0) g(x,f(x-1,g)) f=g(x,f(x-1,g)) (Dex TOP (-1)) (Con 0) (Con 79) TOP BA STP (Dex BA (-3)) (Dex BA (-4)) end f (Dex (Dex BA 0) 1) begin g x y x*y g=x*y 308 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: Pop Jump (Dex TOP (-1)) Push (Con 0) Read (Dex BA 2) Push BA Push (Con 70) PushA (Dex BA 1) Push (Dex BA 2) Push BA Move BA STP Push (Con 89) Jump (Con 2) Pop Save BA Pop Pop Pop Pop Pop Push (Dex BA 0) Write Pop end g read x g g g x jump to f f(x,g) write f(x,g) 309 14 Mehrpässige Compiler Sei G = (S, BS, R) eine CFG und A = (A, Op) eine Σ(G)-Algebra. Wir kommen zurück auf die in Kapitel 12 behandelte Form Av1 × . . . × Avm → Aa1 × . . . × Aan der Trägermengen von A und sehen uns das Schema der Definition einer Operation von A mal etwas genauer an. O.B.d.A. setzen wir für die allgemeine Betrachtung voraus, dass alle Trägermengen von A miteinander übereinstimmen. Sei c : s1 × . . . × sk → s ein Konstruktor von Σ(G). Das Schema einer Interpretation cA : Ak → A von c in A offenbar wie folgt: cA(f1, . . . , fk )(x1, . . . , xm) = (t1, . . . , tn) where (x11, . . . , x1n) = f1(t11, . . . , t1m) ... (1) (xk1, . . . , xkn) = fk (tk1, . . . , tkm) Hier sind f1, . . . , fk , x1, . . . , xm, xi1, . . . , x1n, . . . , xk1, . . . , xkn paarweise verschiedene Variablen und e1, . . . , en, xi1, . . . , t1m, . . . , tk1, . . . , tkm Σ(G)-Terme. 310 Ist s → w0s1w1 . . . sk wk die Grammatikregel, aus der c hervorgeht, dann wird (1) oft als Liste von Zuweisungen an die Attribute der Sorten s, s1, . . . , sk geschrieben: s.a1 := t1 . . . s.an := tn s1.v1 := t11 . . . s1.vm := t1m ... sk .v1 := tk1 . . . sk .vm := tkm cA ist genau dann wohldefiniert, wenn für alle f1, . . . , fk , x1, . . . , xm (1) eine Lösung in A hat, d.h., wenn es eine Belegung g der Variablen xi1, . . . , x1n, . . . , xk1, . . . , xkn in A gibt mit (g(xi1), . . . , g(xin)) = f1(g ∗(ti1), . . . , g ∗(tim)) für alle 1 ≤ i ≤ k. Hinreichend für die Lösbarkeit ist die Zyklenfreiheit der Benutzt-Relation zwischen den Termen und Variablen von (1). Enthält sie einen Zyklus, dann versucht man, die parallele Berechnung und Verwendung von Attributwerten in r hintereinander ausgeführte Schritte (“Pässe”) zu zerlegen, so dass in jedem Schritt zwar nur einige Attribute berechnet bzw. benutzt werden, die Komposition der Schritte jedoch eine äquivalente Definition von cA liefert. Notwendig wird die Zerlegung der Übersetzung zum Beispiel dann, wenn die Quellsprache Deklarationen von Variablen verlangt, diese aber im Text des Quellprogramms bereits vor ihrer Deklaration benutzt werden dürfen. 311 Zerlegt werden müssen die Menge At = {v1, . . . , vm, a1, . . . , an} aller Attribute in r Teilmengen At1, . . . , Atr sowie jedes (funktionale) Argument fi von cA, 1 ≤ i ≤ k, in r Teilfunktionen fi1, . . . , fir derart, dass die Benutzt-Relation in jedem Pass azyklisch ist. Um die Benutzt-Relation zu bestimmen, erweitern wir die Indizierung der äußeren Variablen bzw. Terme von (1) wie folgt: cA(f1, . . . , fk )(x01, . . . , x0m) = (t(k+1)1, . . . , t(k+1)n) where (x11, . . . , x1n) = f1(t11, . . . , t1m) ... (xk1, . . . , xkn) = fk (tk1, . . . , tkm) (2) Für jeden Konstruktor c und jedes Attributpaar (at, at0) ist der Abhängigkeitsgraph depgraph(c)(at, at0) ⊆ {0, . . . , k} × {1, . . . , k + 1} für c und (at, at0) wie folgt definiert: 312 ∃ 1 ≤ i ≤ m, 1 ≤ j ≤ n : at = vi ∧ at0= aj (at vererbt, at0 abgeleitet) {(0, k + 1)} falls x0i in t(k+1)j vorkommt, ⇒ depgraph(c)(at, at0) = ∅ sonst, ∃ 1 ≤ i, j ≤ m : at = vi ∧ at0 = vj (at und at0 vererbt) ⇒ depgraph(c)(at, at0) = {(0, s) | 0 ≤ s ≤ k, x0i kommt in tsj vor}, ∃ 1 ≤ i ≤ n, 1 ≤ j ≤ m : at = ai ∧ at0 = vj (at abgeleitet, at0 vererbt) ⇒ depgraph(c)(at, at0) = {(r, s) | 0 ≤ r, s ≤ k, xri kommt in tsj vor}, ∃ 1 ≤ i, j ≤ n : at = ai ∧ at0 = aj (at und at0 abgeleitet) ⇒ depgraph(c)(at, at0) = {(r, k + 1) | 0 ≤ r ≤ k, xri kommt in t(k+1)j vor}. (2) hat genau dann eine Lösung in A (die durch Auswertung der Terme von (2) berechnet werden kann), wenn • für alle Konstruktoren c von Σ(G), at, at0 ∈ At und (i, j) ∈ depgraph(c)(at, at0) i < j gilt. Ist diese Bedingung verletzt, dann wird At so in Teilmengen At1, . . . , Atr zerlegt, dass für alle at, at0 ∈ At entweder at in einem früheren Pass als at0 berechnet wird oder beide Attribute in demselben Pass berechnet werden und die Bedingung erfüllen. 313 Für alle 1 ≤ p, q ≤ r, at ∈ Atp und at0 ∈ Atq muss also Folgendes gelten: p < q ∨ (p = q ∧ ∀ (i, j) ∈ depgraph(c)(at, at0) : i < j). (3) Für alle 1 ≤ p ≤ r und 1 ≤ i ≤ k sei {vip1 , . . . , vipmp } = {v1, . . . , vm} ∩ Atp, {ajp1 , . . . , ajpnp } = {a1, . . . , an} ∩ Atp, πp : Av1 × . . . × Avm (x1, . . . , xm) πp0 : Aa1 × . . . × Aan (x1, . . . , xn) → 7→ → 7→ Avip1 × . . . × Avipmp (xip1 , . . . , xipmp ), Aajp1 × . . . × Aajpnp (xjp1 , . . . , xjpnp ) und fip : Avi1 × . . . × Avimp → Aaj1 × . . . × Aajnp die eindeutige Funktion, die das folgende Diagramm kommutativ macht: πp Av1 × . . . × Avm Avip1 × . . . × Avipmp fi fip g g πp0 Aa1 × . . . × Aan Aajp1 × . . . × Aajpnp 314 Die zu (2) äquivalente Definition von cA mit r Pässen lautet wie folgt: cA(f1, . . . , fk )(x01, . . . , x0m) = (t(k+1)1, . . . , t(k+1)n) where (x1j11 , . . . , x1j1n1 ) = f11(t1i11 , . . . , t1i1m1 ) ... Pass 1 (xkj11 , . . . , xkj1n1 ) = fk1(tki11 , . . . , tki1m1 ) ... ... (x1jr1 , . . . , x1jrnr ) = f1r (t1ir1 , . . . , t1irmr ) ... Pass r (xkjr1 , . . . , xkjrnr ) = fkr (tkir1 , . . . , tkirmr ) Der LAG-Algorithmus (Left-to-right-Attributed-Grammar) berechnet die kleinste Zerlegung von At, die (3) erfüllt, sofern eine solche existiert. Sei ats = At und constrs die Menge aller Konstruktoren von Σ(G). Ausgehend von der einelementigen Zerlegung [ats] verändert check partition die letzten beiden Elemente (curr und next) der jeweils aktuellen Zerlegung, bis entweder next leer und damit eine Zerlegung gefunden ist, die (3) erfüllt, oder curr leer ist, was bedeutet, dass keine solche Zerlegung existiert. 315 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 von ats, die (3) erfüllt (_,[]) -> [] Es gibt keine Zerlegung, die (3) erfüllt. _ -> check_partition [] (next':curr':partition) Die aktuelle Zerlegung wird um das Element nextfl erweitert. where (next',curr',changed) = foldl check_constr (next,curr,False) constrs check_constr state c = foldl (check_atpair deps) state [(at,at') | at <- ats, at' <- ats, not $ null $ deps (at,at')] where deps = depgraph c check_atpair deps state atpair = foldl (check_dep atpair) state $ deps atpair 316 check_dep (at,at') state@(next,curr,changed) (i,j) = if at' `elem` curr && ((at `elem` curr && i>=j) || at `elem` next) Die aktuelle Zerlegung next:curr:... verletzt (3). then (at':next,curr`minus`[at'],True) atfl wird vom vorletzten Zerlegungselement (curr) zum letzten (next) verschoben. else state 317 15 Funktoren und Monaden in Haskell Typen und Typvariablen höherer Ordnung Während bisher nur Typvariablen erster Ordnung vorkamen, sind Funktoren und Monaden Instanzen der Typklasse Functor bzw. Monad, die eine Typvariable zweiter Ordnung enthält. Typvariablen erster Ordnung werden durch Typen erster Ordnung instanziiert wie z.B. Int oder Bool. Typvariablen zweiter Ordnung werden durch Typen zweiter Ordnung instanziiert wie z.B. durch den folgenden: data Tree a = F a [Tree a] | V a Demnach ist ein Typ erster Ordnung eine Menge und ein Typ zweiter Ordnung eine Funktion von einer Menge von Mengen in eine – i.d.R. andere – Menge von Mengen: Tree bildet jede Menge A auf eine Menge von Bäumen ab, deren Knoteneinträge Elemente von A sind. Funktoren Die meisten der in Abschnitt 5.1 definierten Funktoren sind in Haskell stndardmäßig als Instanzen der Typklasse Functor implementiert: 318 class Functor f where fmap :: (a -> b) -> f a -> f b newtype Id a = Id {run :: a} Identitätsfunktor instance Functor Id where fmap h (Id a) = Id $ h a instance Functor [ ] where fmap = map data Maybe a = Just a | Nothing Listenfunktor Ausnahmefunktoren instance Functor Maybe where fmap f (Just a) = Just $ f a fmap _ _ = Nothing data Either e a = Left e | Right a instance Functor Either e where fmap f (Right a) = Right $ f a fmap _ e = e 319 instance Functor ((->) state) where fmap f h = f . h instance Functor ((,) state) where fmap f (st,a) = (st,f a) Leserfunktor Schreiberfunktor Transitionsfunktoren newtype Trans state a = T {runT :: state -> (a,state)} instance Functor (Trans state) where fmap f (T h) = T $ (\(a,st) -> (f a,st)) . h newtype TransM state m a = TM {runTM :: state -> m (a,state)} instance Monad m => Functor (TransM state m) where s.u. fmap f (TM h) = TM $ (>>= \(a,st) -> return (f a,st)) . h Anforderungen an die Instanzen der Typklasse Functor: Für alle Mengen a, b, c, f : a → b und g : b → c, fmap id fmap (f . g) = = id fmap f . fmap g 320 Monaden Monad ist eine Unterklasse von Functor: class Functor m return :: (>>=) :: (>>) :: fail :: m >> m' = => Monad m where a -> m a Einheit η m a -> (a -> m b) -> m b bind-Operatoren m a -> m b -> m b String -> m a Wert im Fall eines Matchfehlers m >>= const m' instance Monad Id where return = Id Id a >>= f = f a instance Monad [ ] where return a = [a] (>>=) = flip concatMap fail _ = [] instance Monad Maybe where return = Just Just a >>= f = f a e >>= _ = e fail _ = Nothing Identitätsmonade Listenmonade Ausnahmemonaden 321 instance Monad (Either e) where return = Right Right a >>= f = f a e >>= _ = e instance Monad ((->) state) where return = const Lesermonade (h >>= f) st = f (h st) st class Monoid a where mempty :: a; mappend :: a -> a -> a Schreibermonade instance Monoid state => Monad ((,) state) where return a = (mempty,a) (st,a) >>= f = (st `mappend` st',b) where (st',b) = f a instance Monad (Trans state) where Transitionsmonaden return a = T $ \st -> (a,st) T h >>= f = T $ (\(a,st) -> runT (f a) st) . h instance Monad m => Monad (TransM state m) where return a = TM $ \st -> return (a,st) TM h >>= f = TM $ (>>= \(a,st) -> runTM (f a) st) . h 322 Hier komponiert der bind-Operator >>= zwei Zustandstransformationen (h und dann f ) sequentiell. Dabei liefert die von der ersten erzeugte Ausgabe die Eingabe der zweiten. Trans(state) und TransM(state) werden auch Zustandsmonade bzw. (Zustands-)Monadentransformer genannt. Wir bevorzugen den Begriff Transitionsmonade, weil Zustände auch zu Leser- und Schreibermonaden gehören (s.o.), aber nur Transitionsmonaden aus Zustandstransformationen bestehen. Anforderungen an die Instanzen von Monad: Für alle m ∈ m(a), f : a → m(b) und g : b → m(c), (m >>= f) >>= g m >>= return (>>= f) . return m >>= f = = = = m >>= (>>= g . f) m f fmap f m >>= id (1) (2) (3) Anforderungen an die Instanzen von Monoid: (a `mappend` b) `mappend` c mempty `mappend` a a `mappend` mempty = = = a `mappend` (b `mappend` c) a a 323 Plusmonaden MonadPlus ist eine Unterklasse von Monad: class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a scheiternde Berechnung parallele Komposition instance MonadPlus Maybe where mzero = Nothing Nothing `mplus` m = m m `mplus` _ = m instance MonadPlus [ ] where mzero = []; mplus = (++) instance MonadPlus m => MonadPlus (TransM state m) where mzero = TM $ const mzero TM g `mplus` TM h = TM $ liftM2 mplus g h s.u. Anforderungen an die Instanzen von MonadPlus: mzero >>= f m >>= const mzero mzero `mplus` m = = = mzero mzero m (4) (5) 324 m `mplus` mzero = m Wir werden im folgenden Kapitel anstelle von MonadPlus eine auf die Implementierung von Compilern zugeschnittene Unterklasse von Monad einführen, die Bedingungen nicht nur an die Menge state der Zustände (die dort den möglichen Compiler-Eingaben entsprechen), sondern auch an die eingebettete Monade m stellt. Monaden-Kombinatoren sequence :: Monad m => [m a] -> m [a] sequence (m:ms) = do a <- m; as <- sequence ms; return $ a:as sequence _ = return [] sequence(ms) führt die Prozeduren der Liste ms hintereinander aus. Wie bei some(m) und many(m) werden die dabei erzeugten Ausgaben aufgesammelt. Die do-Notation für monadische Ausdrücke wurde in Kapitel 6 eingeführt. sequence_ :: Monad m => [m a] -> m () sequence_ = foldr (>>) $ return () sequence (ms) arbeitet wie sequence(ms), vergisst aber die erzeugten Ausgaben. 325 Die folgenden Funktionen führen die Elemente mit map bzw. zipWith erzeugter Prozedurlisten hintereinander aus: 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 liftM2 liftet eine zweistellige Funktion f : A → (B → C) zu einer Funktion des Typs M (A) → (M (B) → M (C)): liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c liftM2 f ma mb = do a <- ma; b <- mb; return $ f a b liftM2 gehört zum ghc-Modul Control.Monad. 326 16 Monadische Compiler Trans state a state -> (a,state) state = internal system states total functions IO a TransM state m a state -> m (a,state) m = [ ] m = Maybe partial functions nondeterministic functions Monad m compilers returning a target object or an error message compilers returning a list of target objects Compiler input m input = (Pos,String) m = Either String input = String m = [ ] TransM input m a input -> m (a,input) 327 Sei G = (S, BS, R) eine CFG, X = S BS und M eine Compilermonade (siehe Kapitel 5). Ein generischer Compiler ∗ compileG = (compileA G : X → M (A))A∈AlgΣ(G) , genauer gesagt: die von compileG verwendeten Transitionsfunktionen ∗ ∗ transA s : X → M (As × X ), s ∈ S ∪ BS, A ∈ AlgΣ(G), ließen sich als Transitionsmonaden vom Typ TransM(String)(M) implementieren. Oft erfordern die Übersetzung oder auch die Ausgabe differenzierter Fehlermeldungen zusätzliche Informationen über die Eingabe wie z.B. Zeilen- und Spaltenpositionen von Zeichen des Eingabetextes, um Syntaxfehler den Textstellen, an denen sie auftreten können, zuzuordnen. Den Eingabetyp auf String zu beschränken ist dann nicht mehr adäquat. Deshalb ersetzen wir String durch die Typvariable input und fassen die erforderlichen, input bzw. die Compilermonade M betreffenden Grundfunktionen in folgender Unterklasse von Monad zusammen. Die Compiler selbst werden dann als Objekte vom Typ TransM(input)(M) implementiert. class Monad m => Compiler input m | input -> m, m -> input where errmsg :: input -> m a empty :: input -> Bool 328 ht plus :: input -> m (Char, input) (erstes Eingabezeichen, Resteingabe) :: m a -> m a -> m a errmsg behandelt fehlerhafte Eingaben. empty prüft, ob die Eingabe leer ist. Wenn nicht, dann liefert ht das erste Zeichen der Eingabe und die Resteingabe. Kommen im Typ einer Funktion einer Typklasse nicht alle Typvariablen der Klasse vor, dann müssen die fehlenden von den vorkommenden abhängig gemacht werden. So sind z.B. in der Typklasse Compiler die Abhängigkeiten input → m und m → input der Funktion empty bzw. plus geschuldet. Die Menge der im Programm definierten Instanzen einer Typklasse muss deren funktionale Abhängigkeiten tatsächlich erfüllen. Zwei Instanzen von Compiler mit derselben inputInstanz müssen also auch dieselbe m-Instanz haben. Zwei Instanzen der Typklasse Compiler Für mehrere korrekte Ausgaben, aber nur eine mögliche Fehlermeldung: instance Compiler String [ ] where errmsg _ = [] empty = null 329 ht (c:str) = [(c,str)] plus = (++) Für höchstens eine korrekte Ausgabe, aber mehrere mögliche Fehlermeldungen: type Pos = (Int,Int) instance Compiler (Pos,String) (Either String) where errmsg (pos,_) = Left $ "error at position "++show pos empty = null . snd ht ((i,j),c:str) = Right (c,(pos,str)) where pos = if c == '\n' then (i+1,1) else (i,j+1) Left _ `plus` m = m m `plus` _ = m Hier sind die Eingaben vom Typ (Pos,String). Am Argument ((i,j),c:str) von ht erkennt man, dass, bezogen auf die gesamte Eingabe, c in der i-ten Zeile und j-ten Spalte steht. Folglich ist pos im Wert Right (c,(pos,str)) von ht((i,j),c:str) die Anfangsposition des Reststrings str. Scheitert ein Compiler dieses Typs, dann ruft er errmsg mit der Eingabeposition auf, an der er den Fehler erkannt hat, der ihn scheitern ließ. 330 16.1 Compilerkombinatoren runC :: Compiler input m => TransM input m a -> input -> m a runC comp input = do (a,input) <- runTM comp input if empty input then return a else errmsg input runC(comp)(input) führt den Compiler comp auf der Eingabe input aus und scheitert, falls comp eine nichtleere Resteingabe zurücklässt. cplus :: Compiler input m => TransM input m a -> TransM input m a -> TransM input m a TM f `cplus` TM g = TM $ liftM2 plus f g csum :: Compiler input m => [TransM input m a] -> TransM input m a csum = foldr1 cplus some, many :: Compiler input m => TransM input m a -> TransM input m [a] some comp = do a <- comp; as <- many comp; return $ a:as many comp = csum [some comp, return []] some(comp) und many(comp) wenden den Compiler comp auf die Eingabe an. Akzeptiert 331 comp ein Präfix der Eingabe, dann wird comp auf die Resteingabe angewendet und dieser Vorgang wiederholt, bis comp scheitert. Die Ausgabe beider Compiler ist die Liste der Ausgaben der einzelnen Iterationen von comp. some(comp) scheitert, wenn bereits die erste Iteration von comp scheitert. many(comp) scheitert in diesem Fall nicht, sondern liefert die leere Liste von Ausgaben. cguard :: Compiler input m => Bool -> TransM input m () cguard b = if b then return () else TM errmsg Sind (1), (4) und (5) erfüllt, dann gelten die folgenden semantischen Äquivalenzen: do cguard True; m1; ...; mn ist äquivalent zu do cguard False; m1; ...; mn ist äquivalent zu do m1; ...; mn mzero Lifting von (:) : a → [a] → [a] auf Compilerebene append :: Compiler input m => TransM input m a -> TransM input m [a] -> TransM input m [a] append = liftM2 (:) 332 16.2 Monadische Scanner Scanner sind Compiler, die einzelne Symbole erkennen. Der folgende Scanner sat(f ) erwartet, dass das Zeichen am Anfang des Eingabestrings die Bedingung f erfüllt: sat :: Compiler input m => (Char -> Bool) -> TransM input m Char sat f = TM $ \input -> do p@(c,_) <- if empty input then errmsg input else ht input if f c then return p else errmsg input Compiler input m => Char -> TransM input m Char char chr = sat (== chr) nchar :: Compiler input m => String -> TransM input m Char nchar chrs = sat (`notElem` chrs) Darauf aufbauend, erwarten die folgenden Scanner eine Ziffer, einen Buchstaben bzw. einen Begrenzer am Anfang des Eingabestrings: digit,letter,delim :: Compiler input m => TransM input m Char digit = csum $ map char ['0'..'9'] letter = csum $ map char $ ['a'..'z']++['A'..'Z'] delim = csum $ map char " \n\t" 333 Der folgende Scanner string(str) erwartet den String str am Anfang des Eingabestrings: string :: Compiler input m => String -> TransM input m String string = mapM char Die folgenden Scanner erkennen Elemente von Standardtypen und übersetzen sie in entsprechende Haskell-Typen: bool :: Compiler input m => TransM input m Bool bool = csum [do string "True"; return True, do string "False"; return False] nat,int :: Compiler input m => TransM input m Int nat = do ds <- some digit; return $ read ds int = csum [nat, do char '-'; n <- nat; return $ -n] identifier :: Compiler input m => TransM input m String identifier = append letter $ many $ nchar "(){}<>=!&|+-*/^.,:; \n\t" relation :: Compiler input m => TransM input m String relation = csum $ map string $ words "<= >= < > == !=" 334 Die Kommas trennen die Elemente der Argumentliste von csum. token(comp) erlaubt vor und hinter dem von comp erkannten String Leerzeichen, Zeilenumbrüche oder Tabulatoren: token :: Compiler a -> Compiler a token comp = do many delim; a <- comp; many delim; return a tchar tstring tbool tint tidentifier trelation = = = = = = token token token token token token . char . string bool int identifier relation 16.3 Monadische LL-Compiler Sei G = (S, BS, R) eine nicht-linksrekursive CFG. Wir setzen voraus, dass S eine ausgezeichnete Sorte start enthält, und implementieren die Übersetzungsfunktionen ∗ compileA G : X → M (A), A ∈ AlgΣ(G), und ihre Hilfsfunktionen (siehe Kapitel 6) wie folgt: Sei S = {s1, . . . , sk }. 335 compile_G :: Compiler input m => Alg s1...sk -> input -> m start compile_G = runC . trans_start Für alle B ∈ BS ∪ Z sei trans_B :: Compiler input m => TransM input m B gegeben. Seien s ∈ S und r1, . . . , rm die Regeln von R mit linker Seite s. trans_s :: Compiler input m => TransM input m s trans_s alg = csum [try_r1,...,try_rm] where ... try_ri = do a1 <- trans_e1; ...; an <- trans_en return $ f_ri alg a_i1 ... a_ik ... wobei ri = (s → e1 . . . en) und {i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS}. Beispiel Aus der abstrakten Syntax der Grammatik SAB von Beispiel 4.5 ergibt sich die folgende Haskell-Implementierung der Klasse aller Σ(SAB)-Algebren: 336 data SAB s a b = SAB {f_1 :: b -> s, f_2 :: a -> s, f_3 :: s, f_4 :: s -> a, f_5 :: a -> a -> a, f_6 :: s -> b, f_7 :: b -> b -> b,} Nach obigem Schema liefern die Regeln r1 = S → aB, r2 = S → bA, r3 = S → , r4 = A → aS r5 = A → bAA, r6 = B → bS, r7 = B → aBB von SAB die folgende Haskell-Version des generischen SAB-Compilers von Beispiel 6.1: compS :: Compiler input m => SAB s a b -> TransM input m s compS alg = csum [do char 'a'; c <- compB alg; return $ f_1 alg c, do char 'b'; c <- compA alg; return $ f_2 alg c, return $ f_3 alg] compA :: Compiler input m => SAB s a b -> TransM input m a compA alg = csum [do char 'a'; c <- compS alg; return $ f_4 alg c, do char 'b'; c <- compA alg; d <- compA alg; return $ f_5 alg c d] compB :: Compiler input m => SAB s a b -> TransM input m b compB alg = csum [do char 'b'; c <- compS alg; return $ f_6 alg c, do char 'a'; c <- compB alg; d <- compB alg; return $ f_7 alg c d] 337 In Compiler.hs steht dieser Compiler zusammen mit der Zielalgebra SABcount von Beispiel 4.5. Für m = Maybe und alle w ∈ {a, b}∗ und i, j ∈ N gilt compileSAB (SABcount)(w) = Just(i, j) genau dann, wenn i und j die Anzahl der Vorkommen von a bzw. b in w ist. o 16.4 Generischer JavaLight-Compiler (siehe Java.hs) compJava :: Compiler input m => JavaLight s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 -> TransM input m s1 compJava alg = commands where commands = do c <- command csum [do cs <- commands; return $ seq_ alg c cs, return $ embed alg c] command = csum [do tstring "if"; e <- disjunctC; c <- command csum [do tstring "else"; c' <- command return $ cond alg e c c', return $ cond1 alg e c], do tstring "while"; e <- disjunctC; c <- command return $ loop alg e c, do tchar '{'; cs <- commandsC; tchar '}' return $ block alg cs, do x <- tidentifier; tchar '='; e <- sumC tchar ';'; return $ assign alg x e] 338 sumC = do e <- prodC; f <- sumsectC; return $ sum_ alg e f sumsectC = csum [do op <- csum $ map tstring ["+","-"] e <- prodC; f <- sumsectC return $ sumsect alg op e f, return $ nilS alg] prodC = do e <- factor; f <- prodsectC; return $ prod alg e f prodsectC = csum [do op <- csum $ map tstring ["*","/"] e <- factor; f <- prodsectC return $ prodsect alg op e f, return $ nilP alg] factor = csum [do i <- tint; return $ embedI alg i, do x <- tidentifier; return $ var alg x, do tchar '('; e <- sumC; tchar ')' return $ encloseS alg e] disjunctC = do e <- conjunctC csum [do tstring "||"; e' <- disjunctC return $ disjunct alg e e', return $ embedC alg e] conjunctC = do e <- literal csum [do tstring "&&"; e' <- conjunctC return $ conjunct alg e e', return $ embedL alg e] literal = csum [do b <- tbool; return $ embedB alg b, do tchar '!'; e <- literal; return $ not_ alg e, do e <- sumC; rel <- trelation; e' <- sumC 339 return $ atom alg e rel e', do tchar '('; e <- disjunctC; tchar ')' return $ encloseD alg e] Der ensprechende Compiler für JavaLight+ (siehe Kapitel 13) steht hier. 16.5 Korrektheit des JavaLight-Compilers Sei Store = String → Z, comS = {Commands, Command }, expS = {Sum, Prod , Factor }, bexpS = {Disjunct, Conjunct, Literal }, sectS = {Sumsect, Prodsect}, Sem = javaState (siehe Abschnitt 11.5), A = javaStack (siehe Abschnitt 12.5). Dann gilt für alle Sorten s von JavaLight: Store (→ Store falls s ∈ comS , Store → Z falls s ∈ expS , Sem s = Store → 2 falls s ∈ bexpS , Store → (Z → Z) falls s ∈ sectS , 340 As = Z → StackCom ∗. Sei Mach s = (Z∗ × Store) (→ (Z∗ × Store). Die Funktionen evaluate : A → Mach und encode : Sem → Mach des Interpreterdiagramms (1) am Anfang von Kapitel 5 sind im Fall Σ = JavaLight wie folgt definiert: Für alle f ∈ Sem s, g ∈ As, stack ∈ Z∗ und store ∈ Store, (stack, g(store)) falls s ∈ comS und g(store) definiert ist, (g(store) : stack, store) falls s ∈ expS ∪ bexpS und g(store) definiert ist, encodes(g)(stack, store) = (g(store)(head(stack)) : tail(stack), store) falls s ∈ sectS , stack 6= und g(store) definiert ist, undefiniert sonst, evaluates(f )(stack, store) = hπ1, π2i(execute(f (0))(stack, store, 0)) (siehe Abschnitt 12.5). S Sei (S, BS, R) = JavaLight, X = BS und M eine Compilermonade (siehe Kapitel 5). Da compJava ein generischer Compiler für JavaLight ist, kommutiert (1) am Anfang von Kapitel 5 genau dann, wenn das folgende Diagramm kommutiert: 341 X∗ compJava(Sem) g M (Sem) compJava(A) (2) M (encode) M (A) M (evaluate) g M (Mach) (siehe Diagramm (13) in Kapitel 5). (2) beschreibt vor allem folgende Zusammenhänge zwischen compJava(A) und dem Interpreter evaluate der Zielsprache A: • Sei com ein JavaLight-Programm einer Sorte von comS . Wird com von compJava(A) in die Befehlsfolge cs übersetzt und beginnt die Ausführung von cs im Zustand state, dann endet sie in einem Zustand, dessen Speicherkomponente mit der Speicherkomponente von encode(compJava(Sem)(com))(state) übereinstimmt. • Sei exp ein JavaLight-Programm einer Sorte von expS ∪ bexpS . Wird exp von compJava(A) in die Befehlsliste cs übersetzt und beginnt die Ausführung von cs im Zustand state, dann endet sie in einem Zustand, dessen oberstes Kellerelement mit dem obersten Kellerelement von encode(compJava(Sem)(exp))(state) übereinstimmt. 342 Wie in Kapitel 5 erwähnt wurde, kommutiert (1) am Anfang von Kapitel 5 und damit auch das obige Diagramm (2), wenn Mach eine JavaLight-Algebra ist und die mehrsortigen Funktionen encode und evaluate JavaLight-homomorph sind. Um das beweisen zu können, fehlt uns neben der Interpretation des Σ(JavaLight)-Konstruktors loop in A (siehe 11.5) auch die aller Konstruktoren von Σ(JavaLight) in Mach. Wir werden darauf im Abschnitt 19.1 bzw. 19.2 zurückkommen. Eine Testumgebung für Übersetzungen des JavaLight-Compilers in die in den Kapiteln 11 und 12 definierten JavaLight-Algebren liefert die Funktion javaToAlg(file) von Java.hs, die ein Quellprogramm vom Typ commands aus der Datei file und in die jeweilige Zielalgebra überführt. javaToAlg(file)(5) und javaToAlg(file)(6) starten nach jeder Übersetzung eine Schleife, die in jeder Iteration eine Variablenbelegung einliest und die sich aus dem jeweiligen Zielcode entsprechenden Zustandstransformation darauf anwendet. Eine Testumgebung für Übersetzungen des JavaLight-Compilers in die im Abschnitt 13.3 definierte JavaLight+-Algebra javaStackP liefert die Funktion javaToStack(file) von Java2.hs, die ein Quellprogramm vom Typ commands aus der Datei file und nach javaStackP überführt. 343 16.6 Generischer XMLstore-Compiler (siehe Compiler.hs) compXML :: Compiler input m => XMLstore s1 s2 s3 s4 s5 s6 s7 s8 s9 -> TransM input m s1 compXML alg = storeC where storeC = do tstring "<store>" csum [do stck <- stock'; return $ store alg stck, do ords <- ordersC; stck <- stock' return $ storeO alg ords stck] stock' = do tstring "<stock>"; stck <- stockC tstring "</stock>"; tstring "</store>"; return stck ordersC = do (p,is) <- order csum [do os <- ordersC return $ orders alg p is os, return $ embedO alg p is] order = do tstring "<order>"; tstring "<customer>" p <- personC; tstring "</customer>" is <- itemsC; tstring "</order>"; return (p,is) personC = do tstring "<name>"; name <- text; tstring "</name>" csum [do ems <- emailsC return $ personE alg name ems, return $ person alg name] emailsC = csum [do em <- emailC; ems <- emailsC return $ emails alg em ems, return $ none alg] 344 emailC = do tstring "<email>"; em <- text; tstring "</email>" return $ email alg em itemsC = do (id,price) <- item csum [do is <- itemsC; return $ items alg id price is, return $ embedI alg id price] item = do tstring "<item>"; id <- idC; tstring "<price>" price <- text; tstring "</price>" tstring "</item>"; return (id,price) stockC = do (id,qty,supps) <- iqs csum [do is <- stockC return $ stock alg id qty supps is, return $ embedS alg id qty supps] iqs = do tstring "<item>"; id <- idC; tstring "<quantity>" qty <- tint; tstring "</quantity>" supps <- suppliers; tstring "</item>" return (id,qty,supps) suppliers = csum [do tstring "<supplier>"; p <- personC tstring "</supplier>"; return $ supplier alg p, do stck <- stockC; return $ parts alg stck] idC = do tstring "<id>"; t <- text; tstring "</id>" return $ id_ alg t text :: Compiler input m => TransM input m String text = do strs <- some $ token $ some $ nchar "< \n\t" return $ unwords strs 345 17 Induktion, Coinduktion und rekursive Gleichungen Induktion Sei CΣ = (S, I, C) eine konstruktive Signatur. Die zentrale Methode zum Beweis von Eigenschaften der initialen CΣ-Algebra - unabhängig von deren konkreter Repräsentation – ist die Induktion. Ihre Korrektheit (soundness) folgt aus Satz 3.2 (3): Sei A eine initiale CΣ-Algebra. A ist die einzige CΣ-Unteralgebra von A. (1) Sei ϕ eine prädikatenlogische Formel, die eine Eigenschaft der Elemente von A beschreibt, und B = {a ∈ A | a erfüllt ϕ}. Die Frage, ob ϕ für alle Elemente von A gilt, reduziert sich wegen (1) auf die Frage, ob B eine Σ-Unteralgebra von A ist, ob also für alle c : e → s ∈ C cA(Be) ⊆ Bs gilt. (2) Induktion heißt demnach: die Gültigkeit von ∀ x : ϕ(x) in TCΣ aus einem Beweis von (2) schließen. Induktion erlaubt es u.a., die Korrektheit als Gleichungen formulierter rekursiver Programme zu beweisen, indem man zeigt, dass deren gewünschte Semantik die Gleichungen löst: 346 Rekursive Ψ-Gleichungssysteme Sei CΣ = (S, BS, BF, C) eine konstruktive, DΣ = (S, BS 0, BF 0, D) eine destruktive Signatur und Σ = CΣ ∪ DΣ. Dann nennen wir Ψ = (CΣ, DΣ) eine Bisignatur. Eine Menge E = {dc(x1, . . . , xnc ) = td,c | c : e1 × · · · × enc → s ∈ C, d : s → e ∈ D} von Σ-Gleichungen (siehe Kapitel 3) mit folgenden Eigenschaften heißt rekursives ΨGleichungssystem: • Für alle d ∈ D und c ∈ C, free(td,c) ⊆ {x1, . . . , xnc }. • C ist die Vereinigung disjunkter Mengen C1 und C2. • Für alle d ∈ D, c ∈ C1 und Teilterme du von td,c ist u eine Variable und td,c ein Term ohne Elemente von C2. • Für alle d ∈ D, c ∈ C2, Teilterme du und Pfade p (der Baumdarstellung) von td,c besteht u aus Destruktoren und einer Variable und kommt auf p höchstens einmal ein Element von C2 vor. 347 Induktive Lösungen Sei Σ0 eine Teilsignatur einer Signatur Σ und A eine Σ-Algebra. Die in A enthaltene Σ0Algebra heißt Σ0-Redukt von A und wird mit A|Σ0 bezeichnet. Sei E ein rekursives Ψ-Gleichungssystem und A eine CΣ-Algebra. Eine induktive Lösung von E in A ist eine Σ-Algebra, deren CΣ-Redukt mit A übereinstimmt und die E erfüllt. Lambeks Lemma Sei s ∈ S. (1) Sei {c1 : e1 → s, . . . , cn : en → s} = {c : e → s0 ∈ C | s0 = s}. Die Extension A [cA 1 , . . . , cn ] ist bijektiv. Es gibt also eine Funktion dA s : As → Ae1 + · · · + Aen A A A A A mit [cA 1 , . . . , cn ] ◦ ds = idAs und ds ◦ [c1 , . . . , cn ] = idAe1 +···+Aen . (2) Sei {d1 : s1 → e1, . . . , d1 : sn → en} = {d : s0 → e ∈ C | s0 = s}. Die Extension A hdA 1 , . . . , dn i ist bijektiv. Es gibt also eine Funktion cA s : Ae1 × . . . × Aen → As A A A A A mit cA s ◦ hd1 , . . . , dn i = idAs und hd1 , . . . , dn i ◦ cs = idAe1 ×...×Aen . o 348 Satz 17.1 Sei C2 leer und A eine initiale CΣ-Algebra. Dann hat E genau eine induktive Lösung in A. Beweis. Zunächst wird die Existenz einer induktiven Lösung von E in A durch Induktion bewiesen. Wir zeigen, dass B mit Bs = {a ∈ As | für alle d : s → e ∈ D ist dA(a) definiert}, s ∈ S, eine Unteralgebra von A ist. Sei also a ∈ As. Wegen Lambeks Lemma (1) gibt es 1 ≤ i ≤ n und b ∈ Aei mit dA s (a) = (b, i) und cA i (b) = a. Sei d : s → e0 ∈ D und b = (a1, . . . , anci ) ∈ Bei , d.h. für alle 1 ≤ j ≤ nci ist dA(aj ) definiert. Dann ist auch a0 = val[a1/x1, . . . , anci /xnci ]∗(td,ci ) definiert, wobei val eine beliebige Variablenbelegung ist. Um dA an der Stelle a durch a0 zu definieren, bleibt zu zeigen, dass die Darstellung von a als Applikation cA i (b) eines Konstruktors eindeutig ist. 0 Sei 1 ≤ j ≤ n und b0 ∈ Aej mit a = cA j (b ). Dann ist (4) A A 0 A A A 0 0 0 (b, i) = dA s (a) = ds (cj (b )) = ds ([c1 , . . . , cn ](b , j)) = (idAe1 +···+Aen )(b , j) = (b , j), also b = b0 und ci = cj . 0 A Folglich liefert dA(cA i (b)) = a eine eindeutige Definition von d an der Stelle a. Damit gilt 349 (2) und wir schließen aus (1), dass dA(a) für alle a ∈ As (eindeutig) definiert ist. Auch die Eindeutigkeit der induktiven Lösung von E in A lässt sich durch Induktion zeigen: Seien A1, A2 zwei Lösungen von E in A. Wir zeigen, dass B mit Bs = {a ∈ As | für alle d : s → e ∈ D, dA1 (a) = dA2 (a)} eine Unteralgebra von A ist. Sei c : e → s ∈ C, d : s → e0 ∈ D und a = (a1, . . . , anc ) ∈ Be, d.h. für alle 1 ≤ i ≤ nc ist dA1 (ai) = dA2 (ai). Daraus folgt für val1 : V → A1 und val2 : V → A2 mit val1(xi) = val2(xi) = ai: dA1 (cA(a)) A1 löst E = val1∗(td,c) = val2∗(td,c) A2 löst E = dA2 (cA(a)). Damit gilt (2) und wir schließen aus (1), dass dA1 mit dA2 übereinstimmt. o Beispiel 17.2 (CΣ = Nat; siehe 2.9) Seien DΣ = ({nat}, ∅, ∅, {plus : nat → natnat, sum : nat → nat}), 350 Ψ = (Nat, DΣ) und x, y Variablen. Dann bilden die Gleichungen plus(zero) = λx.x, plus(succ(x)) = λy.succ(plus(x)(y)), sum(zero) = zero, sum(succ(x)) = plus(sum(x))(succ(x)) ein rekursives Ψ-Gleichungssystem E, das in der initialen Nat-Algebra Ini mit Ini nat = N, zeroIni = 0 und succIni = λn.n + 1 die induktive Lösung A hat mit plusA = λm.λn.m + n und sumA = λn.n ∗ (n + 1)/2. Nach Satz 17.1 ist A die einzige induktive Lösung von E in Ini. o Beispiel 17.3 (CΣ = List(X); siehe 2.9) Seien X eine Menge mit Halbordnung ≤: X × X → 2, DΣ = ({list}, {X, 2}, {≤: X × X → 2, ∗ : 2 × 2 → 2}, {sorted : list → 2, sorted0 : list → 2X }), Ψ = (List(X), DΣ) und x, y, s Variablen. Dann bilden die Gleichungen sorted(nil) sorted(cons(x, s)) sorted0(nil) sorted0(cons(x, s)) = = = = 1, sorted0(s)(x), λx.1, λy.(y ≤ x) ∗ sorted0(s)(x) 351 ein rekursives Ψ-Gleichungssystem E, das in der initialen List(X)-Algebra Ini mit Ini list = X ∗, nilIni = , und consIni = λ(x, w).xw die induktive Lösung A hat mit sortedA(w) = 1 ⇔ w ist bzgl. ≤ sortiert, sorted0A(w)(x) = 1 ⇔ xw ist bzgl. ≤ sortiert. o Nach Satz 17.1 ist A die einzige induktive Lösung von E in Ini. Beispiel 17.4 (CΣ = Reg(BS); siehe 2.9 und 2.10) Sei X = S CS, DΣ = ({reg}, {2, X}, {max, ∗ : 2 × 2 → 2} ∪ { ∈ C : X → 2 | C ∈ CS}, {δ : reg → reg X , β : reg → 2}) und Ψ = (Reg(BS), DΣ). Dann bildet die Menge BRE der Brzozowski-Gleichungen von Abschnitt 3.11 ein rekursives Ψ-Gleichungssystem, das in der initialen Reg(BS)-Algebra TReg(BS) die in Abschnitt 2.7 durch Bro(BS) definierte induktive Lösung A hat. Nach Satz 17.1 ist A die einzige induktive Lösung von BRE in TReg(BS). o Weitere Induktionsverfahren, mit denen Eigenschaften kleinster Relationen bewiesen werden und die daher nicht nur in initialen Modellen anwendbar sind, werden in meinen LVs über Logisch-Algebraischen Systementwurf behandelt. 352 Coinduktion Sei DΣ = (S, I, D) eine destruktive Signatur. Die zentrale Methode zum Beweis von Eigenschaften der finalen DΣ-Algebra - unabhängig von deren konkreter Repräsentation – ist die Coinduktion. Ihre Korrektheit folgt aus Satz 3.4 (3): Sei A eine finale DΣ-Algebra. Die Diagonale von A ist die einzige DΣ-Kongruenz auf A. (1) Sei E eine Menge von Σ-Gleichungen und A zu einer Σ-Algebra erweiterbar. Wegen (1) gilt E in A, wenn RE = {(g ∗(t), g ∗(u)) ∈ A2 | t = u ∈ E, g : V → A} in einer DΣ-Kongruenz R enthalten ist (siehe Termäquivalenz und Normalformen). Coinduktion heißt demnach: die Gültigkeit von E in A aus der Existenz einer DΣKongruenz schließen, die RE enthält. Beispiel 17.5 Die finale Stream(X)-Algebra A = StreamFun(X ) = (X N, Op) interpretiert die Destruktoren von Stream(X) (siehe 2.10) bzw. die Konstruktoren evens : list → list und 353 zip : list × list → list wie folgt: Für alle f : N → X und n ∈ N, headA(f ) tailA(f ) = f (0), = λn.f (n + 1), f (2n) falls n gerade ist, evensA(f )(n) = f (2n + 1) sonst, zipA(f, g)(n) = f (n/2) falls n gerade ist, g(n/2 + 1) sonst. Fin erfüllt die Gleichungen head(evens(s)) tail(evens(s)) head(zip(s, s0)) tail(zip(s, s0)) = = = = head(s) evens(tail(tail(s))) head(s) zip(s0, tail(s)) (1) (2) (3) (4) mit s, s0 ∈ Vlist. Die Gültigkeit der Gleichung evens(zip(s, s0)) = s (5) in Fin soll allein unter Verwendung von (1)-(4) gezeigt werden. 354 Gemäß Coinduktion gilt (5) in Fin, falls RE = {(evensA(zipA(f, g)), f ) | f, g : N → X} eine Stream(X)-Kongruenz ist, d.h. falls für alle (f, g) ∈ RE Folgendes gilt: headA(f ) = headA(g) ∧ (tailA(f ), tailA(g)) ∈ R. (6) Beweis von (6). Sei (f, g) ∈ RE . Dann gibt es h : N → X mit f = evensA(zipA(g, h)). Daraus folgt (1) (3) headA(f ) = headA(evensA(zipA(g, h))) = headA(zipA(g, h)) = headA(g), (2) tailA(f ) = tailA(evensA(zipA(g, h))) = evensA(tailA(tailA(zipA(g, h)))) (4) (4) = evensA(tailA(zipA(h, tail(g)))) = evensA(zipA(tailA(g), tailA(h))). Daraus folgt (tailA(f ), tailA(g)) = (evensA(zipA(tailA(g), tailA(h))), tailA(g)) ∈ RE . o Sei A eine Σ-Algebra und R ⊆ A2. Der C-Abschluss RC von R ist die kleinste Äquivalenzrelation auf A, die R enthält und für alle c : e → e0 ∈ C und a, b ∈ Ae folgende Bedingung erfüllt: (a, b) ∈ ReC ⇒ (cA(a), cA(b)) ∈ ReC0 . R ist eine DΣ-Kongruenz modulo C, wenn für alle d : s → e ∈ D und a, b ∈ Ae gilt: (a, b) ∈ Rs ⇒ (dA(a), dA(b)) ∈ ReC . 355 Ist A final und gibt es ein rekursives Ψ-Gleichungssystem, dann ist der C-Abschluss einer DΣ-Kongruenz modulo C auf A nach Satz 17.7 (11) eine DΣ-Kongruenz. Deshalb folgt die Gültigkeit von E in A bereits aus der Existenz einer DΣ-Kongruenz modulo C, die RE enthält. 356 Coinduktion modulo C heißt demnach: die Gültigkeit von E in A aus der Existenz einer DΣ-Kongruenz modulo C schließen, die RE enthält. Beispiel 17.6 S Sei X = CS und A die (Reg(BS) ∪ Acc(X))-Algebra mit A|Reg(BS) = Lang(X) und A|Acc(X) = P(X) (siehe 2.7). A erfüllt die Gleichungen δ(par(t, u)) δ(seq(t, u)) β(par(t, u)) β(seq(t, u)) par(par(t1, u1), par(t2, u2)) par(t, mt) = = = = = = λx.par(δ(t)(x), δ(u)(x)) λx.par(seq(δ(t)(x), u), ite(β(t), δ(u)(x), mt)) max{β(t), β(u)} β(t) ∗ β(u) par(par(t1, t2), par(u1, u2)) t (1) (2) (3) (4) (5) (6) mit t, u, v ∈ Vreg und x ∈ VX . Die Gültigkeit des Distributivgesetzes seq(t, par(u, v)) = par(seq(t, u), seq(t, v)) (7) in A soll allein unter Verwendung von (1)-(6) gezeigt werden. 357 Gemäß Coinduktion modulo C = {par} gilt (7) in A, falls RE = {(seq A(f, parA(g, h)), parA(seq A(f, g), seq A(f, h))) | f, g, h ∈ Lang(X)} eine Acc(X)-Kongruenz modulo C ist, d.h. falls für alle (f, g) ∈ RE Folgendes gilt: β A(f ) = β A(g) ∧ (δ A(f ), δ A(g)) ∈ REC . (8) Beweis von (8). Sei (f, g) ∈ RE und x ∈ X. Dann gibt es f 0, g 0, h ∈ A mit f = seq A(f 0, parA(g 0, h)) und g = parA(seq A(f 0, g 0), seq A(f 0, h))). Daraus folgt (4) β A(f ) = β A(seq A(f 0, parA(g 0, h))) = β A(f 0) ∗ β A(parA(g 0, h)) (3) = β A(f 0) ∗ max{β A(g 0), β A(h)} = max{β A(f 0) ∗ β A(g 0), β A(f 0) ∗ β A(h))} (4) = max{β A(seq A(f 0, g 0)), β A(seq A(f 0, h))} (3) = β A(parA(seq A(f 0, g 0), seq A(f 0, h))) = β A(g), δ A(f )(x) = δ A(seq A(f 0, parA(g 0, h))) (2) = parA(seq A(δ A(f 0)(x), parA(g 0, h)), if β A(f 0) = 1 then δ A(parA(g 0, h))(x) else mtA) (1) = parA(seq A(δ A(f 0)(x), parA(g 0, h)), if β A(f 0) = 1 then parA(δ A(g 0)(x) else δ A(h)(x), mtA)), 358 parA(seq A(δ A(f 0)(x), parA(g 0, h)), (6) = parA(δ A(g 0)(x), δ A(h)(x))) falls β A(f 0) = 1 seq A(δ A(f 0)(x), parA(g 0, h)) sonst, δ A(g)(x) = δ A(parA(seq A(f 0, g 0), seq A(f 0, h))))(x) (1) = parA(δ A(seq A(f 0, g 0))(x), δ A(seq A(f 0, h))(x)) (2) = parA(parA(seq A(δ A(f 0)(x), g 0), if β A(f 0) = 1 then δ A(g 0)(x) else mtA), A A A 0 A 0 A A par (seq (δ (f )(x), h), if β (f ) = 1 then δ (h)(x) else mt )) parA(parA(seq A(δ A(f 0)(x), g 0), δ A(g 0)(x)), parA(seq A(δ A(f 0)(x), h), δ A(h)(x))) falls β A(f 0) = 1 = parA(parA(seq A(δ A(f 0)(x), g 0), mtA), parA(seq A(δ A(f 0)(x), h), mtA)) sonst parA(parA(seq A(δ A(f 0)(x), g 0), seq A(δ A(f 0)(x), h)), (5),(6) = parA(δ A(g 0)(x), δ A(h)(x))) falls β A(f 0) = 1 parA(seq A(δ A(f 0)(x), g 0), seq A(δ A(f 0)(x), h)) sonst. Sei f1 = seq A(δ A(f 0)(x), parA(g 0, h)), f2 = parA(seq A(δ A(f 0)(x), g 0), seq A(δ A(f 0)(x), h)), f3 = parA(δ A(g 0)(x), δ A(h)(x)). 359 Wegen (f1, f2) ∈ RE gilt also (δ A(f )(x), δ A(g)(x)) = (parA(f1, f3), parA(f2, f3)) ∈ REC im Fall β A(f 0) = 1 und (δ A(f )(x), δ A(g)(x)) = (f1, f2) ∈ REC im Fall β A(f 0) = 0. o Weitere Coinduktionsverfahren, mit denen Eigenschaften größter Relationen bewiesen werden und die daher nicht nur in finalen Modellen anwendbar sind, werden in meinen LVs über Logisch-Algebraischen Systementwurf behandelt. Coinduktive Lösungen Sei E ein rekursives Ψ-Gleichungssystem und A eine DΣ-Algebra. Eine coinduktive Lösung von E in A ist eine Σ-Algebra, deren DΣ-Redukt mit A übereinstimmt und die E erfüllt. 360 Satz 17.7 Sei A eine finale DΣ-Algebra. Dann hat E genau eine coinduktive Lösung in A, die unten zur Σ-Algebra TE erweiterte Termalgebra TCΣ erfüllt E und es gilt unfold TE = fold A. Beweis. Sei V eine (S∪BS)-sortierte Variablenmenge, die die Trägermengen von A enthält. Wir erweitern die Menge TCΣ(A) der CΣ-Terme über A wie folgt zur Σ-Algebra TE (A): Für alle Operationssymbole f : e → e0 von Σ und t ∈ TCΣ(A)e, f TE (A)(t) = eval(f t), wobei eval : TΣ(V ) → TCΣ(V ) wie unten definiert ist. Die für eine induktive Definition erforderliche wohlfundierte Ordnung auf den Argumenttermen von eval lautet wie folgt: Für alle t, t0 ∈ TΣ(V ), t t0 ⇔def (depC2 (t), depD (t), size(t)) >lex (depC2 (t0), depD (t0), size(t0)). >lex ⊆ N3 ×N3 bezeichnet die lexikographische Erweiterung von >⊆ N×N auf Zahlentripel. Sei G ⊆ F . size(t) bezeichnet die Anzahl der Symbole von t, depG(t) die maximale Anzahl von G-Symbolen auf einem Pfad von t. . Die induktive Definition von eval lautet wie folgt: • Für alle x ∈ V , eval(x) = x. • Für alle f : X → Y ∈ BF ∪ BF 0 und x ∈ X, eval(f x) = f (x). 361 • Für alle f : s → e ∈ D und a ∈ As, eval(f (a)) = f A(a). • Für alle x ∈ V und t ∈ TΣ(V ), eval(λx.t) = λx.eval(t). • Für alle c : e1 × · · · × en → s ∈ C und ti ∈ TΣ(V )ei , 1 ≤ i ≤ n, eval(c(t1, . . . , tn)) = c(eval(t1), . . . , eval(tn)). • Für alle t, u ∈ TΣ(V ), eval(t(u)) = eval(t)(eval(u)). • Für alle t, u, v ∈ TΣ(V ), eval(ite(t, u, v)) = ite(eval(t), eval(u), eval(v)). • Für alle d : s → e0 ∈ D, c : e → s ∈ C und (t1, . . . , tn) ∈ TΣ(V )e, eval(dc(t1, . . . , tn)) = u{t1/x1, . . . , tn/xn, eval(u1σ)/z1, . . . , eval(uk σ)/zk }, wobei u ∈ TCΣ(V ), {z1, . . . , zk } = var(u) \ {x1, . . . , xn}, u1, . . . , uk ∈ TΣ(V ) Destruktoren und Variablen bestehen, σ = {t1/x1, . . . , tn/xn} und (1) (2) (3) aus td,c = u{u1/z1, . . . , uk /zk }. • Für alle d : s → e ∈ D, d0 : s0 → s ∈ D und u ∈ TΣ(V )s0 , eval(dd0u) = eval(d eval(d0u)). (4) (5) Für alle t ∈ TΣ(V ) ist eval(t) definiert und depC2 (eval(t)) ≤ depC2 (t). Beweis von (5) durch Induktion über t entlang . Fall (1): Es gibt f : e → e0 ∈ BF ∪ BF 0 ∪ D und a ∈ Ae mit t = f a. Dann ist eval(t) = f (a) bzw. eval(t) = f A(a) und depC2 (eval(t)) = 0 = depC2 (t). 362 Fall (2): Es gibt c : e → s ∈ C ∪ {λx. | x ∈ V } ∪ { ( ), ite} und (t1, . . . , tn) ∈ TΣ(v)e mit t = c(t1, . . . , tn). Sei 1 ≤ i ≤ n. Ist c ∈ C2, dann gilt depC2 (ti) < depC2 (t). Ist c 6∈ C2, dann gilt depG(eval(ti)) ≤ depG(t) für G ∈ {C2, D}, aber size(ti) < size(t). Demnach gilt t ti in beiden Unterfällen. Also ist nach Induktionsvoraussetzung eval(ti) definiert und depC2 (eval(ti)) ≤ depC2 (ti). Daraus folgt, dass auch eval(t) = c(eval(t1), . . . , eval(tn)) definiert ist und depC2 (eval(t)) = max{depC2 (eval(ti)) | 1 ≤ i ≤ n} ≤ max{depC2 (ti) | 1 ≤ i ≤ n} = depC2 (t) im Fall c ∈ C1 bzw. depC2 (eval(t)) = 1 + max{depC2 (eval(ti)) | 1 ≤ i ≤ n} ≤ 1 + max{depC2 (ti) | 1 ≤ i ≤ n} = depC2 (t) im Fall c ∈ C2. Fall (3): Es gibt d : s → e0 ∈ D, c : e → s ∈ C und (t1, . . . , tn) ∈ TΣ(V )e mit t = dc(t1, . . . , tn). Seien k, u, σ, z1, . . . , zk , u1, . . . , uk wie oben und 1 ≤ i ≤ k. Ist c ∈ C1, dann ist uiσ ein echter Teilterm von t und damit depG(uiσ) ≤ depG(t) für G ∈ {C2, D}, aber size(uiσ) < size(t). Ist c ∈ C2, dann gilt depC2 (uiσ) < depC2 (t). Folglich gilt t uiσ in beiden Unterfällen. Also ist nach Induktionsvoraussetzung eval(uiσ) definiert und depC2 (eval(uiσ)) ≤ depC2 (uiσ). 363 Demnach ist auch eval(t) = u{t1/x1, . . . , tn/xn, eval(u1σ)/z1, . . . , eval(uk σ)/zk } definiert und depC2 (eval(t)) ≤ depC2 (t), weil jeder Pfad von u im Fall c ∈ C1 kein C2Symbol und im Fall c ∈ C2 höchstens eins enthält. Fall (4): Es gibt d : s → e ∈ D, d0 : s0 → s ∈ D und u ∈ TΣ(V )s0 mit t = dd0u. Dann gilt depC2 (d0u) ≤ depC2 (t), aber depD (d0u) ≤ depD (t), also t d0u. Damit ist nach Induktionsvoraussetzung eval(d0u) definiert und depC2 (eval(d0u)) ≤ depC2 (d0u), also auch depC2 (d eval(d0u)) = depC2 (eval(d0u)) ≤ depC2 (t). Wegen eval(d0u) ∈ TCΣ(V ) ist jedoch depD (d eval(d0u)) < depD (t), so dass nach Induktionsvoraussetzung auch eval(t) = eval(d eval(d0u)) definiert ist und depC2 (eval(d eval(d0u))) ≤ depC2 (d eval(d0u)). Daraus folgt schließlich depC2 (eval(t)) ≤ depC2 (d eval(d0u)) ≤ depC2 (t). o Wie man ebenfalls durch Induktion über t entlang zeigen kann, kommen alle Variablen von eval(t) in t vor. Daraus folgt f TE (A)(t) = eval(f t) ∈ TCΣ(A) und f TE (A)(u) = eval(f u) ∈ TCΣ für alle Operationssymbole f : e → e0 von Σ, t ∈ TCΣ(A)e und u ∈ TCΣ,e. Folglich ist die Einschränkung TE von TE (A) auf die Menge TCΣ der CΣ-Grundterme eine DΣ-Unteralgebra von TE (A). 364 (6) Für alle c : e → s ∈ C und (t1, . . . , tn) ∈ TCΣ(A)e, cTE (A)(t1, . . . , tn) = c(t1, . . . , tn). Beweis von (6). eval(t) = t für alle t ∈ TCΣ(A) erhält man durch Induktion über die Größe von t. Daraus folgt c TE (A) (t1, . . . , tn) eval(ti )=ti = Def . cTE (A) = (2) eval(c(t1, . . . , tn)) = c(eval(t1), . . . , eval(tn)) c(t1, . . . , tn). (7) Für alle g : V → TCΣ(A) und Σ-Terme t, die aus Destruktoren und einer Variable x bestehen, gilt eval(tσ) = g ∗(t), wobei σ = {g(x)/x}. Beweis von (7) durch Induktion über die Anzahl der Destruktoren von t. Sei d1, . . . , dn ∈ D und t = d1 . . . dnx. Ist n = 0, dann gilt eval(tσ) = eval(xσ) = eval(g(x)) Andernfalls ist g(x)∈TCΣ (A) = g(x) = g ∗(x) = g ∗(t). (4) eval(tσ) = eval(d1 . . . dnxσ) = eval(d1 eval(d2 . . . dnxσ)) ind. hyp. = Def . g ∗ = T (A) ∗ eval(d1 g (d2 . . . dnx)) g ∗(d1 . . . dnx) = g ∗(t). Def . d1 E = T (A) d1 E (g ∗(d2 . . . dnx)) o 365 (8) TE (A) erfüllt E. Beweis von (8). Für alle c : s1 × · · · × sn → s ∈ C, d : s → e ∈ D und σ = g : V → TCΣ(A), ∗ g (dc(x1, . . . , xn)) Def . dTE (A) = Def . g ∗ = dTE (A)(cTE (A)(g(x1), . . . , g(xn))) (6) eval(dcTE (A)(g(x1), . . . , g(xn))) = eval(dc(g(x1), . . . , g(xn))) (3) = u{x1σ/x1, . . . , xnσ/xn, eval(u1σ)/z1, . . . , eval(uk σ)/zk } (7) = u{x1σ/x1, . . . , xnσ/xn, g ∗(u1)/z1, . . . , g ∗(uk )/zk } u∈TCΣ (V ) = g ∗(td,c). o Da TE (A) eine DΣ-Algebra und A die finale DΣ-Algebra ist, gibt es den eindeutigen DΣHomomorphismus unfold TE (A) : TE (A) → A. (9) Für alle a ∈ A, unfold TE (A)(a) = a. Beweis von (9). Für alle d : s → e ∈ D gilt dA(a) = dTE (A)(a). Folglich sind die Inklusion incA : A → TCΣ(A) und daher auch die Komposition unfold TE (A) ◦ incA : A → A DΣ-homomorph. Also stimmt diese wegen der Finalität von A mit der Identität auf A überein. o 366 A lässt sich zur CΣ-Algebra erweitern: Für alle c : e → s ∈ C und a ∈ Ae, cA(a) =def unfold TE (A)(c(a)). (10) Für alle c : s1 × · · · × sn → s ∈ C, d : s → e ∈ D und g : V → A, g ∗(dc(x1, . . . , xn)) = dA(cA(g(x1), . . . , g(xn))) (10) = dA(unfold TE (A)(c(g(x1), . . . , g(xn)))) unfold TE (A) DΣ−homomorph = unfold TE (A)(dTE (A)(c(g(x1), . . . , g(xn)))) (6) = unfold TE (A)(dTE (A)(cTE (A)(g(x1), . . . , g(xn)))) = unfold TE (A)(g ∗(dc(x1, . . . , xn))) (8) (9) = unfold TE (A)(g ∗(td,c)) = g ∗(td,c). Also gibt es eine coinduktive Lösung von E in A. (11) Die größte DΣ-Kongruenz R ist eine CΣ-Kongruenz. Beweis von (11). Sei RC der C-Abschluss von R (s.o.). Ist RC eine DΣ-Kongruenz, dann ist RC in R enthalten, weil R die größte DΣ-Kongruenz ist. Andererseits ist R in RC enthalten. Also stimmt R mit RC überein, ist also wie RC eine CΣ-Kongruenz. Demnach bleibt zu zeigen, dass RC eine DΣ-Kongruenz ist. Sei also d : s → e ∈ D und (t, u) ∈ RsC . 367 Gehört (t, u) zu R, dann gilt das auch für (dTE (A)(t), dTE (A)(u)), weil R eine DΣ-Kongruenz ist. Wegen R ⊆ RC folgt (dTE (A)(t), dTE (A)(u)) ∈ ReC . Andernfalls gibt es c : s1 × · · · × sn → s ∈ C und t1, . . . , tn, u1, . . . , un ∈ TCΣ(A) mit t = c(t1, . . . , tn), u = c(u1, . . . , un) und (ti, ui) ∈ RC für alle 1 ≤ i ≤ n. Nach Induktionsvoraussetzung gilt (d0TE (A)(ti), d0TE (A)(ui)) ∈ ReC0 für alle 1 ≤ i ≤ n und d0 : si → e0 ∈ D. Seien g, g 0 Belegungen von V in TCΣ(A) mit g(xi) = ti und g 0(xi) = ui für alle 1 ≤ i ≤ n. Wegen (6) (8) dTE (A)(t) = dTE (A)(c(t1, . . . , tn)) = dTE (A)(cTE (A)(t1, . . . , tn)) = g ∗(td,c), (6) (8) dTE (A)(u) = dTE (A)(c(u1, . . . , un) = dTE (A)(cTE (A)(u1, . . . , un)) = g 0∗(td,c) und weil RC eine CΣ-Kongruenz auf TCΣ(A) ist, folgt (dTE (A)(t), dTE (A)(u)) ∈ ReC aus (g(xi), g 0(xi)) ∈ RC für alle 1 ≤ i ≤ n. Also ist RC eine DΣ-Kongruenz. o (11) liefert folgende CΣ-Algebra B mit den Trägermengen von A: Für alle c : e → s ∈ C und t ∈ TCΣ(A)e, cB (unfold TE (A)(t)) =def unfold TE (A)(c(t)). (12) 368 cB ist wohldefiniert: Sei t, u ∈ TCΣ(A)e mit unfold TE (A)(t) = unfold TE (A)(u). Da A final ist, stimmt R nach Satz 3.4 (3) mit dem Kern von unfold TE (A) überein. Also impliziert (11), dass der Kern von unfold TE (A) eine CΣ-Kongruenz auf TCΣ(A) ist. Daraus folgt (12) (6) cB (unfold TE (A)(t)) = unfold TE (A)(c(t)) = unfold TE (A)(cTE (A)(t)) (6) (12) = unfold TE (A)(cTE (A)(u)) = unfold TE (A)(c(u)) = cB (unfold TE (A)(u)). (13) cB stimmt mit cA überein: Für alle a ∈ A, (9) (12) (10) cB (a) = cB (unfold TE (A)(a)) = unfold TE (A)(c(a)) = cA(a). (14) unfold TE (A) ist CΣ-homomorph: Für alle c : e → s ∈ C und t ∈ TCΣ(A)e, (6) (12) (13) unfold TE (A)(cTE (A)(t)) = unfold TE (A)(c(t)) = cB (unfold TE (A)(t)) = cA(unfold TE (A)(t)). Sei TE die DΣ-Unteralgebra von TE (A) mit Trägermenge TCΣ. (15) unfold TE = fold A: Da A eine finale DΣ-Algebra ist, existiert genau ein DΣ-Homomorphismus von TE nach A. Folglich stimmt unfold TE mit der Einschränkung von unfold TE (A) auf TCΣ überein. Da incTCΣ : TCΣ → TE (A) wegen (6) und unfold TE (A) wegen (14) CΣhomomorph ist, folgt (15) aus der Initialität von TCΣ. 369 unfold TE TE id incTCΣ g TCΣ TE (A) A unfold TE (A) (15) fold A id g A Es bleibt zu zeigen, dass je zwei coinduktive Lösungen A1, A2 von E in A miteinander übereinstimmen. Sei Q die kleinste S-sortige Relation auf A1 × A2, die die Diagonale von A enthält und für alle c : e → s ∈ C und a, b ∈ Ae die folgende Implikation erfüllt: (a, b) ∈ Q ⇒ (cA1 (a), cA2 (b)) ∈ Q. (16) (17) Q ist eine DΣ-Kongruenz. Beweis. Sei d : s → e ∈ D und (a, b) ∈ Qs. Gehört (a, b) zu ∆2A, dann gilt a = b, also dA(a) = dA(b). Daraus folgt (dA(a), dA(b)) ∈ Qe, weil Q die Diagonale von A enthält. 370 Andernfalls gibt es c : s1 × · · · × sn → s ∈ C und a1, . . . , an, b1, . . . , bn ∈ A mit a = cA1 (a1, . . . , an), b = cA2 (b1, . . . , bn) und (ai, bi) ∈ Q für alle 1 ≤ i ≤ n. Nach Induktionsvoraussetzung gilt (d0A(ai), d0A(bi)) ∈ Qe0 für alle 1 ≤ i ≤ n und d0 : si → e0 ∈ D. Seien g, g 0 : V → A Belegungen mit g(xi) = ai und g 0(xi) = bi für alle 1 ≤ i ≤ n. Wegen dA(a) = dA(cA1 (a1, . . . , an)) = g ∗(td,c), dA(b) = dA(cA2 (b1, . . . , bn) = g 0∗(td,c) und weil Q ein CΣ-Kongruenz ist, folgt (dA(a), dA(b)) ∈ Qe aus (g(xi), g 0(xi)) ∈ Q für alle 1 ≤ i ≤ n. o Wegen der Finalität von A ist nach Satz 3.4 (3) die Diagonale von A die einzige DΣKongruenz auf A. Also impliziert (17), dass Q mit ∆2A übereinstimmt. Sei c : e → s ∈ C und a ∈ Ae. Aus (a, a) ∈ Q und (16) folgt (cA1 (a), cA2 (a)) ∈ Qe, also cA1 (a) = cA2 (a) wegen Q = ∆2A. Demnach gilt A1 = A2. o Satz 17.8 Sei C2 leer, A die laut Satz 17.7 eindeutige coinduktive Lösung von E in der finalen DΣ-Algebra und B eine induktive Lösung von E in der initialen CΣ-Algebra TCΣ. Dann ist fold A = unfold B . 0 Beweis. Nach Satz 17.7 kann TCΣ zu einer DΣ-algebra B 0, die E und fold A = unfold B erfüllt, erweitert werden. Aus Satz 17.1 folgt B = B 0, also fold A = unfold B . o 371 Beispiel 17.9 Sei CΣ = ({list}, ∅, ∅, {evens, odds, exchange, exchange0 : list → list}), Ψ = (CΣ, Stream(X)) und s eine Variable. Dann bilden die Gleichungen head(evens(s)) head(odds(s)) head(exchange(s)) head(exchange0(s)) = = = = head(s), head(tail(s)), head(tail(s)), head(s), tail(evens(s)) tail(odds(s)) tail(exchange(s)) tail(exchange(s)) = = = = evens(tail(tail(s))), odds(tail(tail(s))), exchange0(s), exchange(tail(tail(s))) ein rekursives Ψ-Gleichungssystem E, das nach Satz 17.7 genau eine coinduktive Lösung in der finalen Stream(X)-Algebra X N hat. evens(s) und odds(s) listen die Elemente von s auf, die dort an geraden bzw. ungeraden Positionen stehen. exchange(s) vertauscht die Elemente an geraden mit denen an ungeraden Positionen. Satz 17.1 ist auf E nicht anwendbar, da hier die Menge C2 der Konstruktoren, in deren Gleichungen auf der rechten Seite Destruktoren geschachtelt auftreten dürfen, nicht leer ist. o Beispiel 17.10 Sei Ψ = (Reg(BS), DΣ) die Bisignatur von Beispiel 17.4, Σ = Reg(BS) ∪ DΣ und A die Σ-Algebra mit A|Reg(BS) = Lang(X) und A|Acc(X) = P(X). 372 A erfüllt das rekursive Ψ-Gleichungssystem BRE von Abschnitt 3.11 und ist daher nach Satz 17.7 die einzige coinduktive Lösung von BRE in P(X). ∗ Da χ : P(X ∗) → 2X (siehe 2.7) Σ-homomorph ist, wird BRE auch von der Bildalgebra χ(A) erfüllt. Demnach ist χ(A) nach Satz 17.7 die einzige coinduktive Lösung von BRE in χ(P(X)) = Beh(X, 2). Wegen TBRE = Bro(BS) folgen die Gleichungen fold Lang(X) = unfold Bro(BS) : Bro(BS) → P(X), fold regB = unfoldB Bro(BS) : Bro(BS) → Beh(X, 2) aus Satz 17.8 und damit die Acc(X)-Homomorphie beider Faltungen. Sei t ∈ TReg(BS) und v = fold Regword (BS)(t). Im Haskell-Modul Compiler.hs entspricht der Erkenner fold regB (t) bzw. unfoldB Bro(BS)(t) dem Aufruf regToAlg "" v n mit n = 1 bzw. n = 3. Er startet eine Schleife, die nach der Eingabe von Wörtern w fragt, auf die der Erkenner angewendet werden soll, um zu prüfen, ob w zur Sprache von t gehört oder nicht. o 373 Entscheidende Argumente im Beweis von Satz 17.7 entstammen dem Beweis von [37], Thm. 3.1, und [38], Thm. A.1, wo rekursive Stream(X)-Gleichungen für Stromkonstruktoren untersucht werden (siehe auch [7], Anhänge A.5 and A.6). Zur Zeit werden Satz 17.7 ähnliche Theoreme entwickelt und auf weitere destruktive Signaturen – neben Stream(X) und Acc(X) – angewendet, z.B. auf unendliche Binärbäume ([39], Thm. 2), nichtdeterministische Systeme [1], Mealy-Automaten [6], nebenläufige Prozesse [10, 35, 11] und formale Potenzreihen ([37], Kapitel 9). Hierbei wird oft von der traditionellen Darstellung rekursiver Gleichungssysteme als strukturell-operationelle Semantikregeln (SOS) ausgegangen, wobei “strukturell” und “operationell” für die jeweiligen Konstruktoren bzw. Destruktoren steht. Z.B. lauten die Gleichungen von BRE als SOS-Regeln wie folgt: δ δ eps −→ λx.mt δ mt −→ λx.mt δ B −→ λx.ite(x ∈ B, eps, mt) δ δ t −→ t0, u −→ u0 δ par(t, u) −→ λx.par(t0(x), u0(x)) δ t −→ t0, u −→ u0 δ seq(t, u) −→ λx.par(seq(t0(x), u), ite(β(t), u0(x), mt)) 374 δ t −→ t0 δ iter(t) −→ λx.seq(t0(x), iter(t)) β eps −→ 1 β β t −→ m, u −→ n β par(t, u) −→ max{m, n} β β mt −→ 0 B −→ 0 β β t −→ m, u −→ n β seq(t, u) −→ m ∗ n β iter(t) −→ 0 Im Gegensatz zur operationellen Semantik besteht eine denotationelle Semantik nicht aus Regeln, sondern aus der Interpretation von Konstruktoren und Destruktoren in einer Algebra. In der Kategorientheorie werden rekursive Gleichungssysteme und die Beziehungen zwischen ihren Komponenten mit Hilfe von distributive laws und Bialgebren verallgemeinert (siehe z.B. [40, 13, 8, 11]). 375 17.11 Cotermbasierte Erkenner regulärer Sprachen Unter Verwendung der Haskell-Darstellung von Cotermen als Elemente des rekursiven Datentyps StateC(x)(y) (siehe Beispiel 10.4) machen wir DTAcc(X) zur Reg(BS)-Algebra regC und zwar so, dass regC mit ξ(χ(Lang(X)) übereinstimmt: Für alle B ∈ BS, f, g : X → DTAcc(X), b, c ∈ 2 und t ∈ DTAcc(X), epsregC = StateC(λx.mtregC )(1), mtregC = StateC(λx.mtregC )(0), regC B = StateC(λx.if (x ∈ B) = 1 then epsregC else mtregC )(0), parregC (StateC(f )(b), StateC(g)(c)) = StateC(λx.parregC (f (x), g(x)))(max{b, c}), seq regC (StateC(f )(1), StateC(g)(c)) = StateC(λx.parregC (seq regC (f (x), StateC(g)(c)), g(x)))(c), seq regC (StateC(f )(0), t) = StateC(λx.seq regC (f (x), t))(0), iterregC (StateC(f )(b)) = StateC(λx.seq regC (f (x), iterregC (StateC(f )(b))))(1). Sei Ψ = (Reg(BS), DΣ) die Bisignatur von Beispiel 17.4, Σ = Reg(BS) ∪ DΣ und A die Σ-Algebra mit A|Reg(BS) = Lang(X) und A|Acc(X) = DTAcc(X). Da das rekursive Ψ-Gleichungssystem BRE (siehe 3.11) von A erfüllt wird und χ : P(X ∗) → ∗ ∗ 2X (siehe 2.7) und die Bijektion ξ : 2X → DTAcc(X) von Beispiel 2.20 Σ-homomorph sind, wird das rekursive Ψ-Gleichungssystem BRE von Abschnitt 3.11 BRE auch von der Bildalgebra ξ(χ(A)) erfüllt. 376 Demnach ist ξ(χ(A)) nach Satz 17.7 die einzige coinduktive Lösung von BRE in ξ(χ(P(X))) = ξ(Beh(X, 2)) = DTAcc(X). Wegen TBRE = Bro(BS) folgt außerdem fold regC = unfoldC Bro(BS) : Bro(BS) → DTAcc(X) – analog zu Beispiel 17.10 – aus Satz 17.8 und damit die Acc(X)-Homomorphie dieser Faltung. Aus der Eindeutigkeit von unfoldC Bro(BS), der Acc(X)-Homomorphie von χ und ξ (s.o.) und (1) in Abschnitt 2.11 erhalten wir unfoldC Bro(BS) = ξ ◦ χ ◦ unfold Bro(BS) = ξ ◦ χ ◦ fold Lang(X). (3) Sei t ∈ TReg(BS). Wegen (3) realisiert der initiale Automat (Bro(BS), t) den Acc(X)Coterm ξ(χ(fold Lang(X)(t))). Sei t ∈ TReg(BS) und v = fold Regword (BS)(t). Im Haskell-Modul Compiler.hs entspricht der Erkenner unfoldB DTAcc(X) (fold regC (t)) dem Aufruf regToAlg "" v 2. Dieser startet eine Schleife, die nach der Eingabe von Wörtern w fragt, auf die der Erkenner angewendet werden soll, um zu prüfen, ob w zur Sprache von t gehört oder nicht. o 377 18 Iterative Gleichungen Sei Σ = (S, I, F ) eine konstruktive Signatur und I, V endliche S-sortige Variablenmengen. Eine S-sortige Funktion E : V → TΣ(V ) heißt iteratives Σ-Gleichungssystem, falls das Bild von E keine Variablen enthält. Demnach sind iterative Gleichungssysteme spezielle Substitutionen (siehe Abschnitt 3.6). Sei A eine Σ-Algebra und AV die Menge der S-sortigen Funktionen von V nach A. g : V → A löst E in A, wenn für alle x ∈ V , g ∗(E(x)) = g(x) gilt. Lemma 18.1 Sei E 0 eine Menge von Σ-Gleichungen, A ∈ AlgΣ,E 0 und reduce : TΣ(V ) → TΣ(V ) eine Funktion, die jeden Σ-Term auf einen E 0-äquivalenten Term abbildet (siehe Kapitel 3). Für alle Lösungen g : V → A von E in A und n ∈ N gilt g ∗ = g ∗ ◦ (reduce ◦ E ∗)n. Beweis durch Induktion über n. g ∗ = g ∗ ◦ idTΣ(V ) = g ∗ ◦ (E ∗)0. 378 Da ≡E 0 die kleinste Σ-Kongruenz auf TΣ(V ) ist, die Inst(E 0) enthält, ist ≡E 0 im Kern von g ∗ enthalten. Daraus folgt nach Voraussetzung g ∗ ◦ reduce = g ∗, also g∗ ind. hyp. = g ∗ ◦ (reduce ◦ E ∗)n 3.7(1) g löst E in A = (1) (g ∗ ◦ E)∗ ◦ (reduce ◦ E ∗)n (1) = g ∗ ◦ E ∗ ◦ (reduce ◦ E ∗)n = g ∗ ◦ reduce ◦ E ∗ ◦ (reduce ◦ E ∗)n = g ∗ ◦ (reduce ◦ E ∗)n+1. o 18.2 Das iterative Gleichungssystem einer CFG S Sei G = (S, BS, R) eine CFG und X = BS. Im Folgenden erlauben wir den Konstruktoren par and seq von Reg(BS) auch mehr als zwei Argumente und schreiben daher • par(t1, . . . , tn) anstelle von par(t1, par(t2, . . . , par(tn−1, tn) . . . )) und • seq(t1, . . . , tn) anstelle von seq(t1, seq(t2, . . . , seq(tn−1, tn) . . . )). par(t) und seq(t) stehen für t. 379 G induziert ein iteratives Reg(BS)-Gleichungssystem: EG : S → TReg(BS)(S) s 7→ par(w1, . . . , wk ), wobei {w1, . . . , wk } = {w ∈ (S ∪ CS)∗ | s → w ∈ R} und für alle n > 1, e1, . . . , en ∈ S ∪ CS and s ∈ S, e1 . . . en = seq(e1, . . . , en), s = s. EG heißt Gleichungssystem von G. Satz 18.3 (Fixpunktsatz für CFGs) (i) Die Funktion solG : S → Lang(X) mit solG(s) = L(G)s für alle s ∈ S löst EG in Lang(X). Sei g : S → P(X ∗) eine Lösung von EG in Lang(X). (ii) Die S-sortige Menge Sol mit Sol s = g(s) für alle s ∈ S ist Trägermenge einer Σ(G)-Unteralgebra von Word (G). (iii) Für alle s ∈ S, solG(s) ⊆ g(s), m.a.W.: die Sprache von G ist die kleinste Lösung von EG in Lang(X). 380 Beweis. Sei g : V → Lang(X), s ∈ S, {r1, . . . , rk } die Menge aller Regeln von G mit linker Seite s. Sei 1 ≤ i ≤ k, ri = (s → wi) und dom(fri ) = ei1 × . . . × eini . Dann gibt es s1, . . . , sn ∈ S ∪ CS mit e1 . . . en = wi und g ∗(wi) = g ∗(e1 . . . en) = g ∗(seq(e1, . . . , en)) = seq Lang(X)(g ∗(e1), . . . , g ∗(en)) Word (G) = g ∗(e1) · · · · · g ∗(en) = fri (g ∗(ei1) · · · · · g ∗(eini )). (1) Beweis von (i). (G) solG(s) = L(G)s = fold Word (TΣ(G),s) s S (G) = ki=1{fold Word (fri (t)) | t ∈ TΣ(G),ei1×...×ein } s i Sk Word (G) Word (G) (fold ei1×...×ein (t)) | t ∈ TΣ(G),ei1×...×ein } = i=1{fri i i Sk Sk Word (G) Word (G) Word (G) = i=1 fri (fold ei1×...×ein (TΣ(G),ei1×...×ein )) = i=1 fri (L(G)ei1×...×eini ) i i S Word (G) = ki=1 fri (L(G)ei1 · · · · · L(G)eini ) 381 Sk Word (G) ∗ ∗ (ei1) · · · · · solG (solG (eini )) i=1 fri (1) Sk ∗ ∗ ∗ (w1), . . . , solG (wk )) = i=1 solG (wi) = parLang(X)(solG = ∗ ∗ = solG (par(w1, . . . , wk )) = solG (EG(s)). ∗ Also ist solG = solG ◦ EG und damit eine Lösung von EG in Lang(X). Beweis von (ii). Zu zeigen: Für alle 1 ≤ i ≤ k, (G) (Sol ei1 · · · · · Sol eini ) ⊆ Sol s. frWord i (2) Nach Definition von Sol gilt Sol eij = g ∗(eij ) für alle 1 ≤ j ≤ ni. Beweis von (2). Word (G) Word (G) (g ∗(ei1) · · · · · g ∗(eini )) (Sol ei1 · · · · · Sol eini ) = fri S (1) ∗ = g (wi) ⊆ ki=1 g ∗(wi) = parLang(X)(g ∗(w1), . . . , g ∗(wk )) = g ∗(par(w1, . . . , wk )) fri Def . EG = g ∗(EG(s)) = g(s) = Sol s. Beweis von (iii). Nach Satz 3.2 (3) ist L(G) = fold Word (G)(TΣ(G)) die kleinste Σ(G)Unteralgebra von Word (G). Wegen (ii) gilt demnach L(G)s ⊆ Sol s für alle s ∈ S, also solG(s) = L(G)s ⊆ Sol s = g(s). o 382 18.4 Beispiele 1. Sei X = {a, b} und G = ({A, B}, ∅, X, {A → BA, A → a, B → b}). EG ist wie folgt definiert: EG(A) = par(seq(A, B), a), EG(B) = b. Die einzige Lösung g von EG in Lang(X) lautet: g(A) = {b}∗ · {a}, g(B) = {b}. 2. Sei G = SAB (siehe Beispiel 4.5). EG ist wie folgt definiert: EG(S) = par(seq(a, B), seq(b, A), eps) EG(A) = par(seq(a, S), seq(b, A, A)), EG(B) = par(seq(b, S), seq(a, B, B)). Die einzige Lösung g von EG in Lang(X) lautet: g(S) = {w ∈ {a, b}∗ | #a(w) = #b(w)}, g(A) = {w ∈ {a, b}∗ | #a(w) = #b(w) + 1}, g(B) = {w ∈ {a, b}∗ | #a(w) = #b(w) − 1}. 383 Andererseits ist g mit g(S) = g(A) = g(B) = {a, b}+ keine Lösung von EG in Lang(X), weil a einerseits zu g(S) gehört, aber nicht zu g ∗(EG(S)) = g ∗(par(seq(a, B), seq(b, A))) = ({a} · g(B)) ∪ ({b} · g(A)). Beide Grammatiken sind nicht linksrekursiv. Deshalb haben ihre Gleichungssysteme nach Satz 18.8 (s.u.) jeweils genau eine Lösung in Lang(X). o 18.5 Von Erkennern regulärer Sprachen zu Erkennern kontextfreier Sprachen Sei Σ = (S, I, F ) eine konstruktive Signatur und V eine S-sortige Menge. ΣV =def (S, BS ∪ {Vs | s ∈ S}, BF, F ∪ {ins : Vs → s | s ∈ S}). Lemma 18.6 σV : V → TΣV bezeichnet die Substitution mit σV (x) = insx für alle x ∈ Vs und s ∈ S. Für alle ΣV -Algebren A gilt (inA)∗ = fold A ◦ σV∗ : TΣ(V ) → A, wobei inA = (inA s : Vs → As )s∈S . 384 Beweis. Wir zeigen zunächst fold A ◦ σV = inA. (3) Beweis von (3). Sei s ∈ S und x ∈ Xs. Da fold A : TΣV → A mit ins verträglich ist, gilt fold A(σV (x)) = fold A(insx) = inA s (x). Aus (3) folgt (inA)∗ = (fold A ◦ σV )∗ Satz 3.7(1) = fold A ◦ σV∗ . o Sei G = (S, BS, R) eine nicht-linksrekursive CFG und reduce die in Beispiel 3.10 beschriebene Reduktionsfunktion für reguläre Ausdrücke. Dann gibt es für alle s ∈ S ks, ns > 0, Bs,1, . . . , Bs,ns ∈ BS und Reg(BS)-Terme ts,1, . . . , ts,ns über S mit (reduce ◦ EG∗ )ks (s) = par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns )) (4) (reduce ◦ EG∗ )ks (s) = par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns ), eps). (5) oder Seps bezeichne die Menge aller Sorten von S, die (5) erfüllen. Sei Reg(BS)0 die Erweiterung von Reg(BS) um die Menge S der Sorten von G als weitere Basismenge und den Konstruktor in =def inreg : S → reg als weiteres Operationssymbol. 385 Sei DΣ wie in Beispiel 17.4 definiert, ΨS = (Reg(BS)0, DΣ) und Σ = Reg(BS)0 ∪ DΣ. Mit den Notationen von (4) und (5) erhalten wir das folgende rekursive ΨS -Gleichungssystem: rec(EG) = {δ(in(s)) = λx.σS∗ (par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))) | s ∈ S} ∪ {β(in(s)) = 1 | s ∈ Seps} ∪ {β(in(s)) = 0 | s ∈ S \ Seps}. Satz 18.7 S Sei X = BS, g : S → Lang(X) eine Lösung von EG in Lang(X) und Ag die Σ-Algebra A mit Ag |Reg(BS) = Lang(X), Ag |Acc(X) = P(X) und ins g = gs für alle s ∈ S. Ag erfüllt das rekursive ΨS -Gleichungssystem rec(EG). Beweis. Zu zeigen ist, dass für alle t = t0 ∈ rec(EG) und h : V → Ag h∗(t) = h∗(t0) gilt. Sei h : V → Ag . Für alle s ∈ S, h∗(in(s)) = inAg (s) = g(s) = g ∗(s) Lemma 18.1 = g ∗((reduce ◦ EG∗ )ks (s)) (6) Lemma 18.6 liefert g ∗ = (inAg )∗ = fold Ag ◦ σS∗ : TReg(BS)(S) → A. (7) 386 Gehört s nicht zu Seps, dann gilt g ∗((reduce ◦ EG∗ )ks (s)) = g ∗(par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns ))) Ag Ag = parAg (seq Ag (Bs,1 , g ∗(ts,1)), . . . , seq Ag (Bs,ns , g ∗(ts,ns ))) S s = ni=1 (Bs,i · g ∗(ts,i)), (8) also (6) h∗(δ(in(s))) = δ Ag (h∗(in(s))) = δ Ag (g ∗((reduce ◦ EG∗ )ks (s))) S (8) A Sns = δ g ( i=1(Bs,i · g ∗(ts,i))) = λx.δ Ag ( ni=1(Bs,i · g ∗(ts,i)))(x) S Def . δ Ag = λx.{w ∈ X ∗ | xw ∈ ni=1(Bs,i · g ∗(ts,i))} = λx.{w ∈ X ∗ | x ∈ Bs,i, w ∈ g ∗(ts,i), 1 ≤ i ≤ n} S s = λx. ni=1 {w ∈ X ∗ | x ∈ Bs,i, w ∈ g ∗(ts,i)} S ns = λx. i=1{w ∈ X ∗ | if x ∈ Bs,i then w ∈ g ∗(ts,i) else w ∈ ∅} S s = λx. ni=1 (if x ∈ Bs,i then g ∗(ts,i) else ∅) S s = λx. ni=1 (if x ∈ Bs,i then g ∗(ts,i) else mtA) S s = λx. ni=1 (if x ∈ Bs,i then g ∗(ts,i) else g ∗(mt)) S s ∗ = λx. ni=1 g (ite(x ∈ Bs,i, ts,i, mt)) = λx.g ∗(par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))) = g ∗(λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))) 387 (7) = fold Ag (σS∗ (λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt)))) = h∗(σS∗ (λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt)))) und (6) h∗(β(in(s))) = β Ag (h∗(in(s))) = β Ag (g ∗((reduce ◦ EG∗ )ks (s))) (8) A Sns Def . β Ag = β g ( i=1(Bs,i · g ∗(ts,i))) = 0 = h∗(0). Gehört s zu Seps, dann gilt g ∗((reduce ◦ EG∗ )ks (s)) = g ∗(par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns ), eps)) Ag Ag = parAg (seq Ag (Bs,1 , g ∗(ts,1)), . . . , seq Ag (Bs,ns , g ∗(ts,ns ), epsAg )) S s = ni=1 (Bs,i · g ∗(ts,i)) ∪ {}, (9) also (6) h∗(δ(in(s))) = δ Ag (h∗(in(s))) = δ Ag (g ∗((reduce ◦ EG∗ )ks (s))) S s (9) A Sns = δ g ( i=1(Bs,i · g ∗(ts,i)) ∪ {}) = λx.δ Ag ( ni=1 (Bs,i · g ∗(ts,i)) ∪ {})(x) S s Def . δ Ag = λx.{w ∈ X ∗ | xw ∈ ni=1 (Bs,i · g ∗(ts,i)) ∪ {}} S s = λx.{w ∈ X ∗ | xw ∈ ni=1 (Bs,i · g ∗(ts,i))} wie oben = h∗(σS∗ (λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt)))) 388 und (6) h∗(β(in(s))) = β Ag A(h∗(in(s))) = β Ag (g ∗((reduce ◦ EG∗ )ks (s))) (9) A Sns Def . β Ag = β g ( i=1(Bs,i · g ∗(ts,i)) ∪ {}) = 1 = h∗(1). o Satz 18.8 AsolG ist die einzige coinduktive Lösung von E = BRE ∪ rec(EG) in P(X). Proof. Nach Satz 18.3 (i) ist solG eine Lösung von EG in Lang(X). Also wird rec(EG) nach Satz 18.7 von AsolG erfüllt. Nach Beispiel 17.10 wird auch BRE von AsolG erfüllt. Folglich ist AsolG nach Satz 17.7 die einzige Lösung von E in P(X). o ∗ ∗ Da χ : P(X ∗) → 2X (siehe 2.7) und die Bijektion ξ : 2X → DTAcc(X) von Beispiel 2.20 Σ-homomorph sind, wird E auch von den Bildalgebren χ(AsolG ) und ξ(χ(AsolG )) erfüllt. Folglich ist χ(AsolG ) bzw. ξ(χ(AsolG )) nach Satz 17.7 die einzige coinduktive Lösung von E in χ(P(X)) = Beh(X, 2) bzw. ξ(χ(P(X))) = ξ(Beh(X, 2)) = DTAcc(X). Satz 18.9 solG ist die einzige Lösung von EG in Lang(X). Beweis. Sei sol eine weitere Lösung von EG in Lang(X). Dann sind nach Satz 18.7 sind AsolG und Asol coinduktive Lösungen von E in P(X). Nach Satz 18.8 stimmen sie miteinander überein. Also sind auch solG und sol gleich. o 389 rec(EG) legt folgende Erweiterung des Brzozowski-Automaten Bro(BS) zur Reg(BS)0Algebra Bro(BS)0 nahe: Für alle s ∈ S, 0 δ Bro(BS) (in(s)) = λx.σS∗ (par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))), 0 β Bro(BS) (in(s)) = if s ∈ Seps then 1 else 0. Sei Lang(X)0 = AsolG |Reg(BS)0 , regB 0 = χ(AsolG )|Reg(BS)0 , regC 0 = ξ(χ(AsolG ))|Reg(BS)0 und Σ = Reg(BS)0 ∪ DΣ. Bro(BS)0 stimmt mit der zur Σ-Algebra erweiterten Termalgebra TReg(BS)0 überein (siehe Satz 17.7). Demnach gelten die Gleichungen 0 0 fold Lang(X) = unfold Bro(BS) : Bro(BS)0 → P(X), 0 0 fold regB = unfoldB Bro(BS) : Bro(BS)0 → Beh(X, 2), 0 0 fold regC = unfoldC Bro(BS) : Bro(BS)0 → DTAcc(X), (10) (11) (12) was die Acc(X)-Homomorphie der drei Faltungen impliziert. Bro(BS)0, regB 0 und regC 0 sind in Compiler.hs durch accT rules, regB rules bzw. regC rules implementiert. Hierbei ist rules eine Liste der Regeln von G. Alle drei Algebren-Implementierungen verwenden die Funktion eqs rules, die σS∗ ◦ EG berechnet. 390 0 0 Sei s ∈ S. Die obigen Definitionen von δ Bro(BS) (in(s)) bzw. β Bro(BS) (in(s)) sind in der Implementierung von accT rules von Bro(BS)0 durch die Gleichungen 0 0 δ Bro(BS) (in(s)) = δ Bro(BS) (σS∗ (EG(s))), β Bro(BS)0 (in(s)) = β Bro(BS)0 (13) (σS∗ (EG(s))) realisiert. Wegen der Nicht-Linksrekursivität von G garantiert Haskells lazy evaluation, 0 0 dass für jeden Reg(BS)0-Grundterm t die Reduktionen von δ Bro(BS) (t) und β Bro(BS) (t) mit Hilfe von BRE und (13) terminieren und die gewünschten Ergebnisse liefern. Wie lässt sich die zugrundeliegende Eindeutigkeit der Lösung von (13) in den “Variablen” 0 0 δ Bro(BS) ◦ in und β Bro(BS) ◦ in nachweisen? 0 0 Aus (10), der Eindeutigkeit von unfoldB Bro(BS) und unfoldC Bro(BS) und der Acc(X)Homomorphie von χ und ξ folgt 0 0 0 unfoldB Bro(BS) = χ ◦ unfold Bro(BS) = χ ◦ fold Lang(X) , also auch 0 0 0 unfoldC Bro(BS) = ξ ◦ χ ◦ unfold Bro(BS) = ξ ◦ χ ◦ fold Lang(X) . 391 Also erkennt für alle Reg(BS)0-Grundterme t der initiale Automat (Bro(BS)0, t) die Spra0 che fold Lang(X) (t) von t (im Sinne von Abschnitt 2.11) und realisiert den Acc(X)-Coterm 0 ξ(χ(fold Lang(X) (t))) (im Sinne von Kapitel 20). Für Reg(BS)-Grundterme t haben wir das bereits in Beispiel 17.10 bzw. Abschnitt 17.11 gezeigt. Zusätzlich erhalten wir für alle s ∈ S: 0 (10) 0 0 unfold Bro(BS) (in(s)) = fold Lang(X) (in(s)) = inLang(X) (s) = solG(s) = L(G)s. (14) Also erkennt der initiale Automat (Bro(BS)0, in(s)) die Sprache von G, realisiert also deren charakteristische Funktion: 0 (14) 0 unfoldB Bro(BS) (in(s)) = χ(unfold Bro(BS) (in(s))) = χ(L(G)s), (15) und den entsprechenden Acc(X)-Coterm: 0 (14) 0 unfoldC Bro(BS) (in(s)) = ξ(χ(unfold Bro(BS) (in(s)))) = ξ(χ(L(G)s)). (16) Daraus folgt 0 0 (11) 0 (15) 0 0 (12) 0 (16) inregB (s) = fold regB (in(s)) = unfoldB Bro(BS) (in(s)) = χ(L(G)s), inregC (s) = fold regC (in(s)) = unfoldC Bro(BS) (in(s)) = ξ(χ(L(G)s)). (17) (18) 392 0 0 Die durch (17) und (18) gegebenen Definitionen von inregB bzw. inregC sind in den Implementierungen regB rules von regB 0 und regC rules von regC 0 durch die Gleichungen 0 0 0 0 inregB = fold regB ◦ σS∗ ◦ EG, inregC = fold regC ◦ σS∗ ◦ EG (19) realisiert. Wieder garantiert Haskells lazy evaluation wegen der Nicht-Linksrekursivität von G, dass 0 0 für jeden Reg(BS)0-Grundterm t die Reduktionen von fold regB (t) und fold regC (t) mit 0 0 Hilfe der induktiven Definition von fold regB bzw. fold regC und (19) terminieren und die gewünschten Ergebnisse liefern. Wie lässt sich die zugrundeliegende Eindeutigkeit der Lösung von (19) in den “Variablen” 0 0 inregB und inregC nachweisen? Sei t ∈ TReg(BS)0 und v = fold Regword (BS)(t). Im Haskell-Modul Compiler.hs entspricht der 0 0 0 Erkenner fold regB (t), unfoldB DTAcc(X) (fold regC (t)) bzw. unfoldB Bro(BS) (t) dem Aufruf regToAlg file v n mit n = 1, n = 2 bzw. n = 3, wobei die Datei file die Regeln von G enthält. Der Aufruf startet eine Schleife, die nach der Eingabe von Wörtern w fragt, auf die er angewendet werden soll, um zu prüfen, ob w zu L(G)t gehört oder nicht. 393 19 Interpretation in stetigen Algebren Eine Menge A ist halbgeordnet, wenn es eine Halbordnung, also eine reflexive, transitive und antisymmetrische binäre Relation auf A gibt. Eine Teilmenge {ai | i ∈ N} von A heißt ω-Kette, wenn für alle i ∈ N ai ≤ ai+1 gilt. Eine halbgeordnete Menge A heißt ω-CPO (ω-complete partially ordered set), wenn A ein bzgl. ≤ kleinstes Element ⊥ und Suprema ti∈Nai aller ω-Ketten {ai | i ∈ N} von A enthält. Für jede Menge A liefert die flache Erweiterung, A⊥ =def A + {⊥}, einen ω-CPO: Die zugrundeliegende Halbordnung ≤ ist {(a, b) ∈ A2 | a = ⊥ ∨ a = b}. Der ω-CPO der partiellen Funktionen von A nach B Für alle f, g : A (→ B, f ≤ g ⇔def def (f ) ⊆ def (g) ∧ ∀ a ∈ def (f ) : f (a) = g(a). Die nirgends definierte Funktion Ω : A (→ B mit def (Ω) =def ∅ ist das kleinste Element von A (→ B bzgl. ≤. 394 Jede ω-Kette f0 ≤ f1 ≤ f2 ≤ . . . von A (→ B hat das folgende Supremum: Für alle a ∈ A, ( fi(a) falls ∃ i ∈ N : a ∈ def (fi), (ti∈Nfi)(a) =def undefiniert sonst. Ein Produkt A1 ×· · ·×An von n ω-CPOs wie auch die Menge AB aller Funktionen von einer Menge B in einen ω-CPO A bilden selbst ω-CPOs: Die Halbordnungen auf A1, . . . , An bzw. A werden – wie im Abschnitt Kongruenzen und Quotienten beschrieben – zur Halbordnung auf A1 × · · · × An bzw. AB geliftet. Das kleinste Element von A1 × · · · × An ist das n-Tupel der kleinsten Elemente von A1, . . . , An. λx.⊥ ist das kleinste Element von AB , wobei ⊥ das kleinste Element von B ist. Suprema sind komponenten- bzw. argumentweise definiert: Für alle ω-Ketten a0 = (a0,1, . . . , a0,n) ≤ a1 = (a1,1, . . . , a1,n) ≤ a2 ≤ . . . von A1 × · · · × An, ti∈N ai =def (ti∈Nai,0, . . . , ti∈Nai,n). (2) Für alle ω-Ketten f0 ≤ f1 ≤ f2 ≤ . . . von B A und a ∈ A, (ti∈Nfi)(a) =def ti∈Nfi(a). (3) 395 Seien A, B halbgeordnete Mengen. Eine Funktion f : A → B ist monoton, wenn für alle a, b ∈ A gilt: a ≤ b ⇒ f (a) ≤ f (b). f ist strikt, wenn f das kleinste Element ⊥A von A auf das kleinste Element ⊥B von B abbildet. Ist A ein Produkt, dann bildet eine strikte Funktion f nicht nur ⊥A, sondern jedes Tupel von A, das ein kleinstes Element enthält, auf ⊥B ab. Seien A, B ω-CPOs. f ist ω-stetig (ω-continuous), wenn f (ti∈Nai) = ti∈Nf (ai) für alle ω-Ketten {ai | i ∈ N} von A gilt. ω-stetige Funktionen sind monoton. Sei A →c B die Menge aller ω-stetigen Funktionen von A nach B. A →c B ist ein ω-CPO, weil Ω und die Suprema ω-Ketten ω-stetiger Funktionen (siehe (2)) selbst ω-stetig sind. Sei Σ = (S, I, F ) eine konstruktive Signatur und A eine Σ-Algebra. A ist monoton, wenn es für alle s ∈ S eine Halbordnung ≤s,A ⊆ A2s und ein bzgl. ≤s,A kleinstes Element ⊥s,A gibt und für alle f ∈ F f A monoton ist. A ist ω-stetig, wenn A monoton ist, für alle s ∈ S As ein ω-CPO ist und für alle f ∈ F f A ω-stetig ist. 396 Fixpunktsatz von Kleene Sei A ein ω-CPO und f : A → A ω-stetig. Da f monoton ist, erhalten wir die ω-Kette ⊥ ≤ f (⊥) ≤ f 2(⊥) ≤ . . . . Deren Supremum lfp(f ) =def tn∈N f n(⊥) ist der (bzgl. ≤) kleinste Fixpunkt von f (siehe [32]). o Anwendung auf iterative Gleichungssysteme Sei E : V → TΣ(V ) ein iteratives Σ-Gleichungssystem (siehe Kapitel 18) und A eine ωstetige Σ-Algebra. Dann können die Halbordnungen, kleinsten Elemente und Suprema von A, wie in Kapitel 2 beschrieben, nach AV geliftet werden, d.h. AV ist ein ω-CPO. Die Funktion EA : AV → AV mit EA(g) = g ∗ ◦ E für alle g ∈ AV nennen wir Schrittfunktion von E. Offenbar wird E von g genau dann in A gelöst, wenn g ein Fixpunkt von EA ist. 397 Nach [5], Proposition 4.13, ist EA ω-stetig. Also folgt aus dem Fixpunktsatz von Kleene, dass (1) lfp(EA) = tn∈N EAn (λx.⊥A) der kleinste Fixpunkt von EA in A ist. Für alle strikten und ω-stetigen Σ-Homomorphismen h : A → B gilt: h ◦ lfp(EA) = lfp(EB (h)). (2) Beweis. Für alle g ∈ AV , h ◦ EA(g) Def . EA = 3.7(1) h ◦ g ∗ ◦ E = (h ◦ g)∗ ◦ E = EB (h ◦ g). (3) Wir nehmen an, dass für alle n ∈ N Folgendes gilt: h ◦ EAn (λx.⊥A) = EB (h)n(λx.⊥B ). (4) Aus (4) folgt (2): h ◦ lfp(EA) = h ◦ tn∈NEAn (λx.⊥A) h ω−stetig = tn∈N(h ◦ EAn (λx.⊥A)) (4) = tn∈NEB (h)n(λx.⊥B ) = lfp(EB (h)). 398 Zu zeigen bleibt (4). Sei n > 0. h ◦ EA0 (λx.⊥A) = h ◦ λx.⊥A h strikt = λx.⊥B = EB (h)0(λx.⊥B ), (3) h ◦ EAn (λx.⊥A) = h ◦ EA(EAn−1(λx.⊥A)) = EB (h)(h ◦ EAn−1(λx.⊥A)) ind. hyp. = EB (h)(EB (h)n−1(λx.⊥B )) = EB (h)n(λx.⊥B ). o Iterative Gleichungssysteme dienen u.a. der Spezifikation partieller Funktionen. Sind z.B. zwei partielle Funktionen f, g gegeben, dann ist die Gleichung h = g ◦ h ◦ f zunächst nur eine Bedingung an eine dritte Funktion h. Die aus der Gleichung gebildete Schrittfunktion λh.(g ◦ h ◦ f ) : (A (→ B) → (A (→ B) ist ω-stetig und hat deshalb nach dem Fixpunktsatz von Kleene einen kleinsten Fixpunkt, der als denotationelle Semantik von h bezeichnet wird. 19.1 Schleifensemantik Sei Σ die abstrakte Syntax von JavaLight (siehe Beispiel 4.6) zusammen mit zwei Konstanten e : 1 → Disjunct und c : 1 → Command . Sei Sem die Σ(JavaLight)-Algebra javaState (siehe 16.5) zusammen mit Interpretationen f ∈ javaState Disjunct von e und g ∈ javaState Command von c. 399 Sem ist ω-stetig. Sei V = {x : Command }, E das iterative Σ-Gleichungssystem E : V → TΣ(V ) x 7→ cond(e, block(seq(c, x)), id) und ESem die Schrittfunktion von E (s.o.). Der kleinste Fixpunkt von ESem liefert uns die Interpretation von loop(e, c) in Sem: loopSem (f, g) =def lfp(ESem )(x) : Store (→ Store. Die Interpretation der anderen Operationen von JavaLight in Sem ergibt sich direkt aus ihrer Haskell-Implementierung in Abschnitt 11.5. Die dortige Haskell-Funktion loop :: St Bool -> St Store -> St Store loop f g = cond f (loop f g . g) id implementiert loopSem . Für alle st ∈ Store terminiert loop(f )(g)(st) genau dann, wenn loopSem (f, g)(st) definiert ist. o 400 19.2 Semantik der Assemblersprache StackCom ∗ Auch jede Befehlsfolge cs ∈ StackCom ∗ lässt sich als iteratives Gleichungssystem eqs(cs) darstellen. Im Gegensatz zum vorigen Abschnitt liegt hier wie in Abschnitt 18.2 der einfache Fall I = ∅ vor. Die konstruktive Signatur Σ(StackCom), aus deren Operationssymbolen die Terme von eqs(cs) gebildet sind, lautet wie folgt: Σ(StackCom) = ( {com}, {Z, String}, F ), F = { push : Z → com, load, save, cmp : String → com, pop, add, sub, mul, div, or, and, inv, nix : 1 → com, cons, fork : com × com → com }. push, load, save, cmp, pop, add, sub, mul, div, or, and, inv entsprechen den Konstruktoren von StackCom. nix, cons, fork ersetzen die Sprungbefehle von StackCom (s.u.). Sei Eqs die Menge der iterativen Σ(Stackcom)-Gleichungssysteme mit Variablen aus N. Die folgendermaßen definierte Funktion eqs : StackCom ∗ → Eqs übersetzt Assemblerprogramme in solche Gleichungssysteme: 401 Sei cs ∈ StackCom ∗ und V (cs) die Menge der Nummern der Sprungziele von cs, also V (cs) = {0} ∪ {n < |cs| | Jump(n) ∈ cs ∨ JumpF (n) ∈ cs}. eqs(cs) : V (cs) → TΣ(StackCom)(V (cs)) n 7→ mkTerm(drop(n)(cs)) mkTerm : StackCom ∗ → TΣ(StackCom)(V (cs)) 7→ nix n nix c : cs0 7→ fork (n, mkTerm(cs0)) fork (nix, mkTerm(cs0)) cons(c, mkTerm(cs0)) falls c = Jump(n) ∧ n < |cs| falls c = Jump(n) ∧ n ≥ |cs| falls c = JumpF (n) ∧ n < |cs| falls c = JumpF (n) ∧ n ≥ |cs| sonst 402 Beispiel Das Assemblerprogramm von Abschnitt 12.5 zur Berechnung der Fakultätsfunktion lautet als iteratives Σ(Stackcom)-Gleichungssystem wie folgt: E : {0, 3} → TΣ(StackCom)({0, 3}) 0 7→ cons(push(1), cons(save(fact), cons(pop, 3))) 3 7→ cons(load(x), cons(push(1), cons(cmp(>), fork (nix, cons(load(fact), cons(load(x), cons(mul, cons(save(fact), cons(pop, cons(load(x), cons(push(1), cons(sub, cons(save(x), cons(pop, 3)))))))))))))) In Abschnitt 16.5 haben wir informell gezeigt, dass compJava(javaStack ) ein Compiler für JavaLight bzgl. javaState ist, was laut Kapitel 5 impliziert, dass folgendes Diagramm kommutiert: TΣ(JavaLight) fold javaState g javaState fold javaStack javaStack (1) encode evaluate g Mach = State (→ State 403 Hier sind • javaStack die in Abschnitt 12.5 definierte, zur JavaLight-Algebra erweiterte Assemblersprache StackCom ∗, • javaState das in Abschnitt 11.5 und 19.1 definierte Zustandsmodell von JavaLight, • Mach der ω-CPO der partiellen Funktionen auf der Zustandsmenge State = Z∗ × ZString , deren Paare aus jeweils einem Kellerinhalt und einer Speicherbelegung bestehen (siehe 12.5), • encode und evaluate wie in Abschnitt 16.5 definiert. Mach : State (→ State wird zunächst zu einer ω-stetigen Σ(StackCom)-Algebra erweitert: Für alle f, g : State (→ State, ⊗ ∈ {add, sub, mul, div, or, and}, (stack, store) ∈ State, a ∈ Z und x ∈ String, pushMach (i)(stack, store) = (a : stack, store), loadMach (x)(stack, store) = (store(x) : stack, store), saveMach (x)(stack, store) = (stack, update(store)(x)(a)), 404 cmpMach (x)(stack, store) = (1 : s, store) falls ∃ a, b, s : stack = a : b : s ∧ evalRel(x)(a)(b), (0 : s, store) falls ∃ a, b, s : stack = a : b : s ∧ ¬evalRel(x)(a)(b), undefiniert sonst, ( (s, store) falls ∃ a, s : stack = a : s, popMach (stack, store) = undefiniert sonst, ( (op(⊗)(b, a) : s, store) falls ∃ a, b, s : stack = a : b : s, ⊗Mach (stack, store) = undefiniert sonst, ( ((a + 1) mod 2 : stack, store) falls ∃ a, s : stack = a : s, inv Mach (stack, store) = undefiniert sonst, nixMach (stack, store) = (stack, store), consMach (f, g) = g ◦ f, fork Mach (f, g)(stack, store) = f (s, store) falls ∃ s : stack = 0 : s, g(s, store) falls ∃ a, s : stack = a : s ∧ a 6= 0, undefiniert sonst, 405 wobei op(add) = (+), op(sub) = (−), op(mul) = op(and) = (∗), op(div) = (/) und op(or) = sign ◦ (+). Sei cs ∈ StackCom ∗. Nach dem Fixpunktsatz von Kleene hat das iterative Σ(StackCom)Gleichungssystem eqs(cs) (s.o.) eine kleinste Lösung in Mach. Sie liefert uns die Funktion execute von Abschnitt 12.5: Für alle cs ∈ StackCom ∗ und n ∈ V (cs), execute(cs) = lfp(eqs(cs)Mach ) : V (cs) → Mach. (4) Tatsächlich entspricht execute0 der Haskell-Funktion execute in Abschnitt 12.5. Im Folgenden wird Mach so zu einer JavaLight-Algebra erweitert, dass execute und encode JavaLight-homomorph werden und aus der Initialität von TΣ(JavaLight) die Kommutativität von (1) folgt (siehe Kapitel 5). Für alle Sorten s von JavaLight, Mach s = State (→ State. Für alle op ∈ {seq, sum, prod, disjunct, conjunct} und f, g : State (→ State, opMach (f, g) = g ◦ f. Für alle op ∈ {embed, block, encloseS, embedC, embedL, encloseD}, opMach = idState(→State . nilS Mach = nilP Mach = idState . 406 Für alle x ∈ String und f : State (→ State, assignMach (x, f ) = popMach ◦ saveMach (x) ◦ f. Für alle f, g, h : State (→ State, condMach (f, g, h) = fork Mach (h, g) ◦ f . Für alle f, g : State (→ State, cond1Mach (f, g) = fork Mach (idMach , g) ◦ f . Für alle f, g : State (→ State, loopMach (f, g) = lfp(EMach )(x), wobei E : {x} → TΣ(StackCom)({x, f, g}) x 7→ cons(f, fork (nix, cons(g, x))) Für alle f, g : State (→ State, sumsectMach (+, f, g) = g ◦ addMach ◦ f, sumsectMach (−, f, g) = g ◦ subMach ◦ f, prodsectMach (∗, f, g) = g ◦ mulMach ◦ f, prodsectMach (/, f, g) = g ◦ div Mach ◦ f. embedI Mach = pushMach . varMach = loadMach . Für alle f : State (→ State, notMach (f ) = inv Mach ◦ f . 407 Für alle x ∈ String und f, g : State (→ State, atomMach (f, x, g) = cmpMach (x) ◦ g ◦ f. Für alle b ∈ 2, embedB Mach (b) = pushMach (b). Erweiterte Σ-Bäume Eine monotone bzw. ω-stetige Σ-Algebra T ist initial in der Klasse PAlg Σ bzw. CAlg Σ aller monotonen bzw. ω-stetigen Σ-Algebren, wenn es zu jeder monotonen bzw. ω-stetigen Σ-Algebra A genau einen strikten (!) und monotonen bzw. ω-stetigen Σ-Homomorphismus A fold A fin : T → A bzw. fold ω : T → A gibt. Alle initialen monotonen bzw. ω-stetigen Σ-Algebren sind durch strikte und monotone bzw. ω-stetige Σ-Isomorphismen miteinander verbunden. Sei Σ⊥ = (S, BS, BF, F ∪ {⊥s : 1 → s | s ∈ S}) und ≤ die kleinste reflexive, transitive und Σ-kongruente S-sortige Relation auf CTΣ⊥ derart, dass für alle s ∈ S and t ∈ CTΣ⊥,s, ⊥s ≤ t, wobei ⊥s hier den Baum bezeichnet, der aus einem mit ⊥s markierten Blatt besteht. Er bildet offenbar das bzgl. ≤ kleinste Element. F TΣ⊥ ist eine monotone und CTΣ⊥ eine ω-stetige Σ-Algebra (siehe [32]). o 408 Satz 19.3 F TΣ⊥ ist initial in PAlg Σ. CTΣ⊥ ist initial in CAlg Σ. Beweisskizze. Sei A eine monotone und B eine ω-stetige Σ-Algebra. Zwei S-sortige FunkB tionen fold A fin : F TΣ⊥ → A und fold ω : F TΣ⊥ → B erhält man wie folgt: Für alle s ∈ S und t ∈ F TΣ⊥,s und u ∈ CTΣ⊥,s, ( ⊥s,A falls t = ⊥s, fold A (t) = def fin A f A(fold A fin (λw.t(iw)), . . . , fold fin (λw.t(iw))) sonst, B fold B ω (u) =def tn∈N fold fin (u|n ), wobei für alle n ∈ N und w ∈ N∗>0, ( u|n(w) =def u(w) falls |w| ≤ n, undefiniert sonst. B Zum Nachweis der geforderten Eigenschaften von fold A fin und fold ω , siehe [5, 32]. o Die Faltung fold A ω (u) eines Σ-Baums u in A ist also das Supremum von Faltungen der endlichen Präfixe u|0, u|1, u|2, . . . von u. 409 Satz 19.4 Jedes iterative Σ-Gleichungssystem E : V → TΣ(V ) hat genau eine Lösung in CTΣ. Beweis. Sei g : V → CTΣ⊥ eine Lösung von E in CTΣ⊥ . Da lfp(ECTΣ ) die kleinste ⊥ Lösung von E in CTΣ⊥ ist, gilt: lfp(ECTΣ ) ≤ g. (5) ⊥ Durch Induktion über n lässt sich zeigen, dass für alle x ∈ V und n ∈ N gilt: n+1 def (g(x)) ∩ Nn ⊆ def (ECT (λx.⊥)(x)) Σ (6) ⊥ (siehe [25], Satz 17). Aus (6) folgt für alle x ∈ V : def (g(x)) ⊆ def n (tn∈NECT Σ ⊥ (λx.⊥)(x)) Def . lfp(ECT ) Σ = ⊥ def (lfp(ECTΣ )(x)), also g(x) = lfp(ECTΣ )(x) ∈ CTΣ wegen (5) und nach Definition von ≤. ⊥ ⊥ o Ein Σ-Baum heißt rational, wenn er nur endlich viele Unterbäume hat. Jeder Pfad eines rationalen Baums ist endlich oder periodisch. Rationale Bäume können als endliche Graphen dargestellt werden, deren Zyklen aus den Pfad-Perioden entstehen. Solche Graphen entsprechen iterativen Gleichungssystemen (siehe Kapitel 18). Ein Σ-Baum ist also genau dann rational, wenn er zum Bild der Lösung eines iterativen Σ-Gleichungssystems in CTΣ⊥ oder CTΣ gehört. 410 Beispiel loop Sei Σ = Σ(JavaLight). Die eindeutige Lösung sol : {x} → CTΣ({b, c}) des iterativen Σ-Gleichungssystems E : {x} → TΣ({x, b, c}) von Abschnitt 19.1 hat folgende Graphdarstellung: Für alle w ∈ N∗, cond b block sol(x)(w) = id seq c falls w ∈ (212)∗ falls w ∈ (212)∗1 falls w ∈ (212)∗2 falls w ∈ (212)∗3 falls w ∈ (212)∗21 falls w ∈ (212)∗211 411 Beispiel StackCom Sei Σ = Σ(StackCom) und cs das Assemblerprogramm von Beispiel 12.6. Die eindeutige Lösung sol : {0, 3} → CTΣ des iterativen Σ-Gleichungssystems eqs(cs) : {0, 3} → TΣ({0, 3}) von Abschnitt 19.2 hat folgende Graphdarstellung: 412 sol(3) = sol(0) = 413 Offenbar repräsentiert jeder Pfad des Σ-Baums sol(0) eine – möglicherweise unendliche – Kommandofolge, die einer Ausführungssequenz von cs entspricht. o Beispiel Sei Σ = Σ(StackCom). Die eindeutige Lösung sol : {x} → CTΣ({b, c}) des iterativen Σ-Gleichungssystems E : {x} → TΣ({x, b, c}), dessen kleinste Lösung in Mach den Konstruktor loop von Σ(StackCom) in Mach interpretiert (siehe 19.2), hat folgende Graphdarstellung: sol(x) = 414 Sei cs ∈ StackCom ∗. Da fold Mach strikt, ω-stetig und Σ-homomorph ist, folgt ω execute0(cs) = fold Mach ◦ lfp(eqs(cs)CTΣ ) : V (cs) → Mach ω (7) aus (2) und (4). Die Ausführung von cs im Zustand state ∈ State liefert also dasselbe – möglicherweise undefinierte – Ergebnis wie die Faltung des Σ-Baums lfp(eqs(cs)CTΣ )(state) in Mach. o Cosignaturen und ihre Algebren Die eindeutige Lösung eines iterativen Σ-Gleichungssystems E in CTΣ lässt sich nicht nur als kleinsten Fixpunkt von ECTΣ darstellen, sondern auch als Entfaltung einer Coalgebra. Dies folgt aus der Tatsache, dass CTΣ eine finale coΣ-Algebra ist, wobei coΣ die folgendermaßen aus (der konstruktiven Signatur) Σ = (S, I, F ) gebildete destruktive Signatur ist: ` coΣ = (S, I, {ds : s → f :e→s∈F e | s ∈ S}). ` Hier bezeichnet f :e→s∈F e das Coprodukt der Domains e aller Konstrukturen von F mit Range s. Das bedeutet, dass ds in einer coΣ-Algebra A als Funktion von As in die disjunkte Vereinigung ] Ae = {(a, f ) | a ∈ Ae, f : e → s ∈ F } f :e→s∈F interpretiert wird 415 Z.B. lautet die Interpretation von ds in CTΣ wie folgt: Für alle s ∈ S und t ∈ CTΣ,s mit n-stelligem Operationssymbol t(), Σ (t) = dCT def ((λw.t(1w), . . . , λw.t(nw)), t()). s CTΣ ist also nicht nur eine Σ-Algebra, sondern auch eine coΣ-Algebra. Mehr noch: Satz 19.5 CTΣ ist eine finale coΣ-Algebra. Beweis. Sei A eine coΣ-Algebra. Die S-sortige Funktion unfold A : A → CTΣ ist wie folgt definiert: Sei s ∈ S, a ∈ As, i > 0, w ∈ N∗ und dA s (a) = ((a1 , . . . , an ), f ). unfold A(a)() = f, ( unfold A(ai)(w) falls 1 ≤ i ≤ n, A unfold (a)(iw) = undefiniert sonst. Der Nachweis der geforderten Eigenschaften von unfold A wird in [32] geführt. o 416 Sei E : V → TΣ(V ) ein iteratives Σ-Gleichungssystem. E macht TΣ(V ) zur coΣ-Algebra T E : Für alle s ∈ S, f : e → s ∈ F , t ∈ TΣ(V )e und x ∈ Vs, TsE =def TΣ(V )s, E dTs (f t) =def (t, f ), E E dTs (x) =def dTs (E(x)). incV (8) V → T T E unfold → E CTΣ löst E in CTΣ (siehe [32]). (9) g : V → CTΣ löst E genau dann in CTΣ, wenn g ∗ : T E → CTΣ coΣ-homomorph ist (siehe [32]). 417 Satz 19.6 (coalgebraische Version von Satz 19.4) Jedes iterative Σ-Gleichungssystem E : V → TΣ(V ) hat genau eine Lösung in CTΣ. Beweis. Wegen (8) hat E eine Lösung in CTΣ. Angenommen, g, h : V → CTΣ lösen E in CTΣ. Dann sind g ∗, h∗ : T E → CTΣ wegen (9) coΣ-homomorph. Da CTΣ eine finale coΣ-Algebra ist, gilt g ∗ = h∗, also g = g ∗ ◦ incV = h∗ ◦ incV = h. o Sei cs ∈ StackCom ∗. Aus (7), (8) und Satz 19.4 (oder 19.6) folgt, dass execute0 die Entfaltung von V (cs) in CTΣ mit der Faltung der entstandenen Σ-Terme in Mach verknüpft: V (cs) incV execute0 fold Mach ω = g TE unfold Mach f TE CTΣ 418 Literatur [1] M. Bonsangue, J. Rutten, A. Silva, An Algebra for Kripke Polynomial Coalgebras, Proc. 24th LICS (2009) 49-58 [2] J.A. Brzozowski, Derivatives of regular expressions, Journal ACM 11 (1964) 481–494 [3] H. Comon et al., Tree Automata: Techniques and Applications, link, Inria 2008 [4] J. Gibbons, R. Hinze, Just do It: Simple Monadic Equational Reasoning, Proc. 16th ICFP (2011) 2-14 Jeremy Gibbons and Ralf Hinze [5] J.A. Goguen, J.W. Thatcher, E.G. Wagner, J.B. Wright, Initial Algebra Semantics and Continuous Algebras, Journal ACM 24 (1977) 68-95 [6] H.H. Hansen, J. Rutten, Symbolic Synthesis of Mealy Machines from Arithmetic Bitstream Functions, CWI Report SEN-1006 (2010) [7] R. Hinze, Functional Pearl: Streams and Unique Fixed Points, Proc. 13th ICFP (2008) 189-200 [8] R. Hinze, D.W.H. James, Proving the Unique-Fixed Point Principle Correct, Proc. 16th ICFP (2011) 359-371 419 [9] J.E. Hopcroft, R. Motwani, J.D. Ullman, Introduction to Automata Theory, Languages, and Computation, Addison Wesley 2001; deutsch: Einführung in die Automatentheorie, Formale Sprachen und Komplexitätstheorie, Pearson Studium 2002 [10] G. Hutton, Fold and unfold for program semantics, Proc. 3rd ICFP (1998) 280-288 [11] B. Jacobs, Introduction to Coalgebra, draft, Radboud University Nijmegen 2012 [12] B. Jacobs, A Bialgebraic Review of Deterministic Automata, Regular Expressions and Languages, in: K. Futatsugi et al. (eds.), Goguen Festschrift, Springer LNCS 4060 (2006) 375–404 [13] B. Klin, Bialgebras for structural operational semantics: An introduction, Theoretical Computer Science 412 (2011) 5043-5069 [14] D. Knuth, Semantics of Context-Free Languages, Mathematical Systems Theory 2 (1968) 127-145; Corrections: Math. Systems Theory 5 (1971) 95-96 [15] D. Kozen, Realization of Coinductive Types, Proc. Math. Foundations of Prog. Lang. Semantics 27, Carnegie Mellon University, Pittsburgh 2011 [16] C. Kupke, J. Rutten, On the final coalgebra of automatic sequences, in: Logic and Program Semantics, Springer LNCS 7230 (2012) 149-164 [17] A. Kurz, Specifying coalgebras with modal logic, Theoretical Computer Science 260 (2001) 119–138 420 [18] P.J. Landin, The Mechanical Evaluation of Expressions, Computer Journal 6 (1964) 308–320 [19] W. Martens, F. Neven, Th. Schwentick, Simple off the shelf abstractions for XML Schema, SIGMOD Record 36,3 (2007) 15-22 [20] K. Meinke, J.V. Tucker, Universal Algebra, in: Handbook of Logic in Computer Science 1 (1992) 189 - 368 [21] E. Moggi, Notions of Computation and Monads, Information and Computation 93 (1991) 55-92 [22] F.L. Morris, Advice on Structuring Compilers and Proving Them Correct, Proc. ACM POPL (1973) 144-152 link [23] T. Mossakowski, L. Schröder, S. Goncharov, A Generic Complete Dynamic Logic for Reasoning About Purity and Effects, Proc. FASE 2008, Springer LNCS 4961 (2008) 199-214 [24] R.N. Moll, M.A. Arbib, A.J. Kfoury, Introduction to Formal Language Theory, Springer (1988) [25] P. Padawitz, Church-Rosser-Eigenschaften von Graphgrammatiken und Anwendungen auf die Semantik von LISP, Diplomarbeit, TU Berlin 1978 [26] P. Padawitz, Formale Methoden des Systementwurfs, TU Dortmund 2015 421 [27] P. Padawitz, Übersetzerbau, TU Dortmund 2015 [28] P. Padawitz, Modellieren und Implementieren in Haskell, TU Dortmund 2015 [29] P. Padawitz, Logik für Informatiker, TU Dortmund 2015 [30] P. Padawitz, Algebraic Model Checking, in: F. Drewes, A. Habel, B. Hoffmann, D. Plump, eds., Manipulation of Graphs, Algebras and Pictures, Electronic Communications of the EASST Vol. 26 (2010) [31] P. Padawitz, From Modal Logic to (Co)Algebraic Reasoning, TU Dortmund 2013 [32] P. Padawitz, Fixpoints, Categories, and (Co)Algebraic Modeling, TU Dortmund 2016 [33] H. Reichel, An Algebraic Approach to Regular Sets, in: K. Futatsugi et al., Goguen Festschrift, Springer LNCS 4060 (2006) 449-458 [34] W.C. Rounds, Mappings and Grammars on Trees, Mathematical Systems Theory 4 (1970) 256-287 [35] J. Rutten, Processes as terms: non-wellfounded models for bisimulation, Math. Struct. in Comp. Science 15 (1992) 257-275 [36] J. Rutten, Automata and coinduction (an exercise in coalgebra), Proc. CONCUR ’98, Springer LNCS 1466 (1998) 194–218 422 [37] J. Rutten, Behavioural differential equations: a coinductive calculus of streams, automata, and power series, Theoretical Computer Science 308 (2003) 1-53 [38] J. Rutten, A coinductive calculus of streams, Math. Struct. in Comp. Science 15 (2005) 93-147 [39] A. Silva, J. Rutten, A coinductive calculus of binary trees, Information and Computation 208 (2010) 578–593 [40] D. Turi, G. Plotkin, Towards a Mathematical Operational Semantics, Proc. LICS 1997, IEEE Computer Society Press (1997) 280–291 [41] J.W. Thatcher, E.G. Wagner, J.B. Wright, More on Advice on Structuring Compilers and Proving Them Correct, Theoretical Computer Science 15 (1981) 223-249 [42] 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 [43] Ph. Wadler, Monads for Functional Programming, Proc. Advanced Functional Programming, Springer LNCS 925 (1995) 24-52 [44] R. Wilhelm, H. Seidl, Übersetzerbau - Virtuelle Maschinen, Springer 2007 423 Index C-Abschluss, 355 E-Normalform, 100 E-Reduktionsrelation, 100 E-Äquivalenz, 97 DAut(X, Y ), 34 Σ(JavaLight), 121 Σ(XMLstore), 124 Σ-Algebra, 40 Σ-Gleichung, 89 Σ-Homomorphismus, 41 Σ-Isomorphismus, 41 Σ-Kongruenz, 87 Σ(G), 117 λ-Abstraktion, 206 λ-Applikation, 206 ω-CPO, 394 ω-stetig, 396 ω-stetige Funktion, 396 hai, 77 →c, 396 T (S, I), 29 state(ϕ), 183 (++), 216 abgeleitetes Attribut, 256 Abl(G), 129 Ableitungsbaum, 129 Ableitungsrelation, 114 absolute Adresse, 277 abstrakte Syntax, 117 Aktion eines Monoids, 70 all, 224 any, 224 Applikationsoperator, 210 Attribut, 230 Ausnahmefunktor, 141 Basisadresse, 277 424 Baumautomat, 80 Baumsprache, 80 Baumverhalten, 80 Befehlszähler, 276 Bild, 19 Bildalgebra, 41 bind-Operator, 145 bottom-up-Compiler, 177 Brzozowski-Automat, 60 Brzozowski-Gleichungen, 102 Calculus of Communicating Systems, 33 CCS(Act), 33 CFG, 109 charakteristische Funktion, 19 Coalgebra, 40 Coextension, 65 Coinduktion, 353, 357 Coinduktionsprinzip, 89 Compiler, 8 Compilermonade, 150 const, 213 Copotenzfunktor, 142 Coprodukt, 25, 415 Coterm, 47 curry, 214 denotationelle Semantik, 375, 399 destruktive Signatur, 30 Destruktor, 30, 230 deterministischer Baum, 18 Diagonale, 19 Diagonalfunktor, 140 Display, 277 Domain, 30 Domain-Tuplung, 25 drop, 217 eindeutig, 128 elem, 224 erkannte Baumsprache, 80 Erkennung einer Sprache, 71 Erreichbarkeitsfunktion, 61 425 execute, 272, 283 executeCom, 270 Extension, 62 Funktionsapplikation, 206 Funktionseinschränkung, 19 Funktionsprodukt, 24 Funktionssumme, 27 Funktionsupdate, 19 Funktor, 139 Färbung, 65 fail, 321 field label, 230 generischer Compiler, 155 filter, 224 Gleichungssystem einer CFG, 380 finale Algebra, 66 goto-Tabelle, 183 Fixpunktsatz für CFGs, 380 Fixpunktsatz für nicht-linksrekursive CFGs, Graph, 19 Grundcoterm, 48 389 Grundinstanz, 95 Fixpunktsatz von Kleene, 397 Grundterm, 46 flache Erweiterung von A, 394 flip, 214 halbgeordnete Menge, 394 fold, 63 Halbordnung, 394 fold2, 221 head, 216 foldl, 220 id, 213 foldr, 223 Identität, 14 Functor, 319 Identitätsfunktor, 140 Funktion höherer Ordnung, 208 Individuenvariable, 205, 212 426 Induktion, 346 Induktionsprinzip, 85 init, 217 initiale Algebra, 63, 408 initialer Automat, 71 Injektion, 25 Inklusion, 14 Instanz, 206 Instanz eines Terms, 95 Instanz eines Typs, 212 Instanzen von E, 97 Interpreter, 8 Invariante, 83 isomorph, 20, 41 iteratives Gleichungssystem, 378 JavaLight, 110 JavaLightP, 285 javaStackP, 289 Kategorie, 138 kausale Stromfunktion, 37 Kern, 19, 88 Kompositionsoperator, 210 Kongruenz modulo C, 355 Konkatenation, 17 konstanter Funktor, 140 konstruktive Signatur, 30 Konstruktor, 30 kontextfreie Grammatik, 109 Lösung eines iterativen Gleichungssystems, 378 LAG-Algorithmus, 315 Lambeks Lemma, 348 last, 217 leere Wort, 16 Leserfunktor, 142 lines, 219 linksrekursive Grammatik, 114 Liste, 16 Listenfunktor, 141 Listenkomprehension, 225 LL-Compiler, 335 427 lookup, 229 LR(k)-Grammatik, 178 LR-Automat für G, 183 map, 218 mapM, 326 Matching, 206 Mengenfunktor, 141 Methode, 230 Monad, 321 Monade, 143 MonadPlus, 324 Monoid, 43 monomorph, 212 monotone Algebra, 396 monotone Funktion, 396 mplus, 324 mzero, 324 natürliche Abbildung, 88 notElem, 224 Operation, 40 Operationen, 30 Parser, 8, 138 Paull-Unger-Verfahren, 91 Plusmonade, 147 polymorph, 212 Potenzfunktor, 142 präfixabgeschlossen, 18 Produkt, 21 Produktextension, 21 Produktfunktor, 140 Produkttyp, 29 Projektion, 21 Quotientenalgebra, 87 Range, 30 Range-Tuplung, 21 rational, 410 rationaler Coterm, 52 rationaler Term, 50 Realisierung eines Coterms, 68 Rechtsreduktion, 177 428 Redukt, 348 Reduktionsfunktion, 100 Reg(BS), 33 Regel, 109 reguläre Sprache, 64 regulärer Ausdruck, 33 rekursives Ψ-Gleichungssystem, 347 Relativadresse, 277 Resultatadresse, 298 return, 321 SAB, 119 Scanner, 7 Schreiberfunktor, 142 Sektion, 209 sequence, 325 Signatur, 30 Sprache über A, 16 Sprache einer CFG, 128 Sprache eines regulären Ausdrucks, 64 StackCom, 270 State, 270 statischer Vorgänger, 277 strikt, 396 strukturell-operationelle Semantik, 374 Substitution, 94 Summe, 25 Summenextension, 25 Summentyp, 29 symbolische Adressen, 278 Symboltabelle, 287 syntaktische Kongruenz, 93 syntaktisches Monoid, 93 Syntaxbaum, 118, 128 tail, 216 take, 217 Term, 45 Termfaltung, 63 Terminal, 109 top-down-Parser, 157 Trägermenge, 40 429 transientes Attribut, 256 Transitionsfunktor, 142 Transitionsmonoid, 92 Typ, 38 typ, 29 Typdeskriptor, 283 Typklassen, 235 Typkonstruktor, 205 Typvariable, 205 typverträglich, 38, 39 uncurry, 214 unfold, 66 unit-Typ, 205 universelle Eigenschaft, 21 unlines, 219 Unteralgebra, 83 unwords, 219 update, 213 Urbild, 19 vererbtes Attribut, 256 Verhaltensfunktion, 58 Verhaltenskongruenz, 89 Wildcard, 212 wohlfundierter Baum, 18 Word(G), 127 words, 219 Wort, 16 Wortalgebra, 127 zip, 218 zipWith, 218 Zustandsentfaltung, 66 Zustandsmonade, 323 Variablenbelegung, 62 430
© Copyright 2024 ExpyDoc