Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 5.1 1 Abstrakte Interpretation, insbesondere zur Striktheitsanalyse Die Idee der Methode der abstrakten Interpretationen besteht darin, Eigenschaften von Programmen nachzuweisen, indem man statt der denotationalen Semantik eine approximative Semantik benutzt, d.h. eine Nicht-StandardInterpretation des Programms. Approximativ bedeutet hier nur, dass diese Interpretation nicht die volle exakte Rechnung durchführt. Abstrakte Interpretation kann im Prinzip für alle Programmiersprachen angewendet werden und ist nicht auf funktionale Programmiersprachen beschränkt. Diese Interpretation wird normalerweise abhängig von der zu zeigenden Eigenschaft gewählt. I.a. verwendet man Interpretationen in eine endliche Menge D, die aber immer auch ein ⊥-Element für undefiniert“ enthältünd ein >-Element ” für keine Information“ bzw. alles ist möglich“. Die Interpretation α ordnet den ” ” Konstanten des Programms Werte in D zu. Der schwierigere Teil ist es, dann den im Programm definierten Funktionen ebenfalls Funktionen über D zuzuordnen. Hierbei sollte im optimalen Fall eine Homomorphie-Eigenschaft gelten: α(f a) = α(f ) α(a). Das kann nicht immer eingehalten werden, da die abstrakte Funktion α(f ) nicht wissen kann, was (f a) ist, und deswegen auch teilweise einen weniger definierten Wert liefern wird. Wenn es eine Informationsordnung ≤ auf der Menge D gibt, die Elemente so ordnet, dass ⊥ ≤ d ≤ >, ist, dann muss man aber erwarten, dass α(f a) ≤ α(f ) α(a) gilt. Beispiel 5.1.1 Der 9er-Test ist eine abstrakte Interpretation. Das ist ein Test (nach Adam Riese), ob eine von Hand ausgeführte Multiplikation von großen ganzen Zahlen korrekt ist. Diese Methode beruht auf den Resten modulo 9. Der 9er Test war früher eine große Hilfe bei Multiplikationen die von Hand ausgeführt wurden. Allerdings gilt auch hier: nicht jeder Fehler kann damit entdeckt werden. Beispiel: 25 ∗ 25 = 625 Die Interpretation α berechnet für jede beteiligte Zahl: die Quersumme, dann wieder die Quersumme der Quersumme, usw. bis man eine einstellige Zahl hat. Das ergibt α(25) = 7 und α(625) = 4, da 625 → 13 → 4. Die Multiplikation der abstrakten Werte wird genauso behandelt und ergibt: α(α(25) ∗ α(25) = 4, da die Quersummenberechnung 7 ∗ 7 = 49 → 13 → 4 ergibt. Damit hat unsere Multiplikation den Test bestanden. Wir sind nun etwas genauer: die endliche Menge D der abstrakten KonstantenWerte muss ein Bereich (engl.: domain) sein, d.h. es gibt darauf eine partielle Ordnung ≤, so dass für alle Teilmengen sowohl eine (eindeutige) größte untere Schranke (glb: greatest lower bound) und auch eine (eindeutige) kleinste untere Schranke (lub: least upper bound) existiert. D.h. D, ≤ ist ein vollständiger Verband. Manchmal wird ein Verband auch mittels der Operationen glb, lub für 2 Elemente definiert. Die Menge {⊥, >} mit ⊥ < > erfüllt diese Bedingungen und ist einfachste nichttriviale vollständige Verband. Es folgt auch, dass D immer Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 2 ein ⊥ und > enthalten muss. Im allgemeinen interessiert man sich dafür, eine möglichst gute abstrakte Interpretation von Funktionen zu finden. Da D zunächst nur für die Konstanten definiert ist, muss man auch noch vollständige Verbände zu den Funktionen vorsehen. Im allgemeinen enthält dann ein solcher Bereich unendlich viele Elemente und trennt nicht zwischen Konstanten und Funktionen. Etwas einfacher wird es, wenn man ein monomorphes Typsystem hat. Der zugehörige Funktions-Bereich ist dann eine Untermenge von Dn → D. Man kann dann anhand der Definitionen der Funktionen die abstrakte interpretierte Funktion über dem endlichen Bereich effektiv berechnen. Diese Funktion approximiert das Abbildungsverhalten der betrachteten Funktion in der Standardinterpretation. Wir beschränken uns auf folgenden auf eine möglichst vereinfachte Darstellung. Wer hier tiefer einsteigen will, muss die Fachliteratur studieren. Beispiel 5.1.2 Ein einfaches Beispiel zur Illustration sind die ganzen Zahlen und Funktionen auf ganzen Zahlen. Wir nehmen an, dass es eine Programmiersprache für Funktionen gibt, so dass es Konstanten für alle ganzen Zahlen gibt. Als Funktionen betrachten wir die Grundrechenarten: Addition, Multiplikation, Subtraktion, Division, < auf Zahlen, . . . . Die Standard-Interpretation ist die Interpretation als Zahlen: Z⊥ . D.h, alle Zahlen als Konstanten, ein ⊥-Element, das kleiner als alle Zahlen ist, und ein >Element, das größer als alle Zahlen ist. Als Beispiel für eine abstrakte Interpretation wollen wir die Abbildungseigenschaften von Funktionen bezüglich der positive/negativen Zahlen, d.h. bzgl. der Vorzeichen der Zahlen berechnen, Z.B. − ∗ − = +. Als Basisbereich der abstrakten Interpretation nehmen wir zunächst die Menge {0, +, −, ⊥, >}. Die Elemente 0, +, − sind unvergleichbar, und ⊥ ≤ 0, +, − ≤ >. Damit ist diese Menge ein vollständiger Verband. Jetzt definieren wir zuerst mal die abstrakte Interpretation α der Konstanten: α(n) = + , wenn n positiv α(n) = −, wenn n negativ α(n) = 0, wenn n = 0 α(⊥) = ⊥ Als abstrakte Interpretation von ∗ als Funktion α(∗) : D × D → D erhalten wir: Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 α(∗) : + 0 0 + − 0 0 + usw. + + − − − ⊥ > > → → → → → → → → 3 + 0 0 − + ⊥ 0 > Alle Werte lassen sich zufriedenstellend berechnen, und es gilt die Homomorphie-Eigenschaft (ausnahmsweise) : Man verliert keine Information bei der Multiplikation. Der Versuch, die abstrakte Interpretation von + anzugeben, zeigt, dass es einen Unterschied zur Standardinterpretation gibt, und dass man bei Berechnungen Information verlieren kann und Approximation notwendig ist. α(+) : aber: + + → + 0 0 → 0 − − → − > ⊥ → ⊥ + − → ?? Das Ergebnis der letzten Zeile kann +, −, 0 sein, abhängig von den Argumenten. Um diesen Fall möglichst einfach zu behandeln, und gleichzeitig die richtige Approximation zu erhalten, müssen wir das Element > als Ergebnis benutzen. Die abstrakte Interpretation von α(+) ergibt somit für die unklaren Fälle: + − → > Wir sehen, dass α(a + b) ≤ α(a) α(+) α(b) ist, aber dass es Elemente gibt mit α(a + b) < α(a) α(+) α(b), z.B. (−1 + 2) = 1, aber α(−1) α(+) α(2) = − α(+) + = >. Bemerkung 5.1.3 Man kann (z.B. im Zahlenbeispiel oben) andere abstrakte Interpretationen folgendermaßen konstruieren: Man nimmt den Grundbereich der Konstanten und wählt bestimmte TeilMengen von Z⊥ aus, die jeweils interessant sind. Diese Mengen müssen alle das Element ⊥ enthalten, und alle Schnitte und Vereinigungen müssen ebenfalls als Mengen vorkommen. Dann besteht Dkonst für die Konstanten aus diesen Mengen (und auch der vollen Menge Z⊥ , und die Ordnung ist die Teilmengenrelation. Zum Beispiel die Eigenschaft positiv“ entspricht dann der Menge ” {1, 2, 3, . . . , } ∪ {⊥}. Damit man für alle Funktionen die Abbildungseigenschaften berechnen kann, berechnet man die Funktionenräume darüber folgendermaßen: Man berechnet für alle Funktionen und Mengen in Dkonst die Ergebnisse bezüglich der Standardinterpretation und nimmt die lub’s der Ergebnisse: Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 4 α(f )(M ) = lub{[[f ]](m) | m ∈ M }. Dieser lub ist gerade die kleinste Menge, die in der Auswahl enthalten ist, und die diese Menge noch enthält. Wenn wir dies erreicht haben und α(.) berechnet haben, dann können wir etwas über die Funktionen aussagen. Diese Konstruktion ergibt für die positiv/negativ/0-Interpretation eine Menge Dkonst = {{n | n > 0} ∪ {⊥}, {0, ⊥}, {n | n < 0} ∪ {⊥}, ⊥, Z ∪ {⊥}} Diese kann man wieder mit {⊥, 0, +, −, >} bezeichnen. Aus dieser Interpretation können wir dann Schlüsse über das Verhalten der arithmetischen Funktion ziehen. Praktisches Problem: Um mit dieser Methode eine abstrakte Interpretation zu berechnen, benötigen wir zuerst die Standardinterpretation. Wenn wir aber die Standardinterpretation kennen, dann wissen wir (fast) alles über die Semantik der Funktion. Normalerweise gilt: 1. Die praktische Berechnung einer abstrakten Interpretation erfolgt ohne Berechnung der Standardinterpretation. I.a. sogar nur anhand des Programms zur Funktion. Bei abstrakter Interpretation zur Striktheitsanalyse benutzt auch ein Element ⊥P in den Programmen, dass eine bekannte Nichtterminierung repräsentiert. 2. I.a. berechnet man nur eine Approximation einer solchen optimalen abstrakten Interpretation. Wir haben nur zu argumentieren, dass die Berechnungsmethode korrekt ist, d.h. eine Approximation liefert, aus der wir noch die Eigenschaften schließen dürfen. Beispiel 5.1.4 zminus x = x - x Wir sehen: diese Funktion liefert 0 oder ⊥, d.h. wir könnten diese abstrakt interpretieren als {> → 0, ⊥ → ⊥}. Dies ist erlaubt als abstrakte Interpretation, aber normalerweise braucht man ad-hoc Nachweise, die für jede Funktion anders aussehen. Die Berechnung des besten Wertes bei beliebig definierten Funktionen ist unentscheidbar,. Wenn man unendlich viele Werte durchgehen muß, um die abstrakte Interpretation zu berechnen, dann kann man das als Hinweis sehen, dass man in die Nähe eines solchen Unentscheidbarkeits-Problems kommt. Normalerweise berechnet man den Wert modular, d.h. man setzt alle abstrakten Werte ein und bemüht sich, die Unterfunktionen zu verwenden, deren Funktionswerte auf den abstrakten Werten möglicherweise schon bekannt sind. Wir erhalten: zminus 0 = 0 − 0 =0 zminus + = + − + = > da man hier jeweils nicht weiß, zminus − = −(−)− = > dass die Argumente gleich waren Es gilt, dass diese Approximation auf jeden Fall größer ist (sein sollte) in der Ordnung als die exakte. Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 5.1.1 5 Formalisierung und Korrektheitsbedingung Wir betrachten zunächst eine einfach getypte Sprache: PCF über num, bool. Was wir hier davon nur brauchen, ist analog zu einem einfach getypten KFPT. Da man vollständige Verbände benötigt, und das auch für Funktionen, z.B, von Zahlen nach {⊥, T rue, F alse, >}, betrachten wir hier nur die Konstruktion für Funktionen erster Ordnung. Man startet mit dem Verband der ganzen Zahlen: Dnum = {⊥, 0, 1, 2, 3, . . . , −1, −2, −3 . . . , >} und dem entsprechenden Verband zu den Booleschen Werten Dbool = {⊥, T rue, F alse, >}. Man braucht nun Domains für Funktionen. Aber man darf nicht alle Funktionen auf den Mengen nehmen, sondern nur die stetigen: Das sind Funktionen, die (i) monoton sind, und die (ii) lub und glb erhalten: d.h. f (lub(M )) = lub(f (M )). Bei endlichen, linear geordneten Bereichen reicht die Monotonie aus. Der Funktionenraum [DBool → DBool ] enthält die Identität, die konstanten Funktionen, und weitere monotone, stetige Funktionen. Kleinstes Element ist λx.⊥, und größtes Element ist λx.>. Wir werden jetzt nur angeben, wie man die abstrakte Interpretation ausrechnen kann, und welche Bedingungen sinnvollerweise erfüllt sein müssen. Funktionenräume sind wieder (endliche) Verbände: • Top-element ist die Funktion, die alles auf > abbildet. • Bot-element ist die Funktion, die alles auf ⊥ abbildet. • Die Ordnung ist punktweise definiert, d.h. f ≤ g gdw. ∀x : f (x) ≤ g(x). • ebenso sind lubs und glbs punktweise definiert. Jedem monomorphen einfachen Typ τ können wir einen Verband zuordnen: ϕ(num) = Dnum ϕ(bool) = Dbool D =: Dnum ∪ Dbool ϕ(τ1 → τ2 ) = [ϕ(τ1 ) → φ(τ2 )] Für jedes Programmkonstrukt geben wir jetzt an, wie die abstrakte Interpretation berechnet werden kann. Der Fixpunktoperator von PCF ist µ. Die Notation ist: α(Programmfragment) Umgebung = Interpretation wobei die Umgebung nur benötigt wird, wenn es im Programmfragment freie Variablen gibt. Die Umgebung enthält dann Bindungen in D für die freien Variablen. Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 6 α(x) ρ α(c) ρ = ρ(x) muß vorgegeben werden für Konstanten c vom Typ num, bool ebenso für Konstruktoren α(f g) ρ = (α(f ) ρ) (α(g) ρ) α(λx.s) ρ = λy.(α(s) (ρ[y/x])) α(µx.s) ρ = lub {(α(λx.s) ρ)i | i = 0, 1, 2, . . .} Wobei f 0 = ⊥ und f i+1 = f (f i ) Für die Konstruktoren und andere Konstanten muß man deren abstrakte Interpretation vordefinieren. Allerdings hat man recht enge Schranken, wie eine solche Interpretation beschaffen sein darf. Zum Teil kann man diese jedoch in natürlicher Weise direkt aus den anderen Definitionen herleiten. Für if und case muß man eine Definition vorgeben. Für if gibt es nicht so viele Fälle: Wenn der abstrakte Bereich True und False enthält, dann ist folgende Definition sinnvoll: α(if e1 then e2 else e3 )ρ = = = = ⊥, wenn α(e1 ) ρ = ⊥ α(e2 ) ρ wenn α(e1) ρ = True α(e3 ) ρ wenn α(e1) ρ = False lub ((α(e2 ) ρ, α(e3 ) ρ) wenn α(e1 ) ρ = > Im letzten Fall geht Information verloren, da man e2 und e3 gemeinsam approximiert. (Dieser Informationsverlust tritt bei der abstrakten Reduktion nicht! ein). Analog kann man case-Ausdrücke behandeln. Man kann auch für andere Funktionen, die durch Ausdrücke definiert sind, eine abstrakte Interpretation bereits vorgeben. In diesem Fall ist die Korrektheit ebenfalls noch zu zeigen. Bedingungen für die Korrektheit der Striktheitsanalyse mittels abstrakter Interpretation Eine wichtige Bedingung, die die abstrakte Interpretation für Anwendungen erfüllen muss ist: α(s t) ≤ α(s) α(t) Diese erlaubt eine modulare Berechnung der abstrakten Interpretation. Ähnliche Bedingungen müssen für die anderen Programmkonstrukte ebenfalls gelten. Vorsicht: Man kann von der abstrakten Interpretation nicht verlangen, dass zB α(s) = ⊥ ⇔ s ∼c ⊥ gilt, da das bedeuten würde, dass die abstrakte Interpretation die Nichtterminierung aller Ausdrücke entscheiden könnte. Das kann für abstrakte Interpretationen mit endlichem Bereich nicht richtig sein. Man kann auch nicht verlangen, dass s ≤c t ⇒ α(s) ≤ α(t), denn das ergäbe Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 7 dasselbe Dilemma. Ebenso ist α(s) ≤ α(t) ⇒ s ≤c t falsch, da es Elemente gibt, über die man nichts weiß, d.h. α(s) = α(t) = >, aber die kontextuell verschieden sind. Wir konzentrieren uns hier auf Korrektheitsbedingungen für Striktheitsaussagen, die aus der abstrakten Interpretation folgen. Wesentliche Bedingungen: α bildet die Bot-Konstante des Programms auf das abstrakte ⊥ ab: α (⊥P ) = ⊥ D.h. das konkrete ⊥P (die Konstante in Programmen) wird auf das abstrakte ⊥ abgebildet. Wenn diese Bedingung nicht erfüllt ist, dann verliert man unnötigerweise zuviel Information! Eine andere, schärfere Bedingung, die man für die Striktheitsanalyse benötigt, ist die Bedingung Bot-Eindeutigkeit (bottom-reflecting): Wenn α(d) = ⊥, dann ist [[d]] = ⊥, bzw. d ∼c ⊥ d.h. d ist undefiniert Diese Bedingung ist notwendig, wenn man Striktheitsinformation folgern will. Denn wenn sie nicht erfüllt ist, dann kann man aus α(d) = ⊥ nicht schließen, dass der konkrete Wert d undefiniert ist. Man kann nachweisen dass obige Bedingungen erfüllt sind, wenn man die Definition der abstrakten Interpretation der Programmkonstrukte richtig wählt. Z.B. Konstanten möglich exakt, und bei den anderen Konstrukten möglichst knapp nach oben abschätzt, wie z.B. beim if-then-else. Um 5.1.2 Berechnung von Striktheitsinformation in PCF Definition 5.1.5 Eine Funktion f ist strikt im k-ten Argument, wenn für alle Argumente ai gilt: f a1 . . . ak−1 ⊥ ak+1 . . . an = ⊥. D.h. wenn in der Standardinterpretation gilt: [[ak ]] = ⊥ ⇒ [[f a1 . . . ak−1 ak ak+1 . . . an ]] = ⊥ Wir suchen eine abstrakte Interpretation, die Striktheitsinformation liefern kann. Der einfachste Basisbereich besteht nur aus {⊥, >}. Die abstrakte Interpretation α soll für geschlossene e erfüllen: α(e) = ⊥ gdw. [[e]] = ⊥ (bottom-reflecting) Bzw. erweitert: α(f > . . . > ⊥ > . . . >) = ⊥ ⇒ f ist strikt im k-ten Argument. 8 Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 Die passende abstrakte Interpretation kann man folgendermaßen definieren: α(n) = > α(True) = > α(False) = > α(pred) = {> → >, ⊥ → ⊥} α(succ) = {> → >, ⊥ → ⊥} α(zero?) = {> → >, ⊥ → ⊥} D.h. diese Funktionen werden als Identität interpretiert. DBeachte, dass diese in PCF strikt definiert sind. α(s t) = α(s) α(t) ⊥ if α(e) ρ = ⊥ α(if e then e1 else e2 ) ρ = lub(α(e1 ) ρ, α(e2 ) ρ) sonst α(λx.e) ρ = (λd.α(e)ρ[x 7→ d]) α(λx.e) = f wobei f (α(d)ρ) := α(e[d/x]) falls e geschlossen α(µx.e) ρ = lub((α(λx.e)ρ)i ) Hierbei ist zu beachten, dass alle Funktionen als monotone Funktionen über dem Bereich interpretiert werden und dass die Funktionsfolge, die bei der Berechnung des Fixpunkts einer FUnktion berechnet wird, aufsteigend ist. D.h. f 0 ≤ f 1 ≤ f 2 ≤ . . .. Dieser einfache Bereich kann zwischen True und False nicht unterscheiden, und verliert somit die entsprechenden Informationen. 5.1.3 Beispiele zur Berechnung (Striktheits-) Interpretation der abstrakten Beispiel 5.1.6 Fixpunkt für Addition: Beachte, dass die Funktionen zero?, succ, pred als Identität auf D interpretiert werden. add = µa, λs, t.if (zero? s) then t else succ (a (pred s) t) Wir nehmen den Bereich {⊥, >}. Im folgenden meinen wir mit (s seq t) die Funktion bzgl des Domains {⊥, >}, die zuerst s auswertet; falls ⊥, wird ⊥ als Wert zurückgegeben, wenn >, dann t auswertet, und der Wert ist der Wert von t. Damit kann man die abstrakte Interpretation für if-then-else auch so schreiben: α(if e then e1 else e2 ) ρ = (α(e) ρ) seq lub(α(e1 )ρ, α(e2 )ρ) D.h. die abstrakte Funktion ⊥ ⊥ > > ⊥ > ⊥ > →⊥ →⊥ →⊥ →> Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 9 Fixpunktiteration für add: 1) i = 0 : 2) i = 1 : 3) i = 2 : a0 = ⊥ a1 = λs, t.s seq t a2 = λs, t.s seq lub(t, succ (pred (s) seq t) = λs, t.s seq lub(t, s seq t) = λs, t.s seq lub(t, t) = λs, t.s seq t D.h. Fixpunkt ist gefunden: add ist strikt in beiden Argumenten. Beispiel 5.1.7 tak x y z = if x <= y tak (tak (tak (tak then z else (x-1) y z) (y-1) z x) (z-1) x y)) Beh: tak ist strikt in allen Argumenten Iteration: 1. tak0 = ⊥ tak1 x y z = λx, y, z.x seq y seq z 2. tak1 = λx, y, z.x seq y seq z tak2 x y z = λx, y, z.x seq y seq z (nach einigem Rechnen und Ausnutzen der Striktheit von −). Vergleich Das Vorgehen bei abstrakter Interpretation ist nicht so sehr verschieden von dem der Abstrakten Reduktion. Unterschiede ergeben sich z.B. • Die Abstrakte Interpretations-Methode kann berechnete Information speichern und ausnutzen. Abstrakte Reduktion kann das im Prinzip auch, aber es gibt ja nur die Striktheitsinformation. • Komplexität: Abstrakte Interpretation ist hyper-exponentiell bei Funktionen höherer Ordnung (als untere Schranke, da alles berechnet werden muß, denn sonst kann man die Fixpunkteigenschaft nicht feststellen. Die Größe der Funktionsdomains wächst stark. Abstrakte Reduktion: Es wird nur benötigte Information berechnet, man kann approximiert und bei bestimmten Schranken abbrechen kann. Abstrakte Reduktion:hat besseres best-case-Verhalten. • Abstrakte Interpretation kann keine Kontextinformation erkennen, d.h. man muß für den vorbestimmten Satz von Funktionen jeweils die abstrakte Interpretation ausrechnen. Man kann nicht automatisch etwas feiner berechnen, als es der Domain zuläßt. Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 10 Abstrakte Reduktion: kann Kontextinformation verwenden. Hierbei werden evtl Berechnungen mehrfach durchgeführt. • Abstrakte Interpretation hat festgelegtes Approximationsverhalten und kann evtl. einen Fixpunkt berechnen, während die Abstrakte Reduktion in eine Schleife läuft. Allerdings kann Abstrakte Reduktion manchmal den Wert berechnen, während Abstrakte Interpretation überapproximiert. • Abstrakte Interpretation kann nicht gut mit Konstruktoren umgehen, da man jedesmal den Domain neu konstruieren muss. Zudem funktioniert abstrakte Interpretation nicht auf unendlich vielen Werten. Wadler’s 4Punkt Bereich unten beschreibt eine nichttriviale Listeninterpretation. • Abstrakte Interpretation kann so gut wie nicht mit Nichtdeterminismus umgehen, da man dafür erst mal einen Domain angeben muss, während abstrakte Reduktion hier anpassungsfähiger ist. 11 Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 5.1.4 4-Punkt Bereich für Listen (nach Phil Wadler) Man nimmt folgende Domain {⊥, Inf, BotElem, >} zur Interpretation: ⊥ ≤ Inf ≤ BotElem ≤ >. ⊥ = undefiniert; WHNF-Auswertung terminiert nicht Inf = unendliche Listen (Tail ist niemals Nil); SpineAuswertung terminiert nicht BotElem = Listen, in denen mindestens ein Element = ⊥ ist, oder deren Tail niemals Nil ist; Auswertung aller Listenelemente terminiert nicht. > = alle Listen Wir erinnern uns an die Definition der Demands: Inf = h⊥, > : Infi BotElem = h⊥, ⊥ : >, > : BotElemi Das hilft der Intuition, aber man muss für die damit definierte abstrakte Interpretation dann nachweisen, dass sie das Erwartete leistet. Interpretation von caselst ist unten definiert. Die Fallunterscheidung ist dabei wie eine Funktion behandelt: Z.B.in (y : ys) → e2 wird e2 wie die Funktion λy, ys.e2 behandelt. α(caselst = = = = e of Nil → e1 ; y : ys → e2 ) ρ ⊥, wenn (α(e2 ) ρ)> Inf wenn lub{(α(e2 ) ρ) ⊥ >, (α(e2 ) ρ) > BotElem} wenn lub{(α(e1 ) ρ), (α(e2 ) ρ) > >} wenn α(e) α(e) α(e) α(e) ρ=⊥ ρ = Inf ρ = BotElem ρ=> Man muss auch die abstrakte Interpretation des Nil und (:)-Konstruktors angeben: α(Nil) α(:) x, ⊥ x, Inf x, BotElem ⊥, > Inf, > BotElem, > >, > = = = = = = = = > Inf x∈D Inf x∈D BotElem x ∈ D BotElem > > > Beispiel 5.1.8 length xs = case_lst xs of Nil -> 0; y:ys -> 1 + (length ys) Iterationen Funktionale Programmierung: Analyse WS 2007/8, Abstrakte Interpretation, 24. Januar 2008 1. length0 2. length1 3. length2 4. length3 12 = ⊥ = {⊥ → ⊥, Inf → ⊥, BotElem → ⊥, > → >} = {⊥ → ⊥, Inf → ⊥, BotElem → >, > → >} Da der rekursive Aufruf > + (length1 >) auftritt. = {⊥ → ⊥, Inf → ⊥, BotElem → >, > → >} Es ändert sich nichts mehr. Wenn wir die Korrektheit mal annehmen, ebenso die Eigenschaft der BotEindeutigkeit, dann haben wir mit dieser Methode gezeigt, dass length Inf = ⊥ d.h. Die Längenfunktion terminiert nicht für unendliche Listen. Beispiel 5.1.9 Man kann auch für die map-Funktion nachweisen, dass diese insbesondere die Abbildungseigenschaft Inf → Inf hat. map f xs = case_lst xs of Nil -> Nil; y:ys -> f y : map f ys Iterationen 1. (map f )0 2. (map f )1 3. (map f )2 4. (map f )3 = ⊥ = {⊥ → ⊥, Inf → Inf, BotElem → Inf, > → >} Da (f >) : ⊥ = Inf = {⊥ → ⊥, Inf → Inf, BotElem → >, > → >} Da > : Inf = Inf. = (map f )2 Beachte, dass (map f )0 ≤ (map f )1 ≤ (map f )2 ≤ . . .. Das kann man so interpretieren, dass map Listen die kein Nil am Ende haben wieder in solche überführt. Man kann daraus nicht schließen , dass unendliche Listen wieder in unendliche überführt werden, da auch zB Nil : ⊥ zu Infgehört, aber keine unendliche Liste im eigentlichen Sinne ist.