Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 1.5 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 denotationale Semantik benutzt, d.h. eine NichtStandard-Interpretation 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 und 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 sich die Funktion α(f ) auch auf den abstrakten Werten konsistent verhalten soll und deswegen auch teilweise einen weniger definierten Wert liefern wird. Wenn es eine Informationsordnung ≤ auf der Menge D gibt (vollständige partielle Ordnung), die Elemente so ordnet, dass ⊥ ≤ d ≤ >, ist, dann muss man aber erwarten, dass α(f a) ≤ α(f ) α(a) gilt. Beispiel 1.5.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 obere Schranke (lub: least upper bound) existiert. D.h. D, ≤ ist ein vollständiger Verband. Wir brauchen auch Domains für Funktionen. Wenn der Verband D endlich ist, dann ist die Menge Dn → D ebenfalls endlich. Man muss sich aber Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 2 immer auf die stetigen Funktionen beschränken. Das sind im endlichen Fall aber genau die monotonen Funktionen. Es gibt immer eine ⊥-Funktion: die alles auf ⊥ abbildet und eine >-Funktion: die alles auf > abbildet. Manchmal wird ein Verband auch mittels der Operationen glb, lub für jeweils 2 Elemente definiert. Die 2-elementige Menge {⊥, >} mit ⊥ < > erfüllt diese Bedingungen und ist der einfachste nichttriviale vollständige Verband. Es folgt auch, dass D immer 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 im folgenden auf eine möglichst vereinfachte Darstellung. Wer hier tiefer einsteigen will, muss die Fachliteratur studieren. Beispiel 1.5.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) α(n) α(n) α(⊥) = = = = + , wenn n positiv −, wenn n negativ 0, wenn n = 0 ⊥ Als abstrakte Interpretation von ∗ als Funktion α(∗) : D × D → D erhalten wir: Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 α(∗) : + 0 0 + − 0 0 + + + − − − ⊥ > > → → → → → → → → 3 + 0 0 − + ⊥ 0 > usw. 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, und dass es Elemente gibt mit α(a + b) < α(a) α(+) α(b), z.B. (−1 + 2) = 1, aber α(−1) α(+) α(2) = − α(+) + = >. Bemerkung 1.5.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 20012/13, Abstrakte Interpretation, 31. Januar 2013 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 man auch ein Element ⊥P in den Programmen, das 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 1.5.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 zminus zminus 0 = 0−0 =0 + = +−+ => − = −(−)− = > da man hier jeweils nicht weiß, 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 20012/13, Abstrakte Interpretation, 31. Januar 2013 1.6 5 PCF: Syntax und Semantik Die Modell-Programmiersprache PCF und deren denotationale Semantik soll hier kurz vorgestellt werden, da wir diese für die abstrakte Interpretation benötigen. PCF ist eine einfach getypte funktionale Programmiersprache, die als Modell dient und in der man eine hinreichend interessante Ausdrucksstärke hat. Wir betrachten hier die call-by-name Variante, aber es gibt in der Literatur auch die call-by-value Variante. Die call-by-name Variante erlaubt die allgemeine Beta-Reduktion (λx.s) t → s[t/x], auch wenn t kein value ist. Auf der Seite der Bereiche (domains), in die man abbildet, muss auch einiges an typischer Arbeit geleistet werden: Der Domain muss konstruiert werden. Wichtige Eigenschaften des Domains sind die Ordnungsrelation und Stetigkeit, die im wesentlichen die Berechnungskraft (Informationsgewinn) und die Terminierung von Berechnungen (von Ausdrücken) modellieren. Man kann in der Bereichstheorie noch weiter gehen und auch für polymorph getypte, und auch nicht-deterministische Programmiersprachen Bereiche konstruieren, die allerdings bei ausdrucksstarken Sprachen weiniger mathematische Bedingungen erfüllen und auch oft sehr anspruchsvolle Konstruktionen erfordern. Eine andere Richtung, die man verfolgen kann, ist die Untersuchung einer operationalen Variante der Semantik: die kontextuelle Semantik, die man immer verwenden kann. 1.6.1 PCF: Programming Language for Computable Functions PCF ist eine einfach getypte funktionale Sprache, für die wir mit unseren Mitteln eine denotationale Semantik mittels Konstruktionen über cpo’s (complete partial order) angeben können. PCF hat als Basisdatentypen natürliche Zahlen und die beiden Wahrheitswerte True, False. Einige wenige Funktionen sind schon vorhanden. Syntax Typen: τ ::= num | Bool | τ → τ Ausdrücke: E ::= True | False | 0 | 1 | 2 | ... | pred E | succ E | zero? E | (if E then E else E ) | x | λx :: τ.E | (E E) | µx :: τ.E Zur Vermeidung eines mehrdeutigen Parse kann man in der Sprachnotation Klammern benutzen. Alle zulässigen Ausdrücke sind einfach (monomorph) getypt. D.h. haben einen Typ, der keine Variablen enthält. Dieser Typ ist zudem Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 6 eindeutig. In den Konstrukten λ, µ müssen die Variablen mit einem (monomorphen) Typ versehen sein, damit man den Typ der Ausdrücke angeben kann. Beachte: Es gibt kein case und keine Konstruktoren. d.h. keine benutzerdefinierten Datentypen. Einige Typen von Ausdrücken: 0, 1, 2,: num zero?: num → Bool if−then−else:: Bool → α → α → α für alle α. D.h. das if-then-else kann man als generisches Konstrukt ansehen, das für jeden Typ τ zu einem extra if-then-elseτ instanziiert wird. Der neue Operator µ ist der Fixpunktoperator, der der Einführung der Rekursion entspricht. Typregel für µ ist: λx.E : τ → τ (µx :: τ.E) : τ Beachte, dass es auch PCF-Definitionen gibt, bei denen der Zahlbereich nur mit succ als Konstruktor und 0 als Konstante aufgebaut wird. 1.6.2 Operationale Semantik von (call-by-name) PCF Die operationale Semantik wird zunächst als “Big-Step“ Semantik angegeben. Mit v, w bezeichnen wir Werte (d.h. Zahlen, True, False) oder einen λ- oder µ-Ausdruck. Die Regeln für die Reduktion →op sind: e1 →op True; e2 →op v if e1 then e2 else e3 →op v e1 →op False; e3 →op v if e1 then e2 else e3 →op v s →op (λx.e) e[a/x] →op w (λx.e) a →op w (e[(µx.e)/x]) →op w (µx.e) →op w t →op pred; e →op n und n > 0 t e →op (n − 1) t →op succ; e →op n t e →op (n + 1) t →op pred; e →op 0 t e →op 0 t →op zero?; e →op 0 t e →op True t →op zero?; e →op n und n 6= 0 t e →op False Beachte, dass nur (pred 0) statt als error als 0 definiert ist. Als Reduktionsrelation (d.h. “Small-Step Semantik“) kann man das auch folgendermaßen definieren: Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 if True then e2 else e3 if False then e2 else e3 pred n pred 0 succ n zero? 0 zero? n (λx.e)a (µx.e) → → → → → → → → → 7 e2 e3 n−1 falls n > 0 0 n+1 True False falls n > 0 e[a/x] (e[µx.e/x]) Wir brauchen noch (call-by-name) Reduktionskontexte in PCF: R ::= [] | [] E | if [] then E else E | pred [] | succ [] | zero? [], und erlauben nur Reduktionen, die in einem Reduktionskontext stattfinden: d.h. es soll gelten: s→t R[s] → R[t] Diese Reduktion im Reduktionskontext bezeichnen wir auch als Normalord∗ nungsreduktion. Die transitive Hülle von → bezeichnen wir als →. ∗ Es gilt, dass →op ⊆ →. Die Sprache PCF ist zwar einfach und monomorph getypt, aber es ist leicht zu sehen, dass sie Turing-mächtig ist. D.h. man kann alle berechenbaren Funktionen definieren. Definition 1.6.1 Sei t ein PCF-Ausdruck. • t ist in Normalform gdw. t nicht mehr reduzierbar ist. • t ist in WHNF (schwache Kopfnormalform): gdw. t ≡ λx.t0 oder t ist ein Basiswert vom Typ num oder Bool. 1.6.3 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 . . . , >} mit den einzigen Relationen ⊥ < n < > und dem entsprechenden Verband zu den Booleschen Werten Dbool = {⊥, T rue, F alse, >}, ebenfalls mit der Ordnung ⊥ < T rue < > und ⊥ < F alse < >. Man braucht nun Domains für Funktionen. Aber man darf nicht alle Funktionen auf den Mengen nehmen, sondern nur die stetigen: Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 8 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 )] Hierbei ist [ϕ(τ1 ) → φ(τ2 )] die Menge der stetigen Funktionen von ϕ(τ1 ) auf φ(τ2 ). Für jedes Programmkonstrukt geben wir jetzt an, wie die abstrakte Interpretation berechnet werden kann. Der Fixpunktoperator von PCF ist µ (zum kleinsten Fixpunkt) 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 von Programmfragmenten. α(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 Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 9 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. Die analoge Bedingung muss 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 dasselbe Dilemma. Zudem gibt es Elemente, ü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 ) = ⊥ Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 10 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 ∼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. 1.7 Berechnung von Striktheitsinformation in PCF Definition 1.7.1 Eine Funktion f ist strikt im k-ten Argument, wenn für alle Argumente ai gilt: f a1 . . . ak−1 ⊥ ak+1 . . . an ∼c ⊥. 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) = ⊥ impliziert e ∼c ⊥ (bottom-reflecting) Bzw. erweitert: α(f > . . . > ⊥ > . . . >) = ⊥ =⇒ f ist strikt im k-ten Argument. Die passende abstrakte Interpretation kann man folgendermaßen definieren: 11 Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 α(n) ρ = > α(True) ρ = > α(False) ρ = > α(pred) ρ = {> → >, ⊥ → ⊥} α(succ) ρ = {> → >, ⊥ → ⊥} α(zero?) ρ = {> → >, ⊥ → ⊥} D.h. diese Funktionen werden als Identität interpretiert. Beachte, 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) ρ = 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. 1.7.1 Beispiele zur Berechnung (Striktheits-) Interpretation der abstrakten Beispiel 1.7.2 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: Hierbei ist ρ die Substitution in freie Variablen. Das ist notwendig, damit man die Interpretation auch für offene Ausdrücke definieren kann. α(if e then e1 else e2 ) ρ = (α(e) ρ) seq lub(α(e1 )ρ, α(e2 )ρ) D.h. die abstrakte Funktion ⊥ ⊥ > > ⊥ > ⊥ > →⊥ →⊥ →⊥ →> Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 12 Fixpunktiteration für add: 1. i = 0 : a0 = ⊥ 2. i = 1 : a1 = λs, t.s seq t 3. i = 2 : 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 1.7.3 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 20012/13, Abstrakte Interpretation, 31. Januar 2013 13 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. 14 Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 1.7.2 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 ) ρ ⊥, (α(e2 ) ρ)> Inf lub{(α(e2 ) ρ) ⊥ >, (α(e2 ) ρ) > BotElem} lub{(α(e1 ) ρ), (α(e2 ) ρ) > >} wenn wenn wenn wenn α(e) α(e) α(e) α(e) ρ=⊥ ρ = Inf ρ = BotElem ρ=> Man muss auch die abstrakte Interpretation des Nil und (:)-Konstruktors angeben: α(Nil) α(:) x, ⊥ x, Inf x, BotElem ⊥, x = = = = = > Inf Inf BotElem BotElem Inf, > = > BotElem, > = > >, > = > Beispiel 1.7.4 length xs = case_lst xs of Nil -> 0; y:ys -> 1 + (length ys) Iterationen x∈D x∈D x∈D x∈D Funktionale Programmierung: Analyse WS 20012/13, Abstrakte Interpretation, 31. Januar 2013 1. length0 2. length1 3. length2 4. length3 15 = ⊥ = {⊥ → ⊥, 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 1.7.5 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.