Algorithmen und Datenstrukturen Übung 1c: Totale Korrektheit, Partielle Korrektheit, Hoare Kalkül, Assertions (Zusicherungen) Partielle Korrektheit Falls ein Programm terminiert und die Spezifikation erfüllt, heißt es partiell korrekt Terminierung kann man allgemein nicht entscheiden, d.h. es gibt kein Verfahren, welches für einen beliebigen Algorithmus feststellt, ob dieser terminiert oder nicht. Totale Korrektheit Ein Programm, das immer terminiert und partiell korrekt ist, heißt total korrekt. Hoare Kalkül Bei dem Hoare-Kalkül handelt es sich um eine Menge von Regeln, die sich aus Prämissen und Schlussfolgerungen zusammensetzen: Prämisse1 Prämisse2 ... Prämissen --------------Konklusion Die Anwendung der Regeln des Hoare-Kalküls führt auf das Hoare Tripel, d.h. auf Gruppen von 3 Elemeneten folgender Art: {P}S{Q} . Hier werden P und Q Zusicherungen genannt. S steht für eine Anweisung (ein Statement) in einer Programmiersprache Mit dem Hoare-Kalkül kann die partielle Korrektheit eines Programms nachgewiesen werden: - Zerlege den Algorithmus in seine einzelne Anweisungen und füge vor (und nach) jeder Anweisung geeignete Vor- und Nachbedingungen ein. Zeige, dass die einzelnen Anweisungen bzgl. dieser Bedingungen (partiell) korrekt sind. Beweise die Korrektheit des gesamten Algorithmus aus der Korrektheit der einzelnen Aussagen. Der Nachweis der Korrektheit erfolgt nach dem Schema: {P}( s1 ;...; s n ){Q} . Der Korektheitsbeweis geht von hinten nach vorne vor, und zwar nach folgendem Schema: s n und spalte den Beweis in {P}( s1 ;...; s n−1 ){P'} und {P' }( s n ){Q} . Die Weiterverarbeitung von {P'}( s n ){Q} hängt von der Art der Anweisung s n - Finde eine Zwischenbedingung {P ' } für - ab. Für jede Anweisungsart benötigt man eine Extraregel. Diese Regeln geben auch z.T. Hinweise, was die richtige Zwischenbedingung ist. Nach dem gleichen Schema werden die Anweisungen s n−1 ;...; s1 behandelt, bis man schließlich eine Situation erreicht {P}(){Pn '} erreicht, wo kein Programmstück zwischen den Bedingungen 1 Algorithmen und Datenstrukturen mehr vorhanden ist. Partielle Korrektheit ist bewiesen, wenn man in dieser Situation {P} {Pn '} mit rein mathematischen Mitteln erreichen kann. Verifikationsregeln schreibt man folgendermaßen: {P1 }s1{Q1 },..., {Pn }s n {Qn } . Sie hat die Bedeutung: {P}s{Q} Wenn Aussagen oberhalb der Trennlinie wahr sind, dann ist auch die Aussage unterhalb wahr. Eine Voraussetzung (oder ein Effekt) {P} wird abgeschwächt zu {P ' } , wenn gilt {P} {P ' } . Die Regeln nach C. A. R. Hoare R0: Axion der leeren Anweisung {P}NOP{P} R1: Zuweisungen IZuweisungsaxiom, Regel für die Ergibt-Anweisung x {PE }x = E{P} x Hier bedeutet PE , dass E durch x substituiert werden muß, damit die Nachbedingung P wahr wird. Da diese Regel keine Prämissen hat, wird sie auch als Axiom bezeichnet. R2: Regel für Anweisungsfolgen, Regel der sequentiellen Komposition, Sequenzregel {P}S1{Q};{Q}S 2 {R} {P}S1 ; S 2 {R} R3a: Regel der bedingten Fallunterscheidung, Regel der bedingten Anweisung {P ∧ B}S1{R} {P ∧ ¬B}S 2 {R} {P}if _ B _ then _ S1 _ else _ S 2 {R} R3b: Regel der bedingten Fallunterscheidung, Regel der bedingten Anweisung. Diese Variante von dient R3a dient der Verifikation von Verzweigungen ohne Alternative (den "else"-Teil). {P ∧ B}S{R} {P ∧ ¬B} {R} {P}if _ B _ then _ S Allgemein kann man hier die Regel 3a anwenden und den Quelltext unter S2 als NOP ansehen. R4a {I ∧ B}S{I } {I }while _ B _ do _ S {I ∧ ¬B} 2 Algorithmen und Datenstrukturen Bei while-Schleifen wird der Rumpf S solange wiederholt, bis die Wiederholungsanweisung B nicht mehr erfüllt ist (also ¬B gilt). Zur Verifikation von Schleifen ist es nötig, eine sogenammte Invariante zu finden. Invarianten gelten nach jedem Schleifendurchlauf und beschreiben innerhalb der Schleifendynamik das Gleichbleibende. Das Finden von Invarianten ist eine der Schwierigkeiten des Hoare-Kalküls. Einen eindeutigen und sicheren Weg zur Bestimmung von Invarianten gibt es nicht. R4b : {I }S{R} {R ∧ ¬B}S 2 {R} {I }repeatS _ until _ B{R ∧ B} R5 {P ∧ ( B = w1}S i {R} __(i = 1(1)n) {P}case _ B _ of _ w1 : S1 ;...wn : S n {R} R6a : Implikationsregel {P}S {Q}, Q {R} {P}S{R} R6b : Implikationsregel P {R}, {R}S {Q} {P}S{Q} Wenn aus einem Zustand P ein Zustand R folgt, aus dem über den Quelltext ein Zustand Q angenommen wird, dann folgt Q bereits aus P. 3 Algorithmen und Datenstrukturen Assertions (Zusicherungen) Aufgabe Assertions sind sinnvoll, wenn - - der Programmierer überprüfen will, ob sein Programm an entscheidenden Stellen die Ergebnisse liefert, die zu erwarten wären. Sonderformen dieser Korrektheits-Checks sind: Preconditions (Vorbedingungen) und Postconditions (Nachbedingungen). Preconditions sichern korrekte Übergabewerte ab, Postconditions überprüfen am Ende der Methode, ob das Ergebnis sinnig ist und den Erwartungen entspricht. Vor- und Nachbedingungen einzelner Programmteile können zum Nachweis der Korrektheit eines Programms benutzt werden. Andere Anwendungen erschließen sich bei komplexen, undurchsichtigen Schleifen, wo jeder Durchlauf kontrolliert wird. man strikt die Überprüfung nach Korrektheit (Assertions) vom Programmfluß trennen (if-else) trennen will. man sichergehen will, dass bestimmte Bereiche beim normalen Betrieb nicht durchlaufen werden. Assertions sollten nicht verwendet werden, wenn - wenn Eingaben von Benutzern oder Dateien, Datenbanken etc. überprüft werden sollen. Bei Assertions wird davon ausgegangen, dass der "boolesche Ausdruck" immer erfüllt ist, solange das Programm korrekt läuft. Bei Benutzereingaben kann man aber zu keiner Zeit davon ausgehen. Sie können zu jeder Zeit falsch bzw. unsinnig sein. Das assert-Makro in C++ zum Verifizieren von Vor- und Nachbedingungen Zusicherungen werden in C++ mit dem Header <cassert> eingebunden, z.B.: #include <cassert> // enthaelt Makrodefinition const int grenze = 100; int Iindex; // .. Berechnung des Index // .. Test auf Einhalten der Grenzen assert(index >= 0 && index < grenze) ... Falls die Annahme (index >= 0 && index < grenze) nicht stimmen sollte, wird das Programm mit einem Fehler abgebrochen, die die zu verifizierende logische Annahme, die Datei und die Nummer der Zeile enthält, in der der Fehler aufgetreten ist. Assertions in Java Assertions werden im Java-Quellcode folgendermaßen genutzt: - assert Ausdruck assert Ausdruck1 : Ausdruck2 Der Ausdruck hinter dem Schlüsselwort assert muß in jedem Fall einen booleschen Wert zurückliefern. In der Regel wird der Ausdruck so gewählt, dass er bei korrekter Arbeitsweise des Programms immer true ergibt. Man spricht deshalb auch von Invarianten. Arbeitet das Programm nicht korrekt und liefert der Ausdruck false zurück, dann wird ein AssertionError ausgelöst. Die zweite Variante erlaubt es, hinter dem Doppelpunkt eine zweite Anweisung an die assert-Anweisung zu übergeben: diese setzt den Fehlertext. Bsp.: Eine Methode div() muß u.a. mit einer Zahl ungleich 0 versorgt werden. Sollte irgendein Programmteil fehlerhaft sein und den Divisor doch mit 0 belegen, muß ein Assert-Error erfolgen. 4 Algorithmen und Datenstrukturen public class Assertiontest { public static int div(int divident,int divisor) { assert divisor != 0 : "Bitte keine Zahl durch 0 teilen."; return divident / divisor; } public static void main(String args[]) { System.out.println("Quotient ist " + div(10,2)); System.out.println("Quotient ist " + div(10,0)); } } Damit das Programm kompiliert und ausgeführt werden kann, muß bei Compiler und Laufzeitumgebung noch ein Schalter gesetzt werden, z.B. javac –source 1.4 AssertionTest.java Die Überprüfung von Assertions ist zur Laufzeit standardmäßig abgeschaltet. Dadurch entsteht kein Geschwindigkeitsverlust bei der Ausführung der Programme. Zur Aktivierung von Assertions muß die Laufzeitumgebung mit dem Schalter –ea (enable assertions) gestartet werden, z.B.: java –ea AssertionTest 5