5.1 Abstrakte Interpretation, insbesondere zur Striktheitsanalyse

Werbung
Funktionale Programmierung: Analyse WS 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 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, 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. 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
immer auf die stetigen Funktionen beschränken. Das sind im endlichen Fall aber
Funktionale Programmierung: Analyse WS 2009/10, Abstrakte Interpretation, 4. Februar 2010
2
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 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) =
α(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 2009/10, Abstrakte Interpretation, 4. Februar 2010
α(∗) :
+
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, 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 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 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
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 2009/10, Abstrakte Interpretation, 4. Februar 2010
5.2
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 passende Bereiche
konstruieren. Eine andere Richtung, die man verfolgen kann, ist die Untersuchung einer operationalen Variante der Semantik: die kontextuelle Semantik.
5.2.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
eindeutig.
In den Konstrukten λ, µ müssen die Variablen mit einem (monomorphen) Typ
versehen sein, damit man den Typ der Ausdrücke angeben kann.
Funktionale Programmierung: Analyse WS 2009/10, Abstrakte Interpretation, 4. Februar 2010
6
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.
5.2.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
Als Reduktionsrelation (d.h. “Small-Step Semantik“) kann man das auch folgendermaßen definieren:
Funktionale Programmierung: Analyse WS 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 5.2.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.
5.2.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 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 2009/10, Abstrakte Interpretation, 4. Februar 2010
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.
Ä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
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 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 ⊥, 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.3
Berechnung von Striktheitsinformation in
PCF
Definition 5.3.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 2009/10, Abstrakte Interpretation, 4. Februar 2010
α(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.
5.3.1
Beispiele
zur
Berechnung
(Striktheits-) Interpretation
der
abstrakten
Beispiel 5.3.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:
α(if e then e1 else e2 ) ρ =
(α(e) ρ) seq lub(α(e1 )ρ, α(e2 )ρ)
D.h. die abstrakte Funktion
⊥
⊥
>
>
⊥
>
⊥
>
→⊥
→⊥
→⊥
→>
Funktionale Programmierung: Analyse WS 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 5.3.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 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 2009/10, Abstrakte Interpretation, 4. Februar 2010
5.3.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
⊥, >
Inf, >
BotElem, >
>, >
=
=
=
=
=
=
=
=
>
Inf
x∈D
Inf
x∈D
BotElem x ∈ D
BotElem
>
>
>
Beispiel 5.3.4 length xs = case_lst xs of
Nil -> 0;
y:ys -> 1 + (length ys)
Iterationen
Funktionale Programmierung: Analyse WS 2009/10, Abstrakte Interpretation, 4. Februar 2010
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 5.3.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.
Herunterladen