Formale Programmentwicklung mit dynamischer Logik [1. Aufl.] 978-3-8244-2031-5;978-3-663-14621-6

355 45 21MB

German Pages IX, 288 [296] Year 1992

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Formale Programmentwicklung mit dynamischer Logik [1. Aufl.]
 978-3-8244-2031-5;978-3-663-14621-6

Table of contents :
Front Matter ....Pages I-IX
Einleitung (Maritta Heisel)....Pages 1-4
Bisherige Ansätze zur Programmentwicklung (Maritta Heisel)....Pages 5-24
Das KIV-System als Werkzeug für die formale Programmentwicklung (Maritta Heisel)....Pages 25-55
Ein programmiersprachenorientierter Ansatz (Maritta Heisel)....Pages 56-104
Formale Programmentwicklung durch sukzessive Etablierung von Teilzielen und Rückwärts­Schleifenentwicklung (Maritta Heisel)....Pages 105-147
Vollautomatische Programmsynthese mit Finite Differencing (Maritta Heisel)....Pages 148-157
Halbautomatische Entwicklung von Divide-and-Conquer-Algorithmen (Maritta Heisel)....Pages 158-169
Ein allgemeines Konzept zur formalen Modellierung von Top-Down-Programmentwicklungsmethoden (Maritta Heisel)....Pages 170-183
Definition einer offenen, integrierten Programmentwicklungsmethode (Maritta Heisel)....Pages 184-224
Fazit (Maritta Heisel)....Pages 225-226
Literatur (Maritta Heisel)....Pages 227-230
Back Matter ....Pages 231-288

Citation preview

DUV: Datenverarbeitung

Maritta Heisel

Formale Programmentwicklung mit dynamischer Logik

Maritta Heisel Formale Programmentwicklung mit dynamischer Logik

Maritta Heisel

Formale Programm· entwicklung mit dynamischer Logik

Springer Fachmedien Wiesbaden GmbH

Die Deutsche Bibliothek - CIP-Einheitsaufnahme

Heisel, Maritta: Formale Programmentwicklung mit dynamischer logik 1 Moritta Heisel.- Wiesbaden: Dt. Univ.-Verl., 1992 (DUV : lnformatik} Zugl.: Karlsruhe, Univ., Diss., 1992 ISBN 978-3-8244-2031-5

Der Deutsche Universităts-Verlag ist ein Unternehmen der Verlogsgruppe Bertelsmann International.

©

Springer Fachmedien Wiesbaden 1992

UrsprOnglich erschienen bei Deutscher Universităts-Verlag GmbH, Wiesbaden 1992

Das Werk einschlieBiich aller seiner Teile ist urheberrechtlich geschOtzt. Jede Verwertung auBerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlags unzulassig und strafbar. Dos gilt insbesondere for Vervielfăltigungen, Obersetzungen, Mikroverfilmungen und die Einspeicherung und Verorbeitung in elektronischen Systemen.

ISBN 978-3-8244-2031-5 ISBN 978-3-663-14621-6 (eBook) DOI 10.1007/978-3-663-14621-6

Danksagungen Ich danke Herrn Prof. Dr. W. Menzel für seine Unterstützung und seine konstruktive Kritik bei der Erstellung dieser Arbeit. Auch die anregenden Diskussionen zum weiteren Umfeld des Themas haben wesentlich zum Gelingen der Arbeit beigetragen. Herrn Prof. Dr. S. Jähnieben danke ich für die Übernahme des Koreferates dieser Dissertation und seine nützlichen und ermutigenden Kommentare. Auch den Kollegen aus dem KIV-Projekt, Wolfgang Reif, Werner Stephan und Andreas Wolpers bin ich zu großem Dank verpflichtet. Die Arbeit ist aus diesem Projekt heraus entstanden und wäre ohne das so geschaffene Umfeld nicht möglich gewesen. Sie alle standen mir stets mit Rat und Tat zur Seite. Den Studierenden Martin Gelfort und Thomas Santen danke ich für die Implementierung der hier geschilderten Verfahren und ihre Anregungen zur Verbesserung des Systems. Norbert Lindenberg und Thomas Santen haben mit ihren Kommentaren zu früheren Versionen der Arbeit wesentlich zur Verbesserung der Präsentation beigetragen.

Inhalt 1 Einleitung

1

2 Bisherige Ansätze zur Programmentwicklung

5

2.1 Der Software-Engineering-Ansatz 2.2 Programmiermethodik mit formaler Grundlage 2.3 Deduktive Ansätze 2.3.1 Deduktive Programmsynthese nach Manna und Waldinger 2.3.2 Syntaxgesteuerte, semantikunterstützte Programmsynthese 2.3.3 Automatische Synthese von Skaiernfunktionen 2.3.4 Programmsynthese mit intuitionistischer Typentheorie 2.4 Transformationelle Ansätze 2.4.1 Transformation von rekursiven Programmen 2.4.2 CIP 2.4.3 Programmsynthese mit Termersetzungssystemen 2.4.4 Der Bird/Meertens-Formalismus 2.4.5 Ein informeller Ansatz 2.5 Ein problemorientierter Ansatz 2.6 Der mit der vorliegenden Arbeit verfolgte Ansatz

3 Das KIV -System als Werkzeug für die formale Programmentwicklung 3.1 Die KIV-Logik 3.1.1 Syntax 3.1.2 Semantik 3.1.3 Ein Sequenzenkalkül für die dynamische Logik 3.2 Die Metasprache PPL 3.2.1 Erzeugung von Beweisbäumen 3.2.2 Kontrollstrukturen von PPL 3.3 Implementierung von Beweismethoden mit dem KIV-System

5 8 9

10 11

13 14 16 16 17 18 19 20 22 23

25 26 26 33 39 48 49 52 54

VIII

Inhalt

4 Ein programmiersprachenorientierter Ansatz 4.1 Syntax und Semantik von Guarded-Command-Programmen 4.2 Beschreibung der Heuristiken 4.2.1 Sätze und Strategien für die Entwicklung von bedingten Anweisungen und Schleifen 4.2.2 Entwicklung von Schleifeninvarianten 4.3 Behandlung des Indeterminismus 4.3.1 Änderung der Formelsemantik 4.3.2 Simulation mit Orakeln 4.3.3 Reihenfolgenunabhängige Entwicklung von bedingten Anweisungen 4.4 Die Rolle von Metavariablen bei der Top-DownProgrammentwicklung 4.5 Die implementierte Strategie 4.5.1 Entwicklung von zusammengesetzten Anweisungen 4.5.2 Entwicklung von bedingten Anweisungen 4.5.3 Entwicklung von Schleifen 4.5.4 Entwicklung von Schleifeninvarianten 4.6 Zusammenhang mit Guarded Commands 4.7 Ein Beispiel 4.8 Abschließende Bemerkungen

5 Formale Programmentwicklung durch sukzessive Etablierung von Teilzielen und RückwärtsSchleifenentwicklung

5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8

Die generelle Methode Problembeschreibungen, Normalform von Spezifikationen Die Grundstrategie Die Strengthening-Strategie Die Disjoint-Goal-Strategie Die Protection-Strategie Die Forward-Loop-Strategie Rückwärtsentwicklung von Schleifen 5.8.1 Das Konzept des invarianten Zieles 5.8.2 Die Preservation-Strategie 5.8.3 Die Preservation-Composition-Strategie

56 57 60 60 62 64 65 66 70 71 74 76 77 80 89 90 97 103

105

106 108 110 110 114 117 119 121 122 130 132

IX

Inhalt

5.9 5.10 5.11 5.12 5.13 5.14

5.8.4 Die Backward-Loop-Strategie Die Conditional-Strategie Die Skip-Strategie Die Assignment-Strategie Berechnung von Nachbedingungen Einführung neuer Sicherungsvariablen Abschließende Bemerkungen

6 Vollautomatische Programmsynthese mit Finite Differencing 6.1 Ausgangspunkt: Ein Beispiel 6.2 Abstraktion des Beispiels zu einer Programmentwicklungsmethode 6.3 Realisierung des Verfahrens in der KIV-Umgebung

7 Halbautomatische Entwicklung von Divide-andConquer-Algorithmen 7.1 Die Divide-and-Conquer-Taktik 7.2 Designstrategien für Divide-and-Conquer-Algorithmen 7.3 Realisierung der Designstrategien 7.3.1 Bibliotheken mit Standardalgorithmen 7.3.2 Abgeleitete Antezedenten 7.3.3 Realisierung von DS1 7.3.4 Realisierung von DS2

8 Ein allgemeines Konzept zur formalen Modeliierung von Top-Down-Programmentwicklungsmethoden 8.1 Programmierprobleme 8.1.1 Platzhalter 8.1.2 Berechnete Nachbedingungen 8.1.3 Definition von Programmierproblemen und ihren Lösungen

134 135 140 141 142 144 146

148 148 150 153

158 158 162 163 164 166 167 168

170

172 172 173 174

Einleitung

1

1 Einleitung Die moderne Gesellschaft ist in zunehmendem Maße von der Einsatzbereitschaft und dem korrekten Funktionieren von Computern abhängig. Insbesondere steigt die Menge der im Einsatz befindlichen Software ständig. In der Praxis wird das zufriedenstellende Funktionieren von Programmen nur durch Testen überprüft. Dadurch kann jedoch nur die Anwesenheit, nicht aber die Abwesenheit von Programmfehlern gezeigt werden. Testen kann insbesondere in sensiblen Bereichen wie z.B. der Medizin oder im Bankbereich nicht als ausreichender Korrektheitsnachweis akzeptiert werden. Das Überprüfen von Programmen ausschließlich anband von Tests wird in zunehmendem Maße als unbefriedigender Zustand empfunden. Ein befriedigende Lösung des Problems muß deshalb einen Korrektheitsbeweis der eingesetzen Programme einschließen. Selbstverständlich sind damit nicht alle Probleme gelöst: Es kann weder das Funktionieren der verwendeten Hardware garantiert noch geklärt werden, ob eine formale Spezifikation, die Voraussetzung für einen Korrektheitsbeweis ist, tatsächlich die Wünsche der Benutzer widerspiegelt. Der vielversprechendste Weg zur Erstellung nachweisbar korrekter Programme ist die formale Programmentwicklung. Das heißt, daß schon bei der Programmentwicklung formale Systeme zum Einsatz kommen. Formale Systeme bestehen aus einer formalen Sprache und Regeln, die die Manipulation von Ausdrücken der Sprache erlauben. Aus gegebenen Ausdrücken können neue abgeleitet werden. Logiken sind spezielle formale Systeme, bei denen die Ausdrücke der Sprache Formeln sind, denen ein Wahrheitswert zugeordnet werden kann. Die formale Programmentwicklung geht von einer Spezifikation aus, die in einer formalen Spezifikationssprache ausgedrückt ist. Durch Regelanwendungen wird aus der Spezifikation ein Programm gewonnen. Die anzuwendenden Regeln müssen so beschaffen sein, daß die Korrektheit des erzeugten Programmes garantiert ist. Die in formalen Systemen zur Verfügung stehenden Regeln bewirken meist nur lokale und offensichtlich einsehbare Änderungen der involvierten Ausdrücke. Ihr Abstraktionsgrad ist also sehr niedrig. Um Argumente, wie sie z.B. in mathematischen Beweisen vorkommen, formal nachvollziehen zu können, sind meist viele Ableitungsschritte nötig. Dies macht die Benutzung formaler Systeme ohne Maschinenunterstützung sehr mühsam und fehleranfällig. Für den Einsatz formaler Systeme bei der Programmentwicklung sind daher sowohl Regeln mit möglichst hohem Abstrationsgrad als auch Maschinenunterstützung anzustreben.

2

Einleitung

Der formale Programmentwicklungsprozeß hat zwei wesentliche Merkmale: Zum einen muß durch ein formales System die Korrektheit der entwickelten Programme sichergestellt werden. Zum anderen muß es möglich sein, Ableitungen in einem formalen System und Programmierwissen zu verbinden. Mit dieser Arbeit verfolgen wir das Ziel, den Programmentwicklungsprozeß so zu gestalten, daß die Benutzer ihr individuelles Programmierwissen auch dann einbringen können, wenn die Programmentwicklung als Ableitung in einem formalen System gestaltet ist. Wir wollen also auch bei einem formalen Vorgehen eine möglichst große Methodenvielfalt zulassen. Deshalb streben wir eine vereinheitlichende Sichtweise des Programmentwicklungsprozesses an. Diese bezieht sich sowohl auf das formale System, das die Korrektheit der entwickelten Programme garantiert, als auch auf einen methodologischen Überbau, der die über die Korrektheit hinausgehenden Aspekte der Programmentwicklung beschreibt. Im ersten Teil der Arbeit beschäftigen wir uns mit der Vereinheitlichung von Programmentwicklungmethoden auf Kalkülebene, und im zweiten Teil mit einer einheitlichen Modeliierung des Zusammenhanges von Programmierwissen einerseits und Ableitungen in einem Kalkül andererseits. Um die erste Aufgabe erfüllen zu können, benötigen wir ein formales System, das die Formalisierung möglichst vieler verschiedener Methoden zur Programmentwicklung erlaubt. Obwohl imperative Programme aufgrund ihrer Effizienz am meisten verbreitet sind, werden bei Untersuchungen zur formalen Programmentwicklung oft nur funktionale Programme betrachtet, da diese angenehme theoretische Eigenschaften haben. Wir hingegen wollen uns mit imperativen Programmen beschäftigen. Dementsprechend wählen wir einen Kalkül, mit dem Eigenschaften solcher Programme ausgedrückt werden können. Die Absicht, Programmierwissen in einem formalen Prozeß verwendbar zu machen, läßt es als Nachteil erscheinen, wenn die zu beweisenden Eigenschaften codiert werden müssen, um sie in dem Kalkül auszudrücken. Statt dessen wollen wir direkt Schlüsse über Programme und ihre Korrektheit ausführen können. Dies macht die dynamische Logik zu einer geeigneten Grundlage für unser Vorhaben. Formeln der dynamischen Logik können explizit imperative Programme enthalten, so daß Eigenschaften von Programmen direkt bewiesen werden können. Die in dieser Arbeit verwendete Variante der dynamischen Logik wurde speziell für die Implementierung eines Systems entwickelt, mit dem Korrektheitsaussagen über Programme bewiesen werden können. Dieses System, genannt Karlsruhe Interactive Verifier (KIV) [Heisel, Reif und

Einleitung

3

Stephan 1988a, 1988b], das auf dem Prinzp des taktischen Theorembeweisens beruht, ist eine Programmierumgebung zur Implementierung von Beweismethoden und bildet zusammen mit der zugrundeliegenden dynamischen Logik die formale Basis der Arbeit. In den ersten Kapiteln formalisieren wir einige Programmentwicklungsmethoden aus der Literatur in dynamischer Logik. Diese Methoden gehen das Problem jeweils sehr unterschiedlich an. Durch die Formalisierung stellen wir die Korrektheitsanforderungen der Methoden auf eine gemeinsame formale Grundlage. Damit zeigen wir, daß die dynamische Logik ein für unsere Zwecke geeigneter Formalismus ist. Der Ablauf, dem eine Programmentwicklung nach den verschiedenen Einzelmethoden folgt, ist jedoch so unterschiedlich, daß die verschiedenen Methoden nicht ohne weiteres kombiniert benutzt werden können. Die Darstellung in einem einheitlichen Kalkül reicht für die angestrebte Methodenvielfalt bei der formalen Programmentwicklung nicht aus. Um auch diese Aufgabe zu lösen, entwickeln wir im zweiten Teil der Arbeit ein Darstellungsmittel, das es gestattet, diejenigen Aspekte einer Methode, die über reine Korrektheitsüberlegungen hinausgehen, adäquat auszudrücken. Es handelt sich um ein Konzept zur Modellierung von Programmentwicklungsmethoden, die nach dem Prinzip der Problemzerlegung arbeiten. Dieses Konzept ist unabhängig von speziellen Programmier- oder Spezifikationssprachen, wie z.B. der dynamischen Logik, und verbindet die Korrektheitsaspekte einer Programmiermethode mit der Frage, welche Probleme in welcher Reihenfolge am besten gelöst werden sollen. Hierzu werden die sonst nicht präzise bestimmten Begriffe des Programmierproblems und dessen Lösung formal gefaßt. Zusammen mit dem allgemeinen Beschreibungsmittel für Programmiermethoden definieren wir einen uniformen Ablaufmechanismus, der spezifiziert, wie Zerlegung eines Problems und das Zusammensetzen von Teillösungen zusammenspielen. Dieser macht die Implementierung von Methoden, die nach unserem Konzept formalisiert wurden, zu einer Routineaufgabe. Die Anwendung dieses Konzeptes auf die zuvor formalisierten Methoden ergibt eine einheitliche, integrierte Methode zur Programmentwicklung, die eine Vielzahl von unterschiedlichen Vorgehensweisen bei der Programmentwicklung unterstützt und insbesondere die Kombination der zuvor einzeln formalisierten Methoden erlaubt. Darüber hinaus kann sie in flexibler und korrektheitserhaltender Weise weiterentwickelt und an die verschiedendsten Programmierstile und Benutzerbedürfnisse angepaßt werden. Die Arbeit hat Bezüge zu drei Forschungsgebieten:

4

Einleitung

• Logik: Die Schaffung einer einheitlichen logischen Grundlage zur Programmentwicklung ist ein wichtiges Anliegen dieser Arbeit, und ein Großteil von ihr beschäftigt sich mit der Entwicklung neuer logischer Regeln zur diesem Zweck. • Programmiermethodik: Die Forderung nach Methodenvielfalt und nach der praktischen Handhabbarkeit unseres Ansatzes macht es unerläßlich, bereits etablierte Methoden zur Erstellung von Programmen in den Syntheseprozeß einfließen zu lassen. Wir arbeiten die Essenz, die eine Top-Down-Programmentwicklungsmethode ausmacht, heraus und machen solche Methoden unabhängig von verschiedenen Formalismen oder unterschiedlichen inhaltlichen Vorgehensweisen formal darstellbar. Dies geschieht durch eine präzise Fassung bislang meist unklarer Begriffe. Dabei ist auch ein Mechanismus vorgesehen, der die Anwendung bereits vorhandener Heuristiken ermöglicht. Das entwickelte Konzept zur Modeliierung von Programmentwicklungsmethoden ist so ausgelegt, daß Weiterentwicklungen einer Methode lokal und ohne größere Revisionen an bereits bestehenden Teilen möglich sind. Dies halten wir für wichtig, da wir Programmiermethodik nicht als ein statisches, abgeschlossenes Gebiet betrachten. • Automatische Deduktion: Streng formale Beweise (d.h. Ableitungen in einem formalen System) sind um einiges länger und schwieriger als mathematische Beweise. Es muß deshalb Aufgabe eines Synthesewerkzeuges sein, diese Nachteile soweit wie möglich auszugleichen und den Benutzern viele der auftretenden Trivialbeweise abzunehmen. Einige Teile der integrierten Methode können voll- oder halbautomatisch ablaufen, und unsere Beschreibungsmethode ist so ausgelegt, daß durch lokale Änderungen interaktive Teile durch voll- oder halbautomatische Teile ersetzt werden können, so daß eine weitere Automatisierung schrittweise und inkrementeil erfolgen kann. Kapitel 3 beschreibt Arbeiten, die zusammen mit Wolfgang Reif und Werner Stephan durchgeführt wurden. Der Inhalt von Kapitel 4 ist im wesentlichen identisch mit dem des Papiers [Heisel 1992].

Der Software-Engineering-Ansatz

5

2 Bisherige Ansätze zur Programmentwicklung In diesem Kapitel wollen wir einen Überblick über die vielfältigen Methoden geben, die konzipiert wurden, um in einer systematischen Weise von einem Problem oder einer Spezifikation zu einem Programm zu kommen. Dabei gehen wir exemplarisch vor und beschreiben einige aus der Literatur bekannte Methoden. Wir wollen für solche Ansätze charakteristische Stellvertreter etwas näher beschreiben, die sich in ihrer Sicht des Problems und seiner Lösungsmöglichkeiten in grundlegenden Aspekten unterscheiden. Eine vollständige Aufzählung aller existierenden Ansätze ist nicht das Anliegen dieses Kapitels.

2.1 Der Software-Engineering-Ansatz Die meiste Software wird heute noch ohne formale Hilfsmittel erstellt. Dabei folgt man meist dem sogenannten Phasenmodell, das die verschiedenen Stadien im Lebenszyklus eines Softwareproduktes enthält, siehe z.B. [Mayrhauser 1990]. Am Anfang steht ein mehr oder weniger klar umrissenes Problem. Die Problemanalyse soll dieses Problem vollständig und eindeutig erfassen. Das Ergebnis der Problemanalyse ist die Anforderungsdefinition, auch Pflichtenheft genannt. Dort werden die Anforderungen an ein Softwaresystem in einer Form beschrieben, die keine Hinweise auf eine konkrete Realisierung enthalten sollte. Die nächste Phase ist die Entwurfsphase. Hier wird ein Entwurf des Gesamtsystems entwickelt. Im allgemeinen wird das Gesamtsystem in Moduln, d.h. unabhängig voneinander realisierbare Einzelbausteine, unterteilt. Ein weit verbreitetes Konzept hierzu ist die hierarchische Modularisierung, die wiederum top-down oder bottom-up durchgeführt werden kann. Bei der Top-Down-Methode (auch schrittweise Verfeinerung genannt) legt man bei jedem Entwurfsschritt fest, was die Untermoduln leisten sollen, nicht jedoch, wie dies zu geschehen hat. Die Bottom-UpMethode setzt bereits vorhandene Moduln zu komplexeren zusammen. Das Ergebnis der Entwurfsphase ist die Spezifikation, in der für jeden Modul seine Funktion und seine Schnittstellen zu anderen Moduln beschrieben sind. Die sich anschließende Implementierungsphase besteht in der Erstellung eines lauffähigen Programmes, das in seinem Ein-/Ausgabeverhalten der Spezifikation entsprechen soll. Auch bei der Implementierung kann

6

Bisherige Ansätze zur Programmentwicklung

man nach der Top-Down- oder der Bottom-Up-Methode vorgehen. Eine Programmdokumentation soll die Beziehung zwischen Spezifikation und Programm herstellen. Das erstellte Programm wird sodann einer Funktionsprüfung (Test) unterzogen. Dabei wird das Ein-/Ausgabeverhalten des Programmes anband einer (notwendigerweise endlichen) Testmenge überprüft. Das Ergebnis der Testphase ist ein modifiziertes Programm mit entsprechend modifizierter Dokumentation. Die letzten beiden Phasen des Software-Lebenszyklus sind Installation und Abnahme sowie die Wartungsphase. Dieses Modell, das auch Wasserfallmodell genannt wird, hat Schwächen, da eine Rückkehr von einer späteren Phase zu einer früheren nicht vorgesehen ist. Entdeckt man aber beispielsweise in der Implementierungsphase einen Fehler, der schon aus der Problemanalyse stammt, müssen alle früheren Schritte wiederholt werden, was unter Umständen hohe Kosten verursachen kann. Ein völlig anderes Modell der Softwareentwicklung ist das evolutionäre Modell [Lowry und Duran 1989], das hauptsächlich in der künstlichen Intelligenz Verwendung findet. Die Softwareentwickung beginnt mit einem Prototyp und setzt sich mit inkrementeHer Veränderung und Wartung des Prototyps fort. Dieser Ansatz ist allerdings nur zu empfehlen, wenn der betreffende Problembereich unzureichend untersucht ist und keine Erfahrungen mit ähnlichen Systemen vorliegen, da die evolutionäre Entwicklung leicht zu unstrukturierten Systemen führen kann, die ab einer gewissen Größe undurchschaubar werden und nicht mehr zu warten sind. Ein Modell, das die Vorteile des Phasenmodells und des evolutionären Modells verbindet, ist das Spiralmodell [Boehm 1986], [Lowry und Duran 1989]. Dieses enthält im wesentlichen dieselben Phasen wie das Phasenmodell , aber eine Wiederholung der Phasen ist dort von vorneherein vorgesehen. Die Grundidee besteht darin, zunächst für eine eingeschränkte Menge der wesentlichsten Anforderungen einen Prototyp zu erstellen, der zur grundsätzlichen Überprüfung der beabsichtigten Funktion des Systems herangezogen werden kann. Die Überprüfung des Prototyps beendet die erste Spirale. Der zweite Durchlauf erweitert die Anforderungen und die Funktion des Systems. Weil der erste Prototyp mit wenig Aufwand erstellt wurde, kann er verworfen werden, falls die Modifikationen, die der zweite Durchlauf erforderlich macht, die Struktur des Systems zerstören würden. Weitere Spiraldurchläufe verlaufen nach demselben Muster, bis sie in Wartungsdurchläufe übergehen. Auf diese Weise minimiert das Spiralmodell sowohl Risiko als auch Kosten, da die wohl-

Der Software-Engineering-Ansatz

7

strukturierten Managementtechniken des Phasenmodells mit den frühen Validierungsmöglichkeiten des evolutionären Modells verbunden werden. Die Ergebnisse der einzelnen Phasen werden als Dokumente dargestellt. Eine Unterstützung bei der Durchführung der Phasen ist hauptsächlich in Form von graphischen Hilfsmitteln zur Erstellung der entsprechenden Dokumente verfügbar. Die SADT-Methode (structured analysis and design technique) [Ross and Schoman 1973] erlaubt es beispielsweise, sowohl die Ergebnisse der Problemanalysephase graphisch darzustellen als auch - in der Entwurfsphase - das Systemverhalten zu spezifizieren. Als Ausdrucksmöglichkeiten stehen Kästchen und Pfeile, die jeweils beschriftet werden können, zur Verfügung. Eine Möglichkeit zur Darstellung des Ergebnisses der Entwurfsphase ist auch die HIPO-Methode (hierarchy of input-process-output) [Katzan 1976]. HIPO-Diagramme sind im wesentlichen dreispaltige Tabellen. Je eine Spalte ist für die Eingabegrößen, eine Prozeßbeschreibung sowie die Ausgabegrößen vorgesehen. Bei der Jackson-Methode [Jackson 1983] werden zunächst die Datenstrukturen genau beschrieben. Dies geschieht mit Hilfe Struktogramm-ähnlicher graphischer Darstellungsmittel (s.u.). Anschließend wird ein Programm entwikkelt, dessen Struktur den Aufbau der Datenstrukturen widerspiegelt. In der Implementierungsphase werden Methoden wie strukturierte Programmierung [Dahl, Dijkstra und Haare 1981] angewandt. Strukturierte Programmierung zerlegt das Problem in Teilprobleme und Beziehungen zwischen Teilproblemen. Diese Zerlegung wird so lange iteriert, bis die Aufgaben so klein geworden sind, daß sie ohne weitere Verfeinerung gelöst werden können. Graphische Hilfsmittel sind hier Struktogramme. Ein Problem bei allen genannten Techniken besteht darin, eine präzise Semantik der graphischen Notation anzugeben. Die Einteilung des Softwareentwicklungsprozesses in die oben genannten Phasen hat sich in der Praxis eingebürgert. Allerdings beschränken sich die verwendeten Hilfsmittel zur Durchführung der einzelnen Phasen häufig auf graphische Hilfsmittel zur Erstellung von Dokumenten. Die mit den Methoden des Software Engineering entwickelten Programme werden i.a. Fehler enthalten, da durch Testen nur die Anwesenheit, niemals aber die Abwesenheit von Fehlern festgestellt werden kann. Dieser Zustand kann auf Dauer nicht als bef~iedigend angesehen werden, da Software immer häufiger auch in sensitiven Bereichen eingesetzt wird. Eine Abhilfe kann der Einsatzformaler Methoden schaffen. Die Voraussetzung hierfür ist die Angabe einer formalen Semantik für die ver-

8

Bisherige Ansätze zur Programmentwicklung

wendeten Spezifikations- und Programmiersprachen. So wird eine präzise Definition des Begriffs "Programm P ist korrekt bezüglich der Spezifikation Sp" erst ermöglicht. Das Problem der formalen Darstellung von Spezifikationen ist ein eigenes Forschungsgebiet, mit dem wir uns hier nicht beschäftigen. Interessierte Leser seien auf die Literatur [Bj!Zirner und Jones 1982], [Ehrig und Mahr 1985], [Spivey 1988] verwiesen. In dieser Arbeit geht es vielmehr um die formale Durchführung der Implementierungsphase, also um Lösungen des Problems, von einer formalen Spezifikation zu einem nachweisbar korrekten Algorithmus bzw. Programm zu kommen. Ein erster Schritt in diese Richtung ist eine von Dijkstra [Dijkstra 1976] erfundene Programmiermethodik, die auf einer formalen Semantik (weakest preconditions) fußt und die Programmentwicklung am Korrektheilsbeweis ausrichtet. Ableitungen in einem Kalkül (meistens Gleichheitsbeweise) werden allerdings nur zur geeigneten Umformung von Spezifikationen und Zusicherungen benutzt. Die beiden verbreitetsten Ansätze, bei denen das Programm durch Ableitung in einem Kalkül entsteht, sind die deduktive und die transformationeHe Programmsynthese. Sie können daduch charakterisiert werden, daß im einen Fall konstruktive Existenzbeweise und im anderen Fall Gleichheitsbeweise geführt werden. Von beiden Ansätzen gibt es verschiedene Ausprägungen, von denen wir einige hier vorstellen wollen.

2.2 Programmiermethodik mit formaler Grundlage In seinem Buch "A Discipline of Programming" [Dijkstra 1976] führte Dijkstra eine Programmiermethodik ein, mit der Programme so entwikkelt werden können, daß ihre Korrektheit mit einem Haare-Kalkül [Haare 1969] sofort nachgewiesen werden könnte. Die Methodik ist allerdings nicht explizit ausgearbeitet, sondern wird an vielen Beispielen demonstriert. Weiterentwicklungen und Anwendungen sind in [Gries 1981] und [Dijkstra 1990] zu finden. Grundlage der Programmiermethodik ist eine imperative, indeterministische Programmiersprache, für die eine Prädikatentransformatorsemantik definiert ist. Die Semantik eines Programmkonstruktes ist durch die schwächste Vorbedingung gegeben, die erfüllt sein muß, damit das Programm terminiert und eine gegebene Nachbedingung etabliert. Aus dieser Semantik werden Beweisregeln für Programme abgeleitet, die sich an der Syntax der Programmkonstrukte orientieren, weshalb wir diesen Ansatz als programmiersprachenorientiert bezeichnen. Als Spezifikationssprache

9

Deduktive Ansätze

wird die Sprache der Prädikatenlogik erster Stufe, angereichert um weitere Quantoren, verwendet. Am Beginn der Programmentwicklung steht die Wahl eines Programmkonstruktes; in den Beispielen ist dies meistens eine Schleife. Die noch unbestimmten Teile des Programmes, im Fall der Schleife Initialisierung, Schleifenbedingung und Schleifenrumpf, müssen so entwickelt werden, daß sie bestimmten durch die Beweisregeln vorgegebenen Bedingungen genügen, die hinreichend für die Korrektheit des Programmes sind. Bei der Entwicklung von Schleifen ist die Entwicklung einer geeigneten Invariante (vor Entwicklung des Rumpfes) wesentlich. Dazu werden verschiedene Heuristiken angegeben. Die Entwicklung von konkreten Programmen wird durch Beweise gesteuert: Dazu werden Zusicherungen (also Bedingungen, die an einer bestimmten Stelle eines Programmes gelten müssen) oder auch Terme, die in vorläufigen, häufig ineffizienten Versionen eines Programmes auftreten, durch gleichheits- oder äquivalenzerhaltende Operationen umgeformt. Ziel der Umformungen ist es, aus Formeln in einfacher Weise Programme ablesen zu können, oder Terme so umzuformen, daß durch Einführung neuer Variablen ein Effizienzgewinn erzielt werden kann. In den Kapiteln 4 und 6 werden wir ausführlich auf diesen Ansatz eingehen.

2. 3 Deduktive Ansätze In diesem Abschnitt beschreiben wir vier verschiedene Arbeiten. Drei davon basieren auf der Prädikatenlogik erster Stufe und unterscheiden sich im wesentlichen durch die angegebenen Strategien, die es ermöglichen sollen, in dem gewählten Kalkül gezielt nach Beweisen zu suchen. Neben der Prädikatenlogik findet auch ein anderer Kalkül zum konstruktiven Beweis von Existenzaussagen immer weitere Verbreitung, nämlich die intuitionistische Typentheorie von Martin-Löf. Auch auf diese wollen wir kurz eingehen. Allen Ansätzen ist gemeinsam, daß mit ihnen funktionale Programme erzeugt werden, die durch eine prädikatenlogische Formel der Form Spec: Vx.3y.(pre(x)

~

post(x,y))

spezifiziert werden. Hier ist .r die Liste der Eingabevariablen und y die Liste der Ausgabevariablen. Die prädikatenlogischen Formeln pre und post sind die Vor- bzw. Nachbedingung der Spezifikation. Ergebnis des

Bisherige Ansätze zur Programmentwicklung

10

Syntheseprozesses ist eine Funktion f, für die gilt Res: \1'3-.(preW

~

post(3.,f(A)))

2.3.1 Deduktive Programmsynthese nach Manna und Waldinger In [Manna und Waldinger 1980] wird ein Kalkül eingeführt, der es erlaubt, gleichzeitig mit einem Beweis eines Satzes der Form Spec eine Funktion f aufzubauen, so daß Res gilt. Dieser Kalkül wird in Tabellenform dargestellt. Eine Tabelle hat die Form Voraussetzungen

Ziele

Ausgaben

In der ersten Spalte sind Voraussetzungen eingetragen, in der zweiten Spalte Ziele, also Behauptungen, die noch zu zeigen sind. Die dritte Spalte kann einen Term enthalten. Falls ein Term in einer Zeile steht, deren erste Spalte die Formel ~ oder deren zweite Spalte die Formel trY.e. enthält, ist er die gesuchte Funktion f, andernfalls ein Zwischenergebnis, das noch unbestimmte Variablen enthält, also noch kein "Zeuge" für den Existenzquantor aus Spec ist. Eine initiale Tabelle hat die Form Voraussetzungen

Ziele

Ausgaben

post(3..~

y_

preW

Die Anwendung von Ableitungsregeln fügt der Tabelle neue Zeilen hinzu. Die Semantik einer Tabelle ist als Implikation definiert: Die Konjunktion der Allabschlüsse aller Voraussetzungen muß die Disjunktion der Existenzabschlüsse aller Ziele implizieren. Eingabevariablen werden während des gesamten Ableitungsprozesses als Konstanten behandelt. Die Re-

Deduktive Ansätze

11

geln sind so definiert, daß das Hinzufügen einer neuen Zeile die Gültigkeit der Tabelle erhält. Es gibt vier Arten von Regeln: Aufspaltungsregeln erlauben es, konjunktive Voraussetzungen, disjunktive Ziele oder Implikationen aufzuspalten. Transformationsregeln dienen zur Verwendung von Wissen über den Problembereich. Gleichheiten oder Äquivalenzen, die (eventuell unter gewissen Voraussetzungen) in dem betrachteten Modell gelten, werden als bedingte Ersetzungsregeln repräsentiert; die Gleichheiten oder Äquivalenzen werden also nur in eine Richtung angewandt. Für die Anwendung von Transformationsregeln wird Unifikation verwendet. Falls ein Term in der Ausgabespalte vorhanden ist, wird die unifizierende Substitution auch auf diesen angewandt. Vier verschiedene Resolutionsregeln (die sich darin unterscheiden, ob die beiden beteiligten Formeln Voraussetzungen oder Ziele sind) dienen zur Verknüpfung von Formeln, die gleichartige Teilformeln enthalten. Unifizierbare komplementäre Teilformeln werden dabei eliminiert. Die Anwendung von Resolutionsregeln führt zur Einführung von bedingten Anweisungen in die Ausgabespalte der neuen Zeile. Die Anwendbarkeit der Resolutionsregeln wird durch eine Polaritätsstrategie eingeschränkt. Eine Induktionsregel erlaubt es, die Induktionshypothese einer Spezifikation als neue Voraussetzung einzuführen. Für ihre Benutzung gibt es eine Rekurrenzstrategie. Die Einführung von Induktionshypothesen ermöglicht in Verbindung mit den Resolutionsregeln die Einführung von rekursiven Aufrufen der synthetisierten Funktion f in der Ausgabespalte. Diese Regeln werden so lange angewandt, bis die Gültigkeit der Tabelle bewiesen ist, also die Voraussetzung~ oder das Ziel true abgeleitet wurde. Die beiden o.g. einfachen Strategien schränken die Anwendung der Resolutionsregeln und der Induktionsregel ein. Heuristiken allerdings, die im Gegensatz hierzu postitive Information darüber geben, wie Beweise zu finden sind, werden nicht betrachtet. Eine Strategie, die auf den Ideen dieses Ansatzes aufbaut, wurde in der KIV-Umgebung (siehe Kapitel 3) realisiert [Müller 1990].

2.3.2

Syntaxgesteuerte, semantikunterstützte Programmsynthese

Etwa gleichzeitig mit Manna und Waldinger stellte Bibel seinen Ansatz zur deduktiven Programmsynthese vor [Bibel 1980]. Eine Formel der Form

12

Bisherige Ansätze zur Programmentwicklung

Spec:

Vx.3~.(pre(x) ~

post(xs))

wird durch äquivalente Umformungen in eine ausführbare Form gebracht. Mit "ausführbar" ist gemeint, daß die resultierende Formel (rekursive) Gleichungen für die Ausgabevariablen y_ enthält. Es werden Strategien angewandt, die heuristisch angesteuert werden. Die wesentlichen Strategien sind GUESS und DOMAIN. Die Strategie GUESS (hier formuliert für einzelne Variablen) transformiert ein Problem der Form Vx.3y.(pre(x)

~

post(x,y))

in ein Problem der Form Vx, y'.3y.(domain-specification ~ (pre(x) ~ post(x,y)" (y =y' v y::;:. y'))) wobei domain-specification die Konjunktion einer echten Untermenge der Konjunktionsglieder von post(x,y') ist. Um zu entscheiden, welche Untermenge gewählt werden sollte, wird die Strategie DOMAIN verwendet. Diese Strategie benötigt eine Maßfunktion, die eine Ordnung auf der Potenzmenge der Konjunktionsglieder von post(x,y') etabliert. Diese Maßfunktion berücksichtigt die Anzahl der Elemente des jeweiligen Grundbereiches, die alle Konjunktionsglieder der betreffenden Untermenge erfüllen. Diese Menge sollte möglichst endlich sein. Als zweiter Faktor geht in die Maßfunktion die Anzahl der Schritte ein, die benötigt wird, um ein solches Element zu berechnen. Auch diese Maßzahl sollte möglichst klein sein. Die DOMAIN-Strategie wählt eine Untermenge mit minimalem Wert der Maßfunktion aus. Die anderen Strategien sind durch die syntaktische Form spezifiziert, in die die aktuelle Formel zu bringen ist. Eine Strategie versucht beispielsweise, die Formel in disjunktive Normalform zu überführen. Damit wird eine Fallunterscheidung in die angestrebte Funktionsdefinition eingeführt. Eine weitere Strategie versucht, ein Rekursionsschema, d.h. rekursive Gleichungen für die beteiligten Variablen zu finden. Hierzu wird eine Wissensbasis verwendet, die für die beteiligten Sorten typische Rekursionsschemata enthält. Außerdem gibt es eine Strategie, die versucht, in der Formel vorkommende Prädikate evaluierbar zu machen. Für viele Probleme führt eine Anwendung der Strategien in obiger Reihenfolge zum Erfolg. Ein wichtiger Aspekt dieses Ansatzes besteht darin, daß versucht wird, durch Anwendung von Heuristiken den Suchraum einzuschränken. Eine Implementierung des Ansatzes ist in [Bibel und Hörnig 1984] beschrieben.

Deduktive Ansätze

13

2.3.3 Automatische Synthese von Skolemfunktionen In einerneueren Arbeit [Biundo 1988] wird noch größerer Wert auf Heuristiken gelegt, da das beschriebene Synthesesystem Teil eines vollautomatischen Induktionsbeweisers ist und damit auf jegliche Benutzerunterstützung verzichten muß. Ausgangspunkt des Verfahrens ist eine Formel der Form 'JI

= 'v'11..3y.V~.cp(A, y, ~

.

Es darf also genau eine existenzquantifizierte Variable vorkommen. Die Formel lfl wird in eine Spezifikation für eine Skolemfunktion I umgeformt. Die so erhaltene Formel

ist hinreichend für die Gültigkeit von lfl. Aus der Spezifikation lflo wird ein Algorithmus für 1 gewonnen. Ein Algorithmus (hier für eine Eingabevariable) ist in diesem Zusammenhang eine Menge von Definitionsformeln der Form Vx.(x =O'i ~ f(x) ='ti). Die Prämissen x = O'i bilden eine vollständige Fallunterscheidung und schließen sich gegenseitig aus. Genauer gesagt wird lflo durch Anwendung von Transformationsregeln in eine Formelmenge 'I' = DEFr u REMr überführt. Die Formelmenge DEFr ist eine Menge von Definitionsformeln für die Skolernfunktion, und REMr ist eine Menge von Restformeln, die bewiesen werden müssen, wobei DEFr als Voraussetzung benutzt werden kann. Es gibt verschiedene Transformationsregeln, z.B. zur Ersetzung von Gleichem durch Gleiches, zur Zusammenfassung von Gleichungen oder zur symbolischen Auswertung von Termen. Diese Transformationsregeln werden gemäß einer Strategie angesteuert: Zuerst wird eine Induktionsregel angewandt. Damit wird gemäß einer Heuristik ein Induktionsschema für die gesuchte Skaiernfunktion erzeugt. Das Ergebnis dieses Schrittes ist eine Menge von Induktionslormeln. Diese werden durch symbolische Auswertung transformiert. Ein Teil der sich so ergebenden Formeln kann entweder als Definitionsformel (dies ist syntaktisch möglich) oder als Restformel (hierzu existieren Heuristiken) identifiziert werden und wird nicht weiter bearbeitet. Auf die verbleibenden Formeln werden gemäß Heuristiken so lange weitere Transformationsregeln angewandt, bis alle Formeln entweder als Definitions- oder Restformeln identifiziert werden konnten. Es ist sichergestellt, daß die Menge der Definitionsformeln fallvollständig und eindeutig ist.

14

Bisherige Ansätze zur Programmentwicklung

2.3.4 Programmsynthese mit intuitionistischer Typentheorie In den siebziger Jahren entwickelte Martin-Löf eine konstruktive Typentheorie [Martin-Löf 1984], die zunehmend in der Programmsynthese Verwendung findet [Nordström 1981], [Constable et al. 1986], [Backhouse 1989]. Die von Martin-Löf definierte Typstruktur ist sehr reichhaltig, so daß es zu jeder prädikatenlogischen Formel einen Typ gibt ("formulae as types"). Die folgende Tabelle gibt die Entsprechungen wieder. Für jeden Typ ist angegeben, welche Form seine Elemente haben. Prädikate entsprechen elementaren Typen. Formel

Typ

AAB kartesisches Produkt AxB AvB disjunkte Summe A+B A-7B Funktionenraum A --7 B A --7 false Funktionenraum A --7 0 wobei 0 der leere Typ ist Allquantor V'x.B abhängiges Produkt OxE A.B(x) für jedes XE A ist B(x) ein Typ Existenzquantor ::Jx.B abhängige Summe 1:xE A.B(x) Konjunktion Disjunktion Implikation Negation

Element

(a,b) ila, jlb A.x.e

A.x.b (a,b)

In [Martin-Löf 1984] ist ein Kalkül definiert, der es gestattet, sogenannte Urteile abzuleiten. Es gibt vier Arten von Urteilen: A set

A=B

XE A =y E A

X

Aisteine Menge bzw. Typ A und B sind gleiche Typen x ist ein Element von A x und y sind gleiche Elemente vom Typ A

Die Regeln zur Ableitung von Urteilen sind in vier Klassen unterteilt: Bildungsregeln geben an, wie aus vorhandenen Typen neue konstruiert werden können. Einführungsregeln geben an, wie sogenannte kanonische Elemente des betreffenden Typs aussehen. Diese Elemente sind in obiger Tabelle angegeben.

Deduktive Ansätze

15

Eliminationsregeln erlauben die Konstruktion von Funktionen, die auf den Typen operieren. Diese Funktionen sind sog. nichtkanonische Elemente, aber ihre Werte sind kanonische Elemente. Der Unterschied zwischen kanonischen und nichtkanonischen Elementen entspricht dem Unterschied zwischen Konstruktorausdrücken und solchen, die nicht nur Konstruktorsymbole enthalten, wie sie aus der Theorie der abstrakten Datentypen bekannt sind. Gleichheitsregeln geben an, wie die eingeführten Funktionen auszuwerten sind, d.h. wie aus nichtkanonischen Elementen kanonische berechnet werden.

Was hat dies alles mit Programmkonstruktion zu tun? Wie wir gesehen haben, betrachtet der deduktive Ansatz die Konstruktion eines Programmes als Beweisaufgabe, bei der eine Aussage der Form Spec zu zeigen ist. Auch zu dieser Formel gibt es einen korrespondierenden Typ. Eine Aussage der Form x E A kann gedeutet werden als "x ist ein Beweis für die Formel A ". Dies ist an der Form der kanonischen Elemente sichtbar: Ein Beweis für eine Formel A A B besteht aus einem Beweis für A und einem Beweis für B. Ein (konstruktiver) Beweis für eine Formel A v B besteht aus einem Beweis für A oder einem Beweis für B, zusammen mit der Angabe, welche der beiden Formeln bewiesen wurde. Ein Beweis für eine Formel A ~ B besteht aus einer Vorschrift, wie aus einem Beweis für A ein Beweis für B zu erhalten ist. Ein Beweis für eine Formel \tx.B besteht aus einer Vorschrift, wie für jedes x ein Beweis für B(x) zu erhalten ist. Ein (konstruktiver) Beweis für eine Formel 3x.B besteht aus der Angabe eines Elementes a und einem Beweis für B(a). Damit ist das Problem, ein Programm zu konstruieren, darauf reduziert, ein Element eines bestimmten Typs zu konstruieren, also ein Urteil der Form x E A abzuleiten. Weil der Kalkül konstruktiv ist, ist gewährleistet, daß als Zeuge für den Existenzquantor in Spec immer eine Funktion konstruiert wird, die die allquantifizierte Variablen als Eingabe hat. Die durch die Eliminationsregeln eingeführten Funktionen haben in der Tat das Aussehen von Kontrollstrukturen funktionaler Programmiersprachen.

16

Bisherige Ansätze zur Programmentwicklung

2. 4 Transformationelle Ansätze Der transformationeile Ansatz zur Programmsynthese versucht, durch gleichheitserhaltende Umformungen eine Spezifikation schrittweise in ein ausführbares Programm zu transformieren. Dies hat zur Folge, daß Spezifikations- und Implementierungssprache gleich sein müssen. In den hier vorzustellenden Ansätzen äußert sich dies darin, daß entweder schon die Spezifkation konstruktiv, also ausführbar sein muß, so daß die Transformationsschritte lediglich der Effizienzsteigerung dienen, oder es wird eine Breitbandsprache verwendet, die sowohl die Elemente von Spezifikationssprachen als auch die von Programmiersprachen enthält.

2.4.1 Transformation von rekursiven Programmen Das Gebiet des transformationeilen Programmierens wurde von Burstall und Darlington maßgeblich mitbegründet In [Burstall und Darlington 1977] werden Regeln vorgestellt, die es ermöglichen, rekursive Programme in andere, in der Regel effizientere rekursive Programme umzuformen. Ausgangspunkt des Verfahrens ist eine Menge von rekursiven Gleichungen, die ein i.a. ineffizientes Programm repräsentieren. Die Regeln erlauben es, dieser Menge neue Gleichungen hinzuzufügen. Die neue, effizientere Definition der zu berechnenden Funktion muß zum Schluß aus der finalen Menge von Gleichungen extrahiert werden. Es gibt Regeln zur • Definition von Gleichungen. Dies entspricht der Aufstellung einer Spezifikation. • Instantiierung von Gleichungen (ersetze Variable durch Term). • Expansion von Definitionen (unfolding). Auf der rechten Seite einer Gleichung wird eine Funktion durch ihre Definition ersetzt. • Kontraktion von Definitionen (folding). Dies ist die zur Expansion inverse Operation. • Abstraktion. Hierunter ist die Einführung von Abkürzungen zu verstehen. • Anwendung von algebraischen Gesetzen, die für Funktionen gelten, die als primitiv betrachtet werden. Eine Strategie empfiehlt die Anwendung der Regeln in folgender Reihenfolge: (a) Führe alle notwendigen Definitionen durch.

Transformationelle Ansätze

17

(b) Instantiiere. (c) Für jede Instantiierung expandiere wiederholt. Für jede Expansion: (d) Versuche, algebraische Gesetze und Abstraktion anzuwenden. (e) Kontrahiere wiederholt. Die Schritte (c) und (e) können automatisch durchgeführt werden. Probleme dieses Ansatzes sind, daß die Terminierung der Funktion im Laufe der Transformation verloren gehen kann und daß nicht formal gefaßt werden konnte, wann ein Transformationsschritt zu einem Effizienzgewinn führt.

2.4.2 CIP Weite Bekanntheit hat auch das Projekt CIP (computer-aided, inuitionguided programming) erlangt [Broy 1984], [Möller 1990]. Das Anliegen des CIP-Projekts war es, eine integrierte Umgebung für transformationelles Programmieren zu entwickeln, die sowohl eine Methodologie als auch eine (Breitband-) Sprache und ein implementiertes System umfaßt. Der Softwareentwicklungsprozeß wird in sechs Phasen eingeteilt: ( 1) Anforderungsanalyse. Ihr Ergebnis ist eine formale Spezifikation. (2) Transformation der Spezifikation. Die erste Spezifikation wird verallgemeinert oder vervollständigt. Sie kann auch in Unterprobleme unterteilt werden. (3) Von der Spezifikation zum Algorithmus. Hier werden rekursive Gleichungen abgeleitet. Dies entspricht dem Ausgangszustand der Methode von Burstall und Darlington (Abschnitt 2.4.1). (4) Transformation von rekursiven, funktionalen Programmen. Wie schon in Abschnitt 2.4.1, soll hier ein Effizienzgewinn erzielt werden. (5) Übergang zu prozeduralen Programmen. (6) Transformation von prozeduralen Programmen. Bis auf die erste werden alle Phasen mittels Anwendung von Transformationsregeln durchgeführt. Transformationsregeln werden als Diagramme der Form

18

Bisherige Ansätze zur Programmentwicklung

Pl

+


.a) und p(zj) = Vj für ~~~m.



Zur Erläuterung der Punkte (ix) und (x) betrachten wir die Umgehungen Pi genauer, wobei wir nur eine Prozedur p deklarieren. Die Umgebung Po hat das Aussehen p[p/(0 I q : ~>.abort)] .

39

KIV-Logik: Ein Sequenzenkalkül

Ein Aufruf von p würde zur Ausführung von abort und damit zur Nichtterminierung des Programmes führen. Die Umgebung p 1 hat die Form p[ p/ (p[ p/(0 I .ap)] .

Ein Aufruf von p führt zu einer Ausführung von ap in der Umgebung PO· Falls in ap ein weiterer Aufruf von p vorkommt, führt dies zur Nichtterminierung. Die Umgebung pz p[ p/ (p[ p/ (p[ p/ (0 I .ap)]

I ~ (a)(Ioop a times i)q>

(dia_loop_ind)

Beweis Die Ableitungen dieser Axiome sind in Anhang A2 zu finden.



Das Axiom dia_while besagt, daß es ein i gibt, so daß die Schleife nach exakt i Durchläufen anhält. Dabei ist die in [Goldblatt 1982] verwendete Omega-Regel durch Induktion über die in Definition 3.9 eingeführte Zählerstruktur ersetzt worden. Für lokale Variablen, Prozedurdeklarationen und -aufrufe benötigen wir keine Axiome, da wir hierfür nur wenige Regeln vorstellen werden, die an anderer Stelle als korrekt nachgewiesen sind. Wir zeigen nun ein Lemma, das sich bei späteren Korrektheitsbeweisen als nützlich erweisen wird. Lemma 3.27 (Ausführung von while-Schleifen) Folgende Sequenz ist ableitbar:

f-

(E ~ ((while E do a od) q> ~ (a)(while E do a od) q>)) (-,E ~ ((while E do a od) q> ~ q>))

1\

(a) (b)

Beweis Da eine vollständige Ableitung der Sequenz in unserem um obige Regeln erweiterten Basiskalkül sehr umfangreich und schwer zu lesen wäre, führen wir einen Beweis, wie er in Lehrbüchern üblich ist. Dabei verwenden wir die Abkürzungen

ß=if E then a eise abort fi E#a

=while E do a od

und

Wir zeigen zunächst das erste Konjunktionsglied (a) und dabei die Richtung"~". Die Behauptung ist hier E A (E#a)q>

~

(a)(E#a)q>

45

KIV-Logik: Ein Sequenzenkalkül

Wegen dia_while ist dies äquivalent zu E A

3i.(loop ß times i)(

1\ -,E) ~ q>)

Wieder nehmen wir eine Fallunterscheidung nach i vor: Im Fall i = zero ist die Prämisse äquivalent zu -,E 1\ q> 1\ -,E , woraus sofort cp folgt. Im Fall i =next(io) ist die Prämisse wegen dia_loop_ind äquivalent zu -.E 1\ (ß)~ . Das Axiom dia_cond ergibt (-,E 1\ (E ~ (a)~)

1\

(-,E ~ (abort)~)) ,

was man umformen kann zu -,E 1\ (abort)~ ; dies ist äquivalent zu false, und deshalb folgt cp. Das zweite Konjunktionsglied von (***)ist äquivalent zu -.t 1\ q> ~ 3j. (Ioop

ß timesj)(q> 1\ -,E)

,

47

KIV-Logik: Ein Sequenzenkalkül

dessen Voraussetzung wegen dia_loop_base äquivalent ist zu (loop tim es zero)(



comp_tac r ~(a;ß)q>

Diese besagt, daß eine Sequenz der Form r ~(a;ß)q> abgeleitet werden kann, falls Sequenzen der Form r ~ ( a)s und S ~ (ß)q> bereits abgeleitet sind. Dabei können für a und ß beliebige Programme, für r eine beliebige Formelliste und für cp und beliebige Formeln eingesetzt werden. Für die Programmverifikation ist die Regel, wobei sie rückwärts angewandt wird, in dieser Form durchaus nützlich, da a, ß, T und cp bekannt sind und die Zwischenbedingung erfragt oder ausgerechnet werden kann. Möchte man aber Programmentwicklung betreiben, so ist es nötig, die Regel anwenden zu können, ohne die Programme a und ß zu kennen, da diese ja erst entwickelt werden sollen. Gegeben eine Vorbedingung rund eine Nachbedingung cp, soll es also möglich sein, die Entwurfsentscheidung zu treffen, daß das zu entwickelnde Programm die Form einer zusammengesetzten Anweisung haben soll. Diese Entwurfsentscheidung spiegelt sich in der Rückwärtsanwendung obiger Regel wider, die damit das Gesamtproblem in zwei Teilaufgaben zerlegt, die unabhängig voneinander gelöst werden können. Die erste Teilaufgabe besteht darin, ein Programm a zu entwickeln, das, ausgehend von einem Zustand, in dem T gilt, die Zwischenbedingung Setabliert. Das zweite Teilproblem besteht in der Entwicklung eines Programmes ß, das im Anschluß an a die Nachbedingung cp etabliert. Dieses Vorgehen macht es zwingend nötig, Regeln auch dann anwenden zu können, wenn noch nicht alle Teile der involvierten Sequenzen bekannt sind. Auch von einem logischen Standpunkt aus ist dies ein sinnvolles Vorgehen: da die Regel für alle a und ß und korrekt ist, brauchen diese bei Anwendung nicht bekannt zu sein. Allerdings gibt es auch Regeln, deren Korrektheit von gewissen Variablenbedingungen abhängig ist, z.B. daß die freien Variablen einer Formel eine Untermenge der Zuweisungs-

s

s

s

Das KIV-System

48

variablen eines Programmes sind. In diesem Fall kann die Regel trotzdem angewandt werden, aber die Prüfung der Variablenbedingung muß so lange verschoben werden, bis alle benötigten Teile der Sequenz bekannt sind. Diese liberale Art der Regelanwendung wird im KIV-System technisch dadurch realisiert, daß Schemavariablen, genannt Metavariablen, syntaktisch in den Sequenzen vorkommen dürfen. Diese Metavariablen gibt es für alle syntaktischen Kategorien, wie z.B. Programme, Formeln oder Formellisten, bis auf Sequenzen und Beweisbäume (siehe Abschnitt 3.2). Um Metavariablen, die syntaktisch in Sequenzen vorkommen, von Platzhaltern zu unterscheiden, die für Objekte stehen, die wiederum Metavariablen enthalten können, führen wir eine notationeHe Konvention ein: Metavariablen werden mit einem "$" syntaktisch gekennzeichnet. Wenn wir also z.B. $a schreiben, meinen wir das syntaktische Objekt $a als solches und nicht einen Wert, den diese Variable haben könnte.Wenn wir a schreiben, steht dies für ein Programm, das auch Metavariablen enthalten kann. Eine Ausnahme von dieser Konvention machen wir bei Regeln, denn hier sind alle Variablen Metavariablen, und die Schreibweise

comp_tac

$; f--($ß)$

i, t2) ist ein Beweisbaum, der als Konklusion die Konklusion von t1 hat, und dessen Prämissen sich aus den Prämissen von t1 außer der i-ten und Instanzen der Prämissen von t2 zusammensetzen. Der Beweisbaum t2 wird also als Regel betrachtet, die auf die i-te Prämisse von t1 rückwärts angewandt wird. Dabei muß eine Instanz von t2 gebildet werden. Dies geschieht wieder durch Matching: Es wird eine Substitution G auf den Metavariablen gesucht, so daß die Konklusion von cr(t2) syntaktisch gleich der i-ten Prämisse von von t1 ist. Bild 3.2 veranschaulicht die refine-Operation.

52

Das XIV-System

refine (t1, i, tz):

Bild 3.2 Die Funktion refine

Diese Funktionen ermöglichen eine sehr große Flexibilität bei der Erzeugung von Beweisbäumen, zumal Vorwärts- und Rückwärtsschritte beliebig verzahnt werden können. Andere PPL-Elementaroperationen umfassen die üblichen Operationen auf Listen (z.B. car, cdr) und auf ganzen Zahlen.

3.2.2 Kontrollstrukturen von PPL Wir führen hier nur die Kontrollstrukturen an, die im weiteren Verlauf der Arbeit, insbesondere in Kapitel 4 und Anhang A3, benötigt werden. Außerdem verwenden wir eine gegenüber der Implementierung geschönte Syntax. Funktionsdefinitionen werden wie üblich notiert: Funktionsname(Argumentliste)

=exp,

wobei exp ein PPL-Ausdruck ist, der abhängig von der Argumentliste den Funktionswert liefert. Im allgemeinen beginnen Funktionsdefinitionen mit der Einführung lokaler Definitionen:

53

Die Metasprache PPL

leh id1 id2

in

= exp1

= exp2

idn = expn exp

führt nacheinander die Definitionen id1 für exp 1, ... , idn für expn ein, die dann lokal für den Ausdruck exp gelten. Fallunterscheidungen haben das Aussehen cond ( cond1

1-+

exp 1

Der Wert der Fallunterscheidung ist der Wert von expb wobei cond; die erste Bedingung ist, die zu true evaluiert (d.h. cond1 ... condi-1 evaluierten alle zu false). Funktionsaufrufe, mittels derer auch Rekursion möglich ist, werden wie üblich dargestellt: Funktionsname(Argumentliste)

Eingabe geschieht mittels der parameterlosen Funktion read , die als Wert den Wert eines Ausdrucks hat, der am Terminal eingegeben wurde. Ausgabe erfolgt mittels des Ausdrucks exp 1 before exp2

.

Diese Funktion wertet zuerst exp 1 aus, gibt das Resultat am Bildschirm aus und gibt dann den Wert von exp2 zurück. Backtracking (und damit Beweissuche) kann mittels der Konstrukte or und fail implementiert werden. Letzteres ist eine in PPL eingebaute Konstante, die das Fehlschlagen eines Beweisversuches (im Unterschied z.B. von Syntaxfehlem) wiedergibt. Sie ist beispielsweise der Wert einer inferoder refine-Operation, wenn keine Substitution gefunden werden konnte, die die in 3.2.1 genannten Bedingungen erfüllt. Es ist aber auch möglich, durch explizite Verwendung von fail in jeder beliebigen Situation einen

54

Das KIV-System

Fehlschlag zu erzeugen. Ein Fehlschlag kann durch die Verwendung von or "abgefangen" werden. Zur Auswertung von exp1 or exp2

wird zunächst exp 1 ausgewertet. Falls der Wert von exp 1 nicht fail ist, ist dies der Wert des gesamten Ausdruckes. Andernfalls ist der Wert des Ausdruckes der Wert von exp2. Eine Sprachbeschreibung von PPL ist in [Heisel, Reif und Stephan 1986] gegeben.

3.3 Implementierung von Beweismethoden mit dem KIV-System Im Idealfall sollte eine Methode zur Verifikation, Transformation oder Entwicklung von Programmen aus einem formalen System bestehen, das die Grundlage der Methode verkörpert. Für die praktische Anwendbarkeit ist dies jedoch nicht ausreichend: Es müssen zusätzlich Heuristiken vorhanden sein, die es ermöglichen, Ableitungen in dem formalen System auch tatsächlich zu finden. Diese verschiedenen Bausteine von Beweismethoden finden sich in PPL-Implementierungen derselben wieder. Wir unterscheiden demnach im wesentlichen zwei Arten von PPL-Programmen: (i) Taktiken werden benutzt, um die logischen Grundbausteine einer Be-

weismethode zu implementieren. Diese PPL-Funktionen generieren im allgemeinen einen Beweisbaum der Höhe 1 (mittels der oben eingeführten Funktion mktree). Wenn, wie das meistens der Fall ist, die Beweise rückwärts geführt werden, hat die Taktik die zu beweisende Sequenz als Argument und generiert Unterziele, die hinreichend für die Gültigkeit der Eingabesequenz sind, unter der Bedingung, daß alle Validierungsprogramme erfolgreich ausgeführt werden können. Abgeleitete Regeln sind ein Spezialfall von Taktiken: Hier kann das Ergebnis schematisch ausgedrückt werden. Dies ist beispielsweise bei der in Abschnitt 3.1 angegebenen Taktik comp_tac der Fall, nicht aber bei dem Axiom dia_asg (siehe Satz 3.26), da die Anzahl n der Einzelzuweisungen nicht von vorneherein bekannt ist und Substitutionen nicht Teil unserer Schemasprache sind.

lmplementhrung von Beweismethoden

55

Das Entwerfen geeigneter Taktiken ist die Hauptaufgabe bei der Entwicklung und Formalisierung von Beweismethoden und wird auch einen großen Teil der weiteren Arbeit ausmachen. (ii) Strategien sind PPL-Programme, die die nichtlogischen Bestandteile einer Beweismethode implementieren. Dazu gehören beispielsweise • Ansteuerung der Taktiken, entweder in einer fest programmierten Reihenfolge, gemäß Heuristiken, oder aufgrund von Benutzerinteraktion, • Implementierung des Benutzerdialogs, • Implementierung der für die jeweilige Methode sinnvollen Backtracking-Struktur • Verwalten von Informationen, die nicht in Form von Sequenzen ausgedrückt werden können, • nichtlogische Datenmanipulationen. Beispiele für Strategien sind in Anhang A3 zu finden. Näheres zur Implementierung von Beweisstrategien in der KIV-Umgebung findet man in [Heisel, Reifund Stephan 1988b] und [Heisel, Reifund Stephan 1990].

56

Ein programmiersprachenorientierter Ansatz

4 Ein programmiersprachenorientierter Ansatz In diesem Kapitel beschreiben wir einen programmiersprachenorientierten Ansatz zur formalen Programmentwicklung, der sich auf eine Methodik stützt, die zuerst von Dijkstra [Dijkstra 1976] eingeführt und dann von Gries [Gries 1981] präzisiert wurde. Damit sollte das Programmieren von einer Kunst, wie es noch von Knuth [Knuth 1973] bezeichnet wurde, zu einer Wissenschaft gemacht werden. Wie schon in Kapitel 2 gesagt, zeichnet sich dieser Ansatz dadurch aus, daß die verwendeten Regeln an der Syntax der Programmiersprache ausgerichtet sind. Anband der Implementierung dieses Ansatzes machen wir deutlich, wie es technisch möglich ist, das KIV-System ohne technische Änderungen auch zur Programmsynthese zu verwenden. Dabei spielt die explizite Verwendung von Metavariablen eine wesentliche Rolle. Wir zeigen, wie sich Theoreme in PPL-Taktiken und Heuristiken in PPL-Strategien umsetzen lassen. Die Beschreibung der Implementierung ist hier ausführlicher gehalten als in den folgenden Kapiteln. Die Programmiersprache, die bei diesem Ansatz zur Programmentwicklung benutzt wird, ist eine indeterministische Guarded-CommandSprache, die von Dijkstra [Dijkstra 1976] eingeführt wurde. Die Semantik der Sprache wird mittels Prädikatentransformatoren (weakest preconditions) definiert. Für das praktische Umgehen mit Schleifen wird eine Invariantenregel abgeleitet. In nächsten Abschnitt werden wir näher auf diese Sprache eingehen. Die eigentliche in [Gries 1981] beschriebene Methode besteht aus Heuristiken für die Entwicklung von bedingten Anweisungen und Schleifen sowie für das Finden von Schleifeninvarianten. Obwohl der Anspruch formuliert wird, die Methode sei am Begriff des formalen Beweises orientiert und der Beweis leite die Programmentwicklung, so ist doch weder die verwendete Spezifikationssprache völlig formal (z.B. wird für Schleifeninvarianten manchmal eine graphische Notation verwendet) noch werden die Beweise in einem formalen System ausgeführt. Dies hat zur Folge, daß sich leicht Fehler einschleichen können. Die Methode enthält aber im Gegensatz zu den meisten anderen Ansätzen zur formalen Programmentwicklung Heuristiken, die das Vorgehen bei der Programmentwicklung leiten, nachdem (allerdings ohne Heuristiken) eine Entscheidung für ein bestimmtes Programmkonstrukt getroffen wurde. Daher erschien es interessant, sie in unserem formalen Rahmen zu modellieren und in der KIV-Umgebung zu implementierten. Hier wird gleichzeitig mit dem Programm ein streng formaler Beweis, d.h. eine Ableitung in einem Kalkül, für seine Korrektheit aufgebaut. Die Leichtsinns-

Guarded-Command-Programme

57

fehler, die beim Arbeiten auf dem Papier entstehen, werden damit vermieden. Besondere Schwierigkeiten bei der Formalisierung in dynamischer Logik bereitete der Indeterminismus der verwendeteten Programmiersprache, da er zu methodischen Zwecken eingeführt wurde. Weil die unserer Logik zugrunde liegende Programmiersprache deterministisch ist, müssen die methodischen Aspekte des Indeterminismus auf andere Weise berücksichtigt werden. Wie dies geschehen kann, wird in Abschnitt 4.3 geschildert. Zunächst aber definieren wir in Abschnitt 4.1 Syntax und Semantik von Guarded-Command-Programmen. Abschnitt 4.2 beschreibt die Methodologie des programmiersprachlichen Ansatzes, deren Implementierung Gegenstand von Abschnitt 4.5 ist. Zuvor gehen wir in Abschnitt 4.4 auf die besondere Rolle ein, die Metavariablen bie der Top-Down-Programmentwicklung spielen. In Abschnitt 4.6 schließlich stellen wir eine formale Beziehung zwischen den mit unserer Implementierung erzeugten Programmen und solchen her, die mit der in [Gries 1981] beschriebenen Methode (in der indeterministischen Programmiersprache) entwickelt wurden. Nach einem Beispiel in Abschnitt 4.7 geben wir eine Zusammenfassung des Erreichten in Abschnitt 4.8.

4.1 Syntax und Semantik von Guarded-CommandProgrammen Guarded-Command-Programme bestehen im Prinzip aus den gleichen Konstrukten wie unsere in Definition 3.10 eingeführte Programmiersprache. Bedingte Anweisungen und Schleifen werden allerdings aus sogenannten Guarded Commands zusammengesetzt, deren Semantik indeterministisch ist.

Definition 4.1 (Syntax von Guarded-Command-Programmen) Sei wie in Definition 3.10 eine Datenstuktur D gegeben. Die Menge der Guarded-Command-Program me über D, GC-Cmd(D), ist die kleinste Menge mit (i) skip E GC-Cmd(D) (ii) abort E GC-Cmd(D) (iii) Wenn Xi E Vz·I mit Xi =I= xJ· für i dann ist x 1, ... , Xn:= 'tJ, ... , 'tn

j und 'ti E Termz.(D) für 1:5;i$;n, I E GC-Cmd(D).

=I=

58

Ein programmiersprachenorientierter Ansatz

(iv) Wenn C1 E GC-Cmd(D) und Cz E GC-Cmd(D), dann ist auch (CI;Cz) E GC-Cmd(D). (v) Wenn Ci E GC-Cmd(D)und EiE Bxp(D) für }g$;n, dann ist if EI~ C1 0 ... 0 En ~ Cn fi E GC-Cmd(D). (vi) Wenn Ci E GC-Cmd(D) und EiE Bxp(D) für 1g~n, dann ist do EI ~ C1 0 ... 0 En ~ Cn od E GC-Cmd(D).



Dabei ist ein Konstrukt der Form E ~ C ein Guarded Command. Die Anweisung C darf nur ausgeführt werden, wenn der Ausdruck e gilt. Falls bei den Konstrukten (v) oder (vi) mehrere Guards zutreffen, wird indeterministisch gewählt, welche Alternative ausgeführt wird. Bei der Notation von Programmen und Formeln werden wir einer Konvention folgen: Programme in der Guarded-Command-Sprache werden mit großen lateinischen Buchstaben wie C oder S bezeichnet, wohingegen Programme in unserer Pascal-artigen Objektsprache weiterhin mit kleinen griechischen Buchstaben wie a und ß bezeichnet werden. Für Formeln, Boole'sche Ausdrücke und Terme halten wir uns an die in Kapitel 3 eingeführte Notation. Nur wenn wir Gries zitieren, übernehmen wir seine Notation, die auch Boole'sche Ausdrücke und Formeln mit großen lateinischen Buchstaben bezeichnet. Die Semantik von Programmen aus GC-Cmd(D) wird, wie schon gesagt, mit schwächsten Vorbedingungen definiert. Dabei bezeichnet wp(C, 1\ s ~ (.!!)

X

f-TJ

r

f-(a;ß)(q>(.Y.)

1\

TJ)

1\

X)

Rückwärtsentwicklung von Schleifen: Konzept des invarianten Zieles

123

Die Elemente von Y..J sind neue Variablen, d.h. es muß y 1 n (Frei(~" 'V u Vars(a;ß)) = 0 gelten. Durch die Gleichung y=yl in der ersten Prämisse können wir auf den Wert von y_ vor Ausführung des Programmes ß in der Nachbedingung von ß bezugnehmen. Da genau über die freien Variablen des ursprünglichen Zieles ) Frei('lf) \ Frei(q>) neue Variablen für .Y 1\yl l\y2 1\ 1\12 .

In der Problembeschreibung für ß kommt zum ersten Mal ein invariantes Ziel Ip 2 vor. Die anderen Parameter sind analog zur ProtectionStrategie definiert. Allerdings müssen zu S 2 die neuen Sicherungsvariablen l!.J hinzugefügt werden. Wir erinnern uns, daß wir in Abschnitt 5.4 zwei verschiedene Möglichkeiten zur Verschärfung unserer initialen Problembeschreibung (0) vorgestellt hatten. Die zweite Möglichkeit war Problembeschreibung (2), die die Spezifikation a>O, b>O f-($ao)( ((divs(a,x) "res=l) v (-,divs(a,x) 1\ x=b 1\ $1;)

1\

res=O))

hatte. Eine Anwendung der Preservation-Strategie legt fest, daß $ao die Form $a1; $a2 haben wird, und ergibt die folgende Problembeschreibung für das erste Teilprogramm:

(8) Yl - {x=b} h - 0

Inp1 = {a, b, res}

!IV. - {x}

s1 - 0 Sp1 - a>O, b>O f-($ai)(x=b" $J.1)

Sukzessive Efllblierung von Teüzielen

132

Das Problem (8) kann einfach durch die Zuweisung x:= b gelöst werden. Die berechnete Nachbedingung $J..L wird mit der Formel x=b " a>O " b>O " x>O instantiiert. Es mag unnötig erscheinen, den Algorithmus zur automatischen Berechnung von Nachbedingungen so zu gestalten, daß die Gleichung x=b in die berechnete Nachbedingung aufgenommen wird. Jedoch kann nur so garantiert werden, daß diese Information auch zur Entwicklung nachfolgender Teilprogramme zur Verfügung steht. In [Heisel und Santen 1990] wird dies an einem Beispiel erläutert. Die Problembeschreibung für das zweite Teilprogramm ist: (9) Y2 - {(divs(a,x) "res=l) v (-,divs(a,x) " res=O)} 12 - 0 Ip2 - Va,b,res.(((divs(a,x) "res=l) v (-,divs(a,x)" res=O)) ~ ((divs(a,x') 1\ res=l) v (-,divs(a,x') " res=O))) Inp2 = {a, b} 'l(z - {res, x} s2 - { x'} Sp2 - x=b, x=b 1\ a>O " b>O " x>O, x=x' f-($a2)( ((divs(a,x) "res=l) v (-,divs(a,x)" res=O)) " Ip2" $x) Man beachte, daß Sp2 die berechnete Nachbedingung, die sich aus der Lösung des Problems (8) ergeben hatte, als Vorbedingung enthält.

5.8.3 Die Preservation-Composition- Strategie Die Preservation-Composition-Strategie ist ein Spezialfall der DisjointGoal-Strategie, die speziell für die Behandlung· von invarianten Zielen konzipiert wurde. Das erste Teilprogramm sollte eine Rückwärtsschleife sein. Für das zweite Teilprogramm gilt die Variablenbedingung, daß die Hilfsvariablen 1: nicht verändert werden dürfen. Deswegen brauchen wir nicht zu fordern, daß 'l'(l!,.Y.) ein invariantes Ziel für das zweite Teilprogramm ist, und können dieses mit der Grundstrategie entwickeln. Die Problembeschreibungen für die Entwicklung der beiden Teilprogramme lauten: Für a:

Y1

~

q

wird interaktiv ausgewählt

Rückwärtsentw. von Schleifen: Preservation-Composition-Strategie

133

Il - 0 Ip1 - Ip Inp u ('!( \ 'R...J.) Inp 1 'R...J. - 'l(n Frei(Aq1) s1 - s Sp1 - Ll, Y. = Y.i f-($a)((p" Ip)" $Jl)

=

Für

ß: Y2 I2 Inp2

'1\2

= -

s2 Sp2 -

(q\ Yl) u (I\ InvJ) Inv1 Inp u 'R...J. u (Frei(Jl) \ ('l(u S)) '1(\'R...J. s

p, Jl f-($ß)(cr" $11)

mit Inv1 = { q> I q> E I und Frei(q>) n 'R...J. = 0 } p - 1\yl cr - l\y2 1\ I\I2 . Die beiden Problembeschreibungen werden analog zur Disjoint-GoalStrategie aufgestellt; insbesondere werden die Ergebnisvariablen des ersten Teilprogrammes zu Eingabevariablen für das zweite Teilprogramm. In unserem Beispiel hat das Ziel der Problembeschreibung (9) keine konjunktive Form. Wir müssen es deshalb verschärfen, bevor wir fortfahren können. Dafür sollten wir eine Bedingung finden, die es erlaubt, das divs-Prädikat zu entscheiden, ohne dazu eine Schleife benutzen zu müssen. Wir stellen fest, daß unter den Bedingungen a~b und b>O die Zahl a genau dann die Zahl b teilt, wenn a=b gilt. Wenn wir also a~x erreichen können, ist das Resultat genau dann 1, wenn a=x gilt. Dies ergibt: (10)

q I

-

-

{a~x.

0

(a=x "res=l) v (a>x" res=O)}

Va,b,res.(((divs(a,x) "res=l) v (-,divs(a,x)" res=O)) ~ ((divs(a,x') "res=l) v (-,divs(a,x')" res=O))) Inp - {a, b} '!( - {res, x} Ip

-

s Sp

-

{ x'}

x=b, x=b " a>O " b>O " x>O, x=x' f- ($a2)(a~x " ((a=x" res=l) v (a>x " res=O)) " Ip" $x)

134

Sukzessive Efllblierung von Teüz.ielen

Nun können wir die Preservation-Composition-Strategie auf (1 0) anwenden: (11) Y1

h

-

{a~}

- 0

Va,b,res.(((divs{a,x) "res=l) v (-,divs(a,x)" res=O)) ~ ((divs(a,x') " res=l) v ( -,divs(a,x') " res=O))) Inp1 = {a, b, res} !lO 1\ b>O 1\ x>O an. Die Metavariable $a3 erhält die Form while -,a~x do $a5 od, und die Strategie stellt die Problembeschreibung für den Schleifenrumpf auf: (13)

Yl - {xO, b>O, x>O}

res= 1) v (-,divs(a,x')

1\

res=O)))}

= {a, b, res} -

{x} {x', t}

a>O 1\ b>O 1\ x>O, -,a~x, x=t, x=x' f- ($as)(xO A b>O A x>O 1\ Va,b,res.(((divs(a,x) 1\ res=l) v (-,divs(a,x) 1\ res=O)) ---7 ((divs(a,x') 1\ res=l) v (-,divs(a,x') 1\ res=O))) 1\ $TJ) Zur Lösung dieses Problems verschärfen wir die Nachbedingung, wobei wir ausnutzen, daß unter der Voraussetzung x'~a die Äquivalenz divs(a,x') ~ divs(a,x'-a) gilt. Wir erhalten als neue Zielmenge {x A ~) (2) -.e', r Hß)(q> A ~) .

Wir zeigen zunächst (1). Anwendung von dia_weak ergibt (3) e', r f-(a)(('lf A e) A Tl) (4) ('lfAE)A'fl f-q>A~.

Das Ziel (4) ist mit aussagenlogischen Regeln auf die erste Prämisse Jl v 11 f-~ und die vierte Prämisse ~ f-(('lf A e) ~ q>) der Taktik reduzierbar, während (3) unmittelbar aus den Prämissen e, r f-(a)(('lf A E) A Tl) und r f-e f-7 e' folgt. Auch zum Beweis von (2) wenden wir dia_weak an und erhalten (5) -.e', r Hß)(q> A Tl) (6) q> A Tl f-q> A ~ .

Ziel (6) folgt aussagenlogisch aus der ersten Prämisse, und (5) folgt unmittelbar aus den Prämissen -.E, r f-(ß)(q> A Jl) und r f-e f-7 e'.



Die Conditional-Strategie wird mit einer Spezifikation aufgerufen, die ein verschärftes Ziel als Nachbedingung enthält. Aus der Menge der Ziele muß die zu testende Bedingung e selektiert werden. Daraus berechnet die

138

Sukzessive Etablierung von Teüzielen

Strategie automatisch die Bedingung e'. Die benötigte Information ist in Form von Gleichungen der Form V = v' in der Vorbedingung r vorhanden. Danach muß die Nachbedingung rp für die gesamte bedingte Anweisung ermittelt werden. Zu diesem Zweck werden die Versehärtungsoperationen herangezogen, die unmittelbar vor Aufruf der Conditional-Strategie durchgeführt wurden. Wurde nur eine Verschärfungsoperation durchgeführt, bestimmt die Strategie die vor dieser Operation geltende Nachbedingung als Nachbedingung für die gesamte bedingte Anweisung. Andernfalls müssen die Benutzer interaktiv eine der früheren Nachbedingungen auswählen. Die Verschärfungsoperationen, die von der gewählten Nachbedingung rp zu den stärkeren Zielen 1f1 und e führten, werden rückgängig gemacht. Ihr Zweck wird nunmehr von der vierten Prämisse der Taktik erfüllt. Beide Zweige der bedingten Anweisung werden unabhängig voneinander durch rekursive Aufrufe der Grundstrategie entwickelt. Nachdem dies geschehen ist, muß die berechnete Nachbedingung der bedingten Anweisung ermittelt werden. Hierfür berechnet die Strategie die Formel

Dies bedeutet, daß Konjunktionsglieder, die sowohl in 1J als auch in J1 vorkommen, Konjunktionsglieder von ~ sind und daß die Disjunktion der verbleibenden Konjunktionsglieder ein Konjunktionsglied von ~ ist. Logisch gesehen würde die Disjunktion 11 v j.l der Nachbedingungen der beiden Zweige hinreichend sein, aber wir streben eine konjunktive Form der Nachbedingung an. Die Problembeschreibungen für die beiden Zweige sind: then-Zweig:

=

Yt (j \ set" (E) It - Ju set" (E) Inpt = Inp ~

-

1(.

St - S Spt - E,

mit 'I'

r f- ($a)(('lf 1\ E) 1\ $TJ)

- 1\(jt 1\ /\I .

Beim else-Zweig wird nur die neue Vorbedingung

--,f

hinzugefügt.

( Yq>• Iq>, Inpq>, ~· Sq>, Spq>) bezeichnet die Problembeschreibung, die vor-

lag, bevor die Nachbedingung rp zu 'I' 1\

E

verschärft wurde.

139

Die Conditio1Ull-Strategie

(j, = I, Inp, ~-

Y

O ~ ($a4)(((a=x "res=l) v

(a>x" res=O))" $Jl).

Eine Disjunktion ist wahr, wenn eines ihrer Disjunktionsglieder wahr ist. Wir verschärfen also das Ziel entsprechend und erhalten - {a=x, res=l} - 0 Inp - {a, b, x} 1( - {res}

(16) (j I

s

Sp

-

{x'}

- a~x, a>O "b>O" x>O f-($a4)(a=x " res=l " $J.1)

Das Ziel a=x kann nicht durch Veränderung der einzigen Ergebnisvariablen res etabliert werden; wir können nur testen, ob a=x gilt. Mit Anwendung der Conditional-Strategie auf (16) und Wahl von E = a=x erhält $a4 die Form if a=x then $a6 else $a7 , und wir erhalten zwei neue Problembeschreibungen für die beiden Teile der bedingten Anweisung.

=

{res=l} (17) Yt It - {a=x} Inpt = {a, b, x}

~ - {res} St - {x'} Spt - a=x, a~x, a>O" b>O" x>O f-($a6)(res=l "a=x" $l))

140

Sukzessive Etablierung von Teilzielen

(18) (j,

=

{(a=x 1\ res=l) v (a>x 1\ res=Ü)} 0 Inp, {a, b, x} ~ - {res} Se - {x'} Spe - -,a=x, ~x. a>O 1\ b>O 1\ x>O ~($a7)(((a=x I,

-

=

1\

1\ res=l) v (a>x res=O)) 1\ $Jl)

Man beachte, daß (18) die ursprüngliche Nachbedingung (a=x 1\ res=l) v (a>x 1\ res=O) als Ziel enthält. Die Assignment-Strategie ist nun in der Lage, die Zuweisung res:= 1 für $a6 zu generieren, und der Algorithmus zur Berechnung von Nachbedingungen instantiiert $17 mit a=x 1\ ~x 1\ a>O 1\ b>O 1\ x>O 1\ res= 1. Um die Problembeschreibung (17) zu erhalten, haben wir die Nachbedingung durch Weglassen des Disjunktionsgliedes a>x 1\ res=O verschärft. Den anderen Zwieg der bedingten Anweisungen entwickeln wir, indem wir das andere Disjunktionsglied a=x 1\ res=l durch Verschärfung eliminieren. Dies ergibt (19) (j

Inp 1(. I

s

-

{a>x, res=O} 0 {a, b, x} {res} {x'}

Sp - -,a=x, a~x. a>O

1\

b>O

1\

x>O ~($a7)(a>x

1\

res=O

1\

$Jl)

Auch hier löst die Assignment-Strategie das Problem. Die Zuweisung res := 0 instantiiert $a7, und der Algorithmus zur Berechnung von Nachbedingungen berechnet Jl -,a=x A a~x 1\ a>O 1\ b>O 1\ x>O 1\ res=O. Nachdem beide Zweige der bedingten Anweisung entwickelt sind, berechnet die Strategie die Nachbedingung für die gesamte bedingte Anweisung. Gemäß der oben angegebenen Berechnungsvorschrift wird mit a~x 1\ a>O 1\ b>O 1\ x>O 1\ ((a=x 1\ res=l) v (-,a=x 1\ res=O)) instantiiert.

=

$s

5.10 Die Skip-Strategie Die Generierung des leeren Programmes skip ist unproblematisch. Die Skip-Strategie instantiiert die Metavariable $a für das zu entwickelnde Programm mit skip und die Metavariable für die berechnete Nachbe-

$s

Die Assignment-Strategie

141

dingung mit der Vorbedingung generiert:

r

r.

Dabei wird folgender Beweisbaum

Hskip)( Y2:= i+1, x+yt. Yt+Y2· ???

od {Po" Pt" P2} mit

Vollautomatische Programmsynthese mit Finite Differencing

150

-

Y2 =6 Y2 = 6(i+ 1) + 6 = (6i + 6) + 6

Hier terminiert das Verfahren, da für die Aufrechterhaltung von P2 keine neue Variable vonnöten ist, und das endgültige Programm lautet: i, x, Yl> y 2:= 0, 0, 1, 6; {Po A Pt A P 2 } while it:n do i, X, Yt· Y2:= i+l, X+YI, Yt+Y2, Y2+6 od {Po A Pt A P2} . Dieses Programm berechnet die Funktion n3 nicht nur sehr effizient, sondern es ist auch ohne die Anwendung des skizzierten Vorgehens nur schwer zu finden.

6.2 Abstraktion des Beispiels zu einer Programmentwicklungsmethode Im Rahmen einer Diplomarbeit [Drexler 1990] wurde das geschilderte Vorgehen verallgemeinert und in der KIV-Umgebung formalisiert und implementiert. Alle eventuell interessierenden Details sind dort nachzulesen, so daß wir hier nur einen kurzen Überblick über die Modeliierung der Methode geben. Zunächst stellen wir fest, daß die im Beispiel gegebene Spezifikation funktional ist, d.h. es soll der Wert einer Funktion ermittelt werden. Die Generierung einer Zuweisung ist aber ausgeschlossen, weil die spezifizierte Funktion f nicht als primitiv deklariert ist. Die wichtigste Idee, die der Methode zugrunde liegt, ist die Entscheidung, f(i+ 1) aus f(i) zu berechnen. Hierzu wird eine Schleife angesetzt. Eine erste, vorläufige Invariante wird mit der in Kapitel 4 geschilderten Heuristik "ersetze Konstante durch Variable" gewonnen. Invariante, Initialisierung und Schleifenrumpf werden simultan entwickelt. Die Terminierungsfunktion n-i ergibt sich trivial. Mit dem Verfahren wird also ein iteratives Programm für eine rekursiv definierte Funktion entwickelt. Ein ähnliches Vorgehen ist aus dem Übersetzerbau unter dem Namen Finite Differencing bekannt. Zur Codeoptimierung werden kostspielige Berechnungen durch billigere inkrementelle Berechnungen ersetzt. Im Beispiel wurde statt der Multiplikation die weniger aufwendige Addition verwendet. Die Anwendung von Finite Differencing zur Programmoptimierung ist in [Paige und Koenig 1982] behandelt.

Abstraktion zu einer Methode

151

Eine erste Verallgemeinerung des oben geschilderten Vorgehens geht von einer Spezifikation der Form n~O f- ($a)x=f(n) aus, wobei f die Funktionalität nat ~ nat hat. Die Spezifikation von f muß der Rekursionsstruktur f(O) =c f(i+ 1) = g(f(i), h(i)) genügen, wobei g eine primitive Funktion (d.h. g darf in einem Programm vorkommen) und h ohne Verwendung von f definiert ist. Im Beispiel gilt o3 =0 (i+l)3 = i3 + (3i2 + 3i + 1) Die Funktion g ist die Addition +, und h ordnet jedem i den Wert 3i2 + 3i + I zu. Wenn die genannten Voraussetzungen erfüllt sind, kann für $a das vorläufige Programm i, x:= 0, c; {o~~n 1\ x=f(i)} while i~n do i, x:= i+ I, g(x, h(i)) od {x=f(n)} angesetzt werden. Falls g(x, h(i)) ein elementarer Term ist, sind wir fertig und das Verfahren terminiert. Andernfalls wird eine neue Variable Y1 eingeführt, und die Zuweisung für x im Schleifenrumpf lautet x:= g(x, Y1). Die neue Variable hat die Invarianzeigenschaft Y1 = h(i), und das Schema wird um die Initialisierung Y1:= h(O) erweitert. Zur Schleifeninvariante kommt das Konjunktionsglied Y1 = h(i) hinzu. Die Funktion h muß nun wieder einem Rekursionsschema h(O) =c1 h(i+ I) = g1 (h(i), h 1(i)) mit primitiver Funktion g1 genügen. Zum Schleifenrumpf kann die Zuweisung Y1:= g1(YI· h1(i)) hinzugefügt werden, falls der Term g1(Ylo h 1(i)) elementar ist. Andernfalls muß eine neue Variable Y2 generiert werden, und die neue Zuweisung im Schleifenrumpf lautet Y1:= g1(Y1· Y2). Dieses Verfahren wird so lange iteriert, bis ein Term der Form

152

Vollautomatische Programmsynthese mit Finite Differencing

gj(Yj· hj(i)) gefunden wird, der keine nichtprimitiven Operationen enthält. Falls die zu berechnende Funktion wohldefiniert ist, ist die Terrninierung des Verfahrens gewährleistet. Mit diesem Schema kann man beispielsweise Inhalte von Arrays aufsummieren oder auch die maximale Segmentsumme eines Array bestimmen, siehe [Drexler 1990]. Dort wurden auch weitere Verallgemeinerungen entwickelt: • Behandlung von Sonderfällen Die zulässige Rekursionsstruktur wurde um endlich viele verschiedene Basisfälle erweitert: f(k) =ck f(b+i+ 1) = g(f(b+i), h(b+i))

für für

O~k~b i~O

Das zur Lösung des Syntheseproblems angesetzte Programmschema hat nun die Form einer bedingten Anweisung, die zuerst die Sonderfälle abfängt. Darin wird die oben beschriebene Schleife eingebettet. • Allgemeinerer Rekursionsfall Hier braucht die Rekursion nicht über den unmittelbaren Vorgänger zu erfolgen: f(k) = Ck f(i+b) = g(f(i), h(i))

für O~k~b für i2'!:0

In diesem Fall wird das angesetzte Programmschema um eine vorgeschaltete Schleife erweitert, die den zu der jeweiligen Eingabe gehörigen Basisfall ermittelt. Diese Verallgemeinerung ermöglicht es beispielsweise, Zahlen zu halbieren, oder festzustellen, ob eine Zahl gerade ist. • Mehrfach rekursive Funktionen Es können auch mehrere Rekursionen der zu berechnenden Funktion zugelassen werden: f(k) f(i+b)

=Ck = g(f(i+b-1), f(i+b-2),

für Ü~~b ... , f(i), h(i)) für i2'!:0

In den Schleifenrumpf müssen von Anfang an so viele neue Variablen eingeführt werden, wie Rekursionen der zu implementierenden Funkti-

Realisierung des Verfahrens in der KIV-Umgebung

153

on in der Spezifikation vorkommen. Diese Verallgemeinerung ermöglicht es beispielsweise, automatisch ein Programm zur Berechnung der Fibonacci-Zahlen zu erzeugen. • Nichtelementarer Basisfall Falls einer der Basisfälle der spezifizierten Funktion kein elementarer Term ist, wird versucht, das Finite-Differencing-Verfahren zur Berechnung des Basisfalles zu verwenden. • Nichtprimitive Funktion g Falls die Funktion g zwar nicht primitiv ist, aber für sie eine rekursive Definition vorhanden ist, die einem der obigen Schemata genügt, wird versucht, das Finite-Differencing-Verfahren zur Berechnung von g zu verwenden. Anstatt der Zuweisung x:= g(x, h(i)) wird also eine Schleife synthetisiert, die den Wert von g(x, h(i)) berechnet. Dies führt zu geschachtelten Schleifen. • Zusätzliche invariante Nachbedingungen Die Nachbedingung muß nicht genau die Form x=f(n) haben. Sie darf zusätzliche Formeln V' enthalten, die aber invariant bezüglich des zu erzeugenden Programmes sein müssen, d.h. x darf nicht frei in V' vorkommen. Die für das Verfahren zulässigen Spezifikationen haben also die Form r ~ ($a)(x=f(n) 1\ 'lf) mit x E Frei('lf). Das sich ergebende Verfahren läßt sich in zwei Schritten zusammenfassen: Im ersten Schritt muß festgestellt werden, welchem der möglichen Rekursionsschemata die Spezifikation der zu implementierenden Funktion genügt. Daraus ergibt sich das anzusetzende Programmschema. Jedes der möglichen Programmschemata enthält eine Schleife. Der zweite Schritt besteht in der Entwicklung der Schleife und führt das eigentliche Finite Differencing durch. Falls eine nichtprimitive Funktion g auftritt, werden geschachtelte Schleifen synthetisiert. Die Realisierung dieses zweiten Schrittes beschreiben wir im folgenden Abschnitt.

6.3 Realisierung des Verfahrens in der KIV-Umgebung Die Grundidee für die Modeliierung des Finite-Differencing-Ansatzes in der KIV-Umgebung besteht darin, ein Schema für eine Schleife zu erzeugen, in dem die Initialisierung, die Invariante und der Schleifenrumpf

Vollautomatische Programmsynthese mit Finite Differencing

154

schrittweise um Zuweisungen bzw. Konjunktionsglieder erweitert werden können. Hierzu bedienen wir uns wieder des Metavariablenmechanismus. Die verwendete Taktik beruht auf while_tac aus Kapitel 4. Dort hatten wir zwei Beweisverpflichtungen aus der Checkliste zum Verständnis von Schleifen zusammengefaßt, um alle für die Entwicklung des Schleifenrumpfes relevanten Informationen für einen rekursiven Aufruf der Strategie zur Verfügung zu haben. Hier ist es jedoch vorteilhafter, die beiden Beweisverpflichtungen wieder zu trennen, da die Terminierungsfunktion feststeht und keinen Beitrag zur weiteren Synthese liefert. Die Taktik erzeugt den Beweisbaum

r

(1) (2)

$'1ft A 'lf, $e (3) $'1ft A 'lf, -.$e (4) 'lf, $e (5) 'lf, $e, 't=t

r

~ ($a l

~($ar)($'1fr A 'lf) ~($a)($'1ft A 'lf) ~ . if primitive then directly_solve eise decompose; g; h; compose fi; pdl-decompose; pdl-compose; pdl-directly_solve do f(lnpc : 1dec f--CI>ctec " 'Yg " 'Yh gezeigt sind. Je nachdem, wie g und h bestimmt wurden, sind Yg und Yh entweder ~ oder eine Instanz von yt. wo die Ein- und Ausgabevariablen durch jeweils neue Variablen ersetzt wurden. (Nach Voraussetzung sind die aktuellen Referenzparameter der verschiedenen Prozeduraufrufe disjunkt. Die Eingabevariablen für die rekursiven Aufrufe sind die Ausgabevariablen von decompose, so daß auch sie neu sein müssen).

7.3.2 Abgeleitete Antezedenten Wenn wir die noch offenen Sequenzen (5) und (6) betrachten, stellen wir fest, daß zwar beide keinerlei Programme enthalten, aber genau eine Metavariable im Antezedenten, die als nächstes zu bestimmen ist. Zu diesem Zweck läßt sich eine Methode einsetzen, die D. Smith Ermittlung von abgeleiteten Antezedenten genannt hat. Gegeben sei eine prädikatenlogische Formel T/. so daß V'vl··· Vn.TJ geschlossen ist. Zu gegebenen v1· .. Vj soll eine prädikatenlogische Formelljlermittelt werden, so daß V'vl···vi.('l' ~ V'vi+l···Vn.TJ) in dem betrachteten Modell gültig ist. Da false eine triviale Lösung für das Problem

Realisierung der Designstrategien

167

ist, muß zusätzlich gefordert werden, daß II' möglichst schwach sein soll. Das für diesen Zweck im KIV-System verwendete Verfahren wurde in [Gelfort 1990] entwickelt und folgt nicht dem Vorgehen von [Smith 1985]. Gegeben sei eine Sequenz der Form y ~dcc " dec " dec " comp f-cpr · Da $cpdec die einzige Metavariable ist und in den Sequenzen keine Programme vorkommen, berechnen wir wieder Antezedenten ~1 und ~2· die wir diesmal konjunktiv verknüpfen. Allerdihgs ist die sich so ergebende Bedingung noch nicht $(/Jdec· Analog wie in Abschnitt 7.3.1 geschildert, muß noch eine Terminierungsaussage hinzugefügt werden. Der Schritt ( 4) Bestimme den decompose-Operator

wird wieder entweder durch rekursiven Aufruf der Divide-and-ConquerStrategie oder mit PLA_STRAT erledigt. Die Schritte (5) Bestimme das primitive- Prädikat (6) Konstruiere ein Programm für directly_solve werden wie bei OS 1 durchgeführt. In der Praxis hat sich gezeigt, daß die Benutzerunterstützung, die diese Strategie bietet, erheblich ist, so daß auch größere Programme mit relativ geringem Aufwand entwickelt werden können.

170

Ein allgemeines Konzept zur formalen Modeliierung von Methoden

8 Ein allgemeines Konzept zur formalen Modeliierung von Top-Down-Programmentwicklungsmethoden In den vergangeneo Kapiteln haben wir Programmentwicklungsmethoden in dynamischer Logik formalisiert, die das Problem auf sehr verschiedene Weise angehen. Damit haben wir nicht nur den Nachweis erbracht, daß die dynamische Logik in Verbindung mit PPL ein mächtiger und für unsere Zwecke geeigneter Formalismus ist, sondern wir haben auch Erkenntnisse darüber gewonnen, welche Beschreibungsmittel für solche Formalisierungen adäquat sind. Diese Erkenntnisse wollen wir nun verallgemeinern und zu einem universellen Konzept zur formalen Modeliierung beliebiger Programmentwicklungsmethoden, die nach dem Prinzip der Problemzerlegung arbeiten, ausbauen. Dieses Konzept soll unabhängig von bestimmten Formalismen sein. Wir wollen also von der dynamischen Logik abstrahieren, so daß auch andere Formalismen "uniform" zur Top-Down-Programmentwicklung eingesetzt werden können. Zunächst gehen wir auf die Voraussetzungen ein, die eine Methode erfüllen muß, um mit dem Modeliierungskonzept darstellbar zu sein, und formulieren dann Anforderungen, die das Konzept erfüllen muß, um mächtig genug für die Modeliierung beliebiger solcher Methoden zu sein. Das Modellierungskonzept soll für Methoden anwendbar sein, die nach dem Prinzip der Problemzerlegung arbeiten: Gegeben ein Problem, wird dieses so lange in Unterprobleme zerteilt, die einzeln lösbar sind, bis die erhaltenen Probleme so einfach geworden sind, daß sie direkt gelöst werden können. Die Lösung des Gesamtproblems wird aus den Lösungen der Teilprobleme zusammengesetzt. Wenn die Modeliierung einer Methode mittels unseres Konzeptes zu beweisbar korrekten Programmen führen soll, muß eine formale Korrektheitsdefinition vorliegen: Programmiersprache und Spezifikationssprache müssen festgelegt sein, und es muß definiert sein, was es heißt, daß ein Programm korrekt bezüglich einer Spezifikation ist. Ein Ableitungsmechanismus, mit dem solche Aussagen formal bewiesen werden können, ist zwar von Vorteil, aber nicht Voraussetzung. Wir verfolgen mit der Entwicklung unseres Konzeptes die Ziele, daß • die entstehenden Programme beweisbar korrekt sind, • die formalisierten Methoden inkrementeil veränderbar, anpaßbar und erweiterbar sind, d.h. Veränderungen lokal erfolgen können,

Ein allgemeines Konzept zur formalen Modeliierung von Methoden

171

• Entwurfsregeln, die verschiedenen Abstraktionsgrad haben, gleichermaßen gut darstellbar sind, • auch strategische Aspekte einer Methode ausdrückbar sind, z.B welche Unterprobleme in welcher Reihenfolge zu lösen sind, • Belange imperativer Sprachen berücksichtigt werden, • der Entwicklungsprozeß uniform darstellbar ist, • das Konzept leicht implementierbar ist, so daß auf Wunsch eine Maschinenunterstützung bei der Programmentwicklung möglich ist, • auch unvollständige Spezifikationen zu behandelt werden können und • eventuelle Abhängigkeiten zwischen Teilproblemen, die bei der Zerlegung entstehen, berücksichtigt werden können. Die letzten beiden Anforderungen bedürfen der Erläuterung. Warum, so könnte man sich fragen, so11te man von den Benutzern nicht verlangen, ihre Programmierprobleme vonständig zu spezifizieren? Dazu muß man bedenken, daß unser Konzept auch mit Spezifikationen, die sich im Laufe der Programmentwicklung durch Problemzerlegung ergeben, umgehen können muß, und solche Spezifikationen können auch dann unvollständig sein, wenn die Ausgangsspezifikation es nicht war. Bei der Entwicklung von bedingten Anweisungen kann es beispielsweise von Vorteil sein, zuerst einen der beiden Zweige zu entwickeln und dann zu versuchen, die Bedingung des Konditionals automatisch zu ermitteln. Dann ist die Spezifikation, die für die Entwicklung des einen Zweiges zur Verfügung steht, unvollständig, da die Bedingung noch nicht feststeht. Und warum kann man nicht verlangen, daß ein Problem in unabhängige Teilprobleme zerlegt wird? Der Grund liegt darin, daß die Programmentwicklung ein dynamischer Prozeß ist: Frühere Entwurfsentscheidungen haben einen Einfluß auf das spätere Vorgehen. Es muß möglich sein, eine entwickelte Teillösung bei der Entwicklung weiterer Teillösungen zu berücksichtigen. Auch diese Anforderung wird klar, wenn wir obiges Beispiel betrachten: Wenn wir eine bedingte Anweisung so entwickeln, daß die Bedingung erst nach Entwicklung des einen Zweiges ermittelt wird, so ist die Entwicklung des zweiten Zweiges von der für den ersten Zweig entwickelten Lösung abhängig, da dieser die Bedingung bestimmt, die Teil der Vorbedingung des zweiten Teils ist. Wir werden nun Beschreibungsmittel entwickeln, die diesen Anforderungen gerecht werden.

172

Ein allgemeines Konzept zur formalen Modeliierung von Methoden

8.1 Programmierprobleme In Kapitel 5 haben wir bereits Problembeschreibungen als Mittel zur expliziten Darstellung der für ein Programmierproblem relevanten Information kennengelernt Dieses Konzept wollen wir nun so verallgemeinern, daß es nicht mehr von speziellen Spezifikations- oder Programmiersprachen abhängig ist. Dabei werden wir im folgenden stets voraussetzen, daß es - gegeben eine Programmier- und eine Spezifikationssprache - einen Korrektheitsbegriff gibt, der definiert, wann ein Programm bezüglich einer Spezifikation (sei es partiell, sei es total) korrekt ist.

8.1.1 Platzhalter Die Verwendung von Platzhaltern ist zentral für unseren Ansatz. Wenn Programmentwicklung schrittweise vor sich geht, muß es ein Mittel geben, auszudrücken, welche Teile von Spezifikationen und Programmen schon bestimmt sind und welche nicht. Dies kann implizit geschehen, z.B. durch eine fest vorgegebene Kontrollstruktur, wie das bei dem in Kapitel 7 geschilderten Divide-and-Conquer-Ansatz der Fall ist. Hier ist der Ablauf der Programmsynthese so gestaltet, daß nach und nach alle unbekannten Teile in einer festgelegten Reihenfolge bestimmt werden. Im Gegensatz hierzu wollen wir ein Konzept entwickeln, das sowohl unabhängig von speziellen Ablaufstrukturen als auch offen und erweiterungsfähig ist. Deshalb ist es unser Ziel, die zur Programmentwicklung verwendete Information und den Ablauf der Programmentwicklung möglichst explizit darzustellen. Zu diesem Zweck sehen wir die explizite Verwendung von Platzhaltern sowohl in der Spezifikations- als auch in der Programmiersprache vor. Platzhalter sind essentiell für die Ausdrückbarkeit von Abhängigkeiten zwischen Teilproblemen. Außerdem kann durch ihre Verwendung eine bestimmte Gestalt des zu entwickelnden Programmes vorgegeben werden. Die Lösung von Programmierproblemen besteht in der Bestimmung der unbekannten Teile. Das Ergebnis des Programmentwicklungsprozesses wird also eine Funktion sein, die die unbekannten Teile auf ihre Lösungen abbildet. Dieser Zusammenhang zwischen Platzhaltern und ihren Instantiierungen ermöglicht es, Abhängigkeiten zwischen Teilzielen ausdrücken. Ist ein Programmierproblem von einem anderen abhängig, wird es eine Instantiierungsfunktion benutzen, die zuvor bei der Lösung des Teilproblems, von dem es abhängig ist, ermittelt wurde.

Programmierprobleme

173

8.1.2 Berechnete Nachbedingungen Um die Belange, die bei der Benutzung von imperativen Sprachen bestehen, angemessen zu berücksichtigen, müssen wir auch Konzepte vorsehen, die es ermöglichen, ein Programm als ein Instrument zur Transformation von Zuständen zu betrachten. Insbesondere muß berücksichtigt werden, daß ein Programm einen Anfangszustand nicht in einem Schritt in einen Endzustand transformiert, sondern daß im allgemeinen Zwischenzustände auftreten. Es ist nicht zumutbar, von den Benutzern einer Programmentwicklungsmethode zu verlangen, bei einer Problemzerlegung einen Zwischenzustand, der nach Ausführung eines Teilprogrammes erreicht wird, bis in alle Einzelheiten zu spezifizieren. Dies würde es erforderlich machen, schon bei der Zerlegung explizit Information anzugeben, die zu diesem Zeitpunkt redundant ist. Dies wird an einem Beispiel deutlich: Um die Summe der Elemente eines Array zu berechnen, müssen wir die Nachbedingung s

etablieren, wobei

O~n

=Ll~i~n a[i]

gelten soll. Dies können wir verschärfen zu s

=Ll~i~j a[i]

1\

j

=n

.

Das erste Konjunktionsglied der so verschärften Nachbedingung kann durch die Zuweisung s, j:= 0, 0 etabliert werden. Das zweite Konjunktionsglied wird etabliert, indem j unter Erhaltung der Invarianz des ersten Konjunktionsgliedes so lange schrittweise erhöht wird, bis n erreicht ist. Hierzu bedarf es aber der Information, daß j~n gilt. Da n nicht verändert wird, gilt nach Ausführung von s, j:= 0, 0 sowohl O~n als auch j = 0, woraus sofort j~n folgt. Wie bereits in Kapitel 5 beschrieben, kann diese Information einfach berechnet werden, ohne daß die Benutzer sie explizit angeben müssen. Wäre die Ermittlung einer solchen berechneten Nachbedingung nicht vorgesehen, hätte die ursprüngliche Nachbedingung z.B. zu

verschärft werden müssen. Dies würde die Benutzer zwingen, explizit redundante Information anzugeben. Damit würde der Programmentwicklungsprozeß mühsam und undurchsichtig. Die berechnete Nachbedingung

174

Ein allgemeines Konzept zur formalen Modeliierung von Methoden

repräsentiert also Informationen über bereits entwickelte Teilprogramme, die über die Spezifikation der jeweiligen Teilprogramme hinausgehen. Wir stellen fest, daß für unser Vorhaben, einen allgemeinen Rahmen für die formale Modeliierung von Top-Down-Programmentwicklungsmethoden zu schaffen, zwei Konzepte von Bedeutung sind: Die explizite Verwendung von Platzhaltern ermöglicht es, Abhängigkeiten zwischen Teilproblemen auszudrücken und Programmschemata zu verwenden. Das Konzept der berechneten Nachbedingung ist notwendig, wenn imperative Programme entwickelt werden sollen, da es implizite Annahmen (in unserem Beispiel die, daß die Wertebereiche von Variablen sich während des Programmentwicklungsprozesses nicht ändern) explizit für einen Korrektheitsbeweis zur Verfügung stellt. Es bleibt noch zu überlegen, wie Programmentwicklungsprobleme sinnvoll repräsentiert werden können.

8.1.3 Definition von Programmierproblemen und ihren Lösungen Bei einem formalen Ansatz zur Entwicklung von imperativen Programmen muß explizit gemacht werden, welche Bedingungen von einem Programm geändert werden sollen und welche das Programm nicht verändern darf. Dies gilt insbesondere für die Variablen, die in der Spezifikation vorkommen: Die Veränderung der Eingabegrößen würde manches Problem trivial lösen. Wie in Kapitel 5 kennzeichnen wir die in einer Spezifikation vorkommenden Variablen als Eingabe-, Ergebnis- oder Sicherungsvariablen. Sicherungsvariablen sind ein Konzept, das nur bei der Benutzung imperativer Sprachen, wo Variablen ihren Wert ändern können, benötigt wird. Wir wollen hier nicht mehr davon ausgehen, daß die Spezifikationssprache die Sprache der Prädikatenlogik erster Stufe ist. Deswegen verlangen wir, daß alle in einer Vor- oder Nachbedingung vorkommenden Variablen klassifiziert werden in solche, die im Programm vorkommen dürfen und solche, die nur in Spezifikationen vorkommen dürfen. Wie gehabt werden die Programmvariablen wieder unterteilt in solche, die nur gelesen werden dürfen und solche, die auch verändert werden dürfen. Ähnliches gilt aber auch für die Nachbedingung: Die Teile, die bereits aus der Vorbedingung folgen und die das Programm invariant lassen soll, werden explizit gekennzeichnet, damit man sich bei der Programmentwicklung auf das wesentliche konzentrieren kann, nämlich auf das Errei-

Programmierprobleme

175

eben der Ziele. Durch die Klassifikation der Teile der Nachbedingung wird die relevante Information hervorgehoben. Weil das Konzept des invarianten Zieles ein spezielleres ist als das der Invarianten, werden wir bei der Definition von Programmierproblemen auf diese Komponente verzichten. Sie kann ebenso gut den Zielen zugeschlagen werden. Um Spezifikationen unabhängig von speziellen Sprachen darstellen zu können, müssen wir die Teile, aus denen eine Spezifikation besteht, trennen. Wir werden also die Vorbedingung als eine eigene Komponente eines Programmierproblems betrachten. Das Gleiche gilt für berechnete Nachbedingungen, da auch deren Repräsentation in Definition 5.1. formalismenabhängig gelöst ist. Wir definieren:

Definition 8.1 (Programmierprobleme) Seien eine Programmiersprache PL und eine Spezifikationssprache SL gegeben. Die in diesen Sprachen verwendeten (Objekt-) Variablen seien Elemente der Menge Ov. Sei weiterhin eine Menge von Variablen Pli (Platzhalter) gegeben, deren Elemente jeweils für Ausdrücke aus PL oder SL stehen können. Seien PL' und SL' die Sprachen, die aus PL oder SL entstehen, wenn für Teilausdrücke Platzhalter stehen dürfen. Ein Programmierproblem PP ist ein Tupel (Pre, q, Inv, cP, PS, Inp, '.R.., S) mit Pre ist ein Tupel von Ausdücken aus SL', genannt Vorbedingungen. q ist ein nichtleeres Tupel von Ausdücken aus SL', genannt Ziele. Inv ist ein Tupel von Ausdücken aus SL~ genannt Invarianten. cP ist ein Ausduck aus SL', genannt berechnete Nachbedingung. PS ist ein Ausdruck aus PL', der mindestens eine Variable aus Pli enthalten muß, genannt Programmschema. Inp ist ein Tupel von Variablen aus Ov, genannt Eingabevariablen. 1(. ist ein Tupel von Variablen aus Ov, genannt Ergebnisvariablen. ist ein Tupel von Variablen aus Ov, genannt SicherungsvariabS len. Jede der in Pre, q, Inv, cP oder PS vorkommenden Variablen aus Ov gehört zu genau einer der disjunkten Mengen Inp, '.R.., oder S.



Wir verwenden Tupel anstatt Mengen, um die Teile der einzelnen Komponenten leichter selektieren zu können. Mehrfache Vorkommen eines Elementes sind zugelassen. Seien, 'T, 'T1, 'T2 Tupel. Der Ausdruck 'T.i selektiert das i-te Element von 'T, und ('Tl , 'T2) bezeichnet die Konkatenation von Tupeln. Mit 'T1 \ 'T2 bezeichnen wir das Tupel, das aus 'T1 entsteht, wenn alle Elemente, die in 'T2 vorkommen, entfernt werden. Das

176

Ein allgemeines Konzept zur formalen Modeliierung von Methoden

Tupel 'It n 'T2 enthält alle sowohl in 'T1 als auch in 'T2 vorkommenden Elemente genau ein Mal und in beliebiger Reihenfolge. Neben der Verwendung von Tupeln besteht der Unterschied zu Definition 5.1 darin, daß wir die Spezifikation in ihre Bestandteile aufgelöst haben, da wir nicht mehr davon ausgehen können, daß Korrektheitsaussagen die Form r f- ($a)(

-t 1\(jo , Zuw(a.) n InPQ =0 Insl{) ={PSo f-4 a., cPo f-4 q>}



Diese Strategien sind wegen der zum Beweis ihrer Korrektheit benutzten Regel aus Kapitel 7 darauf ausgelegt, daß quasifunktionale Programme entwickelt werden und daß die in der Bibliothek enthaltenen Standardalgorithmen unter Verwendung von Metavariablen formuliert sind. In Abschnitt 9.7 werden wir eine Strategie zur Verwendung bereits bekannter Algorithmen definieren, die ohne diese Annahmen auskommt.

9.6 Weiterentwicklung der Methode Wie schon gesagt, wollen wir nicht nur bereits bestehende Methoden als Programmentwicklungsstrategien formalisieren, sondern die so erhaltene integrierte Methode auch inhaltlich weiterentwickeln. In diesem Abschnitt nehmen wir einige naheliegende Verbesserungen an der Methode vor, wobei wir nicht nur neue Strategien definieren, sondern auch Verbesserungen an der Benutzerunterstüztung, also den heuristischen Funktionen, vornehmen.

9.6.1 Eine disjunktive Konditionalstrategie In Abschnitt 5.9 haben wir die Entwicklung einer bedingten Anweisung an einem Beispiel erläutert. Dort war die zu etablierende Nachbedingung eine Disjunktion, die erst durch Weglassen eines Disjunktionsgliedes verschärft werden mußte, bevor die Conditional-Strategie angewandt werden konnte. Da dies kein Ausnahmefall ist, besteht der Bedarf an einer Strategie, die eine direkte Verarbeitung disjunktiver anstatt konjunktiver Ziele erlaubt. Es soll also möglich sein, eine bedingte Anweisung zu ent-

202

Definition einer offenen, integrierten Programmentwicklungsmethode

wickeln, wenn die Menge der Ziele eine Disjunktion enthält. Die entsprechende Taktik lautet: if_disj_tac

Jl

V

Tl f-~

--,e,

r

r

Hß>«'l' A

x> A 11>

e, r Ha)((cp A

x) A

Jl)

f-(ifE then a eise ßfi)(((q> V \j/) A X) A ~)

Korrektheilsbeweis Anwendungen der Regeln dia_cond, con_r und imp_r ergeben die Ziele (1) (2)

E,

-.e,

r

r

f-(a)(((cp v 'V) A X) f-(ß)(((cp v 'V) A x>

A A

~) ~)

Wir zeigen (1). Anwendung von dia_weak ergibt die dritte Prämisse der Taktik sowie

Zwei Anwendungen von con_l und eine Anwendung von con_r ergeben (4) (5)

q>, q>,

x. Jl f-(cp V \j/) A X x. Jl f- ~

Ziel (4) kann mit aussagenlogischen Regeln auf Axiome und (5) kann mit aussagenlogischen Regeln auf die erste Prämisse der Taktik reduziert werden. Ziel (2) wird analog behandelt.



Bei der Definition der Strategie disj-if-strat gehen wir davon aus, daß zuerst der then-Zweig der bedingten Anweisung entwickelt wird. Die Reihenfolge könnte auch anders gewählt werden, wozu eine weitere Strategie definiert würde. Diese beiden Strategien könnten wie beschrieben zusammengefaßt werden. ((~. lnst{)), (PPI. lnstt). {PI'2, lnst'l)) E disj-if·strat :gdw (PPI. PPo) E disj-if-strat 1 und {PI'2, 1Po, (PPI, Inst1)) E disj-ij-strat2 und (lnst{), ~. (PPI, Instt), (PZ'2, lnst'l)) E disj-ij-strat3

203

Weiterentwicklung der Methode

disj-if-stratJ :gdw mit

InstJ)) E

In112.

=

lnf{)

Inn

=

Inro

cP], ist eine neue Metavariable PS2 ist eine neue Metavariable

'10. = ~ s2 = so

Unsto. ~o. (~J. Insti), (~2• Inst2)) E disj-ij-strat3 :gdw Es gibt eine Formel so daß Insti (cPI) v Inst2(c'P]_) ~ 1; und lnsto = { PSo ~----> if Prei.l then Insti (PSI) eise lnsf2(PS2) fi, cPo ~---->

c;,

1;}



Falls mehrere Disjunktionen in (jo vorkommen, muß eine davon interaktiv ausgewählt werden. Zur Ermittlung des Testausdrucks e werden die Mengen setA((a) H 'lf(,Y.) möglich. In unserem Beispiel zur Berechnung des Listenmaximuns könnte sofort der Allquantor aus dem Ziel (*) eliminiert werden, was auch zu einfacheren Verfikationsbedingungen führt. Das mit der speziellen Verschärfungsfunktion erzeugte Ziel ist max(mm, listmax(ll)) =max(mm 1, listmax(ll 1)) In diesem Fall ist es noch nicht ratsam, das Ziel an die Zuweisungsstrategie zu übergeben. Es wäre zwar durch skip zu etablieren, da mm 1 und ll1 Sicherungsvariablen für mm bzw. ll sind. In der Zielmenge, die bei

208

Definition einer offenen, integrierten Programmentwicklungsmethode

diesem Beispiel vorliegt, ist jedoch auch das Ziel enthalten, die Länge der Liste ll zu verkleinern, so daß diese Entwicklung nicht zum Ziel führen würde. Es müssen zunächst noch die Definition von listmax expandiert und die Assoziativität von max angewandt werden: max(mm, listmax(ll)) =max(max(mm 1, car(ll 1)), listmax(cdr(ll 1)))) Mit dem im vorigen Abschnitt beschriebenen Verfahren kann dieses Ziel sofort gelöst werden. Wie schon gesagt, ist es nicht sinnvoll, die drei Schritte (i)

Definition expandieren

(ii) algebraisches Gesetz anwenden

(iii) Implikation in Gleichungen umformen zusammenzufassen. Es ist aber möglich, für die Schritte (i) und (ii) ebenso wie hier für Schritt (iii) spezielle heuristische Funktionen zu konzipieren. Diese haben nicht nur den Vorteil, den Benutzern lästige Tipparbeit abzunehmen, sondern es kann auch automatisch überprüft werden, ob eine solche Funktion anwendbar ist.

9. 7 Wiederverwendbarkeit von entwickelten Programmen Bei vielen Programmentwicklungsaufgaben gibt es Unterprobleme, die mehrfach vorkommen, z.B. Such- oder Sortierprobleme. Es ist für die Benutzer eines Synthesesystems eine wesentliche Erleichterung, Programme für derartige Unterprobleme nicht immer neu entwickeln zu müssen. Stattdessen ist es wünschenswert, auf eine Bibliothek zurückgreifen zu können, die bereits fertig entwickelte Algorithmen enthält. Wir sehen also eine Bibliothek vor, die bereits entwickelte Programme in Form von verallgemeinerten Korrektheitsaussagen enthält. Diese nennen wir Makros. Unser Ziel ist nun, eine Strategie zu definieren, die den Gebrauch einer solchen Bibliothek erlaubt. Dazu müssen wir zunächst eine Taktik entwickeln, die es erlaubt, ein bereits entwickeltes Programm an eine vorhandene Spezifikation anzupassen. Gegeben seien ein Programmierproblem PPo = (Preo, fio, Invo. cPo, PSo, Inro, 'l(o, So) sowie ein Makro (cPrem, cfim• clnvm, ccPm, Progm, lnPm, ~.Sm). Dann gilt

Wiederverwendbarkeit von entwickelten Programmen

209

Frei(cPrfm) u Frei((!!,y)) Invi cPI PSI Inpi

= 0 ist eine neue Metavariable ist eine neue Metavariable = 1!

1(]. SI

= 0

= .Y.

(~. 1Po, (PPI, /nsti)) E central-recursion-strat-left-of- rec :gdw Es gibt neue Variablen .I zu InpJ = 1! und eine Wohlordnung .$body; $pdli do $body end 1\

9I

Inpi .1{]_

si

(InstQ,

=~

=~ = !l.'

P.1tJ, (Pl'I,

Insti)) e new-call-strat2 :gdw

o; o;

PSo hat die Form proc $pdlo do $a end PSI hat die Form proc p " Jl) S f-'1' 1\ E ~ q>

r

r

f-E f-7 E'

f-(if E' then a eise

ß fi)( q> 1\ S)

Hier werden die beiden Zweige der bedingten Anweisung asymmetrisch behandelt. Der then-Zweig hat eine andere Nachbedingung als die gesamte bedingte Anweisung. Die Entwicklung kann also zielgerichtet, d.h. ausgehend von der Nachbedingung, erfolgen. Der else-Zweig jedoch hat dieselbe Nachbedingung wie das ursprüngliche Problem, so daß hier genauso wie bei if_tac ein gezieltes Ausnutzen der Vorbedingung nötig ist. Die dritte Strategie schließlich ermöglich es, bei beiden Zweigen die Entwicklung an der Nachbedingung auszurichten: if_disj_tac Jl v 11

r-s

-,e,

r

r Hß)CC'I'" x) "TJ)

E,

r

f-(a)((q>" X)" Jl)

f-(ifE then a else ßfi)(((q> V 'lf) 1\ X) 1\ S)

Beide Zweige haben andere Nachbedingungen als das ursprüngliche Problem. Ein gezieltes Ausnutzen der Vorbedingung ist nicht nötig. Deshalb kann die Bedingung e auch nachträglich berechnet werden, was aller-

Benutzung der Methode

223

dings bei if-conj-strat ebenso möglich ist. Um die Strategie anwenden zu können, muß die Nachbedingung eine Disjunktion enthalten. Für jede dieser Strategien gibt es spezifische Situationen, in denen ihre Anwendung empfehlenswert ist. Wenn die Nachbedingung in disjunktiver Form vorliegt oder einfach in eine solche zur transformieren ist, ist ifdisj-strat sicher die komfortabelste und angenehmste Strategie. Hat die Nachbedingung konjunktive Form oder ist diese einfach zu erhalten, wird man if-conj-strat wählen. Die Strategie if-strat hat den Vorteil, auch dann anwendbar zu sein, wenn die Nachbedingung keine dieser Formen aufweist, aber den Nachteil, daß der Testausdruck im voraus bekannt sein muß und daß dies das Einzige ist, das alte und neue Probleme voneinander unterscheidet. Ähnliches läßt sich auch über die Strategien zur Entwicklung von zusammengesetzten Anweisungen sagen. Falls die Nachbedingung konjunktive Form hat, und die Ergebnisvariablen, die zur Etablierung der beiden Konjunktionsglieder verändert werden müssen, disjunkt sind, ist die Anwendung von disjoint-goal-strat (Abschnitt 5.5) von Vorteil. Hat die Nachbedingung konjunktive Form, aber die beiden Teilziele sind nicht unabhängig voneinander zu erreichen, ist es sinnvoll, protection-strat (Abschnitte 5.6, 8.2) anzuwenden. Wurde eine Nachbedingung cp(l!) verschärft zu ~(y) und 'lf(Y.,Y.), mit neuen Variablen y, so kann sich die Anwendung von preservation-strat (Abschnitt 5.8.1) als nützlich erweisen. Falls bereits eine Bedingung bekannt ist, die durch das erste Teilprogramm etabliert werden soll und die nicht als Konjunktionsglied in der Nachbedingung vorkommt, leistet camp-strat (Abschnitt 9.3.1) gute Dienste. Das Gleiche gilt für Fälle, wo die Nachbedingung nicht in konjunktiver Form vorliegt und es auch nicht sinnvoll ist, sie dementsprechend zu verschärfen. Allerdings muß dann eine Zwischenbedingung gefunden werden, und der zweite Teil der zusammengesetzten Anweisung hat dieselbe Nachbedingung wie die gesamte Anweisung, so daß wieder die Vorbedingung im Entwicklungsprozeß die wichtigere Rolle spielt. Die Möglichkeiten zur Schleifenentwicklung korrespondieren zu den Strategien zur Entwicklung von zusammengesetzten Anweisungen: Die Anwendung der Protection-Strategie führt zur Entwicklung von Schleifen mittels Invarianten, die Anwendung der Preservation-Strategie führt zur Entwicklung von Schleifen mittels invarianten Zielen. Verschiedene Strategien für dasselbe Programmkonstrukt sind also wünschenwert, da ein Programmkonstrukt in vielen verschiedenen Kontexten entwickelt werden kann. Jedes der Konstrukte von Programmiersprachen ist so allgemein, daß es nicht möglich ist, es mit bestimmten Problemarten zu verknüpfen. Kaum ein Programm, egal für welches Pro-

224

Definition einer offenen, integrierten Programmentwicklungsmethode

blem, wird ohne Schleifen, bedingte Anweisungen und zusammengesetzte Anweisungen auskommen. Die verschiedenen Strategien tragen den verschiedenen Kontexten, an denen Bedarf nach einem bestimmten Konstrukt besteht, Rechnung. Diese Kontexte, die wir oben geschildert haben, hängen vom Problem selbst und von stilistischen Vorlieben der Programmierer ab. Ein Beispiel einer Programmentwicklung mit einem System, das die hier vorgestellten Strategien implementiert, ist in Anhang A4 zu finden.

Falit

225

10 Fazit Kehren wir zum Ausgangspunkt unserer Überlegungen zurück: Anstoß für unsere Bemühungen war die Unzufriedenheit mit der Tatsache, daß die Korrektheit von Programmen in der Praxis ausschließlich durch Testen mehr schlecht als recht überprüft wird. Der einzige Weg, die Korrektheit eines Programmes positiv festzugstellen, ist ein mathematischer Korrektheitsbeweis. Es erscheint sinnvoll, ein Programm gleich so zu entwickeln, daß seine Korrektheit garantiert werden kann, obwohl auch durch nachträgliche Verifikation die Korrektheit eines Programmes gezeigt werden kann. Die Idee der formalen Programmentwicklung ist nicht neu, und es gibt vielfältige Ansätze hierzu, von denen wir in Kapitel 2 einige kurz beschrieben haben. Diesen Ansätzen ist gemeinsam, daß sie jeweils eine in sich geschlossene Methode der Programmentwicklung vorschlagen, die zu beweisbar korrekten Programmen führt. Dies kann zur Folge haben, daß eine formale Programmentwicklungsmethode ihren Benutzern bestimmte Vorgehensweisen aufzwingt, die entweder einem konkreten Problem oder ihren persönlichen Vorlieben zuwiderlaufen. Außerdem sind Spezifikations- und Programmiersprache meist untrennbar mit dem Formalismus verbunden und können nicht geändert werden. Bis heute sind formale Methoden zur Programmentwicklung noch kaum akzeptiert. Dies ist unter anderem eine Folge der Starrheit der heute existierenden Ansätze. Darüberhinaus sind die meisten formalen Methoden nicht abstrakt genug, d.h. die Schritte, die man mit ihnen ausführen kann, sind zu klein, so daß die Entwicklung eines Programmes sehr mühsam ist. Die mangelnde Akzeptanz zeigt auch, daß für die Beurteilung formaler Entwicklungsmethoden theoretische Vollständigkeit nicht von entscheidendem Interesse ist. Die Frage, welche Probleme man prinzipiell mit einer Methode lösen kann, tritt hinter der Frage zurück, wie schwierig eine Lösung praxisnaher Probleme ist. Aus diesen Gründen sind wir in dieser Arbeit einen anderen Weg gegangen als die Autoren der bisher exisitierenden Ansätze. Es war nicht unser Ziel, eine neue Vorgehensweise zur Programmentwicklung zu erfinden. Vielmehr wollen wir zu einer Verbesserung der Akzeptanz formaler Methoden dadurch beitragen, daß wir die Formalisierung einer Vielzahl unterschiedlicher Methoden mit Hilfe eines einheitlichen Beschreibungsmittels erlauben. Dadurch ist es eher als bisher möglich, daß Programmierer ihre Erfahrung und ihr Programmierwissen auch in einen formalen Entwicklungsprozeß einbringen können. Von besonderem Interesse war es dabei für uns, Konzepte zu ent-

226

Fazit

wickeln, die auch ein Umgehen mit imperativen Programmiersprachen erlauben, da der Bedarf hierfür in besonderem Maße besteht: Die Mehrzahl der Programme ist in imperativen Sprachen geschrieben, aber die Mehrzahl der formalen Programmentwicklungsmethoden läßt nur funktionale Programme zu. Um die Anforderungen an ein Formalisierungshilfsmittel herauszuarbeiten, haben wir einige inhaltlich sehr verschiedene Methoden zur Programmentwicklung in dynamischer Logik formalisiert, die ein geeigneter Rahmen für die formale Behandlung von imperativen Programmen ist. Die dabei gewonnenen Erkenntnisse haben zur Entwicklung des Konzeptes der Programmentwicklungsstrategie als einheitlichem Beschreibungsmittel geführt. Dazu war es notwendig, die Begriffe "Programmierproblem" und "Lösung eines Programmierproblems" zu formalisieren. Dies ist möglich, ohne sich auf bestimmte Programmier- oder Spezifikationssprachen festzulegen. Die Betrachtung von Entwicklungsstrategien als Relationen ermöglicht zudem in natürlicher Weise die Kombination unterschiedlicher Vorgehensweisen bei der Programmentwicklung, indem eine Programmiermethodik einfach als eine Menge von Entwicklungsstrategien angesehen wird. Der vorgestellte uniforme Ablaufmechanismus erlaubt die nahezu beliebige Kombination der einzelnen Strategien. Das entwickelte Konzept erlaubt es weiterhin, den Abstraktionsgrad einer Methode durch einfache Komposition von Relationen zu steigern. Dies haben wir ausführlich demonstriert, indem wir die zuvor einzeln formalisierten Ansätze mit Hilfe des neuen Beschreibungsmittels dargestellt und damit zu einer homogenen Methode integriert haben. Von großer Wichtigkeit ist auch die Tatsache, daß die Benutzung unseres Konzeptes zu offenen Methoden führt, die inkrementeil erweitert und entwickelt werden können. Die Allgemeinheit des Konzeptes und seine Unabhängigkeit von speziellen Spezifikations- und Programmiersprachen ergeben eine Flexibilität bei der Anwendung formaler Methoden zur Programmentwicklung, die wir bei den bisherigen Methoden vermißt haben. Zum Schluß wollen wir noch bemerken, daß wir den Einsatz formaler Methoden nur in der Implementierungsphase nicht für ausreichend halten. Die Entwurfsphase, also die Erstellung der Spezifikation, ist genauso wichtig dafür, ob ein Programm letztendlich zufriedenstellend funktioniert wie die Implementierungsphase. Das Gleiche gilt für die Wartungsphase, da eine Anpassung von Programmen an veränderte Bedingungen die Regel ist. Eine zufriedenstellende Situtation kann wohl erst dann erreicht werden, wenn auch diese beiden Phasen mit formalen Mitteln unterstützt werden können.

Uteratur

227

11 Literatur [Backhouse 1989] Backhouse, R. 1989. Do-it-Yourself Type Theory. Formal Aspects of Computing 1: 19-84. [Bibe11980] Bibel, W. 1980. Syntax-Directed, Semantics-Supported Program Synthesis. Artificiallntelligence 14: 243-261. [Bibel und Hömig 1984] Bibel, W. und Hömig, K. 1984. LOPS- A System Based on a Strategical Approach to Program Synthesis. In Automatie Program Construction Techniques, eds. A. Biermann, G. Guiho und Y. Kodratoff, 69-90. New York: Macmillan Publishing Company. [Bird 1989] Bird, R. 1989. Lectures on Constructive Functional Programming. In Constructive Methods in Computing Science, ed. M. Broy, 151-218. Berlin: Springer-Verlag. [Biundo 1988] Biundo, S. 1988. Automated Synthesis of Recursive Algorithms as a Theorem Proving Tool. In Proceedings of the 8-th European Conference on Artificial Intelligence. [Bj!6rner und Jones 1982] Bj(6rner, D. und Jones, C. 1982. Formal Specification and Software Development. London: Prentice-Hall. [Boehm 1986] Boehm, B. W. 1986. A Spiral Model of Software Development and Enbancement. ACM SIGSOFT Software Engineering Notes 11(4): 22-42. [Broy 1984] Broy, M. 1984. Algebraic Methods for Program Construction: The Project CIP. In Program Trasnsformation and Programming Environments, ed. P. Pepper, 199-222. Berlin: SpringerVerlag. [Burstall und Darlington 1977] Burstall, R. und Darlington. J. 1977. A Transformation System for Developing Recursive Programs. Journal ofthe ACM 24: 44-67. [Clarke 1979] Clarke, E. 1979. Programming Language Constructs for which it is Impossible to Obtain Good Hoare Axiom Systems. Journal ofthe ACM 26: 129-147. [Constable et al. 1986] Constab1e, R. et al. 1986. lmplementing Mathematics with the Nurpl Proof Development System. Englewood Cliffs: Prentice-Hall. [Dahl, Dijkstra und Hoare 1981] Dahl, 0., Dijkstra, E. und Hoare, A. 1981. Structured Programming. London: Academic Press. [Dershowitz 1983] Dershowitz, N. 1983. The evolution of programs. Boston: Birkhäuser. [Drexler 1990] Drexler, R. 1990. Programmsynthese durch sukzessive Entwickung von Schleifeninvarianten. Diplomarbeit, Fakultät für Informatik, Universität Karlsruhe. [Dijkstra 1976] Dijkstra, E.W. 1976. A Discipline of Programming. Englewood Cliffs: Prentice-Hall.

228

Literatur

[Dijkstra 1988] Dijkstra, E.W. Vorlesung vom 4.8.1988, International Summer School, Marktoberdorf 1988, nicht veröffentlicht. [Dijkstra 1990] Dijkstra, E.W. ed. 1990. Formal Development of Programs and Proofs. Reading: Addision-Wesley. [Ehrig und Mahr 1985] Ehrig, H. und Mahr, B. 1985. Fundamentals of Algebraic Specification 1. Berlin: Springer-Verlag. [Gelfort 1989] Gelfort, M. 1989. Implementierung einer Strategie zur Prograrnmentwicklung. Studienarbeit, Fakultät für Informatik, Universität Karlsruhe. [Gelfort 1990] Gelfort, M. 1990. Ein Synthesesystem für Divid-and-Conquer Algorithmen. Diplomarbeit, Fakultät für Informatik, Universität Karlsruhe. [Gordon, Milner, und Wadsworth 1979] Gordon, M., Milner, R. und Wadsworth,C. 1979. Edinburgh LCF. Berlin: Springer Lecture Notes in Computer Science 78. [Goldblatt 1982] Goldblatt, R. 1982. Axiomatising the Logic of Computer Programming. Berlin: Springer Lecture Notes in Computer Science 130. [Gries 1981] Gries, D. 1981. The Science of Programming. Berlin: Springer-Verlag. [Harel 1984] Harel, D. 1984. Dynamic Logic. In Handbook of Philosophical Logic, Vol. 2, eds. D. Gabbay und F.Guenther, 496-604. Dordrecht: Reidel. [Heisel 1989] Heisel, M. 1989. A Formalization and Implementation of Gries's Program Development Method within the KIV Environment, Interner Bericht 3/89, Fakultät für Informatik, Universität Karlsruhe. [Heisel 1992] Heisel, M. 1992. Formalizing and Implementing Gries's Program Development Method in Dynamic Logic. Science of Computer Programming 18: 107-137. [Heisel, Reif und Stephan 1986] Heisel, M., Reif, W. und Stephan, W. 1986. A Functional Language to Construct Proofs, Interner Bericht 1186, Fakultät für Informatik, Universität Karlsruhe. [Heisel, Reif und Stephan 1987] Heisel, M., Reif, W. und Stephan, W. 1987. Program Verification by Symbolic Execution and Induction. In Proceedings of the 11-th German Workshop on Artificial Intelligence, 201-210. Berlin: Springer Informatik Fachberichte 152. [Heisel, Reif und Stephan 1988a] Heisel, M., Reif, W. und Stephan, W. 1988. Program Verification Using Dynamic Logic. In Proceedings of the first Workshop on Computer Science Logic, I 02-117. Berlin: Springer Lecture Notes in Computer Science 329. [Heisel, Reif und Stephan 1988b] Heisel, M., Reif, W. und Stephan, W. 1988. Implementing Verification Strategies in the KIV System. In Proceedings of the 9-th International Conference on Automated De-

Literatur

229

duction, 131-140. Berlin: Springer Lecture Notes in Computer Science 310. [Heisel, Reif und Stephan 1989] Heisel, M., Reif, W. und Stephan, W. 1989. Machine-Assisted Program Construction and Modification. In In Proceedings of the 13-th German Workshop on Artificial Intelligence, 338-347. Berlin: Springer Informatik Fachberichte 216. [Heisel, Reif und Stephan 1990] Heisel, M., Reif, W. und Stephan, W. 1990. Tactical Theorem Proving in Program Verification. In Proceedings of the 10-th International Conference on Automated Deduction, 115-131. Berlin: Springer Lecture Notes in Artificial Intelligence 449. [Heisel, Reif und Stephan 1991] Heisel, M., Reif, W. und Stephan, W. 1991. Formal Software Development in the KIV System. In Automating Software Design, eds. Michael Lowry und Robert McCartney, 547-574. Menlo Park: AAAI Press. [Heisel und Santen 1990] Heisel, M., und Santen, T. 1990. Formal Program Development by Goal Splitting and Backward Loop Formation, Interner Bericht 32/90, Fakultät für Informatik, Universität Karlsruhe. [Hoare 1969] Hoare, A. An Axiomatic Basis for Computer Programrning. Journal ofthe ACM 12: 576-580. [Jackson 1983] Jackson, M. 1983. System Development. Englewood Cliffs: Prentice- Hall. [Katzan 1976] Katzan, H. 1976. Systems Design and Documentation. New York: Van Nordstrand Reinhold Company. [Knuth 1973] Knuth, D.E. 1973. The Art of Computer Programming. 3 Vols. Reading: Addison-Wesley. [Knuth und Bendix 1970] Knuth, D. und Bendix, P. 1970. Simple Word Problems in Universal Algebras. In Computational Problems in Abstract Algebra, ed. J. Leech, 263-297. Pergarnon Press. [Lowry und Duran 1989] Lowry, M. und Duran, R. 1989. Knowledge Based Software Engineering. In Handbook of Artificial Intelligence, Vol IV, ed. A. Barr, P. Cohen und E. Feigenbaum, 241321. Reading: Addison Wesley. [Manna und Waldinger 1980] Manna, Z. und Waldinger, R. 1980. ADeductive Approach to Program Synthesis. ACM Transactions on Programming Languages and Systems 2: 90-121. [Martin-Löf 1984] Martin-Löf, P. 1984. Intuitionistic Type Theory. Neapel: Bibliopolis. [Mayrhauser 1990] Mayrhauser, A. von 1990. Software Engineering. San Diego: Acadernic Press. [Möller 1990] Möller, B. 1990. A Survey of the Project CIP - ComputerAided, Intuition-Guided Programrning. In Sichere Software, ed. H. Kersten, 280-298. Heidelberg: Hüthig Buch Verlag.

230

Literatur

[Morris and Wegbreit 1977] Morris, J. H.; and Wegbreit, B. 1977. Subgoal Induction. Communications of the ACM 20: 209-222. [Müller 1990] Müller, H. 1990. Formalisierung einer ProgrammsyntheseMethode von Manna und Waldinger in dynamischer Logik und Implementierung auf dem KIV-System. Diplomarbeit, Fakultät für Informatik, Universität Karlsruhe. [Nordström 1981] Nordström B. 1981. Programming in Constructive Set Theory: Some Examples. In Proceedings of the ACM Conference on Functional Programming Languages and Computer Architecture, 141-153. [Paige und Koenig 1982] Paige, R. und Koenig, S. 1982. Finite Differencing of Computable Expressions. ACM Transactions on Programming Languages and Systems 4: 402-453. [Reddy 1989] Reddy, R. 1989. Rewriting Techniques for Program Synthesis. In Proceedings Rewriting Techniques and Applications, ed. N. Dershowitz, 388-403. Berlin: Springer Lecture Notes in Computer Science 355. [Reif 1984] Reif, W. 1984. Vollständigkeit einer modifizierten GoldblattLogik und Approximation der Omegaregel durch Induktion. Diplomarbeit, Fakultät für Informatik, Universität Karlsruhe. [Reif 1987] Reif, W. 1987. A Modal Logic with Countably Many Modal Operators. Internes Arbeitspapier. [Richter 1978] Richter, M. Logikkalküle. Stuttgart: Teubner. [Ross and Schoman 1973] Ross, D. und Schoman, K. 1973. Structured Analysis for Requirements Definition. IEEE Transactions on Software Engineering SE-3:6-15. [Santen 1990] Santen, T. 1990. Programmsynthese nach Dershowitz. Studienarbeit, Fakultät für Informatik, Universität Karlsruhe. [Smith 1985] Smith, D. R. 1985. Top-down synthesis of divide-and-conquer algorithms. Artificiallntelligence 27:43-96. [Smith 1990] Smith, D. R. 1990. KIDS - A Setni-Automatie Program Development System. IEEE Transactions on Software Engineering 16: 1024-1043. [Spivey 1988] Spivey, M. 1988. Understanding Z. Cambridge: Cambridge University Press. [Stephan 1989] Stephan, W. 1989. Axiomatisierung Rekursiver Prozeduren in der Dynamischen Logik. Habilitationsschrift, Fakultät für Informatik, Universität Karlsruhe.

Verwendete Regeln

231

Anhang Al Verwendete Regeln In diesem Anhang führen wir alle in der Arbeit verwendeten Regeln bzw. Taktiken in alphabetischer Reihenfolge auf. Basisregeln sind mit einem Stern gekennzeichnet. Zum Schluß des Anhangs geben wir zusätzlich einige Funktionen an, die die Anwendung von Axiomen ermöglichen.

abort*

1- [abort]cp adjust_pre_and_post

r· l-($a)cp'

cp' l-cp

r 1-(a)cp all_r*

r 1- cp,

L1

r 1-Vycp, L1

, falls y n (Frei(r) u Frei(Ll)) = 0

asg*

attach_macro

r m Ham)(, d

backward_loop_tac q>, -p, 't =t, y

d, y

=Yl

Ha)((('t < t A q>) 1\ V.u.(\j/(l!,y) ~ 'I'C!!.Y 1))) d f-q> q> 1\ -p f- 01 < 't 1\ ... 1\ On< 't

A

TJ)

=Yl Hwhile -p do a od){(p 1\ V!!.(\j/(l!,y) ~ \jl(l!,y 1))) 1\ , d

composition*

comp_tac

conditional* f-[if Ethen a else ß fi]q>

H

((E ~ [a]q>)

1\

(--,E ~ [ß]q>))

Verwendete Regeln

233

con_l*

f- Ll

q>, \jf, [' q>

1\

\jf, ['

f- Ll

con_r*

[' f-cp, Ll [' f-cp

1\

\jl, Ll

cut*

[' f-cp, Ll

q>,f'f-il

determinism_of_assignment*

dia_abort

f- ---,(abort)q> din_asg

234

Verwendete Regeln

dia_comp

dia_cond

dia_con_r

r

f-h f-- ( proc pdl-compose do compose(lnpcompl , Inpcomp 2 : -"\omp) end)2)) r f-cp1 1\ q>2 q>2, -\jl f- OJ < 't 1\ ... 1\ On < 't

-\jf, 1\

r Hwhile -"' do a

od)(('l' 1\ cp 1) 1\ cp2)

gc_body_tac

E V Ej,

r 1 f-(if Ej

then

U]

eJse a fi)q>

A

s)

y

238

Verwendete Regeln

gc_if_tac

r

f- (if e then a 1 else a2 fi) "

TJ)

Ha;ß)CC q>" 'V) "TJ)

1\

X)

242

Verwendete Regeln

save_variable_tac v

=v1o r

~(a)(q> "11)

r falls VJ

e

11 ~~

~(a)(q> "~)

Frei(Ar" q>" ~) u Vars(a).

save_var_intro*((vl, ... ,vn), (wl, ... ,wn)) =

r, VJ =WJ,

... , Vn

=Wn ~,1

skip* ~ [skip]q> +-? q>

skip_tac

r

~(skip)q>

strengthening_tac

r

~(a)(q>" ~)

r

~ (a)('l' " ~)

term_substitution Xi = cr Hx~o

... , Xj,

... , Xn := 'tJ, ... , f(Xj, )'.), ... , 'tn)Xj = f(cr, )'.)

falls {xt. ... , Xn} n ()'. u Vars(cr))

=0

Verwendete Regeln

243

true_r*

variable_substitution

add_elem_bag(x57, bag_list(list59)) ( = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) and le_elem_bag{x88, add_elem_bag(x90, bag_list(list9l))} and ordered_list{list9l})) Please enter an additional formula in the variables (x57 list59 x88 x90 list9l) to combine with the antecedent, or type 0 in ordernot to strengthen antecedent.

Bei DS2 müssen wir den decompose-Operator entwickeln, ohne das primitive- Prädikat zu kennen. Dies kann dazu führen, daß das Programmierproblem der Entwicklung des decompose-Operators, der nur aufgerufen wird, wenn das primitive- Prädikat nicht gilt, unlösbar ist. Dies ist hier der Fall. Deswegen muß zu der Vorbedingung weitere Information hinzugefügt werden, was natürlich einen Einfluß auf das später zu ermittelnde primitive-Prädikat hat.

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

273

Die erste Formel der Nachbedingung der Prozedur add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) kann nur etabliert werden, wenn list59 nicht leer ist. PPL 2> %"not list59•nil_list"

the precondition is: (ordered_list{list59) and not list59

= nil_list)

the conjuncts of the postcondition are classified as GOALS: (add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))), le_elem_bag{x88, add_elem_bag(x90, bag_list(list91))), ordered_list{list91)) INVARIANTS:

()

input variables: result variables:

(x57 list59) (x88 x90 list91)

save variables are:

()

Wir haben nun wieder die gesamte Methode zur Verfügung. Die Elemente x88, x90 und die Liste list91 sollen so berechnet werden, daß sie zusammengeraßt eine geordente Liste ergeben. Hierzu stehen die geordnete nichtleere Liste list59 und das Element x57 zur Verfügung. Damit ist klar, daß list91 die Liste list59 ohne deren erstes Element sein muß. Um x88 und x90 ermitteln zu können, muß man x57 mit dem ersten Element von list59 vergleichen. Dies drücken wir mittels einer Verschärfungsoperation aus. PPL 2> str invariants due to variable conditions: (ordered_list{list59), not list59 = nil_list) current GOALS: 1: add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) 2: le_elem_bag{x88, add_elem_bag(x90, bag_list(list91))) 3: ordered_list{list91) choose by entering one number or a list of numbers PPL 2> (list 1 2) enter formula implying add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) and le_elem_bag{x88, add_elem_bag(x90, bag_list(list91))) in context of the invariants stated above

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

274

PPL 2> %•list9l•cdr_list(list59) and ( ls_elam(x57, car_list(list59)) and xBB•x57 and x90•car_list(list59) or not ls_elam(x57, car_list(list59)) and xBB•car_list(list59) and x90•x57)•

the precondition is: (ordered_list(list59} and not list59

= nil_list}

the conjuncts of the postcondition are classified as GOALS: (ordered_list{list91}, list91 = cdr_list(list59}, ls_elem{x57, car_list(list59}} and x88 = x57 and x90 car_list(list59} or not ls_elem{x57, car_list(list59)} and x88 car_list(list59) and x90 = x57)

=

INVARIANTS :

()

input variables: result variables:

(x57 list59) (x90 x88 list91)

save variables are:

()

Die Zielmenge läßt sich in zwei unabhängige Teilmengen zerlegen; deswegen wählen wir die Disjoint-Goal-Strategie. PPL 2> dg choose goal for first statement 1: ordered_list{list91} 2: list91 = cdr_list(list59) 3: ls_elem{x57, car_list(list59)} and x88 x57 and x90 = car_list(list59) or not ls_elem{x57, car_list(list59)} and x88 car_list ( list59) and x90 = x57 choose by entering one nurober or a list of numbers PPL 2> 2

Zuerst etablieren wir Ziel 2. ********** developing first goal now ...

====================================================

the precondition is: (ordered_list{list59} and not list59

= nil_list)

the conjuncts of the postcondition are classified as GOALS: (list91 = cdr_list(list59))

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

INVARIANTS:

275

()

input variables: result variables:

(list59 x57 x88 x90) (list91)

save variables are:

()

Die automatische Zuweisungsgenerierung löst dieses Problem. PPL 2> aa automatic assignment generation computed list91 := cdr_list(list59) to establish the postcondition list91 = cdr_list(1ist59) wrt. to the precondition (ordered_list{list59) and not list59 = nil_list) Is this assignment sequence sufficient? answer 1 for yes, 0 for no: PPL 2> 1 the computed postcondition is: ordered_list{list59) and not list59 list91 = cdr_list(list59)

nil list and

do you want to enter an additional conjunct? answer 1 for yes, P·PL 2> 0

0 for no:

proving vc by propositional prover ... Propositional logic prover left 2 premises upon proving (ordered_list{list59) and not list59 = nil_list) 1( cdr_list(list59) = cdr_list(list59) and ordered_list{1ist59) and not list59 = nil_list and cdr_list(list59) = cdr_list(list59)). Try full predicate logic prover? answer 1 for yes, 0 for no: PPL 2> 1 proving vc by predicate logic prover ... Congratulations! Verification condition has been proven.

Nachdem der Aussagenlogikbeweiser nicht erfolgreich war, gelang es dem Prädikatenlogikbeweiser, eine Verifikationsbedingung zu zeigen. ********** developing secend goal now ... the precondition is:

276

Beispiel: EntwicklUIJg eines Divide-and-Conquer-Algorithmus

list91 = cdr_list(list59) and ordered_list(list59) and not list59 = nil_list and list91 = cdr_list(list59)) the conjuncts of the postcondition are classified as GOALS:

( or

ls_elem(x57, car_list(list59)) and x88 and x90 = car_list(list59)

= x57

not ls_elem(x57, car_list(list59)) and x88 and x90 = x57, ordered_list(list91)) INVARIANTS :

car_list ( list59)

()

input variables: (list59 x57 list91) result variables: (x88 x90) save variables are: ()

Das Ziel ist eine Disjunktion, weshalb wir die disjunktive Konditionalstrategie wählen. PPL< 2> cdp do you want to enter a concrete test expression (otherwise it will be computed after development of the then-part) answer 1 for yes, 0 for no:

Wir legen den Testausdruck vor Entwicklung der beiden Zweige fest. PPL 2> 1 select test expression for conditional 1: ls_elem(x57, car_list(list59)) 2: not ls_elem{x57, car_list(list59)) 3: type my own expression choose an element by entering its number PPL 2> 1 ********** developing then-part now ... the precondition is: (ls_elem(x57, car_list(list59)), list91 = cdr_list(list59) and ordered_list(list59) and not list59 nil_list and list91 cdr_list(list59))

=

=

the conjuncts of the postcondition are classified as GOALS:

(x90 = car_list(list59), ls_elem(x57, car_list(list59)), x88 ordered_list(list91))

INVARIANTS :

()

input variables: (list59 x57 list91)

x57,

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

result variables:

277

(x88 x90)

save variables are:

()

PPL 2> aa automatic assignment generation computed BEGIN x90 .- car_list(list59) ; x88 := x57 END to establish the postcondition ls_elem{x57, car_list(list59)) and x88 and x90 = car_list(list59) and ordered_list{list91)

x57

wrt. to the precondition (ls_elem{x57, car_list(list59)), list91 = cdr_list(list59) and ordered_list(list59) and not list59 = nil list and list91 = cdr_list(list59)) Is this assignment sequence sufficient? answer 1 for yes, PPL 2> 1

0 for no:

the computed postcondition is: ls_elem{x88, x90) and ls_elem{x57, x90) and ls_elem{x88, car_list(list59)) and ls_elem{x57, car_list(list59)) and ordered_list{list59) and not list59 = nil list and list9l cdr_list(list59) and x90 = car_list(list59) and x88 = x57 do you want to enter an additional conjunct? answer 1 for yes, 0 for no: PPL 2> 0 proving vc by propositional prover ... Propositional logic prover left 5 premises upon proving (ls_elem{x57, car_list(list59)), list91 = cdr_list(list59) and ordered_list{list59) and not list59 = nil list and list91 = cdr_list(list59)) 1( ls_elem{x57, car_list(list59)) and x57 = x57 and car_list(list59) = car_list(list59) and ordered_list{list9l) and ls_elem{x57, car_list(list59)) and ls_elem{x57, car_list(list59)) and ls_elem{x57, car_list(list59)) and ls_elem{x57, car_list(list59)) and ordered_list{list59) and not list59 = nil_list and list91 = cdr_list(list59) and car_list(list59) = car_list(list59) and x57 = x57). Try full predicate logic prover? answer 1 for yes, 0 for no: PPL 2> 1 proving vc by predicate logic prover ...

278

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

if you BACKTRACK now, you may examine the propsitional proof!

S-7 the prover left 1 open premise. you may now 1 ... accept the generated proof (leaving premises as verification conditions) 2 ... reject the proof and leave the original verification condition 3 ... reject both proof and verification condition (which will cause a backtrack) PPL 2> %s-11 (ls_elem{x57, car_list(list59)}, cdr_list(list59)

cdr_list(list59),

ordered_l1st(l1st59J)

l(l1st59 • n1l_l1st, ordered_l1st(cdr_list(list59)})

PPL 2> 1

Wenn eine Liste geordnet ist, ist sie leer, oder die Liste ohne ihr erstes Element ist geordnet. ********** developing else-part now ... the precondition is: (not 1s_e1em{x57, car_1ist(list59)}, list91 = cdr_list(list59) and ordered_list(list59} and not list59 = nil_list and list91 = cdr_list(list59)} the conjuncts of the postcondition are classified as GOALS:

(x90 x88

x57, not ls_elem{x57, car_list(list59)}, ordered_list{list91})

= car_list(list59),

INVARIJ\NTS: () input variables: (list59 x57 list91) result variables: (x88 x90) save variables are: () PPL 2> aa automatic assignment generation computed BEGIN x90 .- x57 ; x88 := car_list(list59) END to establish the postcondition not ls_elem{x57, car_list(list59)} and x88 and x90 = x57 and ordered_list{list91}

car_list ( list59)

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

279

wrt. to the precondition (not ls_elern{x57, car_list(list59)), list91 = cdr_list(list59) and ordered_list{list59} and not list59 = nil_list and list91 = cdr_list(list59}) Is this assignrnent sequence sufficient? answer 1 for yes, 0 for no: PPL 2> 1 the cornputed postcondition is: x88 car_list(list59) and list91 cdr_list(list59) and not list59 = nil_list and ordered_list{list59) and not ls_elern{x90, car_list(list59)} and not ls_elern{x90, x88} and x90 = x57

=

=

do you want to enter an additional conjunct? answer 1 for yes, 0 for no: PPL 2> 0 proving vc by propositional prover ... Propositional logic prover left 5 prernises upon proving (not ls_elern{x57, car_list(list59)), list91 = cdr_list(list59) and ordered_list{list59) and not list59 = nil_list and list91 = cdr_list(list59)} 1( not ls_elern{x57, car_list(list59)) and car_list(list59) = car_list(list59) and x57 = x57 and ordered_list{list91) and car_list(list59) = car_list(list59) and list91 = cdr_list(list59) and not list59 = nil list and ordered_list{list59} and not ls_elern{x57, car_list(list59)} and not ls_elern{x57, car_list(list59)} and x57 x57). Try full predicate logic prover? answer 1 for yes, 0 for no: PPL 2> 1

=

proving vc by predicate logic prover ... if you BACKTRACK now, you rnay exarnine the propsitional proof!



S-14 the prover left 1 open prernise. you rnay now 1 ... accept the generated proof (leaving prernises as verification conditions) 2 ... reject the proof and leave the original verification condition

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

280

3 ... reject both proof and verification condition (which will cause a backtrack) PPL 2> %a-19 (ordered_l1at(l1at59})

l(11•t59 • n11_11at, ls_elem(x57, car_list(list59)), ordered_l1at(cdr_l1at(l1at59}})

PPL 2> 1

Diese Verflkationsbedingung ist aus denselben Griinden gültig wie die vorige. proving vc by propositional prover ... proving propositional verification condition of conditional Congratulations! Verification condition has been proven. proving vc by propositional prover ... Propositional logic prover left 6 premises upon proving ( list91 cdr_list(list59) and not list59 nil_list and ordered_list{list59) and ( x88 = x57 and x90 = car_list(list59) and ls_elem{x57, car_list(list59)) and ls_elem{x88, car_list(list59)) and ls_elem{x57, x90) and ls_elem{x88, x90) or x90 = x57 and not ls_elem{x90, x88) and not ls_elem{x90, car_list(list59)) and x88 = car_list(list59))) 1(( ordered_list{list91) and list91 = cdr_list(list59) and ( ls_elem{x57, car_list(list59)) and x88 = x57 and x90 = car_list(list59) or not ls_elem{x57, car_list(list59)) and x88 = car_list(list59) and x90 x57)) -> add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) and le_elem_bag{x88, add_elem_bag(x90, bag_list(list91))) and ordered_list{list91)))). Try full predicate logic prover? answer 1 for yes, 0 for no: PPL 2> 1

=

=

=

proving vc by predicate logic prover ... if you BACKTRACK now, you may examine the propsitional proof!

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

281





























S-28

S-33

S-38

S-43

S-22 the prover left 4 open premises. you may now l ... accept the generated proof (leaving premises as verification conditions) 2 ... reject the proof and leave the original verification condition 3 ... reject both proof and verification condition (which will cause a backtrack) PPL 2> %a-27 (ordered_list(list59), x57 = x57, ls_elem{x57, car_list(list59)), ls_elem{x57, car_list(list59)), ordered_list{cdr_list(list59)),

car_list(list59) = car_list(list59), ls_elem{x57, car_list(list59)), ls_elem(x57, car_list(list59)), ls_elem{x57, car_list{list59)))

l(add_elem_bag(x57, bag_list(list59)) • add_elem_bag(x57, add_elem_bag(car_list(list59), bag_list(cdr_list(list59)))), list59 = nil_list) PPL 2> %a-32 (ordered_list{list59}, x57 = x57, car_list(list59) = car_list(list59), ls_elem{x57, car_list(list59)}, ls_elem{x57, car_list{list59)), ls_elem{x57, car_list(list59)), ls_elem{x57, car_list(list59)}, ordered_list{cdr_list{list59)}, ls_elem{x57, car_list(list59})) l(le_e1em_bag{x57, add_elem_bag(car_list(list59), bag_list(cdr_list(list59)))}, list59 = nil_list) PPL 2> %a-48 (ordered_list{list59), x57 = x57, ordered_list{cdr_list(list59)))

l(add_elem_bag(x57, bag_list(list59)) = add_elem_bag(car_list(list59), add_elem_bag(x57,bag_list(cdr_list(list59)))), ls_elem{x57, car_list(list59)), ls_elem{x57, car_list(list59)}, list59 = nil_list, ls_elem{x57, car_list(list59)))

282

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

PPL 2> %a-54 (ord•r•d_list{list59), x57 = x57, ordered_list(cdr_list(list59)) )

l-

(l•_•lam_bag{car_list(list59), add_•l•m_bag(x57, bag_list(cdr_list(list59)))), ls_slam{x57, car_list(list59)), ls_elem(x57, car_list(list59)), list59 • nil_list, ls_elem(x57, car_list(list59))) PPL 2> 1

Bei den Verifikationsbedingungen gilt, daß list59 entweder leer ist oder die Funktion cdr angewandt werden kann. In diesen Fällen gelten einige mengen- bzw. ordnungstheoretische Aussagen. Please define a new procedure (by DEFPROC) with mode ( ((SORT elementtype) (SORT listtype)) ; ( (SORT elementtype) (SORT elementtype) (SORT listtype)) and enter the procedure's name before EXIT or type 0 to let the system itself choose a name for the new procedure. PPL 2> 0 proving vc by propositional prover ... Congratulations! Verification condition has been proven. proving vc by propositional prover ... Congratulations! Verification condition has been proven . ... finding a primitive predicate ... The specifications are (ordered_list{list59) and not $primitive74) l(ordered_list{list59) and not list59 = nil_list) ordered_list{list59) and not $primitive74 and add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) and le_elem_bag{x88, add_elem_bag(x90, bag_list(list91))) and ordered_list{list91))

1(

add_elem_bag(x57, bag_list(list59)) = add_elem_bag(x88, add_elem_bag(x90, bag_list(list91))) and le_elem_bag{x88, add_elem_bag(x90, bag_list(list91))) and true and ordered_list{list91))

The system generated for the primitive predicate the proposal list59 = nil list 0 ....... backtrack 1 ....... calculate primitive predicate (by antecedent development) 2 ....... insert your own predicate 3 ....... use proposal

Beispiel: Entwicklung ei11es Divide-a11d-Co11quer-Algo_rithmus

283

Das System schlägt aufgrund eines Eintrages in seiner Bibliothek gerade die Bedingung als primitive- Prädikat vor, die wir vorhin hinzugefügt haben. Deshalb können wir auf eine Antezedentenberechnung verzichten. PPL 2> 3 proving vc by propositional prover ... Congratulations! Verification condition has been proven. proving vc by propositional prover ... Congratulations! Verification condition has been proven . ... finding a directly_solve operator ... The specification is (ordered_list{list59} and list59 nil_list) 1( ( add_elem_bag(x57, bag_list(list59}} and ordered_list{list2}))

bag_list ( list2)

The system generated a SKIP as an instance for the programmetavariable. O..... Solution is false (i.e. backtrack and try to use another strategy for developing assignments) . l ..... Use skip as (preliminary) solution and have a look at the verification conditions (if existant). 2 ..... Develop your own solution.

Eine automatische Strategie war leider nicht erfolgreich und liefert deswegen den Standardvorschlag skip zurück. Ein Blick auf die Verfikationsbedingungen, die in diesem Fall gültig sein müßten, kann uns bei dem weiteren Vorgehen helfen. PPL 2> 1 The system generated the following vc's during automatic program synthesis: (ordered_list{nil_list})

l(add_elem_bag(x57, bag_list(nil_list)) (ordered_list{nil_list})

= bag_list(list2))

1- (ordered_list{list2})

O..... At least one vc is invalid (i.e. backtrack). l ..... All vc's are valid.

Beide Bedingungen sind nicht gültig, aber man kann aus ihnen den Hinweis erhalten, list2 zu einer Liste mit genau dem Element x57 zu setzen. PPL 2> 0 The system was not able to compute a conditional or assignment for a primitive case.

284

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

Use another program synthesis method to do so. the precondition is: (ordered_list(list59} and list59 = nil_list) the conjuncts of the postcondition are classified as GOALS: (add_elem_bag(x57, bag_list(list59)) bag_list ( list2), ordered_list(list2}) INVARIANTS:

(}

input variables: result variables:

(x57 list59) (list2)

save variables are:

()

Dies tun wir mit der interaktiven Zuweisungsgcnerierung. PPL 2> iaa you may now enter a (composition of) assignment(s) to establish (ordered_list(list59} and list59 = nil_list) l( ( add_elem_bag(x57, bag_list(list59)) = bag_list(list2) and ordered_list(list2} and $xill7)) or enter 0 to let the assignment strategy FAIL PPL 2> %"list2:•cons_list(x57, nil_list)" the computed postcondition is: ordered_list(list59} and list59 = nil list and list2 = cons_list(x57, nil_list) do you want to enter an additional conjunct? answer l for yes, 0 for no: PPL 2> 0 proving vc by propositional prover ... Propositional logic prover left 3 premises upccn proving (ordered_list(list59} and list59 = nil_list) 1( add_elem_bag(x57, bag_list(list59)) = bag_list(cons_list(x57, nil_list)) and ordered_list(cons_list(x57, nil_list)} and ordered_list(list59} and list59 = nil_list and cons_list(x57, nil_list) = cons_list(x57, nil_list)). Try full predicate logic prover? answer 1 for yes, 0 for no: PPL 2> 1

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

285

proving vc by predicate logic prover ... if you BACKTRACK now, you may examine the propsitional proof!



S-55 the prover left 2 open premises. you may now 1 ... accept the generated proof (leaving premises as verification conditions) 2 ... reject the proof and leave the original verification condition 3 ... reject both proof and verification condition (which will cause a backtrack) PPL 2> %s-57 (ordered_list{nil_list)) l(add_elem_bag(x57, bag_list(nil_list)) = bag_list(cons_list(x57, nil_list))) PPL 2> %s-59 (ordered_list{nil_list)) 1- {ordered_list(cons_list(x57, nil_list)})

Beide Verifikationsbedingungen stellen einfache Sachverhalte dar. PPL 2> 1 Please define a new procedure (by DEFPROC) with mode ( ((SORT elementtype) (SORT listtype)) ; ((SORT listtype)) and enter the procedure's name before EXIT or type 0 to let the system itself choose a name for the new procedure. PPL 2> (defproc directly-solvel (mkmode (liat (mksort elementtype) (mksort listtype)) (liat (mksort listtype)) (liat)) J PPL 2> %"directly-aolvel" proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ...

has been proven. has been proven. has been proven. has been proven.

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

286

Congratulations! Verification condition has been proven. proving vc by propositional prover ... Congratulations! Verification condition has been proven. proving vc by propositional prover ... Congratulations! Verification condition has been proven. proving vc by propositional prover ... Congratulations! Verification condition has been proven. proving vc by propositional prover ... Propositional logic prover left 1 premises upon proving ( true and list1 = cons_list(x57, list58) and ls{length_list(list58), length_list(list1)} and true and bag_list(list58) = bag_list(list59) and ordered_list{list59} and add_elem_bag(x57, bag_list(list59)) = bag_list(list2) and ordered_list{list2} and ordered_list(list59}) l(bag_list(list1) bag_list(list2) and ordered_list{list2}). Try full predicate logic prover? answer 1 for yes, 0 for no: PPL 2> 1

=

proving vc by predicate logic prover ... if you BACKTRACK now, you may examine the propsitional proof!

S-61 the prover left 1 open premise. you may now 1 ... accept the generated proof (leaving premises as verification conditions) 2 ... reject tloe proof and leave the original verification condition 3 ... reject both proof and verification condition (which will cause a backtrack) PPL 2> %a-63 (ls{length_list(list58), length_list(cons_list(x57, list58))}, true, bag_list(list58) = bag_list(list59), ordered_list{list59}, add_elem_bag(x57, bag_l1st(l1st59)) = bag_l1st(l1st2),

ordered_list{list2}, ordered_list{list59}, true) l(bag_l1st(cons_l1st(x57, l1st58))

bag_Hst (l1st2))

PPL 2> 1 finding a directly_solve operator ... The system generated the following vc's during automatic program synthesis: (true) 1- (ordered_list{nil_list})

Beispiel: Entwicklung eines Divide-and-Conquer-Algorithmus

287

O..... At least one vc is invalid (i.e. backtrack). l ..... All vc's are valid.

Die Strategie zur automatischen Generierung von Zuweisungen und bedingten Anweisungen konnte als directly_solve-Operator für das Sortierproblem die Zuweisung list2:= listl finden, was korrekt ist, da obige Verfikationsbedingung gültig ist PPL 2> 1 Please define a new procedure (by DEFPROC) with mode ( ((SORT listtype)) ; ((SORT listtype)) ) and enter the procedure's name before EXIT or type 0 to let the system itself choose a name for the new procedure. PPL 2> (detproc directly-aolve2 (mkmode (liat (mksort liattypa)) (liat (mkaort liattype)) (liat))) PPL 2> %"directly-aolve2" proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition proving vc by propositional prover ... Congratulations! Verification condition enter name for developed program: PPL 2> "inaertion-aort"

has been proven. has been proven. has been proven. has been proven. has been proven.

PPL 1>

Damit ist die Programmsynthese beendet. Es wurde das in folgendem Bild gezeigte Programm und der zugehörige Beweisbaum erzeugt. Seine Prämissen sind gerade die vorher geprüften Verfikationsbedingungen.

(liauorting (listl; nr list2) BEGII IF lis t1 = nil_lia t TBEI dir@ctl:r-aoln2(liatl; liat2) ELSE BEGII car_cdr(liatl; x57, liat58) atip ; liataorting(liat58; liat59) inaert-in-ord@red-liat(x57, liat59; liat2) EID EID, car_cdr (lliatl; Tar lliatelea, lliat2) BEGII lliatelea := car_liat(lliatl) lliat2 := cdr_liat(lliat1) EID, inaert-in-ordered-liat (x57, liat59; Tar liat2) BEGII IF liat59 = nil_liat TBEI directl:r-ao1Tel(x57, liat59; liat2) ELSE BEGII procl07(x57, liat59; x88, x90, liat91) atip ; ina@rt-in-ordered-liat(x90, liat91; liat89) liatcona(x88, list89; liat2) EID EID, procl07 (x57, liat59; Tar x88, x90, liat91) BEGII list91 := cdr_liot(list59) ; IF b_elem{x57, car_liot(list59)} TREI BEGII x90 := car_list(list59) ; x88 := x57 EID ELSE BEGII x90 := x57 ; x88 := car_list(list59) EID EID, listcono (llistelem, Uist1; var llist2) BEGII llist2 := cons_list(llistelem, llist1) EID, directl;r-so1Ye1 (x57, list59; nr list2) BEGII list2 := cons_list(x57, aiLlist) EID, directly-so1Ye2 (liatl; nr list2) BEGII list2 : = list1 EID) IN BEGIW listsorting(list1; list2) EID) (bag_list(listl) = bag_list(list2) A ordered_list{list2} A true))

~

Ei

51

!i-

::1.

I:>

i' :I>. dQ'

"'

::: ~

g

~

§

'!'

~



tl

~

1::·

"'

~

~

~

~

~

~

!:;•

8::

IV